diff --git a/.agent/.DS_Store b/.agent/.DS_Store deleted file mode 100644 index 1f2c43e08..000000000 Binary files a/.agent/.DS_Store and /dev/null differ diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..f6fca8c5e --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: ['https://github.com/sponsors/steipete'] diff --git a/.github/labeler.yml b/.github/labeler.yml index 5d2837a6c..5c19fa418 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -24,6 +24,7 @@ - changed-files: - any-glob-to-any-file: - "extensions/line/**" + - "docs/channels/line.md" "channel: matrix": - changed-files: - any-glob-to-any-file: @@ -132,6 +133,53 @@ - "docs/**" - "docs.acp.md" +"cli": + - changed-files: + - any-glob-to-any-file: + - "src/cli/**" + +"commands": + - changed-files: + - any-glob-to-any-file: + - "src/commands/**" + +"scripts": + - changed-files: + - any-glob-to-any-file: + - "scripts/**" + +"docker": + - changed-files: + - any-glob-to-any-file: + - "Dockerfile" + - "Dockerfile.*" + - "docker-compose.yml" + - "docker-setup.sh" + - ".dockerignore" + - "scripts/**/*docker*" + - "scripts/**/Dockerfile*" + - "scripts/sandbox-*.sh" + - "src/agents/sandbox*.ts" + - "src/commands/sandbox*.ts" + - "src/cli/sandbox-cli.ts" + - "src/docker-setup.test.ts" + - "src/config/**/*sandbox*" + - "docs/cli/sandbox.md" + - "docs/gateway/sandbox*.md" + - "docs/install/docker.md" + - "docs/multi-agent-sandbox-tools.md" + +"agents": + - changed-files: + - any-glob-to-any-file: + - "src/agents/**" + +"security": + - changed-files: + - any-glob-to-any-file: + - "docs/cli/security.md" + - "docs/gateway/security.md" + "extensions: copilot-proxy": - changed-files: - any-glob-to-any-file: diff --git a/.github/workflows/auto-response.yml b/.github/workflows/auto-response.yml index 7f242a094..b610e1718 100644 --- a/.github/workflows/auto-response.yml +++ b/.github/workflows/auto-response.yml @@ -3,7 +3,7 @@ name: Auto response on: issues: types: [labeled] - pull_request: + pull_request_target: types: [labeled] permissions: @@ -14,9 +14,15 @@ jobs: auto-response: runs-on: ubuntu-latest steps: + - uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: "2729701" + private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} - name: Handle labeled items uses: actions/github-script@v7 with: + github-token: ${{ steps.app-token.outputs.token }} script: | const rules = [ { diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 8d078774b..2b2f80130 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -21,3 +21,4 @@ jobs: with: configuration-path: .github/labeler.yml repo-token: ${{ steps.app-token.outputs.token }} + sync-labels: true diff --git a/.gitignore b/.gitignore index b2c1de9a2..d3fdee6b5 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,8 @@ apps/ios/*.xcfilelist # Vendor build artifacts vendor/a2ui/renderers/lit/dist/ +src/canvas-host/a2ui/*.bundle.js +src/canvas-host/a2ui/*.map .bundle.hash # fastlane (iOS) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9742150a3..447e77846 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,44 +2,100 @@ Docs: https://docs.clawd.bot -## 2026.1.25 +## 2026.1.26 Status: unreleased. ### Changes +- Rebrand: rename the npm package/CLI to `moltbot`, add a `clawdbot` compatibility shim, and move extensions to the `@moltbot/*` scope. +- Commands: group /help and /commands output with Telegram paging. (#2504) Thanks @hougangdev. +- macOS: limit project-local `node_modules/.bin` PATH preference to debug builds (reduce PATH hijacking risk). +- Tools: add per-sender group tool policies and fix precedence. (#1757) Thanks @adam91holt. +- Agents: summarize dropped messages during compaction safeguard pruning. (#2509) Thanks @jogi47. +- Skills: add multi-image input support to Nano Banana Pro skill. (#1958) Thanks @tyler6204. +- Agents: honor tools.exec.safeBins in exec allowlist checks. (#2281) +- Matrix: switch plugin SDK to @vector-im/matrix-bot-sdk. +- Docs: tighten Fly private deployment steps. (#2289) Thanks @dguido. +- Docs: add migration guide for moving to a new machine. (#2381) +- Docs: add Northflank one-click deployment guide. (#2167) Thanks @AdeboyeDN. +- Gateway: warn on hook tokens via query params; document header auth preference. (#2200) Thanks @YuriNachos. +- Gateway: add dangerous Control UI device auth bypass flag + audit warnings. (#2248) - Doctor: warn on gateway exposure without auth. (#2016) Thanks @Alex-Alaniz. +- Discord: add configurable privileged gateway intents for presences/members. (#2266) Thanks @kentaro. - Docs: add Vercel AI Gateway to providers sidebar. (#1901) Thanks @jerilynzheng. - Agents: expand cron tool description with full schema docs. (#1988) Thanks @tomascupr. - Skills: add missing dependency metadata for GitHub, Notion, Slack, Discord. (#1995) Thanks @jackheuberger. - Docs: add Render deployment guide. (#1975) Thanks @anurag. - Docs: add Claude Max API Proxy guide. (#1875) Thanks @atalovesyou. - Docs: add DigitalOcean deployment guide. (#1870) Thanks @0xJonHoldsCrypto. +- Docs: add Oracle Cloud (OCI) platform guide + cross-links. (#2333) Thanks @hirefrank. - Docs: add Raspberry Pi install guide. (#1871) Thanks @0xJonHoldsCrypto. - Docs: add GCP Compute Engine deployment guide. (#1848) Thanks @hougangdev. +- Docs: add LINE channel guide. Thanks @thewilloftheshadow. - Docs: credit both contributors for Control UI refresh. (#1852) Thanks @EnzeD. - Onboarding: add Venice API key to non-interactive flow. (#1893) Thanks @jonisjongithub. +- Onboarding: strengthen security warning copy for beta + access control expectations. - Tlon: format thread reply IDs as @ud. (#1837) Thanks @wca4a. - Gateway: prefer newest session metadata when combining stores. (#1823) Thanks @emanuelst. - Web UI: keep sub-agent announce replies visible in WebChat. (#1977) Thanks @andrescardonas7. - CI: increase Node heap size for macOS checks. (#1890) Thanks @realZachi. - macOS: avoid crash when rendering code blocks by bumping Textual to 0.3.1. (#2033) Thanks @garricn. - Browser: fall back to URL matching for extension relay target resolution. (#1999) Thanks @jonit-dev. +- Browser: route browser control via gateway/node; remove standalone browser control command and control URL config. +- Browser: route `browser.request` via node proxies when available; honor proxy timeouts; derive browser ports from `gateway.port`. - Update: ignore dist/control-ui for dirty checks and restore after ui builds. (#1976) Thanks @Glucksberg. +- Build: bundle A2UI assets during build and stop tracking generated bundles. (#2455) Thanks @0oAstro. - Telegram: allow caption param for media sends. (#1888) Thanks @mguellsegarra. +- Telegram: support plugin sendPayload channelData (media/buttons) and validate plugin commands. (#1917) Thanks @JoshuaLelon. - Telegram: avoid block replies when streaming is disabled. (#1885) Thanks @ivancasco. +- Docs: keep docs header sticky so navbar stays visible while scrolling. (#2445) Thanks @chenyuan99. +- Security: use Windows ACLs for permission audits and fixes on Windows. (#1957) - Auth: show copyable Google auth URL after ASCII prompt. (#1787) Thanks @robbyczgw-cla. - Routing: precompile session key regexes. (#1697) Thanks @Ray0907. - TUI: avoid width overflow when rendering selection lists. (#1686) Thanks @mossein. - Telegram: keep topic IDs in restart sentinel notifications. (#1807) Thanks @hsrvc. +- Telegram: add optional silent send flag (disable notifications). (#2382) Thanks @Suksham-sharma. +- Telegram: support editing sent messages via message(action="edit"). (#2394) Thanks @marcelomar21. +- Telegram: add sticker receive/send with vision caching. (#2629) Thanks @longjos. +- Telegram: send sticker pixels to vision models. (#2650) - Config: apply config.env before ${VAR} substitution. (#1813) Thanks @spanishflu-est1918. - Slack: clear ack reaction after streamed replies. (#2044) Thanks @fancyboi999. - macOS: keep custom SSH usernames in remote target. (#2046) Thanks @algal. +### Breaking +- **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed). + ### Fixes +- Memory Search: keep auto provider model defaults and only include remote when configured. (#2576) Thanks @papago2355. +- macOS: auto-scroll to bottom when sending a new message while scrolled up. (#2471) Thanks @kennyklee. +- Gateway: suppress AbortError and transient network errors in unhandled rejections. (#2451) Thanks @Glucksberg. +- TTS: keep /tts status replies on text-only commands and avoid duplicate block-stream audio. (#2451) Thanks @Glucksberg. +- Security: pin npm overrides to keep tar@7.5.4 for install toolchains. +- Security: properly test Windows ACL audit for config includes. (#2403) Thanks @dominicnunez. +- CLI: recognize versioned Node executables when parsing argv. (#2490) Thanks @David-Marsh-Photo. +- BlueBubbles: coalesce inbound URL link preview messages. (#1981) Thanks @tyler6204. +- Cron: allow payloads containing "heartbeat" in event filter. (#2219) Thanks @dwfinkelstein. +- CLI: avoid loading config for global help/version while registering plugin commands. (#2212) Thanks @dial481. +- Agents: include memory.md when bootstrapping memory context. (#2318) Thanks @czekaj. +- Agents: release session locks on process termination and cover more signals. (#2483) Thanks @janeexai. +- Agents: skip cooldowned providers during model failover. (#2143) Thanks @YiWang24. +- Telegram: harden polling + retry behavior for transient network errors and Node 22 transport issues. (#2420) Thanks @techboss. +- Telegram: wrap reasoning italics per line to avoid raw underscores. (#2181) Thanks @YuriNachos. +- Telegram: centralize API error logging for delivery and bot calls. (#2492) Thanks @altryne. +- Voice Call: enforce Twilio webhook signature verification for ngrok URLs; disable ngrok free tier bypass by default. +- Security: harden Tailscale Serve auth by validating identity via local tailscaled before trusting headers. +- Build: align memory-core peer dependency with lockfile. +- Security: add mDNS discovery mode with minimal default to reduce information disclosure. (#1882) Thanks @orlyjamie. +- Security: harden URL fetches with DNS pinning to reduce rebinding risk. Thanks Chris Zheng. - Web UI: improve WebChat image paste previews and allow image-only sends. (#1925) Thanks @smartprogrammer93. +- Security: wrap external hook content by default with a per-hook opt-out. (#1827) Thanks @mertcicekci0. +- Gateway: default auth now fail-closed (token/password required; Tailscale Serve identity remains allowed). +- Gateway: treat loopback + non-local Host connections as remote unless trusted proxy headers are present. +- Onboarding: remove unsupported gateway auth "off" choice from onboarding/configure flows and CLI flags. ## 2026.1.24-3 ### Fixes +- Slack: fix image downloads failing due to missing Authorization header on cross-origin redirects. (#1936) Thanks @sanderhelgesen. - Gateway: harden reverse proxy handling for local-client detection and unauthenticated proxied connects. (#1795) Thanks @orlyjamie. - Security audit: flag loopback Control UI with auth disabled as critical. (#1795) Thanks @orlyjamie. - CLI: resume claude-cli sessions and stream CLI replies to TUI clients. (#1921) Thanks @rmorse. @@ -660,7 +716,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic ### Highlights - Web search: `web_search`/`web_fetch` tools (Brave API) + first-time setup in onboarding/configure. -- Browser control: Chrome extension relay takeover mode + remote browser control via `clawdbot browser serve`. +- Browser control: Chrome extension relay takeover mode + remote browser control support. - Plugins: channel plugins (gateway HTTP hooks) + Zalo plugin + onboarding install flow. (#854) — thanks @longmaba. - Security: expanded `clawdbot security audit` (+ `--fix`), detect-secrets CI scan, and a `SECURITY.md` reporting policy. @@ -678,7 +734,7 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic - Security: add detect-secrets CI scan and baseline guidance. (#227) — thanks @Hyaxia. - Tools: add `web_search`/`web_fetch` (Brave API), auto-enable `web_fetch` for sandboxed sessions, and remove the `brave-search` skill. - CLI/Docs: add a web tools configure section for storing Brave API keys and update onboarding tips. -- Browser: add Chrome extension relay takeover mode (toolbar button), plus `clawdbot browser extension install/path` and remote browser control via `clawdbot browser serve` + `browser.controlToken`. +- Browser: add Chrome extension relay takeover mode (toolbar button), plus `clawdbot browser extension install/path` and remote browser control (standalone server + token auth). ### Fixes - Sessions: refactor session store updates to lock + mutate per-entry, add chat.inject, and harden subagent cleanup flow. (#944) — thanks @tyler6204. diff --git a/Dockerfile b/Dockerfile index a33f0077d..642cfd612 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,4 +32,9 @@ RUN pnpm ui:build ENV NODE_ENV=production +# Security hardening: Run as non-root user +# The node:22-bookworm image includes a 'node' user (uid 1000) +# This reduces the attack surface by preventing container escape via root privileges +USER node + CMD ["node", "dist/index.js"] diff --git a/README.md b/README.md index 217a4b61c..a3f0b11d1 100644 --- a/README.md +++ b/README.md @@ -384,7 +384,6 @@ Browser control (optional): { browser: { enabled: true, - controlUrl: "http://127.0.0.1:18791", color: "#FF4500" } } @@ -477,34 +476,36 @@ Special thanks to [Mario Zechner](https://mariozechner.at/) for his support and Thanks to all clawtributors:

- steipete plum-dawg bohdanpodvirnyi iHildy joaohlisboa mneves75 MatthieuBizien MaudeBot Glucksberg rahthakor - vrknetha radek-paclt Tobias Bischoff joshp123 czekaj mukhtharcm sebslight maxsumrall xadenryan rodrigouroz - juanpablodlc hsrvc magimetal meaningfool tyler6204 patelhiren NicholasSpisak jonisjongithub zerone0x abhisekbasu1 - jamesgroat claude JustYannicc Hyaxia dantelex SocialNerd42069 daveonkels google-labs-jules[bot] lc0rp mousberg - vignesh07 mteam88 dbhurley Mariano Belinky Eng. Juan Combetto TSavo julianengel bradleypriest benithors rohannagpal - timolins f-trycua benostein nachx639 pvoo sreekaransrinath gupsammy cristip73 stefangalescu nachoiacovino - Vasanth Rao Naik Sabavat petter-b cpojer scald gumadeiras andranik-sahakyan davidguttman sleontenko denysvitali orlyjamie - thewilloftheshadow sircrumpet peschee rafaelreis-r ratulsarna lutr0 danielz1z emanuelst KristijanJovanovski rdev - joshrad-dev kiranjd osolmaz adityashaw2 CashWilliams sheeek artuskg Takhoffman onutc pauloportella - neooriginal manuelhettich minghinmatthewlam myfunc travisirby buddyh connorshea kyleok mcinteerj dependabot[bot] - John-Rood timkrase uos-status gerardward2007 obviyus roshanasingh4 tosh-hamburg azade-c JonUleis bjesuiter - cheeeee Josh Phillips robbyczgw-cla dlauer pookNast Whoaa512 YuriNachos chriseidhof ngutman ysqander - aj47 superman32432432 Yurii Chukhlib grp06 antons austinm911 blacksmith-sh[bot] damoahdominic dan-dr HeimdallStrategy - imfing jalehman jarvis-medmatic kkarimi mahmoudashraf93 pkrmf RandyVentures Ryan Lisse dougvk erikpr1994 - Ghost jonasjancarik Keith the Silly Goose L36 Server Marc mitschabaude-bot mkbehr neist sibbl chrisrodz - Friederike Seiler gabriel-trigo iamadig Jonathan D. Rhyne (DJ-D) Kit koala73 manmal ogulcancelik pasogott petradonka - rubyrunsstuff siddhantjain suminhthanh svkozak VACInc wes-davis zats 24601 adam91holt ameno- - Chris Taylor Django Navarro evalexpr henrino3 humanwritten larlyssa odysseus0 oswalpalash pcty-nextgen-service-account rmorse - Syhids Aaron Konyer aaronveklabs andreabadesso Andrii cash-echo-bot Clawd ClawdFx dguido EnzeD - erik-agens Evizero fcatuhe itsjaydesu ivancasco ivanrvpereira jayhickey jeffersonwarrior jeffersonwarrior jverdi - longmaba mickahouan mjrussell odnxe p6l-richard philipp-spiess robaxelsen Sash Catanzarite T5-AndyML travisp - VAC william arzt zknicker abhaymundhara alejandro maza andrewting19 anpoirier arthyn Asleep123 bolismauro - conhecendoia dasilva333 Developer Dimitrios Ploutarchos Drake Thomsen fal3 Felix Krause foeken ganghyun kim grrowl - gtsifrikas HazAT hrdwdmrbl hugobarauna Jamie Openshaw Jarvis Jefferson Nunn Kevin Lin kitze levifig - Lloyd loukotal louzhixian martinpucik Matt mini Miles mrdbstn MSch Mustafa Tag Eldeen ndraiman - nexty5870 Noctivoro prathamdby ptn1411 reeltimeapps RLTCmpe Rolf Fredheim Rony Kelner Samrat Jha senoldogann - Seredeep sergical shiv19 shiyuanhai siraht snopoke testingabc321 The Admiral thesash Ubuntu - voidserf Vultr-Clawd Admin Wimmie wstock yazinsai ymat19 Zach Knickerbocker 0xJonHoldsCrypto aaronn Alphonse-arianee - atalovesyou Azade carlulsoe ddyo Erik hougangdev latitudeki5223 Manuel Maly Mourad Boustani odrobnik - pcty-nextgen-ios-builder Quentin Randy Torres rhjoh ronak-guliani William Stock + steipete plum-dawg bohdanpodvirnyi iHildy jaydenfyi joaohlisboa mneves75 MatthieuBizien MaudeBot Glucksberg + rahthakor vrknetha radek-paclt Tobias Bischoff joshp123 czekaj mukhtharcm sebslight maxsumrall xadenryan + rodrigouroz juanpablodlc hsrvc magimetal zerone0x meaningfool tyler6204 vignesh07 patelhiren NicholasSpisak + jonisjongithub abhisekbasu1 jamesgroat claude JustYannicc Hyaxia dantelex SocialNerd42069 daveonkels google-labs-jules[bot] + lc0rp mousberg mteam88 hirefrank joeynyc orlyjamie dbhurley Mariano Belinky Eng. Juan Combetto TSavo + julianengel bradleypriest benithors rohannagpal timolins f-trycua benostein nachx639 pvoo sreekaransrinath + gupsammy cristip73 stefangalescu nachoiacovino Vasanth Rao Naik Sabavat petter-b cpojer scald gumadeiras andranik-sahakyan + davidguttman thewilloftheshadow sleontenko denysvitali shakkernerd sircrumpet peschee rafaelreis-r dominicnunez ratulsarna + lutr0 danielz1z AdeboyeDN Alg0rix emanuelst KristijanJovanovski rdev rhuanssauro joshrad-dev kiranjd + osolmaz adityashaw2 CashWilliams sheeek artuskg Takhoffman onutc pauloportella neooriginal manuelhettich + minghinmatthewlam myfunc travisirby buddyh connorshea kyleok mcinteerj dependabot[bot] John-Rood obviyus + timkrase uos-status gerardward2007 roshanasingh4 tosh-hamburg azade-c JonUleis bjesuiter cheeeee robbyczgw-cla + dlauer Josh Phillips YuriNachos pookNast Whoaa512 chriseidhof ngutman ysqander aj47 superman32432432 + Yurii Chukhlib grp06 antons austinm911 blacksmith-sh[bot] damoahdominic dan-dr HeimdallStrategy imfing jalehman + jarvis-medmatic kkarimi mahmoudashraf93 pkrmf RandyVentures Ryan Lisse dougvk erikpr1994 Ghost jonasjancarik + Keith the Silly Goose L36 Server Marc mitschabaude-bot mkbehr neist sibbl chrisrodz Friederike Seiler gabriel-trigo + iamadig Jonathan D. Rhyne (DJ-D) Joshua Mitchell Kit koala73 manmal ogulcancelik pasogott petradonka rubyrunsstuff + siddhantjain suminhthanh svkozak VACInc wes-davis zats 24601 adam91holt ameno- Chris Taylor + dguido Django Navarro evalexpr henrino3 humanwritten larlyssa odysseus0 oswalpalash pcty-nextgen-service-account rmorse + Syhids Aaron Konyer aaronveklabs andreabadesso Andrii cash-echo-bot Clawd ClawdFx EnzeD erik-agens + Evizero fcatuhe itsjaydesu ivancasco ivanrvpereira jayhickey jeffersonwarrior jeffersonwarrior jverdi longmaba + mickahouan mjrussell odnxe p6l-richard philipp-spiess Pocket Clawd robaxelsen Sash Catanzarite T5-AndyML travisp + VAC william arzt zknicker abhaymundhara alejandro maza Alex-Alaniz alexstyl andrewting19 anpoirier arthyn + Asleep123 bolismauro chenyuan99 Clawdbot Maintainers conhecendoia dasilva333 David-Marsh-Photo Developer Dimitrios Ploutarchos Drake Thomsen + fal3 Felix Krause foeken ganghyun kim grrowl gtsifrikas HazAT hrdwdmrbl hugobarauna Jamie Openshaw + Jane Jarvis Jefferson Nunn kentaro Kevin Lin kitze Kiwitwitter levifig Lloyd loukotal + louzhixian martinpucik Matt mini mertcicekci0 Miles mrdbstn MSch Mustafa Tag Eldeen ndraiman nexty5870 + Noctivoro ppamment prathamdby ptn1411 reeltimeapps RLTCmpe Rolf Fredheim Rony Kelner Samrat Jha senoldogann + Seredeep sergical shiv19 shiyuanhai siraht snopoke Suksham-sharma techboss testingabc321 The Admiral + thesash Ubuntu voidserf Vultr-Clawd Admin Wimmie wolfred wstock yazinsai ymat19 Zach Knickerbocker + 0xJonHoldsCrypto aaronn Alphonse-arianee atalovesyou Azade carlulsoe ddyo Erik hougangdev latitudeki5223 + Manuel Maly Mourad Boustani odrobnik pcty-nextgen-ios-builder Quentin Randy Torres rhjoh ronak-guliani William Stock

diff --git a/SECURITY.md b/SECURITY.md index 43d493996..5bc9c9112 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,6 +1,6 @@ # Security Policy -If you believe you’ve found a security issue in Clawdbot, please report it privately. +If you believe you've found a security issue in Clawdbot, please report it privately. ## Reporting @@ -12,3 +12,50 @@ If you believe you’ve found a security issue in Clawdbot, please report it pri For threat model + hardening guidance (including `clawdbot security audit --deep` and `--fix`), see: - `https://docs.clawd.bot/gateway/security` + +### Web Interface Safety + +Clawdbot's web interface is intended for local use only. Do **not** bind it to the public internet; it is not hardened for public exposure. + +## Runtime Requirements + +### Node.js Version + +Clawdbot requires **Node.js 22.12.0 or later** (LTS). This version includes important security patches: + +- CVE-2025-59466: async_hooks DoS vulnerability +- CVE-2026-21636: Permission model bypass vulnerability + +Verify your Node.js version: + +```bash +node --version # Should be v22.12.0 or later +``` + +### Docker Security + +When running Clawdbot in Docker: + +1. The official image runs as a non-root user (`node`) for reduced attack surface +2. Use `--read-only` flag when possible for additional filesystem protection +3. Limit container capabilities with `--cap-drop=ALL` + +Example secure Docker run: + +```bash +docker run --read-only --cap-drop=ALL \ + -v clawdbot-data:/app/data \ + clawdbot/clawdbot:latest +``` + +## Security Scanning + +This project uses `detect-secrets` for automated secret detection in CI/CD. +See `.detect-secrets.cfg` for configuration and `.secrets.baseline` for the baseline. + +Run locally: + +```bash +pip install detect-secrets==1.5.0 +detect-secrets scan --baseline .secrets.baseline +``` diff --git a/apps/android/app/build.gradle.kts b/apps/android/app/build.gradle.kts index a015c0e36..85dc9c566 100644 --- a/apps/android/app/build.gradle.kts +++ b/apps/android/app/build.gradle.kts @@ -21,8 +21,8 @@ android { applicationId = "com.clawdbot.android" minSdk = 31 targetSdk = 36 - versionCode = 202601250 - versionName = "2026.1.25" + versionCode = 202601260 + versionName = "2026.1.26" } buildTypes { diff --git a/apps/ios/Sources/Info.plist b/apps/ios/Sources/Info.plist index e1cf2b71d..fb5212e59 100644 --- a/apps/ios/Sources/Info.plist +++ b/apps/ios/Sources/Info.plist @@ -19,9 +19,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2026.1.25 + 2026.1.26 CFBundleVersion - 20260125 + 20260126 NSAppTransportSecurity NSAllowsArbitraryLoadsInWebContent diff --git a/apps/ios/Tests/Info.plist b/apps/ios/Tests/Info.plist index 6ff977b05..7a6bc5cec 100644 --- a/apps/ios/Tests/Info.plist +++ b/apps/ios/Tests/Info.plist @@ -17,8 +17,8 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 2026.1.25 + 2026.1.26 CFBundleVersion - 20260125 + 20260126 diff --git a/apps/ios/project.yml b/apps/ios/project.yml index 0073b4ef9..c955bce24 100644 --- a/apps/ios/project.yml +++ b/apps/ios/project.yml @@ -81,8 +81,8 @@ targets: properties: CFBundleDisplayName: Clawdbot CFBundleIconName: AppIcon - CFBundleShortVersionString: "2026.1.25" - CFBundleVersion: "20260125" + CFBundleShortVersionString: "2026.1.26" + CFBundleVersion: "20260126" UILaunchScreen: {} UIApplicationSceneManifest: UIApplicationSupportsMultipleScenes: false @@ -130,5 +130,5 @@ targets: path: Tests/Info.plist properties: CFBundleDisplayName: ClawdbotTests - CFBundleShortVersionString: "2026.1.25" - CFBundleVersion: "20260125" + CFBundleShortVersionString: "2026.1.26" + CFBundleVersion: "20260126" diff --git a/apps/macos/Sources/Clawdbot/CommandResolver.swift b/apps/macos/Sources/Clawdbot/CommandResolver.swift index 7661c48f1..ba5f96d75 100644 --- a/apps/macos/Sources/Clawdbot/CommandResolver.swift +++ b/apps/macos/Sources/Clawdbot/CommandResolver.swift @@ -83,7 +83,10 @@ enum CommandResolver { "/usr/bin", "/bin", ] + #if DEBUG + // Dev-only convenience. Avoid project-local PATH hijacking in release builds. extras.insert(projectRoot.appendingPathComponent("node_modules/.bin").path, at: 0) + #endif let clawdbotPaths = self.clawdbotManagedPaths(home: home) if !clawdbotPaths.isEmpty { extras.insert(contentsOf: clawdbotPaths, at: 1) @@ -189,9 +192,13 @@ enum CommandResolver { } static func projectClawdbotExecutable(projectRoot: URL? = nil) -> String? { + #if DEBUG let root = projectRoot ?? self.projectRoot() let candidate = root.appendingPathComponent("node_modules/.bin").appendingPathComponent(self.helperName).path return FileManager().isExecutableFile(atPath: candidate) ? candidate : nil + #else + return nil + #endif } static func nodeCliPath() -> String? { @@ -282,22 +289,6 @@ enum CommandResolver { guard !settings.target.isEmpty else { return nil } guard let parsed = self.parseSSHTarget(settings.target) else { return nil } - var args: [String] = [ - "-o", "BatchMode=yes", - "-o", "StrictHostKeyChecking=accept-new", - "-o", "UpdateHostKeys=yes", - ] - if parsed.port > 0 { args.append(contentsOf: ["-p", String(parsed.port)]) } - let identity = settings.identity.trimmingCharacters(in: .whitespacesAndNewlines) - if !identity.isEmpty { - // Only use IdentitiesOnly when an explicit identity file is provided. - // This allows 1Password SSH agent and other SSH agents to provide keys. - args.append(contentsOf: ["-o", "IdentitiesOnly=yes"]) - args.append(contentsOf: ["-i", identity]) - } - let userHost = parsed.user.map { "\($0)@\(parsed.host)" } ?? parsed.host - args.append(userHost) - // Run the real clawdbot CLI on the remote host. let exportedPath = [ "/opt/homebrew/bin", @@ -324,7 +315,7 @@ enum CommandResolver { } else { """ PRJ=\(self.shellQuote(userPRJ)) - cd \(self.shellQuote(userPRJ)) || { echo "Project root not found: \(userPRJ)"; exit 127; } + cd "$PRJ" || { echo "Project root not found: $PRJ"; exit 127; } """ } @@ -378,7 +369,16 @@ enum CommandResolver { echo "clawdbot CLI missing on remote host"; exit 127; fi """ - args.append(contentsOf: ["/bin/sh", "-c", scriptBody]) + let options: [String] = [ + "-o", "BatchMode=yes", + "-o", "StrictHostKeyChecking=accept-new", + "-o", "UpdateHostKeys=yes", + ] + let args = self.sshArguments( + target: parsed, + identity: settings.identity, + options: options, + remoteCommand: ["/bin/sh", "-c", scriptBody]) return ["/usr/bin/ssh"] + args } @@ -427,8 +427,11 @@ enum CommandResolver { } static func parseSSHTarget(_ target: String) -> SSHParsedTarget? { - let trimmed = target.trimmingCharacters(in: .whitespacesAndNewlines) + let trimmed = self.normalizeSSHTargetInput(target) guard !trimmed.isEmpty else { return nil } + if trimmed.rangeOfCharacter(from: CharacterSet.whitespacesAndNewlines.union(.controlCharacters)) != nil { + return nil + } let userHostPort: String let user: String? if let atRange = trimmed.range(of: "@") { @@ -444,13 +447,31 @@ enum CommandResolver { if let colon = userHostPort.lastIndex(of: ":"), colon != userHostPort.startIndex { host = String(userHostPort[.. 0, parsedPort <= 65535 else { + return nil + } + port = parsedPort } else { host = userHostPort port = 22 } - return SSHParsedTarget(user: user, host: host, port: port) + return self.makeSSHTarget(user: user, host: host, port: port) + } + + static func sshTargetValidationMessage(_ target: String) -> String? { + let trimmed = self.normalizeSSHTargetInput(target) + guard !trimmed.isEmpty else { return nil } + if trimmed.hasPrefix("-") { + return "SSH target cannot start with '-'" + } + if trimmed.rangeOfCharacter(from: CharacterSet.whitespacesAndNewlines.union(.controlCharacters)) != nil { + return "SSH target cannot contain spaces" + } + if self.parseSSHTarget(trimmed) == nil { + return "SSH target must look like user@host[:port]" + } + return nil } private static func shellQuote(_ text: String) -> String { @@ -468,6 +489,64 @@ enum CommandResolver { return URL(fileURLWithPath: expanded) } + private static func normalizeSSHTargetInput(_ target: String) -> String { + var trimmed = target.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.hasPrefix("ssh ") { + trimmed = trimmed.replacingOccurrences(of: "ssh ", with: "") + .trimmingCharacters(in: .whitespacesAndNewlines) + } + return trimmed + } + + private static func isValidSSHComponent(_ value: String, allowLeadingDash: Bool = false) -> Bool { + if value.isEmpty { return false } + if !allowLeadingDash, value.hasPrefix("-") { return false } + let invalid = CharacterSet.whitespacesAndNewlines.union(.controlCharacters) + return value.rangeOfCharacter(from: invalid) == nil + } + + static func makeSSHTarget(user: String?, host: String, port: Int) -> SSHParsedTarget? { + let trimmedHost = host.trimmingCharacters(in: .whitespacesAndNewlines) + guard self.isValidSSHComponent(trimmedHost) else { return nil } + let trimmedUser = user?.trimmingCharacters(in: .whitespacesAndNewlines) + let normalizedUser: String? + if let trimmedUser { + guard self.isValidSSHComponent(trimmedUser) else { return nil } + normalizedUser = trimmedUser.isEmpty ? nil : trimmedUser + } else { + normalizedUser = nil + } + guard port > 0, port <= 65535 else { return nil } + return SSHParsedTarget(user: normalizedUser, host: trimmedHost, port: port) + } + + private static func sshTargetString(_ target: SSHParsedTarget) -> String { + target.user.map { "\($0)@\(target.host)" } ?? target.host + } + + static func sshArguments( + target: SSHParsedTarget, + identity: String, + options: [String], + remoteCommand: [String] = []) -> [String] + { + var args = options + if target.port > 0 { + args.append(contentsOf: ["-p", String(target.port)]) + } + let trimmedIdentity = identity.trimmingCharacters(in: .whitespacesAndNewlines) + if !trimmedIdentity.isEmpty { + // Only use IdentitiesOnly when an explicit identity file is provided. + // This allows 1Password SSH agent and other SSH agents to provide keys. + args.append(contentsOf: ["-o", "IdentitiesOnly=yes"]) + args.append(contentsOf: ["-i", trimmedIdentity]) + } + args.append("--") + args.append(self.sshTargetString(target)) + args.append(contentsOf: remoteCommand) + return args + } + #if SWIFT_PACKAGE static func _testNodeManagerBinPaths(home: URL) -> [String] { self.nodeManagerBinPaths(home: home) diff --git a/apps/macos/Sources/Clawdbot/GeneralSettings.swift b/apps/macos/Sources/Clawdbot/GeneralSettings.swift index 18dd423a2..b315ad32e 100644 --- a/apps/macos/Sources/Clawdbot/GeneralSettings.swift +++ b/apps/macos/Sources/Clawdbot/GeneralSettings.swift @@ -243,25 +243,36 @@ struct GeneralSettings: View { } private var remoteSshRow: some View { - HStack(alignment: .center, spacing: 10) { - Text("SSH target") - .font(.callout.weight(.semibold)) - .frame(width: self.remoteLabelWidth, alignment: .leading) - TextField("user@host[:22]", text: self.$state.remoteTarget) - .textFieldStyle(.roundedBorder) - .frame(maxWidth: .infinity) - Button { - Task { await self.testRemote() } - } label: { - if self.remoteStatus == .checking { - ProgressView().controlSize(.small) - } else { - Text("Test remote") + let trimmedTarget = self.state.remoteTarget.trimmingCharacters(in: .whitespacesAndNewlines) + let validationMessage = CommandResolver.sshTargetValidationMessage(trimmedTarget) + let canTest = !trimmedTarget.isEmpty && validationMessage == nil + + return VStack(alignment: .leading, spacing: 4) { + HStack(alignment: .center, spacing: 10) { + Text("SSH target") + .font(.callout.weight(.semibold)) + .frame(width: self.remoteLabelWidth, alignment: .leading) + TextField("user@host[:22]", text: self.$state.remoteTarget) + .textFieldStyle(.roundedBorder) + .frame(maxWidth: .infinity) + Button { + Task { await self.testRemote() } + } label: { + if self.remoteStatus == .checking { + ProgressView().controlSize(.small) + } else { + Text("Test remote") + } } + .buttonStyle(.borderedProminent) + .disabled(self.remoteStatus == .checking || !canTest) + } + if let validationMessage { + Text(validationMessage) + .font(.caption) + .foregroundStyle(.red) + .padding(.leading, self.remoteLabelWidth + 10) } - .buttonStyle(.borderedProminent) - .disabled(self.remoteStatus == .checking || self.state.remoteTarget - .trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) } } @@ -540,8 +551,15 @@ extension GeneralSettings { } // Step 1: basic SSH reachability check + guard let sshCommand = Self.sshCheckCommand( + target: settings.target, + identity: settings.identity) + else { + self.remoteStatus = .failed("SSH target is invalid") + return + } let sshResult = await ShellExecutor.run( - command: Self.sshCheckCommand(target: settings.target, identity: settings.identity), + command: sshCommand, cwd: nil, env: nil, timeout: 8) @@ -587,20 +605,20 @@ extension GeneralSettings { return !host.isEmpty } - private static func sshCheckCommand(target: String, identity: String) -> [String] { - var args: [String] = [ - "/usr/bin/ssh", + private static func sshCheckCommand(target: String, identity: String) -> [String]? { + guard let parsed = CommandResolver.parseSSHTarget(target) else { return nil } + let options = [ "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", "-o", "StrictHostKeyChecking=accept-new", "-o", "UpdateHostKeys=yes", ] - if !identity.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - args.append(contentsOf: ["-i", identity]) - } - args.append(target) - args.append("echo ok") - return args + let args = CommandResolver.sshArguments( + target: parsed, + identity: identity, + options: options, + remoteCommand: ["echo", "ok"]) + return ["/usr/bin/ssh"] + args } private func formatSSHFailure(_ response: Response, target: String) -> String { diff --git a/apps/macos/Sources/Clawdbot/NodePairingApprovalPrompter.swift b/apps/macos/Sources/Clawdbot/NodePairingApprovalPrompter.swift index b3f7e9295..e81b7a914 100644 --- a/apps/macos/Sources/Clawdbot/NodePairingApprovalPrompter.swift +++ b/apps/macos/Sources/Clawdbot/NodePairingApprovalPrompter.swift @@ -559,22 +559,21 @@ final class NodePairingApprovalPrompter { let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/ssh") - var args = [ - "-o", - "BatchMode=yes", - "-o", - "ConnectTimeout=5", - "-o", - "NumberOfPasswordPrompts=0", - "-o", - "PreferredAuthentications=publickey", - "-o", - "StrictHostKeyChecking=accept-new", + let options = [ + "-o", "BatchMode=yes", + "-o", "ConnectTimeout=5", + "-o", "NumberOfPasswordPrompts=0", + "-o", "PreferredAuthentications=publickey", + "-o", "StrictHostKeyChecking=accept-new", ] - if port > 0, port != 22 { - args.append(contentsOf: ["-p", String(port)]) + guard let target = CommandResolver.makeSSHTarget(user: user, host: host, port: port) else { + return false } - args.append(contentsOf: ["-l", user, host, "/usr/bin/true"]) + let args = CommandResolver.sshArguments( + target: target, + identity: "", + options: options, + remoteCommand: ["/usr/bin/true"]) process.arguments = args let pipe = Pipe() process.standardOutput = pipe diff --git a/apps/macos/Sources/Clawdbot/OnboardingView+Pages.swift b/apps/macos/Sources/Clawdbot/OnboardingView+Pages.swift index 5c5eead34..9abbcf972 100644 --- a/apps/macos/Sources/Clawdbot/OnboardingView+Pages.swift +++ b/apps/macos/Sources/Clawdbot/OnboardingView+Pages.swift @@ -206,6 +206,16 @@ extension OnboardingView { .textFieldStyle(.roundedBorder) .frame(width: fieldWidth) } + if let message = CommandResolver.sshTargetValidationMessage(self.state.remoteTarget) { + GridRow { + Text("") + .frame(width: labelWidth, alignment: .leading) + Text(message) + .font(.caption) + .foregroundStyle(.red) + .frame(width: fieldWidth, alignment: .leading) + } + } GridRow { Text("Identity file") .font(.callout.weight(.semibold)) diff --git a/apps/macos/Sources/Clawdbot/RemotePortTunnel.swift b/apps/macos/Sources/Clawdbot/RemotePortTunnel.swift index 8eaee1c05..4206a3750 100644 --- a/apps/macos/Sources/Clawdbot/RemotePortTunnel.swift +++ b/apps/macos/Sources/Clawdbot/RemotePortTunnel.swift @@ -70,7 +70,7 @@ final class RemotePortTunnel { "ssh tunnel using default remote port " + "host=\(sshHost, privacy: .public) port=\(remotePort, privacy: .public)") } - var args: [String] = [ + let options: [String] = [ "-o", "BatchMode=yes", "-o", "ExitOnForwardFailure=yes", "-o", "StrictHostKeyChecking=accept-new", @@ -81,16 +81,11 @@ final class RemotePortTunnel { "-N", "-L", "\(localPort):127.0.0.1:\(resolvedRemotePort)", ] - if parsed.port > 0 { args.append(contentsOf: ["-p", String(parsed.port)]) } let identity = settings.identity.trimmingCharacters(in: .whitespacesAndNewlines) - if !identity.isEmpty { - // Only use IdentitiesOnly when an explicit identity file is provided. - // This allows 1Password SSH agent and other SSH agents to provide keys. - args.append(contentsOf: ["-o", "IdentitiesOnly=yes"]) - args.append(contentsOf: ["-i", identity]) - } - let userHost = parsed.user.map { "\($0)@\(parsed.host)" } ?? parsed.host - args.append(userHost) + let args = CommandResolver.sshArguments( + target: parsed, + identity: identity, + options: options) let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/ssh") diff --git a/apps/macos/Sources/Clawdbot/Resources/Info.plist b/apps/macos/Sources/Clawdbot/Resources/Info.plist index ee9e3113d..c3031805e 100644 --- a/apps/macos/Sources/Clawdbot/Resources/Info.plist +++ b/apps/macos/Sources/Clawdbot/Resources/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2026.1.25 + 2026.1.26 CFBundleVersion - 202601250 + 202601260 CFBundleIconFile Clawdbot CFBundleURLTypes diff --git a/apps/macos/Tests/ClawdbotIPCTests/CommandResolverTests.swift b/apps/macos/Tests/ClawdbotIPCTests/CommandResolverTests.swift index 827057888..d8daa17f6 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/CommandResolverTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/CommandResolverTests.swift @@ -123,11 +123,16 @@ import Testing configRoot: [:]) #expect(cmd.first == "/usr/bin/ssh") - #expect(cmd.contains("clawd@example.com")) + if let marker = cmd.firstIndex(of: "--") { + #expect(cmd[marker + 1] == "clawd@example.com") + } else { + #expect(Bool(false)) + } #expect(cmd.contains("-i")) #expect(cmd.contains("/tmp/id_ed25519")) if let script = cmd.last { - #expect(script.contains("cd '/srv/clawdbot'")) + #expect(script.contains("PRJ='/srv/clawdbot'")) + #expect(script.contains("cd \"$PRJ\"")) #expect(script.contains("clawdbot")) #expect(script.contains("status")) #expect(script.contains("--json")) @@ -135,6 +140,12 @@ import Testing } } + @Test func rejectsUnsafeSSHTargets() async throws { + #expect(CommandResolver.parseSSHTarget("-oProxyCommand=calc") == nil) + #expect(CommandResolver.parseSSHTarget("host:-oProxyCommand=calc") == nil) + #expect(CommandResolver.parseSSHTarget("user@host:2222")?.port == 2222) + } + @Test func configRootLocalOverridesRemoteDefaults() async throws { let defaults = self.makeDefaults() defaults.set(AppState.ConnectionMode.remote.rawValue, forKey: connectionModeKey) diff --git a/apps/macos/Tests/ClawdbotIPCTests/MasterDiscoveryMenuSmokeTests.swift b/apps/macos/Tests/ClawdbotIPCTests/MasterDiscoveryMenuSmokeTests.swift index 10630c202..2541e0634 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/MasterDiscoveryMenuSmokeTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/MasterDiscoveryMenuSmokeTests.swift @@ -11,7 +11,12 @@ struct MasterDiscoveryMenuSmokeTests { discovery.statusText = "Searching…" discovery.gateways = [] - let view = GatewayDiscoveryInlineList(discovery: discovery, currentTarget: nil, onSelect: { _ in }) + let view = GatewayDiscoveryInlineList( + discovery: discovery, + currentTarget: nil, + currentUrl: nil, + transport: .ssh, + onSelect: { _ in }) _ = view.body } @@ -32,7 +37,12 @@ struct MasterDiscoveryMenuSmokeTests { ] let currentTarget = "\(NSUserName())@office.tailnet-123.ts.net:2222" - let view = GatewayDiscoveryInlineList(discovery: discovery, currentTarget: currentTarget, onSelect: { _ in }) + let view = GatewayDiscoveryInlineList( + discovery: discovery, + currentTarget: currentTarget, + currentUrl: nil, + transport: .ssh, + onSelect: { _ in }) _ = view.body } diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatView.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatView.swift index 44399a3e6..99b32779f 100644 --- a/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatView.swift +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatView.swift @@ -13,6 +13,7 @@ public struct ClawdbotChatView: View { @State private var showSessions = false @State private var hasPerformedInitialScroll = false @State private var isPinnedToBottom = true + @State private var lastUserMessageID: UUID? private let showsSessionSwitcher: Bool private let style: Style private let markdownVariant: ChatMarkdownVariant @@ -132,8 +133,28 @@ public struct ClawdbotChatView: View { self.hasPerformedInitialScroll = false self.isPinnedToBottom = true } + .onChange(of: self.viewModel.isSending) { _, isSending in + // Scroll to bottom when user sends a message, even if scrolled up. + guard isSending, self.hasPerformedInitialScroll else { return } + self.isPinnedToBottom = true + withAnimation(.snappy(duration: 0.22)) { + self.scrollPosition = self.scrollerBottomID + } + } .onChange(of: self.viewModel.messages.count) { _, _ in - guard self.hasPerformedInitialScroll, self.isPinnedToBottom else { return } + guard self.hasPerformedInitialScroll else { return } + if let lastMessage = self.viewModel.messages.last, + lastMessage.role.lowercased() == "user", + lastMessage.id != self.lastUserMessageID { + self.lastUserMessageID = lastMessage.id + self.isPinnedToBottom = true + withAnimation(.snappy(duration: 0.22)) { + self.scrollPosition = self.scrollerBottomID + } + return + } + + guard self.isPinnedToBottom else { return } withAnimation(.snappy(duration: 0.22)) { self.scrollPosition = self.scrollerBottomID } diff --git a/apps/shared/ClawdbotKit/Tools/CanvasA2UI/rolldown.config.mjs b/apps/shared/ClawdbotKit/Tools/CanvasA2UI/rolldown.config.mjs index 52c392fdd..75bef97e4 100644 --- a/apps/shared/ClawdbotKit/Tools/CanvasA2UI/rolldown.config.mjs +++ b/apps/shared/ClawdbotKit/Tools/CanvasA2UI/rolldown.config.mjs @@ -39,7 +39,7 @@ export default defineConfig({ output: { file: outputFile, format: "esm", - inlineDynamicImports: true, + codeSplitting: false, sourcemap: false, }, }); diff --git a/assets/chrome-extension/options.html b/assets/chrome-extension/options.html index 4e701826d..f66608f43 100644 --- a/assets/chrome-extension/options.html +++ b/assets/chrome-extension/options.html @@ -168,8 +168,7 @@

Getting started

If you see a red ! badge on the extension icon, the relay server is not reachable. - Start Clawdbot’s browser relay on this machine (Gateway or clawdbot browser serve), - then click the toolbar button again. + Start Clawdbot’s browser relay on this machine (Gateway or node host), then click the toolbar button again.

Full guide (install, remote Gateway, security): docs.clawd.bot/tools/chrome-extension diff --git a/dist/control-ui/assets/index-08nzABV3.css b/dist/control-ui/assets/index-08nzABV3.css deleted file mode 100644 index 58e39560c..000000000 --- a/dist/control-ui/assets/index-08nzABV3.css +++ /dev/null @@ -1 +0,0 @@ -@import"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap";:root{--bg: #0c0d12;--bg-accent: #0d0e14;--bg-elevated: #181a21;--bg-hover: #252830;--bg-muted: #252830;--card: #13151c;--card-foreground: #f8fafc;--card-highlight: rgba(255, 255, 255, .04);--popover: #13151c;--popover-foreground: #f8fafc;--panel: #0c0d12;--panel-strong: #181a21;--panel-hover: #252830;--chrome: rgba(12, 13, 18, .95);--chrome-strong: rgba(12, 13, 18, .98);--text: #f8fafc;--text-strong: #ffffff;--chat-text: #f8fafc;--muted: #94a3b8;--muted-strong: #64748b;--muted-foreground: #94a3b8;--border: #333842;--border-strong: #454d5c;--border-hover: #5a6373;--input: #333842;--ring: #ff4d4d;--accent: #ff4d4d;--accent-hover: #ff6666;--accent-muted: #ff4d4d;--accent-subtle: rgba(255, 77, 77, .12);--accent-foreground: #f8fafc;--primary: #ff4d4d;--primary-foreground: #ffffff;--secondary: #252830;--secondary-foreground: #f8fafc;--accent-2: #3b82f6;--accent-2-muted: rgba(59, 130, 246, .7);--ok: #22c55e;--ok-muted: rgba(34, 197, 94, .7);--ok-subtle: rgba(34, 197, 94, .1);--destructive: #ef4444;--destructive-foreground: #fafafa;--warn: #eab308;--warn-muted: rgba(234, 179, 8, .7);--warn-subtle: rgba(234, 179, 8, .1);--danger: #ef4444;--danger-muted: rgba(239, 68, 68, .7);--danger-subtle: rgba(239, 68, 68, .1);--info: #3b82f6;--focus: rgba(255, 77, 77, .2);--focus-ring: 0 0 0 2px var(--bg), 0 0 0 4px var(--ring);--grid-line: rgba(255, 255, 255, .03);--theme-switch-x: 50%;--theme-switch-y: 50%;--mono: "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace;--font-body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;--font-display: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, .05);--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, .1), 0 2px 4px -2px rgba(0, 0, 0, .1);--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, .1), 0 4px 6px -4px rgba(0, 0, 0, .1);--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, .1), 0 8px 10px -6px rgba(0, 0, 0, .1);--radius-sm: 4px;--radius-md: 6px;--radius-lg: 8px;--radius-xl: 12px;--radius-full: 9999px;--radius: 6px;--ease-out: cubic-bezier(.16, 1, .3, 1);--ease-in-out: cubic-bezier(.4, 0, .2, 1);--duration-fast: .15s;--duration-normal: .2s;--duration-slow: .3s;color-scheme:dark}:root[data-theme=light]{--bg: #f8f8f7;--bg-accent: #f3f2f0;--bg-elevated: #ffffff;--bg-hover: #eae8e6;--bg-muted: #eae8e6;--bg-content: #f0efed;--card: #ffffff;--card-foreground: #1c1917;--card-highlight: rgba(0, 0, 0, .04);--popover: #ffffff;--popover-foreground: #1c1917;--panel: #f8f8f7;--panel-strong: #f0efed;--panel-hover: #e5e3e1;--chrome: rgba(248, 248, 247, .95);--chrome-strong: rgba(248, 248, 247, .98);--text: #44403c;--text-strong: #292524;--chat-text: #44403c;--muted: #5c5856;--muted-strong: #44403c;--muted-foreground: #5c5856;--border: #e0dedc;--border-strong: #d6d3d1;--border-hover: #a8a5a0;--input: #e0dedc;--accent: #b91c1c;--accent-hover: #dc2626;--accent-muted: #b91c1c;--accent-subtle: rgba(185, 28, 28, .18);--accent-foreground: #ffffff;--primary: #b91c1c;--primary-foreground: #ffffff;--secondary: #eae8e6;--secondary-foreground: #44403c;--ok: #15803d;--ok-muted: rgba(21, 128, 61, .75);--ok-subtle: rgba(21, 128, 61, .12);--destructive: #b91c1c;--destructive-foreground: #fafafa;--warn: #a16207;--warn-muted: rgba(161, 98, 7, .75);--warn-subtle: rgba(161, 98, 7, .12);--danger: #b91c1c;--danger-muted: rgba(185, 28, 28, .75);--danger-subtle: rgba(185, 28, 28, .12);--info: #1d4ed8;--focus: rgba(185, 28, 28, .25);--grid-line: rgba(0, 0, 0, .06);color-scheme:light}*{box-sizing:border-box}html,body{height:100%}body{margin:0;font:400 14px/1.5 var(--font-body);letter-spacing:-.011em;background:var(--bg);color:var(--text);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}@keyframes theme-circle-transition{0%{clip-path:circle(0% at var(--theme-switch-x, 50%) var(--theme-switch-y, 50%))}to{clip-path:circle(150% at var(--theme-switch-x, 50%) var(--theme-switch-y, 50%))}}html.theme-transition{view-transition-name:theme}html.theme-transition::view-transition-old(theme){mix-blend-mode:normal;animation:none;z-index:1}html.theme-transition::view-transition-new(theme){mix-blend-mode:normal;z-index:2;animation:theme-circle-transition .4s var(--ease-out) forwards}@media(prefers-reduced-motion:reduce){html.theme-transition::view-transition-old(theme),html.theme-transition::view-transition-new(theme){animation:none!important}}clawdbot-app{display:block;position:relative;z-index:1;min-height:100vh}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}button,input,textarea,select{font:inherit;color:inherit}::selection{background:var(--accent-subtle);color:var(--text-strong)}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border);border-radius:var(--radius-full)}::-webkit-scrollbar-thumb:hover{background:var(--border-strong)}@keyframes rise{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}@keyframes scale-in{0%{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}@keyframes dashboard-enter{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}@keyframes shimmer{0%{background-position:-200% 0}to{background-position:200% 0}}:focus-visible{outline:none;box-shadow:var(--focus-ring)}.shell{--shell-pad: 16px;--shell-gap: 16px;--shell-nav-width: 220px;--shell-topbar-height: 56px;--shell-focus-duration: .2s;--shell-focus-ease: var(--ease-out);height:100vh;display:grid;grid-template-columns:var(--shell-nav-width) minmax(0,1fr);grid-template-rows:var(--shell-topbar-height) 1fr;grid-template-areas:"topbar topbar" "nav content";gap:0;animation:dashboard-enter .4s var(--ease-out);transition:grid-template-columns var(--shell-focus-duration) var(--shell-focus-ease);overflow:hidden}@supports (height: 100dvh){.shell{height:100dvh}}.shell--chat{min-height:100vh;height:100vh;overflow:hidden}@supports (height: 100dvh){.shell--chat{height:100dvh}}.shell--nav-collapsed,.shell--chat-focus{grid-template-columns:0px minmax(0,1fr)}.shell--onboarding{grid-template-rows:0 1fr}.shell--onboarding .topbar{display:none}.shell--onboarding .content{padding-top:0}.shell--chat-focus .content{padding-top:0;gap:0}.topbar{grid-area:topbar;position:sticky;top:0;z-index:40;display:flex;justify-content:space-between;align-items:center;gap:16px;padding:0 20px;height:var(--shell-topbar-height);border-bottom:1px solid var(--border);background:var(--bg)}.topbar-left{display:flex;align-items:center;gap:12px}.topbar .nav-collapse-toggle{width:36px;height:36px;margin-bottom:0}.topbar .nav-collapse-toggle__icon{width:20px;height:20px}.topbar .nav-collapse-toggle__icon svg{width:20px;height:20px}.brand{display:flex;align-items:center;gap:10px}.brand-logo{width:28px;height:28px;flex-shrink:0}.brand-logo img{width:100%;height:100%;object-fit:contain}.brand-text{display:flex;flex-direction:column;gap:1px}.brand-title{font-size:15px;font-weight:600;letter-spacing:-.02em;line-height:1.1;color:var(--text-strong)}.brand-sub{font-size:11px;font-weight:500;color:var(--muted);letter-spacing:.02em;line-height:1}.topbar-status{display:flex;align-items:center;gap:8px}.topbar-status .pill{padding:6px 10px;gap:6px;font-size:12px;font-weight:500;height:32px;box-sizing:border-box}.topbar-status .pill .mono{display:flex;align-items:center;line-height:1;margin-top:0}.topbar-status .statusDot{width:6px;height:6px}.topbar-status .theme-toggle{--theme-item: 24px;--theme-gap: 2px;--theme-pad: 3px}.topbar-status .theme-icon{width:12px;height:12px}.nav{grid-area:nav;overflow-y:auto;overflow-x:hidden;padding:16px 12px;border-right:1px solid var(--border);background:var(--bg);transition:width var(--shell-focus-duration) var(--shell-focus-ease),padding var(--shell-focus-duration) var(--shell-focus-ease),opacity var(--shell-focus-duration) var(--shell-focus-ease);min-height:0}.shell--chat-focus .nav{width:0;padding:0;border-width:0;overflow:hidden;pointer-events:none;opacity:0}.nav--collapsed{width:0;min-width:0;padding:0;overflow:hidden;border:none;opacity:0;pointer-events:none}.nav-collapse-toggle{width:32px;height:32px;display:flex;align-items:center;justify-content:center;background:transparent;border:1px solid transparent;border-radius:var(--radius-md);cursor:pointer;transition:background var(--duration-fast) ease,border-color var(--duration-fast) ease;margin-bottom:16px}.nav-collapse-toggle:hover{background:var(--bg-hover);border-color:var(--border)}.nav-collapse-toggle__icon{display:flex;align-items:center;justify-content:center;width:18px;height:18px;color:var(--muted);transition:color var(--duration-fast) ease}.nav-collapse-toggle__icon svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:1.5px;stroke-linecap:round;stroke-linejoin:round}.nav-collapse-toggle:hover .nav-collapse-toggle__icon{color:var(--text)}.nav-group{margin-bottom:20px;display:grid;gap:2px}.nav-group:last-child{margin-bottom:0}.nav-group__items{display:grid;gap:1px}.nav-group--collapsed .nav-group__items{display:none}.nav-label{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;padding:6px 10px;font-size:11px;font-weight:500;color:var(--muted);margin-bottom:4px;background:transparent;border:none;cursor:pointer;text-align:left;border-radius:var(--radius-sm);transition:color var(--duration-fast) ease,background var(--duration-fast) ease}.nav-label:hover{color:var(--text);background:var(--bg-hover)}.nav-label--static{cursor:default}.nav-label--static:hover{color:var(--muted);background:transparent}.nav-label__text{flex:1}.nav-label__chevron{font-size:10px;opacity:.5;transition:transform var(--duration-fast) ease}.nav-group--collapsed .nav-label__chevron{transform:rotate(-90deg)}.nav-item{position:relative;display:flex;align-items:center;justify-content:flex-start;gap:10px;padding:8px 10px;border-radius:var(--radius-md);border:1px solid transparent;background:transparent;color:var(--muted);cursor:pointer;text-decoration:none;transition:border-color var(--duration-fast) ease,background var(--duration-fast) ease,color var(--duration-fast) ease}.nav-item__icon{width:16px;height:16px;display:flex;align-items:center;justify-content:center;flex-shrink:0;opacity:.7;transition:opacity var(--duration-fast) ease}.nav-item__icon svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:1.5px;stroke-linecap:round;stroke-linejoin:round}.nav-item__text{font-size:13px;font-weight:500;white-space:nowrap}.nav-item:hover{color:var(--text);background:var(--bg-hover);text-decoration:none}.nav-item:hover .nav-item__icon{opacity:1}.nav-item.active{color:var(--text-strong);background:var(--accent-subtle)}.nav-item.active .nav-item__icon{opacity:1;color:var(--accent)}.content{grid-area:content;padding:8px 8px 24px;display:flex;flex-direction:column;gap:20px;min-height:0;overflow-y:auto;overflow-x:hidden}:root[data-theme=light] .content{background:var(--bg-content)}.content--chat{overflow:hidden;padding-bottom:0}.content-header{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;padding:4px 8px;overflow:hidden;transform-origin:top center;transition:opacity var(--shell-focus-duration) var(--shell-focus-ease),transform var(--shell-focus-duration) var(--shell-focus-ease),max-height var(--shell-focus-duration) var(--shell-focus-ease),padding var(--shell-focus-duration) var(--shell-focus-ease);max-height:80px}.shell--chat-focus .content-header{opacity:0;transform:translateY(-8px);max-height:0px;padding:0;pointer-events:none}.page-title{font-size:24px;font-weight:600;letter-spacing:-.02em;line-height:1.2;color:var(--text-strong)}.page-sub{color:var(--muted);font-size:13px;font-weight:400;margin-top:4px}.page-meta{display:flex;gap:8px}.content--chat .content-header{flex-direction:row;align-items:center;justify-content:space-between;gap:16px}.content--chat .content-header>div:first-child{text-align:left}.content--chat .page-meta{justify-content:flex-start}.content--chat .chat-controls{flex-shrink:0}.grid{display:grid;gap:16px}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.stat-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(140px,1fr))}.note-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}.row{display:flex;gap:10px;align-items:center}.stack{display:grid;gap:12px}.filters{display:flex;flex-wrap:wrap;gap:8px;align-items:center}@media(max-width:1100px){.shell{--shell-pad: 12px;--shell-gap: 12px;grid-template-columns:1fr;grid-template-rows:auto auto 1fr;grid-template-areas:"topbar" "nav" "content"}.nav{position:static;max-height:none;display:flex;gap:6px;overflow-x:auto;border-right:none;border-bottom:1px solid var(--border);padding:10px 14px;background:var(--bg)}.nav-group{grid-auto-flow:column;grid-template-columns:repeat(auto-fit,minmax(100px,1fr));margin-bottom:0}.grid-cols-2,.grid-cols-3{grid-template-columns:1fr}.topbar{position:static;padding:12px 14px;gap:10px}.topbar-status{flex-wrap:wrap}.table-head,.table-row,.list-item{grid-template-columns:1fr}}@media(max-width:1100px){.nav{display:flex;flex-direction:row;flex-wrap:nowrap;gap:4px;padding:10px 14px;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none}.nav::-webkit-scrollbar{display:none}.nav-group,.nav-group__items{display:contents}.nav-label{display:none}.nav-group--collapsed .nav-group__items{display:contents}.nav-item{padding:8px 14px;font-size:13px;border-radius:var(--radius-md);white-space:nowrap;flex-shrink:0}}@media(max-width:600px){.shell{--shell-pad: 8px;--shell-gap: 8px}.topbar{padding:10px 12px;gap:8px;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:center}.brand{flex:1;min-width:0}.brand-title{font-size:14px}.brand-sub{display:none}.topbar-status{gap:6px;width:auto;flex-wrap:nowrap}.topbar-status .pill{padding:4px 8px;font-size:11px;gap:4px}.topbar-status .pill .mono{display:none}.topbar-status .pill span:nth-child(2){display:none}.nav{padding:8px 10px;gap:4px;-webkit-overflow-scrolling:touch;scrollbar-width:none}.nav::-webkit-scrollbar{display:none}.nav-group{display:contents}.nav-label{display:none}.nav-item{padding:6px 10px;font-size:12px;border-radius:var(--radius-md);white-space:nowrap;flex-shrink:0}.content-header{display:none}.content{padding:4px 4px 16px;gap:12px}.card{padding:12px;border-radius:var(--radius-md)}.card-title{font-size:13px}.stat-grid{gap:8px;grid-template-columns:repeat(2,1fr)}.stat{padding:10px;border-radius:var(--radius-md)}.stat-label{font-size:11px}.stat-value{font-size:18px}.note-grid{grid-template-columns:1fr;gap:8px}.form-grid{grid-template-columns:1fr;gap:10px}.field input,.field textarea,.field select{padding:8px 10px;border-radius:var(--radius-md);font-size:14px}.btn{padding:8px 12px;font-size:12px}.pill{padding:4px 10px;font-size:12px}.chat-header{flex-direction:column;align-items:stretch;gap:8px}.chat-header__left{flex-direction:column;align-items:stretch}.chat-header__right{justify-content:space-between}.chat-session{min-width:unset;width:100%}.chat-thread{margin-top:8px;padding:12px 8px}.chat-msg{max-width:90%}.chat-bubble{padding:8px 12px;border-radius:var(--radius-md)}.chat-compose{gap:8px}.chat-compose__field textarea{min-height:60px;padding:8px 10px;border-radius:var(--radius-md);font-size:14px}.log-stream{border-radius:var(--radius-md);max-height:380px}.log-row{grid-template-columns:1fr;gap:4px;padding:8px}.log-time{font-size:10px}.log-level{font-size:9px}.log-subsystem{font-size:11px}.log-message{font-size:12px}.list-item{padding:10px;border-radius:var(--radius-md)}.list-title{font-size:13px}.list-sub{font-size:11px}.code-block{padding:8px;border-radius:var(--radius-md);font-size:11px}.theme-toggle{--theme-item: 24px;--theme-gap: 2px;--theme-pad: 3px}.theme-icon{width:12px;height:12px}}@media(max-width:400px){.shell{--shell-pad: 4px}.topbar{padding:8px 10px}.brand-title{font-size:13px}.nav{padding:6px 8px}.nav-item{padding:6px 8px;font-size:11px}.content{padding:4px 4px 12px;gap:10px}.card{padding:10px}.stat{padding:8px}.stat-value{font-size:16px}.chat-bubble{padding:8px 10px}.chat-compose__field textarea{min-height:52px;padding:8px 10px;font-size:13px}.btn{padding:6px 10px;font-size:11px}.topbar-status .pill{padding:3px 6px;font-size:10px}.theme-toggle{--theme-item: 22px;--theme-gap: 2px;--theme-pad: 2px}.theme-icon{width:11px;height:11px}}.chat{position:relative;display:flex;flex-direction:column;flex:1 1 0;height:100%;min-height:0;overflow:hidden;background:transparent!important;border:none!important;box-shadow:none!important}.chat-header{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:nowrap;flex-shrink:0;padding-bottom:12px;margin-bottom:12px;background:transparent}.chat-header__left{display:flex;align-items:center;gap:12px;flex-wrap:wrap;min-width:0}.chat-session{min-width:180px}.chat-thread{flex:1 1 0;overflow-y:auto;overflow-x:hidden;padding:12px 4px;margin:0 -4px;min-height:0;border-radius:12px;background:transparent}.chat-focus-exit{position:absolute;top:12px;right:12px;z-index:100;width:32px;height:32px;border-radius:50%;border:1px solid var(--border);background:var(--panel);color:var(--muted);font-size:20px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .15s ease-out,color .15s ease-out,border-color .15s ease-out;box-shadow:0 4px 12px #0003}.chat-focus-exit:hover{background:var(--panel-strong);color:var(--text);border-color:var(--accent)}.chat-focus-exit svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2px;stroke-linecap:round;stroke-linejoin:round}.chat-compose{position:sticky;bottom:0;flex-shrink:0;display:flex;align-items:stretch;gap:12px;margin-top:auto;padding:12px 4px 4px;background:linear-gradient(to bottom,transparent,var(--bg) 20%);z-index:10}:root[data-theme=light] .chat-compose{background:linear-gradient(to bottom,transparent,var(--bg-content) 20%)}.chat-compose__field{flex:1 1 auto;min-width:0;display:flex;align-items:stretch}.chat-compose__field>span{display:none}.chat-compose .chat-compose__field textarea{width:100%;height:40px;min-height:40px;max-height:150px;padding:9px 12px;border-radius:8px;resize:vertical;white-space:pre-wrap;font-family:var(--font-body);font-size:14px;line-height:1.45}.chat-compose__field textarea:disabled{opacity:.7;cursor:not-allowed}.chat-compose__actions{flex-shrink:0;display:flex;align-items:stretch;gap:8px}.chat-compose .chat-compose__actions .btn{padding:0 16px;font-size:13px;height:40px;min-height:40px;max-height:40px;line-height:1;white-space:nowrap;box-sizing:border-box}.chat-controls{display:flex;align-items:center;justify-content:flex-start;gap:12px;flex-wrap:wrap}.chat-controls__session{min-width:140px}.chat-controls__thinking{display:flex;align-items:center;gap:6px;font-size:13px}.btn--icon{padding:8px!important;min-width:36px;height:36px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--border);background:#ffffff0f}.chat-controls__separator{color:#fff6;font-size:18px;margin:0 8px;font-weight:300}:root[data-theme=light] .chat-controls__separator{color:#1018284d}.btn--icon:hover{background:#ffffff1f;border-color:#fff3}:root[data-theme=light] .btn--icon{background:#fff;border-color:var(--border);box-shadow:0 1px 2px #1018280d;color:var(--muted)}:root[data-theme=light] .btn--icon:hover{background:#fff;border-color:var(--border-strong);color:var(--text)}.btn--icon svg{display:block;width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:1.5px;stroke-linecap:round;stroke-linejoin:round}.chat-controls__session select{padding:6px 10px;font-size:13px}.chat-controls__thinking{display:flex;align-items:center;gap:4px;font-size:12px;padding:4px 10px;background:#ffffff0a;border-radius:6px;border:1px solid var(--border)}:root[data-theme=light] .chat-controls__thinking{background:#ffffffe6;border-color:#10182826}@media(max-width:640px){.chat-session{min-width:140px}.chat-compose{grid-template-columns:1fr}.chat-controls{flex-wrap:wrap;gap:8px}.chat-controls__session{min-width:120px}}.chat-thinking{margin-bottom:10px;padding:10px 12px;border-radius:10px;border:1px dashed rgba(255,255,255,.18);background:#ffffff0a;color:var(--muted);font-size:12px;line-height:1.4}:root[data-theme=light] .chat-thinking{border-color:#10182840;background:#1018280a}.chat-text{font-size:14px;line-height:1.5;word-wrap:break-word;overflow-wrap:break-word}.chat-text :where(p+p,p+ul,p+ol,p+pre,p+blockquote){margin-top:.75em}.chat-text :where(ul,ol){padding-left:1.5em}.chat-text :where(a){color:var(--accent);text-decoration:underline;text-underline-offset:2px}.chat-text :where(a:hover){opacity:.8}.chat-text :where(:not(pre)>code){background:#00000026;padding:.15em .4em;border-radius:4px}.chat-text :where(pre){background:#00000026;border-radius:6px;padding:10px 12px;overflow-x:auto}.chat-text :where(pre code){background:none;padding:0}.chat-text :where(blockquote){border-left:3px solid var(--border-strong);margin-left:0;color:var(--muted);background:#ffffff05;padding:8px 12px;border-radius:0 var(--radius-sm) var(--radius-sm) 0}.chat-text :where(blockquote blockquote){margin-top:8px;border-left-color:var(--border-hover);background:#ffffff08}.chat-text :where(blockquote blockquote blockquote){border-left-color:var(--muted-strong);background:#ffffff0a}:root[data-theme=light] .chat-text :where(blockquote){background:#00000008}:root[data-theme=light] .chat-text :where(blockquote blockquote){background:#0000000d}:root[data-theme=light] .chat-text :where(blockquote blockquote blockquote){background:#0000000a}:root[data-theme=light] .chat-text :where(:not(pre)>code){background:#00000014;border:1px solid rgba(0,0,0,.1)}:root[data-theme=light] .chat-text :where(pre){background:#0000000d;border:1px solid rgba(0,0,0,.1)}.chat-text :where(hr){border:none;border-top:1px solid var(--border);margin:1em 0}.chat-group{display:flex;gap:12px;align-items:flex-start;margin-bottom:16px;margin-left:4px;margin-right:16px}.chat-group.user{flex-direction:row-reverse;justify-content:flex-start}.chat-group-messages{display:flex;flex-direction:column;gap:2px;max-width:min(900px,calc(100% - 60px))}.chat-group.user .chat-group-messages{align-items:flex-end}.chat-group.user .chat-group-footer{justify-content:flex-end}.chat-group-footer{display:flex;gap:8px;align-items:baseline;margin-top:6px}.chat-sender-name{font-weight:500;font-size:12px;color:var(--muted)}.chat-group-timestamp{font-size:11px;color:var(--muted);opacity:.7}.chat-avatar{width:40px;height:40px;border-radius:8px;background:var(--panel-strong);display:grid;place-items:center;font-weight:600;font-size:14px;flex-shrink:0;align-self:flex-end;margin-bottom:4px}.chat-avatar.user{background:var(--accent-subtle);color:var(--accent)}.chat-avatar.assistant,.chat-avatar.other,.chat-avatar.tool{background:var(--secondary);color:var(--muted)}img.chat-avatar{display:block;object-fit:cover;object-position:center}.chat-bubble{position:relative;display:inline-block;border:1px solid transparent;background:var(--card);border-radius:var(--radius-lg);padding:10px 14px;box-shadow:none;transition:background .15s ease-out,border-color .15s ease-out;max-width:100%;word-wrap:break-word}.chat-bubble.has-copy{padding-right:36px}.chat-copy-btn{position:absolute;top:6px;right:8px;border:1px solid var(--border);background:var(--bg);color:var(--muted);border-radius:var(--radius-md);padding:4px 6px;font-size:14px;line-height:1;cursor:pointer;opacity:0;pointer-events:none;transition:opacity .12s ease-out,background .12s ease-out}.chat-copy-btn__icon{display:inline-flex;width:14px;height:14px;position:relative}.chat-copy-btn__icon svg{width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:1.5px;stroke-linecap:round;stroke-linejoin:round}.chat-copy-btn__icon-copy,.chat-copy-btn__icon-check{position:absolute;top:0;left:0;transition:opacity .15s ease}.chat-copy-btn__icon-check,.chat-copy-btn[data-copied="1"] .chat-copy-btn__icon-copy{opacity:0}.chat-copy-btn[data-copied="1"] .chat-copy-btn__icon-check{opacity:1}.chat-bubble:hover .chat-copy-btn{opacity:1;pointer-events:auto}.chat-copy-btn:hover{background:var(--bg-hover)}.chat-copy-btn[data-copying="1"]{opacity:0;pointer-events:none}.chat-copy-btn[data-error="1"]{opacity:1;pointer-events:auto;border-color:var(--danger-subtle);background:var(--danger-subtle);color:var(--danger)}.chat-copy-btn[data-copied="1"]{opacity:1;pointer-events:auto;border-color:var(--ok-subtle);background:var(--ok-subtle);color:var(--ok)}.chat-copy-btn:focus-visible{opacity:1;pointer-events:auto;outline:2px solid var(--accent);outline-offset:2px}@media(hover:none){.chat-copy-btn{opacity:1;pointer-events:auto}}:root[data-theme=light] .chat-bubble{border-color:var(--border);box-shadow:inset 0 1px 0 var(--card-highlight)}.chat-bubble:hover{background:var(--bg-hover)}.chat-group.user .chat-bubble{background:var(--accent-subtle);border-color:transparent}:root[data-theme=light] .chat-group.user .chat-bubble{border-color:#ea580c33;background:#fb923c1f}.chat-group.user .chat-bubble:hover{background:#ff4d4d26}.chat-bubble.streaming{animation:pulsing-border 1.5s ease-out infinite}@keyframes pulsing-border{0%,to{border-color:var(--border)}50%{border-color:var(--accent)}}.chat-bubble.fade-in{animation:fade-in .2s ease-out}@keyframes fade-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.chat-tool-card{border:1px solid var(--border);border-radius:8px;padding:12px;margin-top:8px;background:var(--card);box-shadow:inset 0 1px 0 var(--card-highlight);transition:border-color .15s ease-out,background .15s ease-out;max-height:120px;overflow:hidden}.chat-tool-card:hover{border-color:var(--border-strong);background:var(--bg-hover)}.chat-tool-card:first-child{margin-top:0}.chat-tool-card--clickable{cursor:pointer}.chat-tool-card--clickable:focus{outline:2px solid var(--accent);outline-offset:2px}.chat-tool-card__header{display:flex;justify-content:space-between;align-items:center;gap:8px}.chat-tool-card__title{display:inline-flex;align-items:center;gap:6px;font-weight:600;font-size:13px;line-height:1.2}.chat-tool-card__icon{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;flex-shrink:0}.chat-tool-card__icon svg{width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:1.5px;stroke-linecap:round;stroke-linejoin:round}.chat-tool-card__action{display:inline-flex;align-items:center;gap:4px;font-size:12px;color:var(--accent);opacity:.8;transition:opacity .15s ease-out}.chat-tool-card__action svg{width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:1.5px;stroke-linecap:round;stroke-linejoin:round}.chat-tool-card--clickable:hover .chat-tool-card__action{opacity:1}.chat-tool-card__status{display:inline-flex;align-items:center;color:var(--ok)}.chat-tool-card__status svg{width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:2px;stroke-linecap:round;stroke-linejoin:round}.chat-tool-card__status-text{font-size:11px;margin-top:4px}.chat-tool-card__detail{font-size:12px;color:var(--muted);margin-top:4px}.chat-tool-card__preview{font-size:11px;color:var(--muted);margin-top:8px;padding:8px 10px;background:var(--secondary);border-radius:var(--radius-md);white-space:pre-wrap;overflow:hidden;max-height:44px;line-height:1.4;border:1px solid var(--border)}.chat-tool-card--clickable:hover .chat-tool-card__preview{background:var(--bg-hover);border-color:var(--border-strong)}.chat-tool-card__inline{font-size:11px;color:var(--text);margin-top:6px;padding:6px 8px;background:var(--secondary);border-radius:var(--radius-sm);white-space:pre-wrap;word-break:break-word}.chat-reading-indicator{background:transparent;border:1px solid var(--border);padding:12px;display:inline-flex}.chat-reading-indicator__dots{display:flex;gap:6px;align-items:center}.chat-reading-indicator__dots span{width:6px;height:6px;border-radius:50%;background:var(--muted);animation:reading-pulse 1.4s ease-in-out infinite}.chat-reading-indicator__dots span:nth-child(1){animation-delay:0s}.chat-reading-indicator__dots span:nth-child(2){animation-delay:.2s}.chat-reading-indicator__dots span:nth-child(3){animation-delay:.4s}@keyframes reading-pulse{0%,60%,to{opacity:.3;transform:scale(.8)}30%{opacity:1;transform:scale(1)}}.chat-split-container{display:flex;gap:0;flex:1;min-height:0;height:100%}.chat-main{min-width:400px;display:flex;flex-direction:column;overflow:hidden;transition:flex .25s ease-out}.chat-sidebar{flex:1;min-width:300px;border-left:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden;animation:slide-in .2s ease-out}@keyframes slide-in{0%{opacity:0;transform:translate(20px)}to{opacity:1;transform:translate(0)}}.sidebar-panel{display:flex;flex-direction:column;height:100%;background:var(--panel)}.sidebar-header{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid var(--border);flex-shrink:0;position:sticky;top:0;z-index:10;background:var(--panel)}.sidebar-header .btn{padding:4px 8px;font-size:14px;min-width:auto;line-height:1}.sidebar-title{font-weight:600;font-size:14px}.sidebar-content{flex:1;overflow:auto;padding:16px}.sidebar-markdown{font-size:14px;line-height:1.5}.sidebar-markdown pre{background:#0000001f;border-radius:4px;padding:12px;overflow-x:auto}.sidebar-markdown code{font-family:var(--mono);font-size:13px}@media(max-width:768px){.chat-split-container--open{position:fixed;inset:0;z-index:1000}.chat-split-container--open .chat-main{display:none}.chat-split-container--open .chat-sidebar{width:100%;min-width:0;border-left:none}}.card{border:1px solid var(--border);background:var(--card);border-radius:var(--radius-lg);padding:16px;animation:rise .3s var(--ease-out);transition:border-color var(--duration-fast) ease;box-shadow:inset 0 1px 0 var(--card-highlight)}.card:hover{border-color:var(--border-strong)}.card-title{font-size:14px;font-weight:600;color:var(--text-strong)}.card-sub{color:var(--muted);font-size:13px;margin-top:4px}.stat{background:var(--card);border-radius:var(--radius-md);padding:12px 14px;border:1px solid var(--border);transition:border-color var(--duration-fast) ease;box-shadow:inset 0 1px 0 var(--card-highlight)}.stat:hover{border-color:var(--border-strong)}.stat-label{color:var(--muted);font-size:12px;font-weight:500}.stat-value{font-size:20px;font-weight:600;margin-top:4px;letter-spacing:-.02em}.stat-value.ok{color:var(--ok)}.stat-value.warn{color:var(--warn)}.stat-card{display:grid;gap:4px}.note-title{font-weight:600}.status-list{display:grid;gap:8px}.status-list div{display:flex;justify-content:space-between;gap:12px;padding:8px 0;border-bottom:1px solid var(--border)}.status-list div:last-child{border-bottom:none}.account-count{margin-top:10px;font-size:12px;font-weight:500;color:var(--muted)}.account-card-list{margin-top:16px;display:grid;gap:12px}.account-card{border:1px solid var(--border);border-radius:var(--radius-md);padding:12px;background:var(--bg-elevated);transition:border-color var(--duration-fast) ease}.account-card:hover{border-color:var(--border-strong)}.account-card-header{display:flex;justify-content:space-between;align-items:baseline;gap:12px}.account-card-title{font-weight:500}.account-card-id{font-family:var(--mono);font-size:12px;color:var(--muted)}.account-card-status{margin-top:10px;font-size:13px}.account-card-status div{padding:4px 0}.account-card-error{margin-top:8px;color:var(--danger);font-size:12px}.label{color:var(--muted);font-size:12px;font-weight:500}.pill{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--border);padding:6px 12px;border-radius:var(--radius-full);background:var(--secondary);font-size:13px;font-weight:500;transition:border-color var(--duration-fast) ease}.pill:hover{border-color:var(--border-strong)}.pill.danger{border-color:var(--danger-subtle);background:var(--danger-subtle);color:var(--danger)}.theme-toggle{--theme-item: 28px;--theme-gap: 2px;--theme-pad: 4px;position:relative}.theme-toggle__track{position:relative;display:grid;grid-template-columns:repeat(3,var(--theme-item));gap:var(--theme-gap);padding:var(--theme-pad);border-radius:var(--radius-full);border:1px solid var(--border);background:var(--secondary)}.theme-toggle__indicator{position:absolute;top:50%;left:var(--theme-pad);width:var(--theme-item);height:var(--theme-item);border-radius:var(--radius-full);transform:translateY(-50%) translate(calc(var(--theme-index, 0) * (var(--theme-item) + var(--theme-gap))));background:var(--accent);transition:transform var(--duration-normal) var(--ease-out);z-index:0}.theme-toggle__button{height:var(--theme-item);width:var(--theme-item);display:grid;place-items:center;border:0;border-radius:var(--radius-full);background:transparent;color:var(--muted);cursor:pointer;position:relative;z-index:1;transition:color var(--duration-fast) ease}.theme-toggle__button:hover{color:var(--text)}.theme-toggle__button.active{color:var(--accent-foreground)}.theme-toggle__button.active .theme-icon{stroke:var(--accent-foreground)}.theme-icon{width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:1.5px;stroke-linecap:round;stroke-linejoin:round}.statusDot{width:8px;height:8px;border-radius:var(--radius-full);background:var(--danger)}.statusDot.ok{background:var(--ok)}.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;border:1px solid var(--border);background:var(--bg-elevated);padding:8px 14px;border-radius:var(--radius-md);font-size:13px;font-weight:500;cursor:pointer;transition:border-color var(--duration-fast) ease,background var(--duration-fast) ease}.btn:hover{background:var(--bg-hover);border-color:var(--border-strong)}.btn:active{background:var(--secondary)}.btn svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:1.5px;stroke-linecap:round;stroke-linejoin:round;flex-shrink:0}.btn.primary{border-color:var(--accent);background:var(--accent);color:var(--primary-foreground)}.btn.primary:hover{background:var(--accent-hover);border-color:var(--accent-hover)}.btn-kbd{display:inline-flex;align-items:center;justify-content:center;margin-left:6px;padding:2px 5px;font-family:var(--mono);font-size:11px;font-weight:500;line-height:1;border-radius:4px;background:#ffffff26;color:inherit;opacity:.8}.btn.primary .btn-kbd{background:#fff3}:root[data-theme=light] .btn-kbd{background:#00000014}:root[data-theme=light] .btn.primary .btn-kbd{background:#ffffff40}.btn.active{border-color:var(--accent);background:var(--accent-subtle);color:var(--accent)}.btn.danger{border-color:transparent;background:var(--danger-subtle);color:var(--danger)}.btn.danger:hover{background:#ef444426}.btn--sm{padding:6px 10px;font-size:12px}.btn:disabled{opacity:.5;cursor:not-allowed}.field{display:grid;gap:6px}.field.full{grid-column:1 / -1}.field span{color:var(--muted);font-size:13px;font-weight:500}.field input,.field textarea,.field select{border:1px solid var(--input);background:var(--card);border-radius:var(--radius-md);padding:8px 12px;outline:none;box-shadow:inset 0 1px 0 var(--card-highlight);transition:border-color var(--duration-fast) ease,box-shadow var(--duration-fast) ease}.field input:focus,.field textarea:focus,.field select:focus{border-color:var(--ring);box-shadow:var(--focus-ring)}.field select{appearance:none;padding-right:36px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23a1a1aa' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center;cursor:pointer}.field textarea{font-family:var(--mono);min-height:160px;resize:vertical;white-space:pre;line-height:1.5}.field.checkbox{grid-template-columns:auto 1fr;align-items:center}.config-form .field.checkbox{grid-template-columns:18px minmax(0,1fr);column-gap:10px}.config-form .field.checkbox input[type=checkbox]{margin:0;width:16px;height:16px;accent-color:var(--accent)}.form-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}:root[data-theme=light] .field input,:root[data-theme=light] .field textarea,:root[data-theme=light] .field select{background:var(--card);border-color:var(--input)}:root[data-theme=light] .btn{background:var(--bg);border-color:var(--input)}:root[data-theme=light] .btn:hover{background:var(--bg-hover)}:root[data-theme=light] .btn.primary{background:var(--accent);border-color:var(--accent)}.muted{color:var(--muted)}.mono{font-family:var(--mono)}.callout{padding:12px 14px;border-radius:var(--radius-md);background:var(--secondary);border:1px solid var(--border);font-size:13px}.callout.danger{border-color:var(--danger-subtle);background:var(--danger-subtle);color:var(--danger)}.callout.info{border-color:#3b82f633;background:#3b82f61a;color:var(--info)}.callout.success{border-color:var(--ok-subtle);background:var(--ok-subtle);color:var(--ok)}.compaction-indicator{font-size:13px;padding:10px 12px;margin-bottom:8px;animation:fade-in .2s var(--ease-out)}.compaction-indicator--active{animation:compaction-pulse 1.5s ease-in-out infinite}.compaction-indicator--complete{animation:fade-in .2s var(--ease-out)}@keyframes compaction-pulse{0%,to{opacity:.7}50%{opacity:1}}.code-block{font-family:var(--mono);font-size:13px;line-height:1.5;background:var(--secondary);padding:12px;border-radius:var(--radius-md);border:1px solid var(--border);max-height:360px;overflow:auto}:root[data-theme=light] .code-block,:root[data-theme=light] .list-item,:root[data-theme=light] .table-row,:root[data-theme=light] .chip{background:var(--bg)}.list{display:grid;gap:8px;container-type:inline-size}.list-item{display:grid;grid-template-columns:minmax(0,1fr) minmax(200px,260px);gap:16px;align-items:start;border:1px solid var(--border);border-radius:var(--radius-md);padding:12px;background:var(--card);transition:border-color var(--duration-fast) ease}.list-item-clickable{cursor:pointer}.list-item-clickable:hover{border-color:var(--border-strong)}.list-item-selected{border-color:var(--accent);box-shadow:var(--focus-ring)}.list-main{display:grid;gap:4px;min-width:0}.list-title{font-weight:500}.list-sub{color:var(--muted);font-size:12px}.list-meta{text-align:right;color:var(--muted);font-size:12px;display:grid;gap:4px;min-width:200px}.list-meta .btn{padding:6px 10px}.list-meta .field input,.list-meta .field textarea,.list-meta .field select{width:100%}@container (max-width: 560px){.list-item{grid-template-columns:1fr}.list-meta{min-width:0;text-align:left}}.chip-row{display:flex;flex-wrap:wrap;gap:6px}.chip{font-size:12px;font-weight:500;border:1px solid var(--border);border-radius:var(--radius-full);padding:4px 10px;color:var(--muted);background:var(--secondary);transition:border-color var(--duration-fast) ease}.chip input{margin-right:6px}.chip-ok{color:var(--ok);border-color:var(--ok-subtle)}.chip-warn{color:var(--warn);border-color:var(--warn-subtle)}.table{display:grid;gap:6px}.table-head,.table-row{display:grid;grid-template-columns:1.4fr 1fr .8fr .7fr .8fr .8fr .8fr .8fr .6fr;gap:12px;align-items:center}.table-head{font-size:12px;font-weight:500;color:var(--muted);padding:0 12px}.table-row{border:1px solid var(--border);padding:10px 12px;border-radius:var(--radius-md);background:var(--card);transition:border-color var(--duration-fast) ease}.table-row:hover{border-color:var(--border-strong)}.session-link{text-decoration:none;color:var(--accent);font-weight:500}.session-link:hover{text-decoration:underline}.log-stream{border:1px solid var(--border);border-radius:var(--radius-md);background:var(--card);max-height:500px;overflow:auto;container-type:inline-size}.log-row{display:grid;grid-template-columns:90px 70px minmax(140px,200px) minmax(0,1fr);gap:12px;align-items:start;padding:8px 12px;border-bottom:1px solid var(--border);font-size:12px;transition:background var(--duration-fast) ease}.log-row:hover{background:var(--bg-hover)}.log-row:last-child{border-bottom:none}.log-time{color:var(--muted);font-family:var(--mono)}.log-level{font-size:11px;font-weight:500;border:1px solid var(--border);border-radius:var(--radius-sm);padding:2px 6px;width:fit-content}.log-level.trace,.log-level.debug{color:var(--muted)}.log-level.info{color:var(--info);border-color:#3b82f64d}.log-level.warn{color:var(--warn);border-color:var(--warn-subtle)}.log-level.error,.log-level.fatal{color:var(--danger);border-color:var(--danger-subtle)}.log-chip.trace,.log-chip.debug{color:var(--muted)}.log-chip.info{color:var(--info);border-color:#3b82f64d}.log-chip.warn{color:var(--warn);border-color:var(--warn-subtle)}.log-chip.error,.log-chip.fatal{color:var(--danger);border-color:var(--danger-subtle)}.log-subsystem{color:var(--muted);font-family:var(--mono)}.log-message{white-space:pre-wrap;word-break:break-word;font-family:var(--mono)}@container (max-width: 620px){.log-row{grid-template-columns:70px 60px minmax(0,1fr)}.log-subsystem{display:none}}.chat{display:flex;flex-direction:column;min-height:0}.shell--chat .chat{flex:1}.chat-header{display:flex;justify-content:space-between;align-items:flex-end;gap:16px;flex-wrap:wrap}.chat-header__left{display:flex;align-items:flex-end;gap:12px;flex-wrap:wrap;min-width:0}.chat-header__right{display:flex;align-items:center;gap:8px}.chat-session{min-width:240px}.chat-thread{margin-top:16px;display:flex;flex-direction:column;gap:12px;flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;padding:16px 12px;min-width:0;border-radius:0;border:none;background:transparent}.chat-queue{margin-top:12px;padding:12px;border-radius:var(--radius-lg);border:1px solid var(--border);background:var(--card);display:grid;gap:8px}.chat-queue__title{font-family:var(--mono);font-size:12px;font-weight:500;color:var(--muted)}.chat-queue__list{display:grid;gap:8px}.chat-queue__item{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:start;gap:12px;padding:10px 12px;border-radius:var(--radius-md);border:1px dashed var(--border-strong);background:var(--secondary)}.chat-queue__text{color:var(--chat-text);font-size:13px;line-height:1.45;white-space:pre-wrap;overflow:hidden;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical}.chat-queue__remove{align-self:start;padding:4px 10px;font-size:12px;line-height:1}.chat-line{display:flex}.chat-line.user{justify-content:flex-end}.chat-line.assistant,.chat-line.other{justify-content:flex-start}.chat-msg{display:grid;gap:6px;max-width:min(700px,82%)}.chat-line.user .chat-msg{justify-items:end}.chat-bubble{border:1px solid transparent;background:var(--card);border-radius:var(--radius-lg);padding:10px 14px;min-width:0}:root[data-theme=light] .chat-bubble{border-color:var(--border);background:var(--bg)}.chat-line.user .chat-bubble{border-color:transparent;background:var(--accent-subtle)}:root[data-theme=light] .chat-line.user .chat-bubble{border-color:#ea580c33;background:#fb923c1f}.chat-line.assistant .chat-bubble{border-color:transparent;background:var(--secondary)}:root[data-theme=light] .chat-line.assistant .chat-bubble{border-color:var(--border);background:var(--bg-muted)}@keyframes chatStreamPulse{0%,to{border-color:var(--border)}50%{border-color:var(--accent)}}.chat-bubble.streaming{animation:chatStreamPulse 1.5s ease-in-out infinite}@media(prefers-reduced-motion:reduce){.chat-bubble.streaming{animation:none;border-color:var(--accent)}}.chat-bubble.chat-reading-indicator{width:fit-content;padding:10px 16px}.chat-reading-indicator__dots{display:inline-flex;align-items:center;gap:4px;height:12px}.chat-reading-indicator__dots>span{display:inline-block;width:6px;height:6px;border-radius:var(--radius-full);background:var(--muted);opacity:.6;transform:translateY(0);animation:chatReadingDot 1.2s ease-in-out infinite;will-change:transform,opacity}.chat-reading-indicator__dots>span:nth-child(2){animation-delay:.15s}.chat-reading-indicator__dots>span:nth-child(3){animation-delay:.3s}@keyframes chatReadingDot{0%,80%,to{opacity:.4;transform:translateY(0)}40%{opacity:1;transform:translateY(-3px)}}@media(prefers-reduced-motion:reduce){.chat-reading-indicator__dots>span{animation:none;opacity:.6}}.chat-text{overflow-wrap:anywhere;word-break:break-word;color:var(--chat-text);line-height:1.5}.chat-text :where(p,ul,ol,pre,blockquote,table){margin:0}.chat-text :where(p+p,p+ul,p+ol,p+pre,p+blockquote,p+table){margin-top:.75em}.chat-text :where(ul,ol){padding-left:1.2em}.chat-text :where(li+li){margin-top:.25em}.chat-text :where(a){color:var(--accent)}.chat-text :where(a:hover){text-decoration:underline}.chat-text :where(blockquote){border-left:2px solid var(--border-strong);padding-left:12px;color:var(--muted)}.chat-text :where(hr){border:0;border-top:1px solid var(--border);margin:1em 0}.chat-text :where(code){font-family:var(--mono);font-size:.9em}.chat-text :where(:not(pre)>code){padding:.15em .35em;border-radius:var(--radius-sm);border:1px solid var(--border);background:var(--secondary)}:root[data-theme=light] .chat-text :where(:not(pre)>code){background:var(--bg-muted)}.chat-text :where(pre){margin-top:.75em;padding:10px 12px;border-radius:var(--radius-md);border:1px solid var(--border);background:var(--secondary);overflow:auto}:root[data-theme=light] .chat-text :where(pre){background:var(--bg-muted)}.chat-text :where(pre code){font-size:12px;white-space:pre}.chat-text :where(table){margin-top:.75em;border-collapse:collapse;width:100%;font-size:13px}.chat-text :where(th,td){border:1px solid var(--border);padding:6px 10px;vertical-align:top}.chat-text :where(th){font-family:var(--mono);font-weight:500;color:var(--muted);background:var(--secondary)}.chat-tool-card{margin-top:8px;padding:10px 12px;border-radius:var(--radius-md);border:1px solid var(--border);background:var(--secondary);display:grid;gap:4px}:root[data-theme=light] .chat-tool-card{background:var(--bg-muted)}.chat-tool-card__title{font-family:var(--mono);font-size:12px;font-weight:500;color:var(--text)}.chat-tool-card__detail{font-family:var(--mono);font-size:11px;color:var(--muted)}.chat-tool-card__details{margin-top:6px}.chat-tool-card__summary{font-family:var(--mono);font-size:11px;color:var(--muted);cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px}.chat-tool-card__summary::-webkit-details-marker{display:none}.chat-tool-card__summary-meta{color:var(--muted);opacity:.7}.chat-tool-card__details[open] .chat-tool-card__summary{color:var(--text)}.chat-tool-card__output{margin-top:8px;font-family:var(--mono);font-size:11px;line-height:1.5;white-space:pre-wrap;color:var(--chat-text);padding:8px 10px;border-radius:var(--radius-md);border:1px solid var(--border);background:var(--card)}:root[data-theme=light] .chat-tool-card__output{background:var(--bg)}.chat-stamp{font-size:11px;color:var(--muted)}.chat-line.user .chat-stamp{text-align:right}.chat-compose{margin-top:12px;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:end;gap:10px}.shell--chat .chat-compose{position:sticky;bottom:0;z-index:5;margin-top:0;padding-top:12px;background:linear-gradient(180deg,transparent 0%,var(--bg) 40%)}.shell--chat-focus .chat-compose{bottom:calc(var(--shell-pad) + 8px);padding-bottom:calc(12px + env(safe-area-inset-bottom,0px));border-bottom-left-radius:var(--radius-lg);border-bottom-right-radius:var(--radius-lg)}.chat-compose__field{gap:4px}.chat-compose__field textarea{min-height:72px;padding:10px 14px;border-radius:var(--radius-lg);resize:vertical;white-space:pre-wrap;font-family:var(--font-body);line-height:1.5;border:1px solid var(--input);background:var(--card);box-shadow:inset 0 1px 0 var(--card-highlight);transition:border-color var(--duration-fast) ease,box-shadow var(--duration-fast) ease}.chat-compose__field textarea:focus{border-color:var(--ring);box-shadow:var(--focus-ring)}.chat-compose__field textarea:disabled{opacity:.5;cursor:not-allowed}.chat-compose__actions{justify-content:flex-end;align-self:end}@media(max-width:900px){.chat-session{min-width:180px}.chat-compose{grid-template-columns:1fr}}.qr-wrap{margin-top:16px;border-radius:var(--radius-md);background:var(--card);border:1px dashed var(--border-strong);padding:16px;display:inline-flex}.qr-wrap img{width:160px;height:160px;border-radius:var(--radius-sm);image-rendering:pixelated}.exec-approval-overlay{position:fixed;inset:0;background:#000c;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;padding:24px;z-index:200}.exec-approval-card{width:min(540px,100%);background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:20px;animation:scale-in .2s var(--ease-out)}.exec-approval-header{display:flex;align-items:center;justify-content:space-between;gap:16px}.exec-approval-title{font-size:14px;font-weight:600}.exec-approval-sub{color:var(--muted);font-size:13px;margin-top:4px}.exec-approval-queue{font-size:11px;font-weight:500;color:var(--muted);border:1px solid var(--border);border-radius:var(--radius-full);padding:4px 10px}.exec-approval-command{margin-top:12px;padding:10px 12px;background:var(--secondary);border:1px solid var(--border);border-radius:var(--radius-md);word-break:break-word;white-space:pre-wrap;font-family:var(--mono);font-size:13px}.exec-approval-meta{margin-top:12px;display:grid;gap:6px;font-size:13px;color:var(--muted)}.exec-approval-meta-row{display:flex;justify-content:space-between;gap:12px}.exec-approval-meta-row span:last-child{color:var(--text);font-family:var(--mono)}.exec-approval-error{margin-top:10px;font-size:13px;color:var(--danger)}.exec-approval-actions{margin-top:16px;display:flex;flex-wrap:wrap;gap:8px}.config-layout{display:grid;grid-template-columns:260px minmax(0,1fr);gap:0;min-height:calc(100vh - 160px);margin:-16px;border-radius:var(--radius-xl);overflow:hidden;border:1px solid var(--border);background:var(--panel)}.config-sidebar{display:flex;flex-direction:column;background:var(--bg-accent);border-right:1px solid var(--border)}:root[data-theme=light] .config-sidebar{background:var(--bg-hover)}.config-sidebar__header{display:flex;align-items:center;justify-content:space-between;padding:18px;border-bottom:1px solid var(--border)}.config-sidebar__title{font-weight:600;font-size:14px;letter-spacing:-.01em}.config-sidebar__footer{margin-top:auto;padding:14px;border-top:1px solid var(--border)}.config-search{position:relative;padding:14px;border-bottom:1px solid var(--border)}.config-search__icon{position:absolute;left:28px;top:50%;transform:translateY(-50%);width:16px;height:16px;color:var(--muted);pointer-events:none}.config-search__input{width:100%;padding:11px 36px 11px 42px;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg-elevated);font-size:13px;outline:none;transition:border-color var(--duration-fast) ease,box-shadow var(--duration-fast) ease,background var(--duration-fast) ease}.config-search__input::placeholder{color:var(--muted)}.config-search__input:focus{border-color:var(--accent);box-shadow:var(--focus-ring);background:var(--bg-hover)}:root[data-theme=light] .config-search__input{background:#fff}:root[data-theme=light] .config-search__input:focus{background:#fff}.config-search__clear{position:absolute;right:22px;top:50%;transform:translateY(-50%);width:22px;height:22px;border:none;border-radius:var(--radius-full);background:var(--bg-hover);color:var(--muted);font-size:14px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background var(--duration-fast) ease,color var(--duration-fast) ease}.config-search__clear:hover{background:var(--border-strong);color:var(--text)}.config-nav{flex:1;overflow-y:auto;padding:10px}.config-nav__item{display:flex;align-items:center;gap:12px;width:100%;padding:11px 14px;border:none;border-radius:var(--radius-md);background:transparent;color:var(--muted);font-size:13px;font-weight:500;text-align:left;cursor:pointer;transition:background var(--duration-fast) ease,color var(--duration-fast) ease}.config-nav__item:hover{background:var(--bg-hover);color:var(--text)}:root[data-theme=light] .config-nav__item:hover{background:#0000000a}.config-nav__item.active{background:var(--accent-subtle);color:var(--accent)}.config-nav__icon{width:20px;height:20px;display:flex;align-items:center;justify-content:center;font-size:15px;opacity:.7}.config-nav__item:hover .config-nav__icon,.config-nav__item.active .config-nav__icon{opacity:1}.config-nav__icon svg{width:18px;height:18px;stroke:currentColor;fill:none}.config-nav__label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.config-mode-toggle{display:flex;padding:4px;background:var(--bg-elevated);border-radius:var(--radius-md);border:1px solid var(--border)}:root[data-theme=light] .config-mode-toggle{background:#fff}.config-mode-toggle__btn{flex:1;padding:9px 14px;border:none;border-radius:var(--radius-sm);background:transparent;color:var(--muted);font-size:12px;font-weight:600;cursor:pointer;transition:background var(--duration-fast) ease,color var(--duration-fast) ease,box-shadow var(--duration-fast) ease}.config-mode-toggle__btn:hover{color:var(--text)}.config-mode-toggle__btn.active{background:var(--accent);color:#fff;box-shadow:var(--shadow-sm)}.config-main{display:flex;flex-direction:column;min-width:0;background:var(--panel)}.config-actions{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:14px 22px;background:var(--bg-accent);border-bottom:1px solid var(--border)}:root[data-theme=light] .config-actions{background:var(--bg-hover)}.config-actions__left,.config-actions__right{display:flex;align-items:center;gap:10px}.config-changes-badge{padding:6px 14px;border-radius:var(--radius-full);background:var(--accent-subtle);border:1px solid rgba(255,77,77,.3);color:var(--accent);font-size:12px;font-weight:600}.config-status{font-size:13px;color:var(--muted)}.config-diff{margin:18px 22px 0;border:1px solid rgba(255,77,77,.25);border-radius:var(--radius-lg);background:var(--accent-subtle);overflow:hidden}.config-diff__summary{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;cursor:pointer;font-size:13px;font-weight:600;color:var(--accent);list-style:none}.config-diff__summary::-webkit-details-marker{display:none}.config-diff__chevron{width:16px;height:16px;transition:transform var(--duration-normal) var(--ease-out)}.config-diff__chevron svg{width:100%;height:100%}.config-diff[open] .config-diff__chevron{transform:rotate(180deg)}.config-diff__content{padding:0 18px 18px;display:grid;gap:10px}.config-diff__item{display:flex;align-items:baseline;gap:14px;padding:10px 14px;border-radius:var(--radius-md);background:var(--bg-elevated);font-size:12px;font-family:var(--mono)}:root[data-theme=light] .config-diff__item{background:#fff}.config-diff__path{font-weight:600;color:var(--text);flex-shrink:0}.config-diff__values{display:flex;align-items:baseline;gap:10px;min-width:0;flex-wrap:wrap}.config-diff__from{color:var(--danger);opacity:.85}.config-diff__arrow{color:var(--muted)}.config-diff__to{color:var(--ok)}.config-section-hero{display:flex;align-items:center;gap:16px;padding:16px 22px;border-bottom:1px solid var(--border);background:var(--bg-accent)}:root[data-theme=light] .config-section-hero{background:var(--bg-hover)}.config-section-hero__icon{width:30px;height:30px;color:var(--accent);display:flex;align-items:center;justify-content:center}.config-section-hero__icon svg{width:100%;height:100%;stroke:currentColor;fill:none}.config-section-hero__text{display:grid;gap:3px;min-width:0}.config-section-hero__title{font-size:16px;font-weight:600;letter-spacing:-.01em}.config-section-hero__desc{font-size:13px;color:var(--muted)}.config-subnav{display:flex;gap:8px;padding:12px 22px 14px;border-bottom:1px solid var(--border);background:var(--bg-accent);overflow-x:auto}:root[data-theme=light] .config-subnav{background:var(--bg-hover)}.config-subnav__item{border:1px solid transparent;border-radius:var(--radius-full);padding:7px 14px;font-size:12px;font-weight:600;color:var(--muted);background:var(--bg-elevated);cursor:pointer;transition:background var(--duration-fast) ease,color var(--duration-fast) ease,border-color var(--duration-fast) ease;white-space:nowrap}:root[data-theme=light] .config-subnav__item{background:#fff}.config-subnav__item:hover{color:var(--text);border-color:var(--border)}.config-subnav__item.active{color:var(--accent);border-color:#ff4d4d66;background:var(--accent-subtle)}.config-content{flex:1;overflow-y:auto;padding:22px}.config-raw-field textarea{min-height:500px;font-family:var(--mono);font-size:13px;line-height:1.55}.config-loading{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;padding:80px 24px;color:var(--muted)}.config-loading__spinner{width:40px;height:40px;border:3px solid var(--border);border-top-color:var(--accent);border-radius:var(--radius-full);animation:spin .75s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.config-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:18px;padding:80px 24px;text-align:center}.config-empty__icon{font-size:56px;opacity:.35}.config-empty__text{color:var(--muted);font-size:15px}.config-form--modern{display:grid;gap:26px}.config-section-card{border:1px solid var(--border);border-radius:var(--radius-lg);background:var(--bg-elevated);overflow:hidden;transition:border-color var(--duration-fast) ease}.config-section-card:hover{border-color:var(--border-strong)}:root[data-theme=light] .config-section-card{background:#fff}.config-section-card__header{display:flex;align-items:flex-start;gap:16px;padding:20px 22px;background:var(--bg-accent);border-bottom:1px solid var(--border)}:root[data-theme=light] .config-section-card__header{background:var(--bg-hover)}.config-section-card__icon{width:34px;height:34px;color:var(--accent);flex-shrink:0}.config-section-card__icon svg{width:100%;height:100%}.config-section-card__titles{flex:1;min-width:0}.config-section-card__title{margin:0;font-size:17px;font-weight:600;letter-spacing:-.01em}.config-section-card__desc{margin:5px 0 0;font-size:13px;color:var(--muted);line-height:1.45}.config-section-card__content{padding:22px}.cfg-fields{display:grid;gap:22px}.cfg-field{display:grid;gap:8px}.cfg-field--error{padding:14px;border-radius:var(--radius-md);background:var(--danger-subtle);border:1px solid rgba(239,68,68,.3)}.cfg-field__label{font-size:13px;font-weight:600;color:var(--text)}.cfg-field__help{font-size:12px;color:var(--muted);line-height:1.45}.cfg-field__error{font-size:12px;color:var(--danger)}.cfg-input-wrap{display:flex;gap:10px}.cfg-input{flex:1;padding:11px 14px;border:1px solid var(--border-strong);border-radius:var(--radius-md);background:var(--bg-accent);font-size:14px;outline:none;transition:border-color var(--duration-fast) ease,box-shadow var(--duration-fast) ease,background var(--duration-fast) ease}.cfg-input::placeholder{color:var(--muted);opacity:.7}.cfg-input:focus{border-color:var(--accent);box-shadow:var(--focus-ring);background:var(--bg-hover)}:root[data-theme=light] .cfg-input{background:#fff}:root[data-theme=light] .cfg-input:focus{background:#fff}.cfg-input--sm{padding:9px 12px;font-size:13px}.cfg-input__reset{padding:10px 14px;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg-elevated);color:var(--muted);font-size:14px;cursor:pointer;transition:background var(--duration-fast) ease,color var(--duration-fast) ease}.cfg-input__reset:hover:not(:disabled){background:var(--bg-hover);color:var(--text)}.cfg-input__reset:disabled{opacity:.5;cursor:not-allowed}.cfg-textarea{width:100%;padding:12px 14px;border:1px solid var(--border-strong);border-radius:var(--radius-md);background:var(--bg-accent);font-family:var(--mono);font-size:13px;line-height:1.55;resize:vertical;outline:none;transition:border-color var(--duration-fast) ease,box-shadow var(--duration-fast) ease}.cfg-textarea:focus{border-color:var(--accent);box-shadow:var(--focus-ring)}:root[data-theme=light] .cfg-textarea{background:#fff}.cfg-textarea--sm{padding:10px 12px;font-size:12px}.cfg-number{display:inline-flex;border:1px solid var(--border-strong);border-radius:var(--radius-md);overflow:hidden;background:var(--bg-accent)}:root[data-theme=light] .cfg-number{background:#fff}.cfg-number__btn{width:44px;border:none;background:var(--bg-elevated);color:var(--text);font-size:18px;font-weight:300;cursor:pointer;transition:background var(--duration-fast) ease}.cfg-number__btn:hover:not(:disabled){background:var(--bg-hover)}.cfg-number__btn:disabled{opacity:.4;cursor:not-allowed}:root[data-theme=light] .cfg-number__btn{background:var(--bg-hover)}:root[data-theme=light] .cfg-number__btn:hover:not(:disabled){background:var(--border)}.cfg-number__input{width:85px;padding:11px;border:none;border-left:1px solid var(--border);border-right:1px solid var(--border);background:transparent;font-size:14px;text-align:center;outline:none;-moz-appearance:textfield}.cfg-number__input::-webkit-outer-spin-button,.cfg-number__input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}.cfg-select{padding:11px 40px 11px 14px;border:1px solid var(--border-strong);border-radius:var(--radius-md);background-color:var(--bg-accent);background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center;font-size:14px;cursor:pointer;outline:none;appearance:none;transition:border-color var(--duration-fast) ease,box-shadow var(--duration-fast) ease}.cfg-select:focus{border-color:var(--accent);box-shadow:var(--focus-ring)}:root[data-theme=light] .cfg-select{background-color:#fff}.cfg-segmented{display:inline-flex;padding:4px;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg-accent)}:root[data-theme=light] .cfg-segmented{background:var(--bg-hover)}.cfg-segmented__btn{padding:9px 18px;border:none;border-radius:var(--radius-sm);background:transparent;color:var(--muted);font-size:13px;font-weight:500;cursor:pointer;transition:background var(--duration-fast) ease,color var(--duration-fast) ease,box-shadow var(--duration-fast) ease}.cfg-segmented__btn:hover:not(:disabled):not(.active){color:var(--text)}.cfg-segmented__btn.active{background:var(--accent);color:#fff;box-shadow:var(--shadow-sm)}.cfg-segmented__btn:disabled{opacity:.5;cursor:not-allowed}.cfg-toggle-row{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:16px 18px;border:1px solid var(--border);border-radius:var(--radius-lg);background:var(--bg-accent);cursor:pointer;transition:background var(--duration-fast) ease,border-color var(--duration-fast) ease}.cfg-toggle-row:hover:not(.disabled){background:var(--bg-hover);border-color:var(--border-strong)}.cfg-toggle-row.disabled{opacity:.55;cursor:not-allowed}:root[data-theme=light] .cfg-toggle-row{background:#fff}:root[data-theme=light] .cfg-toggle-row:hover:not(.disabled){background:var(--bg-hover)}.cfg-toggle-row__content{flex:1;min-width:0}.cfg-toggle-row__label{display:block;font-size:14px;font-weight:500;color:var(--text)}.cfg-toggle-row__help{display:block;margin-top:3px;font-size:12px;color:var(--muted);line-height:1.45}.cfg-toggle{position:relative;flex-shrink:0}.cfg-toggle input{position:absolute;opacity:0;width:0;height:0}.cfg-toggle__track{display:block;width:50px;height:28px;background:var(--bg-elevated);border:1px solid var(--border-strong);border-radius:var(--radius-full);position:relative;transition:background var(--duration-normal) ease,border-color var(--duration-normal) ease}:root[data-theme=light] .cfg-toggle__track{background:var(--border)}.cfg-toggle__track:after{content:"";position:absolute;top:3px;left:3px;width:20px;height:20px;background:var(--text);border-radius:var(--radius-full);box-shadow:var(--shadow-sm);transition:transform var(--duration-normal) var(--ease-out),background var(--duration-normal) ease}.cfg-toggle input:checked+.cfg-toggle__track{background:var(--ok-subtle);border-color:#22c55e66}.cfg-toggle input:checked+.cfg-toggle__track:after{transform:translate(22px);background:var(--ok)}.cfg-toggle input:focus+.cfg-toggle__track{box-shadow:var(--focus-ring)}.cfg-object{border:1px solid var(--border);border-radius:var(--radius-lg);background:var(--bg-accent);overflow:hidden}:root[data-theme=light] .cfg-object{background:#fff}.cfg-object__header{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;cursor:pointer;list-style:none;transition:background var(--duration-fast) ease}.cfg-object__header:hover{background:var(--bg-hover)}.cfg-object__header::-webkit-details-marker{display:none}.cfg-object__title{font-size:14px;font-weight:600;color:var(--text)}.cfg-object__chevron{width:18px;height:18px;color:var(--muted);transition:transform var(--duration-normal) var(--ease-out)}.cfg-object__chevron svg{width:100%;height:100%}.cfg-object[open] .cfg-object__chevron{transform:rotate(180deg)}.cfg-object__help{padding:0 18px 14px;font-size:12px;color:var(--muted);border-bottom:1px solid var(--border)}.cfg-object__content{padding:18px;display:grid;gap:18px}.cfg-array{border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden}.cfg-array__header{display:flex;align-items:center;gap:14px;padding:14px 18px;background:var(--bg-accent);border-bottom:1px solid var(--border)}:root[data-theme=light] .cfg-array__header{background:var(--bg-hover)}.cfg-array__label{flex:1;font-size:14px;font-weight:600;color:var(--text)}.cfg-array__count{font-size:12px;color:var(--muted);padding:4px 10px;background:var(--bg-elevated);border-radius:var(--radius-full)}:root[data-theme=light] .cfg-array__count{background:#fff}.cfg-array__add{display:inline-flex;align-items:center;gap:6px;padding:7px 14px;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg-elevated);color:var(--text);font-size:12px;font-weight:500;cursor:pointer;transition:background var(--duration-fast) ease}.cfg-array__add:hover:not(:disabled){background:var(--bg-hover)}.cfg-array__add:disabled{opacity:.5;cursor:not-allowed}.cfg-array__add-icon{width:14px;height:14px}.cfg-array__add-icon svg{width:100%;height:100%}.cfg-array__help{padding:12px 18px;font-size:12px;color:var(--muted);border-bottom:1px solid var(--border)}.cfg-array__empty{padding:36px 18px;text-align:center;color:var(--muted);font-size:13px}.cfg-array__items{display:grid;gap:1px;background:var(--border)}.cfg-array__item{background:var(--panel)}.cfg-array__item-header{display:flex;align-items:center;justify-content:space-between;padding:12px 18px;background:var(--bg-accent);border-bottom:1px solid var(--border)}:root[data-theme=light] .cfg-array__item-header{background:var(--bg-hover)}.cfg-array__item-index{font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.05em}.cfg-array__item-remove{width:30px;height:30px;display:flex;align-items:center;justify-content:center;border:none;border-radius:var(--radius-md);background:transparent;color:var(--muted);cursor:pointer;transition:background var(--duration-fast) ease,color var(--duration-fast) ease}.cfg-array__item-remove svg{width:16px;height:16px}.cfg-array__item-remove:hover:not(:disabled){background:var(--danger-subtle);color:var(--danger)}.cfg-array__item-remove:disabled{opacity:.4;cursor:not-allowed}.cfg-array__item-content{padding:18px}.cfg-map{border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden}.cfg-map__header{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:14px 18px;background:var(--bg-accent);border-bottom:1px solid var(--border)}:root[data-theme=light] .cfg-map__header{background:var(--bg-hover)}.cfg-map__label{font-size:13px;font-weight:600;color:var(--muted)}.cfg-map__add{display:inline-flex;align-items:center;gap:6px;padding:7px 14px;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--bg-elevated);color:var(--text);font-size:12px;font-weight:500;cursor:pointer;transition:background var(--duration-fast) ease}.cfg-map__add:hover:not(:disabled){background:var(--bg-hover)}.cfg-map__add-icon{width:14px;height:14px}.cfg-map__add-icon svg{width:100%;height:100%}.cfg-map__empty{padding:28px 18px;text-align:center;color:var(--muted);font-size:13px}.cfg-map__items{display:grid;gap:10px;padding:14px}.cfg-map__item{display:grid;grid-template-columns:150px 1fr auto;gap:10px;align-items:start}.cfg-map__item-key,.cfg-map__item-value{min-width:0}.cfg-map__item-remove{width:34px;height:34px;display:flex;align-items:center;justify-content:center;border:none;border-radius:var(--radius-md);background:transparent;color:var(--muted);cursor:pointer;transition:background var(--duration-fast) ease,color var(--duration-fast) ease}.cfg-map__item-remove svg{width:16px;height:16px}.cfg-map__item-remove:hover:not(:disabled){background:var(--danger-subtle);color:var(--danger)}.pill--sm{padding:5px 12px;font-size:11px}.pill--ok{border-color:#22c55e59;color:var(--ok)}.pill--danger{border-color:#ef444459;color:var(--danger)}@media(max-width:768px){.config-layout{grid-template-columns:1fr}.config-sidebar{border-right:none;border-bottom:1px solid var(--border)}.config-sidebar__header{padding:14px 16px}.config-nav{display:flex;flex-wrap:nowrap;gap:6px;padding:10px 14px;overflow-x:auto;-webkit-overflow-scrolling:touch}.config-nav__item{flex:0 0 auto;padding:9px 14px;white-space:nowrap}.config-nav__label{display:inline}.config-sidebar__footer{display:none}.config-actions{flex-wrap:wrap;padding:14px 16px}.config-actions__left,.config-actions__right{width:100%;justify-content:center}.config-section-hero{padding:14px 16px}.config-subnav{padding:10px 16px 12px}.config-content{padding:18px}.config-section-card__header{padding:16px 18px}.config-section-card__content{padding:18px}.cfg-toggle-row{padding:14px 16px}.cfg-map__item{grid-template-columns:1fr;gap:10px}.cfg-map__item-remove{justify-self:end}}@media(max-width:480px){.config-nav__icon{width:26px;height:26px;font-size:17px}.config-nav__label{display:none}.config-section-card__icon{width:30px;height:30px}.config-section-card__title{font-size:16px}.cfg-segmented{flex-wrap:wrap}.cfg-segmented__btn{flex:1 0 auto;min-width:70px}} diff --git a/dist/control-ui/assets/index-DQcOTEYz.js b/dist/control-ui/assets/index-DQcOTEYz.js deleted file mode 100644 index e897319f1..000000000 --- a/dist/control-ui/assets/index-DQcOTEYz.js +++ /dev/null @@ -1,3119 +0,0 @@ -(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))s(i);new MutationObserver(i=>{for(const a of i)if(a.type==="childList")for(const o of a.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&s(o)}).observe(document,{childList:!0,subtree:!0});function n(i){const a={};return i.integrity&&(a.integrity=i.integrity),i.referrerPolicy&&(a.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?a.credentials="include":i.crossOrigin==="anonymous"?a.credentials="omit":a.credentials="same-origin",a}function s(i){if(i.ep)return;i.ep=!0;const a=n(i);fetch(i.href,a)}})();const qt=globalThis,Ts=qt.ShadowRoot&&(qt.ShadyCSS===void 0||qt.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,Cs=Symbol(),Pi=new WeakMap;let qa=class{constructor(t,n,s){if(this._$cssResult$=!0,s!==Cs)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=n}get styleSheet(){let t=this.o;const n=this.t;if(Ts&&t===void 0){const s=n!==void 0&&n.length===1;s&&(t=Pi.get(n)),t===void 0&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),s&&Pi.set(n,t))}return t}toString(){return this.cssText}};const Or=e=>new qa(typeof e=="string"?e:e+"",void 0,Cs),Dr=(e,...t)=>{const n=e.length===1?e[0]:t.reduce((s,i,a)=>s+(o=>{if(o._$cssResult$===!0)return o.cssText;if(typeof o=="number")return o;throw Error("Value passed to 'css' function must be a 'css' function result: "+o+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+e[a+1],e[0]);return new qa(n,e,Cs)},Br=(e,t)=>{if(Ts)e.adoptedStyleSheets=t.map(n=>n instanceof CSSStyleSheet?n:n.styleSheet);else for(const n of t){const s=document.createElement("style"),i=qt.litNonce;i!==void 0&&s.setAttribute("nonce",i),s.textContent=n.cssText,e.appendChild(s)}},Ni=Ts?e=>e:e=>e instanceof CSSStyleSheet?(t=>{let n="";for(const s of t.cssRules)n+=s.cssText;return Or(n)})(e):e;const{is:Fr,defineProperty:Ur,getOwnPropertyDescriptor:Kr,getOwnPropertyNames:Hr,getOwnPropertySymbols:zr,getPrototypeOf:jr}=Object,nn=globalThis,Oi=nn.trustedTypes,qr=Oi?Oi.emptyScript:"",Vr=nn.reactiveElementPolyfillSupport,bt=(e,t)=>e,Gt={toAttribute(e,t){switch(t){case Boolean:e=e?qr:null;break;case Object:case Array:e=e==null?e:JSON.stringify(e)}return e},fromAttribute(e,t){let n=e;switch(t){case Boolean:n=e!==null;break;case Number:n=e===null?null:Number(e);break;case Object:case Array:try{n=JSON.parse(e)}catch{n=null}}return n}},Es=(e,t)=>!Fr(e,t),Di={attribute:!0,type:String,converter:Gt,reflect:!1,useDefault:!1,hasChanged:Es};Symbol.metadata??=Symbol("metadata"),nn.litPropertyMetadata??=new WeakMap;let Ye=class extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,n=Di){if(n.state&&(n.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(t)&&((n=Object.create(n)).wrapped=!0),this.elementProperties.set(t,n),!n.noAccessor){const s=Symbol(),i=this.getPropertyDescriptor(t,s,n);i!==void 0&&Ur(this.prototype,t,i)}}static getPropertyDescriptor(t,n,s){const{get:i,set:a}=Kr(this.prototype,t)??{get(){return this[n]},set(o){this[n]=o}};return{get:i,set(o){const c=i?.call(this);a?.call(this,o),this.requestUpdate(t,c,s)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)??Di}static _$Ei(){if(this.hasOwnProperty(bt("elementProperties")))return;const t=jr(this);t.finalize(),t.l!==void 0&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties)}static finalize(){if(this.hasOwnProperty(bt("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(bt("properties"))){const n=this.properties,s=[...Hr(n),...zr(n)];for(const i of s)this.createProperty(i,n[i])}const t=this[Symbol.metadata];if(t!==null){const n=litPropertyMetadata.get(t);if(n!==void 0)for(const[s,i]of n)this.elementProperties.set(s,i)}this._$Eh=new Map;for(const[n,s]of this.elementProperties){const i=this._$Eu(n,s);i!==void 0&&this._$Eh.set(i,n)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(t){const n=[];if(Array.isArray(t)){const s=new Set(t.flat(1/0).reverse());for(const i of s)n.unshift(Ni(i))}else t!==void 0&&n.push(Ni(t));return n}static _$Eu(t,n){const s=n.attribute;return s===!1?void 0:typeof s=="string"?s:typeof t=="string"?t.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(t=>this.enableUpdating=t),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(t=>t(this))}addController(t){(this._$EO??=new Set).add(t),this.renderRoot!==void 0&&this.isConnected&&t.hostConnected?.()}removeController(t){this._$EO?.delete(t)}_$E_(){const t=new Map,n=this.constructor.elementProperties;for(const s of n.keys())this.hasOwnProperty(s)&&(t.set(s,this[s]),delete this[s]);t.size>0&&(this._$Ep=t)}createRenderRoot(){const t=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return Br(t,this.constructor.elementStyles),t}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(t=>t.hostConnected?.())}enableUpdating(t){}disconnectedCallback(){this._$EO?.forEach(t=>t.hostDisconnected?.())}attributeChangedCallback(t,n,s){this._$AK(t,s)}_$ET(t,n){const s=this.constructor.elementProperties.get(t),i=this.constructor._$Eu(t,s);if(i!==void 0&&s.reflect===!0){const a=(s.converter?.toAttribute!==void 0?s.converter:Gt).toAttribute(n,s.type);this._$Em=t,a==null?this.removeAttribute(i):this.setAttribute(i,a),this._$Em=null}}_$AK(t,n){const s=this.constructor,i=s._$Eh.get(t);if(i!==void 0&&this._$Em!==i){const a=s.getPropertyOptions(i),o=typeof a.converter=="function"?{fromAttribute:a.converter}:a.converter?.fromAttribute!==void 0?a.converter:Gt;this._$Em=i;const c=o.fromAttribute(n,a.type);this[i]=c??this._$Ej?.get(i)??c,this._$Em=null}}requestUpdate(t,n,s,i=!1,a){if(t!==void 0){const o=this.constructor;if(i===!1&&(a=this[t]),s??=o.getPropertyOptions(t),!((s.hasChanged??Es)(a,n)||s.useDefault&&s.reflect&&a===this._$Ej?.get(t)&&!this.hasAttribute(o._$Eu(t,s))))return;this.C(t,n,s)}this.isUpdatePending===!1&&(this._$ES=this._$EP())}C(t,n,{useDefault:s,reflect:i,wrapped:a},o){s&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,o??n??this[t]),a!==!0||o!==void 0)||(this._$AL.has(t)||(this.hasUpdated||s||(n=void 0),this._$AL.set(t,n)),i===!0&&this._$Em!==t&&(this._$Eq??=new Set).add(t))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(n){Promise.reject(n)}const t=this.scheduleUpdate();return t!=null&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[i,a]of this._$Ep)this[i]=a;this._$Ep=void 0}const s=this.constructor.elementProperties;if(s.size>0)for(const[i,a]of s){const{wrapped:o}=a,c=this[i];o!==!0||this._$AL.has(i)||c===void 0||this.C(i,void 0,a,c)}}let t=!1;const n=this._$AL;try{t=this.shouldUpdate(n),t?(this.willUpdate(n),this._$EO?.forEach(s=>s.hostUpdate?.()),this.update(n)):this._$EM()}catch(s){throw t=!1,this._$EM(),s}t&&this._$AE(n)}willUpdate(t){}_$AE(t){this._$EO?.forEach(n=>n.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(t){return!0}update(t){this._$Eq&&=this._$Eq.forEach(n=>this._$ET(n,this[n])),this._$EM()}updated(t){}firstUpdated(t){}};Ye.elementStyles=[],Ye.shadowRootOptions={mode:"open"},Ye[bt("elementProperties")]=new Map,Ye[bt("finalized")]=new Map,Vr?.({ReactiveElement:Ye}),(nn.reactiveElementVersions??=[]).push("2.1.2");const Ls=globalThis,Bi=e=>e,Yt=Ls.trustedTypes,Fi=Yt?Yt.createPolicy("lit-html",{createHTML:e=>e}):void 0,Va="$lit$",xe=`lit$${Math.random().toFixed(9).slice(2)}$`,Wa="?"+xe,Wr=`<${Wa}>`,Oe=document,$t=()=>Oe.createComment(""),xt=e=>e===null||typeof e!="object"&&typeof e!="function",Ms=Array.isArray,Gr=e=>Ms(e)||typeof e?.[Symbol.iterator]=="function",On=`[ -\f\r]`,rt=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,Ui=/-->/g,Ki=/>/g,Le=RegExp(`>|${On}(?:([^\\s"'>=/]+)(${On}*=${On}*(?:[^ -\f\r"'\`<>=]|("|')|))|$)`,"g"),Hi=/'/g,zi=/"/g,Ga=/^(?:script|style|textarea|title)$/i,Yr=e=>(t,...n)=>({_$litType$:e,strings:t,values:n}),r=Yr(1),Se=Symbol.for("lit-noChange"),g=Symbol.for("lit-nothing"),ji=new WeakMap,Pe=Oe.createTreeWalker(Oe,129);function Ya(e,t){if(!Ms(e)||!e.hasOwnProperty("raw"))throw Error("invalid template strings array");return Fi!==void 0?Fi.createHTML(t):t}const Qr=(e,t)=>{const n=e.length-1,s=[];let i,a=t===2?"":t===3?"":"",o=rt;for(let c=0;c"?(o=i??rt,u=-1):d[1]===void 0?u=-2:(u=o.lastIndex-d[2].length,p=d[1],o=d[3]===void 0?Le:d[3]==='"'?zi:Hi):o===zi||o===Hi?o=Le:o===Ui||o===Ki?o=rt:(o=Le,i=void 0);const v=o===Le&&e[c+1].startsWith("/>")?" ":"";a+=o===rt?l+Wr:u>=0?(s.push(p),l.slice(0,u)+Va+l.slice(u)+xe+v):l+xe+(u===-2?c:v)}return[Ya(e,a+(e[n]||"")+(t===2?"":t===3?"":"")),s]};let ts=class Qa{constructor({strings:t,_$litType$:n},s){let i;this.parts=[];let a=0,o=0;const c=t.length-1,l=this.parts,[p,d]=Qr(t,n);if(this.el=Qa.createElement(p,s),Pe.currentNode=this.el.content,n===2||n===3){const u=this.el.content.firstChild;u.replaceWith(...u.childNodes)}for(;(i=Pe.nextNode())!==null&&l.length0){i.textContent=Yt?Yt.emptyScript:"";for(let v=0;v2||s[0]!==""||s[1]!==""?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=g}_$AI(t,n=this,s,i){const a=this.strings;let o=!1;if(a===void 0)t=Je(this,t,n,0),o=!xt(t)||t!==this._$AH&&t!==Se,o&&(this._$AH=t);else{const c=t;let l,p;for(t=a[0],l=0;l{const s=n?.renderBefore??t;let i=s._$litPart$;if(i===void 0){const a=n?.renderBefore??null;s._$litPart$=i=new sn(t.insertBefore($t(),a),a,void 0,n??{})}return i._$AI(e),i};const Is=globalThis;let Ze=class extends Ye{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){const t=super.createRenderRoot();return this.renderOptions.renderBefore??=t.firstChild,t}update(t){const n=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(t),this._$Do=il(n,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return Se}};Ze._$litElement$=!0,Ze.finalized=!0,Is.litElementHydrateSupport?.({LitElement:Ze});const al=Is.litElementPolyfillSupport;al?.({LitElement:Ze});(Is.litElementVersions??=[]).push("4.2.2");const Ja=e=>(t,n)=>{n!==void 0?n.addInitializer(()=>{customElements.define(e,t)}):customElements.define(e,t)};const ol={attribute:!0,type:String,converter:Gt,reflect:!1,hasChanged:Es},rl=(e=ol,t,n)=>{const{kind:s,metadata:i}=n;let a=globalThis.litPropertyMetadata.get(i);if(a===void 0&&globalThis.litPropertyMetadata.set(i,a=new Map),s==="setter"&&((e=Object.create(e)).wrapped=!0),a.set(n.name,e),s==="accessor"){const{name:o}=n;return{set(c){const l=t.get.call(this);t.set.call(this,c),this.requestUpdate(o,l,e,!0,c)},init(c){return c!==void 0&&this.C(o,void 0,e,c),c}}}if(s==="setter"){const{name:o}=n;return function(c){const l=this[o];t.call(this,c),this.requestUpdate(o,l,e,!0,c)}}throw Error("Unsupported decorator location: "+s)};function on(e){return(t,n)=>typeof n=="object"?rl(e,t,n):((s,i,a)=>{const o=i.hasOwnProperty(a);return i.constructor.createProperty(a,s),o?Object.getOwnPropertyDescriptor(i,a):void 0})(e,t,n)}function y(e){return on({...e,state:!0,attribute:!1})}const ll=50,cl=200,dl="Assistant";function qi(e,t){if(typeof e!="string")return;const n=e.trim();if(n)return n.length<=t?n:n.slice(0,t)}function ns(e){const t=qi(e?.name,ll)??dl,n=qi(e?.avatar??void 0,cl)??null;return{agentId:typeof e?.agentId=="string"&&e.agentId.trim()?e.agentId.trim():null,name:t,avatar:n}}function ul(){return ns(typeof window>"u"?{}:{name:window.__CLAWDBOT_ASSISTANT_NAME__,avatar:window.__CLAWDBOT_ASSISTANT_AVATAR__})}const Xa="clawdbot.control.settings.v1";function pl(){const t={gatewayUrl:`${location.protocol==="https:"?"wss":"ws"}://${location.host}`,token:"",sessionKey:"main",lastActiveSessionKey:"main",theme:"system",chatFocusMode:!1,chatShowThinking:!0,splitRatio:.6,navCollapsed:!1,navGroupsCollapsed:{}};try{const n=localStorage.getItem(Xa);if(!n)return t;const s=JSON.parse(n);return{gatewayUrl:typeof s.gatewayUrl=="string"&&s.gatewayUrl.trim()?s.gatewayUrl.trim():t.gatewayUrl,token:typeof s.token=="string"?s.token:t.token,sessionKey:typeof s.sessionKey=="string"&&s.sessionKey.trim()?s.sessionKey.trim():t.sessionKey,lastActiveSessionKey:typeof s.lastActiveSessionKey=="string"&&s.lastActiveSessionKey.trim()?s.lastActiveSessionKey.trim():typeof s.sessionKey=="string"&&s.sessionKey.trim()||t.lastActiveSessionKey,theme:s.theme==="light"||s.theme==="dark"||s.theme==="system"?s.theme:t.theme,chatFocusMode:typeof s.chatFocusMode=="boolean"?s.chatFocusMode:t.chatFocusMode,chatShowThinking:typeof s.chatShowThinking=="boolean"?s.chatShowThinking:t.chatShowThinking,splitRatio:typeof s.splitRatio=="number"&&s.splitRatio>=.4&&s.splitRatio<=.7?s.splitRatio:t.splitRatio,navCollapsed:typeof s.navCollapsed=="boolean"?s.navCollapsed:t.navCollapsed,navGroupsCollapsed:typeof s.navGroupsCollapsed=="object"&&s.navGroupsCollapsed!==null?s.navGroupsCollapsed:t.navGroupsCollapsed}}catch{return t}}function fl(e){localStorage.setItem(Xa,JSON.stringify(e))}function eo(e){const t=(e??"").trim();if(!t)return null;const n=t.split(":").filter(Boolean);if(n.length<3||n[0]!=="agent")return null;const s=n[1]?.trim(),i=n.slice(2).join(":");return!s||!i?null:{agentId:s,rest:i}}const hl=[{label:"Chat",tabs:["chat"]},{label:"Control",tabs:["overview","channels","instances","sessions","cron"]},{label:"Agent",tabs:["skills","nodes"]},{label:"Settings",tabs:["config","debug","logs"]}],to={overview:"/overview",channels:"/channels",instances:"/instances",sessions:"/sessions",cron:"/cron",skills:"/skills",nodes:"/nodes",chat:"/chat",config:"/config",debug:"/debug",logs:"/logs"},no=new Map(Object.entries(to).map(([e,t])=>[t,e]));function rn(e){if(!e)return"";let t=e.trim();return t.startsWith("/")||(t=`/${t}`),t==="/"?"":(t.endsWith("/")&&(t=t.slice(0,-1)),t)}function kt(e){if(!e)return"/";let t=e.trim();return t.startsWith("/")||(t=`/${t}`),t.length>1&&t.endsWith("/")&&(t=t.slice(0,-1)),t}function Rs(e,t=""){const n=rn(t),s=to[e];return n?`${n}${s}`:s}function so(e,t=""){const n=rn(t);let s=e||"/";n&&(s===n?s="/":s.startsWith(`${n}/`)&&(s=s.slice(n.length)));let i=kt(s).toLowerCase();return i.endsWith("/index.html")&&(i="/"),i==="/"?"chat":no.get(i)??null}function gl(e){let t=kt(e);if(t.endsWith("/index.html")&&(t=kt(t.slice(0,-11))),t==="/")return"";const n=t.split("/").filter(Boolean);if(n.length===0)return"";for(let s=0;s`,barChart:r``,link:r``,radio:r``,fileText:r``,zap:r``,monitor:r``,settings:r``,bug:r``,scrollText:r``,folder:r``,menu:r``,x:r``,check:r``,copy:r``,search:r``,brain:r``,book:r``,loader:r``,wrench:r``,fileCode:r``,edit:r``,penLine:r``,paperclip:r``,globe:r``,image:r``,smartphone:r``,plug:r``,circle:r``,puzzle:r``},bl=/<\s*\/?\s*(?:think(?:ing)?|thought|antthinking|final)\b/i,Dt=/<\s*\/?\s*final\b[^>]*>/gi,Vi=/<\s*(\/?)\s*(?:think(?:ing)?|thought|antthinking)\b[^>]*>/gi;function yl(e,t){return e.trimStart()}function wl(e,t){if(!e||!bl.test(e))return e;let n=e;Dt.test(n)?(Dt.lastIndex=0,n=n.replace(Dt,"")):Dt.lastIndex=0,Vi.lastIndex=0;let s="",i=0,a=!1;for(const o of n.matchAll(Vi)){const c=o.index??0,l=o[1]==="/";a?l&&(a=!1):(s+=n.slice(i,c),l||(a=!0)),i=c+o[0].length}return s+=n.slice(i),yl(s)}function At(e){return!e&&e!==0?"n/a":new Date(e).toLocaleString()}function O(e){if(!e&&e!==0)return"n/a";const t=Date.now()-e;if(t<0)return"just now";const n=Math.round(t/1e3);if(n<60)return`${n}s ago`;const s=Math.round(n/60);if(s<60)return`${s}m ago`;const i=Math.round(s/60);return i<48?`${i}h ago`:`${Math.round(i/24)}d ago`}function io(e){if(!e&&e!==0)return"n/a";if(e<1e3)return`${e}ms`;const t=Math.round(e/1e3);if(t<60)return`${t}s`;const n=Math.round(t/60);if(n<60)return`${n}m`;const s=Math.round(n/60);return s<48?`${s}h`:`${Math.round(s/24)}d`}function is(e){return!e||e.length===0?"none":e.filter(t=>!!(t&&t.trim())).join(", ")}function as(e,t=120){return e.length<=t?e:`${e.slice(0,Math.max(0,t-1))}…`}function ao(e,t){return e.length<=t?{text:e,truncated:!1,total:e.length}:{text:e.slice(0,Math.max(0,t)),truncated:!0,total:e.length}}function Qt(e,t){const n=Number(e);return Number.isFinite(n)?n:t}function Dn(e){return wl(e)}const $l=/^\[([^\]]+)\]\s*/,xl=["WebChat","WhatsApp","Telegram","Signal","Slack","Discord","iMessage","Teams","Matrix","Zalo","Zalo Personal","BlueBubbles"],Bn=new WeakMap,Fn=new WeakMap;function kl(e){return/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}Z\b/.test(e)||/\d{4}-\d{2}-\d{2} \d{2}:\d{2}\b/.test(e)?!0:xl.some(t=>e.startsWith(`${t} `))}function Un(e){const t=e.match($l);if(!t)return e;const n=t[1]??"";return kl(n)?e.slice(t[0].length):e}function os(e){const t=e,n=typeof t.role=="string"?t.role:"",s=t.content;if(typeof s=="string")return n==="assistant"?Dn(s):Un(s);if(Array.isArray(s)){const i=s.map(a=>{const o=a;return o.type==="text"&&typeof o.text=="string"?o.text:null}).filter(a=>typeof a=="string");if(i.length>0){const a=i.join(` -`);return n==="assistant"?Dn(a):Un(a)}}return typeof t.text=="string"?n==="assistant"?Dn(t.text):Un(t.text):null}function oo(e){if(!e||typeof e!="object")return os(e);const t=e;if(Bn.has(t))return Bn.get(t)??null;const n=os(e);return Bn.set(t,n),n}function Wi(e){const n=e.content,s=[];if(Array.isArray(n))for(const c of n){const l=c;if(l.type==="thinking"&&typeof l.thinking=="string"){const p=l.thinking.trim();p&&s.push(p)}}if(s.length>0)return s.join(` -`);const i=Sl(e);if(!i)return null;const o=[...i.matchAll(/<\s*think(?:ing)?\s*>([\s\S]*?)<\s*\/\s*think(?:ing)?\s*>/gi)].map(c=>(c[1]??"").trim()).filter(Boolean);return o.length>0?o.join(` -`):null}function Al(e){if(!e||typeof e!="object")return Wi(e);const t=e;if(Fn.has(t))return Fn.get(t)??null;const n=Wi(e);return Fn.set(t,n),n}function Sl(e){const t=e,n=t.content;if(typeof n=="string")return n;if(Array.isArray(n)){const s=n.map(i=>{const a=i;return a.type==="text"&&typeof a.text=="string"?a.text:null}).filter(i=>typeof i=="string");if(s.length>0)return s.join(` -`)}return typeof t.text=="string"?t.text:null}function _l(e){const t=e.trim();if(!t)return"";const n=t.split(/\r?\n/).map(s=>s.trim()).filter(Boolean).map(s=>`_${s}_`);return n.length?["_Reasoning:_",...n].join(` -`):""}function Gi(e){e[6]=e[6]&15|64,e[8]=e[8]&63|128;let t="";for(let n=0;n>>8&255,e[2]^=t>>>16&255,e[3]^=t>>>24&255,e}function Ps(e=globalThis.crypto){if(e&&typeof e.randomUUID=="function")return e.randomUUID();if(e&&typeof e.getRandomValues=="function"){const t=new Uint8Array(16);return e.getRandomValues(t),Gi(t)}return Gi(Tl())}async function Xe(e){if(!(!e.client||!e.connected)){e.chatLoading=!0,e.lastError=null;try{const t=await e.client.request("chat.history",{sessionKey:e.sessionKey,limit:200});e.chatMessages=Array.isArray(t.messages)?t.messages:[],e.chatThinkingLevel=t.thinkingLevel??null}catch(t){e.lastError=String(t)}finally{e.chatLoading=!1}}}async function Cl(e,t){if(!e.client||!e.connected)return!1;const n=t.trim();if(!n)return!1;const s=Date.now();e.chatMessages=[...e.chatMessages,{role:"user",content:[{type:"text",text:n}],timestamp:s}],e.chatSending=!0,e.lastError=null;const i=Ps();e.chatRunId=i,e.chatStream="",e.chatStreamStartedAt=s;try{return await e.client.request("chat.send",{sessionKey:e.sessionKey,message:n,deliver:!1,idempotencyKey:i}),!0}catch(a){const o=String(a);return e.chatRunId=null,e.chatStream=null,e.chatStreamStartedAt=null,e.lastError=o,e.chatMessages=[...e.chatMessages,{role:"assistant",content:[{type:"text",text:"Error: "+o}],timestamp:Date.now()}],!1}finally{e.chatSending=!1}}async function El(e){if(!e.client||!e.connected)return!1;const t=e.chatRunId;try{return await e.client.request("chat.abort",t?{sessionKey:e.sessionKey,runId:t}:{sessionKey:e.sessionKey}),!0}catch(n){return e.lastError=String(n),!1}}function Ll(e,t){if(!t||t.sessionKey!==e.sessionKey||t.runId&&e.chatRunId&&t.runId!==e.chatRunId)return null;if(t.state==="delta"){const n=os(t.message);if(typeof n=="string"){const s=e.chatStream??"";(!s||n.length>=s.length)&&(e.chatStream=n)}}else t.state==="final"||t.state==="aborted"?(e.chatStream=null,e.chatRunId=null,e.chatStreamStartedAt=null):t.state==="error"&&(e.chatStream=null,e.chatRunId=null,e.chatStreamStartedAt=null,e.lastError=t.errorMessage??"chat error");return t.state}async function st(e){if(!(!e.client||!e.connected)&&!e.sessionsLoading){e.sessionsLoading=!0,e.sessionsError=null;try{const t={includeGlobal:e.sessionsIncludeGlobal,includeUnknown:e.sessionsIncludeUnknown},n=Qt(e.sessionsFilterActive,0),s=Qt(e.sessionsFilterLimit,0);n>0&&(t.activeMinutes=n),s>0&&(t.limit=s);const i=await e.client.request("sessions.list",t);i&&(e.sessionsResult=i)}catch(t){e.sessionsError=String(t)}finally{e.sessionsLoading=!1}}}async function Ml(e,t,n){if(!e.client||!e.connected)return;const s={key:t};"label"in n&&(s.label=n.label),"thinkingLevel"in n&&(s.thinkingLevel=n.thinkingLevel),"verboseLevel"in n&&(s.verboseLevel=n.verboseLevel),"reasoningLevel"in n&&(s.reasoningLevel=n.reasoningLevel);try{await e.client.request("sessions.patch",s),await st(e)}catch(i){e.sessionsError=String(i)}}async function Il(e,t){if(!(!e.client||!e.connected||e.sessionsLoading||!window.confirm(`Delete session "${t}"? - -Deletes the session entry and archives its transcript.`))){e.sessionsLoading=!0,e.sessionsError=null;try{await e.client.request("sessions.delete",{key:t,deleteTranscript:!0}),await st(e)}catch(s){e.sessionsError=String(s)}finally{e.sessionsLoading=!1}}}const Yi=50,Rl=80,Pl=12e4;function Nl(e){if(!e||typeof e!="object")return null;const t=e;if(typeof t.text=="string")return t.text;const n=t.content;if(!Array.isArray(n))return null;const s=n.map(i=>{if(!i||typeof i!="object")return null;const a=i;return a.type==="text"&&typeof a.text=="string"?a.text:null}).filter(i=>!!i);return s.length===0?null:s.join(` -`)}function Qi(e){if(e==null)return null;if(typeof e=="number"||typeof e=="boolean")return String(e);const t=Nl(e);let n;if(typeof e=="string")n=e;else if(t)n=t;else try{n=JSON.stringify(e,null,2)}catch{n=String(e)}const s=ao(n,Pl);return s.truncated?`${s.text} - -… truncated (${s.total} chars, showing first ${s.text.length}).`:s.text}function Ol(e){const t=[];return t.push({type:"toolcall",name:e.name,arguments:e.args??{}}),e.output&&t.push({type:"toolresult",name:e.name,text:e.output}),{role:"assistant",toolCallId:e.toolCallId,runId:e.runId,content:t,timestamp:e.startedAt}}function Dl(e){if(e.toolStreamOrder.length<=Yi)return;const t=e.toolStreamOrder.length-Yi,n=e.toolStreamOrder.splice(0,t);for(const s of n)e.toolStreamById.delete(s)}function Bl(e){e.chatToolMessages=e.toolStreamOrder.map(t=>e.toolStreamById.get(t)?.message).filter(t=>!!t)}function rs(e){e.toolStreamSyncTimer!=null&&(clearTimeout(e.toolStreamSyncTimer),e.toolStreamSyncTimer=null),Bl(e)}function Fl(e,t=!1){if(t){rs(e);return}e.toolStreamSyncTimer==null&&(e.toolStreamSyncTimer=window.setTimeout(()=>rs(e),Rl))}function Ns(e){e.toolStreamById.clear(),e.toolStreamOrder=[],e.chatToolMessages=[],rs(e)}const Ul=5e3;function Kl(e,t){const n=t.data??{},s=typeof n.phase=="string"?n.phase:"";e.compactionClearTimer!=null&&(window.clearTimeout(e.compactionClearTimer),e.compactionClearTimer=null),s==="start"?e.compactionStatus={active:!0,startedAt:Date.now(),completedAt:null}:s==="end"&&(e.compactionStatus={active:!1,startedAt:e.compactionStatus?.startedAt??null,completedAt:Date.now()},e.compactionClearTimer=window.setTimeout(()=>{e.compactionStatus=null,e.compactionClearTimer=null},Ul))}function Hl(e,t){if(!t)return;if(t.stream==="compaction"){Kl(e,t);return}if(t.stream!=="tool")return;const n=typeof t.sessionKey=="string"?t.sessionKey:void 0;if(n&&n!==e.sessionKey||!n&&e.chatRunId&&t.runId!==e.chatRunId||e.chatRunId&&t.runId!==e.chatRunId||!e.chatRunId)return;const s=t.data??{},i=typeof s.toolCallId=="string"?s.toolCallId:"";if(!i)return;const a=typeof s.name=="string"?s.name:"tool",o=typeof s.phase=="string"?s.phase:"",c=o==="start"?s.args:void 0,l=o==="update"?Qi(s.partialResult):o==="result"?Qi(s.result):void 0,p=Date.now();let d=e.toolStreamById.get(i);d?(d.name=a,c!==void 0&&(d.args=c),l!==void 0&&(d.output=l),d.updatedAt=p):(d={toolCallId:i,runId:t.runId,sessionKey:n,name:a,args:c,output:l,startedAt:typeof t.ts=="number"?t.ts:p,updatedAt:p,message:{}},e.toolStreamById.set(i,d),e.toolStreamOrder.push(i)),d.message=Ol(d),Dl(e),Fl(e,o==="result")}function ln(e,t=!1){e.chatScrollFrame&&cancelAnimationFrame(e.chatScrollFrame),e.chatScrollTimeout!=null&&(clearTimeout(e.chatScrollTimeout),e.chatScrollTimeout=null);const n=()=>{const s=e.querySelector(".chat-thread");if(s){const i=getComputedStyle(s).overflowY;if(i==="auto"||i==="scroll"||s.scrollHeight-s.clientHeight>1)return s}return document.scrollingElement??document.documentElement};e.updateComplete.then(()=>{e.chatScrollFrame=requestAnimationFrame(()=>{e.chatScrollFrame=null;const s=n();if(!s)return;const i=s.scrollHeight-s.scrollTop-s.clientHeight;if(!(t||e.chatUserNearBottom||i<200))return;t&&(e.chatHasAutoScrolled=!0),s.scrollTop=s.scrollHeight,e.chatUserNearBottom=!0;const o=t?150:120;e.chatScrollTimeout=window.setTimeout(()=>{e.chatScrollTimeout=null;const c=n();if(!c)return;const l=c.scrollHeight-c.scrollTop-c.clientHeight;(t||e.chatUserNearBottom||l<200)&&(c.scrollTop=c.scrollHeight,e.chatUserNearBottom=!0)},o)})})}function ro(e,t=!1){e.logsScrollFrame&&cancelAnimationFrame(e.logsScrollFrame),e.updateComplete.then(()=>{e.logsScrollFrame=requestAnimationFrame(()=>{e.logsScrollFrame=null;const n=e.querySelector(".log-stream");if(!n)return;const s=n.scrollHeight-n.scrollTop-n.clientHeight;(t||s<80)&&(n.scrollTop=n.scrollHeight)})})}function zl(e,t){const n=t.currentTarget;if(!n)return;const s=n.scrollHeight-n.scrollTop-n.clientHeight;e.chatUserNearBottom=s<200}function jl(e,t){const n=t.currentTarget;if(!n)return;const s=n.scrollHeight-n.scrollTop-n.clientHeight;e.logsAtBottom=s<80}function ql(e){e.chatHasAutoScrolled=!1,e.chatUserNearBottom=!0}function Vl(e,t){if(e.length===0)return;const n=new Blob([`${e.join(` -`)} -`],{type:"text/plain"}),s=URL.createObjectURL(n),i=document.createElement("a"),a=new Date().toISOString().slice(0,19).replace(/[:T]/g,"-");i.href=s,i.download=`clawdbot-logs-${t}-${a}.log`,i.click(),URL.revokeObjectURL(s)}function Wl(e){if(typeof ResizeObserver>"u")return;const t=e.querySelector(".topbar");if(!t)return;const n=()=>{const{height:s}=t.getBoundingClientRect();e.style.setProperty("--topbar-height",`${s}px`)};n(),e.topbarObserver=new ResizeObserver(()=>n()),e.topbarObserver.observe(t)}function De(e){return typeof structuredClone=="function"?structuredClone(e):JSON.parse(JSON.stringify(e))}function et(e){return`${JSON.stringify(e,null,2).trimEnd()} -`}function lo(e,t,n){if(t.length===0)return;let s=e;for(let a=0;a0&&(n.timeoutSeconds=s),n}async function ec(e){if(!(!e.client||!e.connected||e.cronBusy)){e.cronBusy=!0,e.cronError=null;try{const t=Jl(e.cronForm),n=Xl(e.cronForm),s=e.cronForm.agentId.trim(),i={name:e.cronForm.name.trim(),description:e.cronForm.description.trim()||void 0,agentId:s||void 0,enabled:e.cronForm.enabled,schedule:t,sessionTarget:e.cronForm.sessionTarget,wakeMode:e.cronForm.wakeMode,payload:n,isolation:e.cronForm.postToMainPrefix.trim()&&e.cronForm.sessionTarget==="isolated"?{postToMainPrefix:e.cronForm.postToMainPrefix.trim()}:void 0};if(!i.name)throw new Error("Name required.");await e.client.request("cron.add",i),e.cronForm={...e.cronForm,name:"",description:"",payloadText:""},await cn(e),await Tt(e)}catch(t){e.cronError=String(t)}finally{e.cronBusy=!1}}}async function tc(e,t,n){if(!(!e.client||!e.connected||e.cronBusy)){e.cronBusy=!0,e.cronError=null;try{await e.client.request("cron.update",{id:t.id,patch:{enabled:n}}),await cn(e),await Tt(e)}catch(s){e.cronError=String(s)}finally{e.cronBusy=!1}}}async function nc(e,t){if(!(!e.client||!e.connected||e.cronBusy)){e.cronBusy=!0,e.cronError=null;try{await e.client.request("cron.run",{id:t.id,mode:"force"}),await po(e,t.id)}catch(n){e.cronError=String(n)}finally{e.cronBusy=!1}}}async function sc(e,t){if(!(!e.client||!e.connected||e.cronBusy)){e.cronBusy=!0,e.cronError=null;try{await e.client.request("cron.remove",{id:t.id}),e.cronRunsJobId===t.id&&(e.cronRunsJobId=null,e.cronRuns=[]),await cn(e),await Tt(e)}catch(n){e.cronError=String(n)}finally{e.cronBusy=!1}}}async function po(e,t){if(!(!e.client||!e.connected))try{const n=await e.client.request("cron.runs",{id:t,limit:50});e.cronRunsJobId=t,e.cronRuns=Array.isArray(n.entries)?n.entries:[]}catch(n){e.cronError=String(n)}}async function oe(e,t){if(!(!e.client||!e.connected)&&!e.channelsLoading){e.channelsLoading=!0,e.channelsError=null;try{const n=await e.client.request("channels.status",{probe:t,timeoutMs:8e3});e.channelsSnapshot=n,e.channelsLastSuccess=Date.now()}catch(n){e.channelsError=String(n)}finally{e.channelsLoading=!1}}}async function ic(e,t){if(!(!e.client||!e.connected||e.whatsappBusy)){e.whatsappBusy=!0;try{const n=await e.client.request("web.login.start",{force:t,timeoutMs:3e4});e.whatsappLoginMessage=n.message??null,e.whatsappLoginQrDataUrl=n.qrDataUrl??null,e.whatsappLoginConnected=null}catch(n){e.whatsappLoginMessage=String(n),e.whatsappLoginQrDataUrl=null,e.whatsappLoginConnected=null}finally{e.whatsappBusy=!1}}}async function ac(e){if(!(!e.client||!e.connected||e.whatsappBusy)){e.whatsappBusy=!0;try{const t=await e.client.request("web.login.wait",{timeoutMs:12e4});e.whatsappLoginMessage=t.message??null,e.whatsappLoginConnected=t.connected??null,t.connected&&(e.whatsappLoginQrDataUrl=null)}catch(t){e.whatsappLoginMessage=String(t),e.whatsappLoginConnected=null}finally{e.whatsappBusy=!1}}}async function oc(e){if(!(!e.client||!e.connected||e.whatsappBusy)){e.whatsappBusy=!0;try{await e.client.request("channels.logout",{channel:"whatsapp"}),e.whatsappLoginMessage="Logged out.",e.whatsappLoginQrDataUrl=null,e.whatsappLoginConnected=null}catch(t){e.whatsappLoginMessage=String(t)}finally{e.whatsappBusy=!1}}}async function dn(e){if(!(!e.client||!e.connected)&&!e.debugLoading){e.debugLoading=!0;try{const[t,n,s,i]=await Promise.all([e.client.request("status",{}),e.client.request("health",{}),e.client.request("models.list",{}),e.client.request("last-heartbeat",{})]);e.debugStatus=t,e.debugHealth=n;const a=s;e.debugModels=Array.isArray(a?.models)?a?.models:[],e.debugHeartbeat=i}catch(t){e.debugCallError=String(t)}finally{e.debugLoading=!1}}}async function rc(e){if(!(!e.client||!e.connected)){e.debugCallError=null,e.debugCallResult=null;try{const t=e.debugCallParams.trim()?JSON.parse(e.debugCallParams):{},n=await e.client.request(e.debugCallMethod.trim(),t);e.debugCallResult=JSON.stringify(n,null,2)}catch(t){e.debugCallError=String(t)}}}const lc=2e3,cc=new Set(["trace","debug","info","warn","error","fatal"]);function dc(e){if(typeof e!="string")return null;const t=e.trim();if(!t.startsWith("{")||!t.endsWith("}"))return null;try{const n=JSON.parse(t);return!n||typeof n!="object"?null:n}catch{return null}}function uc(e){if(typeof e!="string")return null;const t=e.toLowerCase();return cc.has(t)?t:null}function pc(e){if(!e.trim())return{raw:e,message:e};try{const t=JSON.parse(e),n=t&&typeof t._meta=="object"&&t._meta!==null?t._meta:null,s=typeof t.time=="string"?t.time:typeof n?.date=="string"?n?.date:null,i=uc(n?.logLevelName??n?.level),a=typeof t[0]=="string"?t[0]:typeof n?.name=="string"?n?.name:null,o=dc(a);let c=null;o&&(typeof o.subsystem=="string"?c=o.subsystem:typeof o.module=="string"&&(c=o.module)),!c&&a&&a.length<120&&(c=a);let l=null;return typeof t[1]=="string"?l=t[1]:!o&&typeof t[0]=="string"?l=t[0]:typeof t.message=="string"&&(l=t.message),{raw:e,time:s,level:i,subsystem:c,message:l??e,meta:n??void 0}}catch{return{raw:e,message:e}}}async function Os(e,t){if(!(!e.client||!e.connected)&&!(e.logsLoading&&!t?.quiet)){t?.quiet||(e.logsLoading=!0),e.logsError=null;try{const s=await e.client.request("logs.tail",{cursor:t?.reset?void 0:e.logsCursor??void 0,limit:e.logsLimit,maxBytes:e.logsMaxBytes}),a=(Array.isArray(s.lines)?s.lines.filter(c=>typeof c=="string"):[]).map(pc),o=!!(t?.reset||s.reset||e.logsCursor==null);e.logsEntries=o?a:[...e.logsEntries,...a].slice(-lc),typeof s.cursor=="number"&&(e.logsCursor=s.cursor),typeof s.file=="string"&&(e.logsFile=s.file),e.logsTruncated=!!s.truncated,e.logsLastFetchAt=Date.now()}catch(n){e.logsError=String(n)}finally{t?.quiet||(e.logsLoading=!1)}}}const fo={p:0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffedn,n:0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3edn,h:8n,a:0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffecn,d:0x52036cee2b6ffe738cc740797779e89800700a4d4141d8ab75eb4dca135978a3n,Gx:0x216936d3cd6e53fec0a4e231fdd6dc5c692cc7609525a7b2c9562d608f25d51an,Gy:0x6666666666666666666666666666666666666666666666666666666666666658n},{p:V,n:Vt,Gx:Ji,Gy:Xi,a:Kn,d:Hn,h:fc}=fo,Be=32,Ds=64,hc=(...e)=>{"captureStackTrace"in Error&&typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(...e)},H=(e="")=>{const t=new Error(e);throw hc(t,H),t},gc=e=>typeof e=="bigint",vc=e=>typeof e=="string",mc=e=>e instanceof Uint8Array||ArrayBuffer.isView(e)&&e.constructor.name==="Uint8Array",_e=(e,t,n="")=>{const s=mc(e),i=e?.length,a=t!==void 0;if(!s||a&&i!==t){const o=n&&`"${n}" `,c=a?` of length ${t}`:"",l=s?`length=${i}`:`type=${typeof e}`;H(o+"expected Uint8Array"+c+", got "+l)}return e},un=e=>new Uint8Array(e),ho=e=>Uint8Array.from(e),go=(e,t)=>e.toString(16).padStart(t,"0"),vo=e=>Array.from(_e(e)).map(t=>go(t,2)).join(""),ve={_0:48,_9:57,A:65,F:70,a:97,f:102},ea=e=>{if(e>=ve._0&&e<=ve._9)return e-ve._0;if(e>=ve.A&&e<=ve.F)return e-(ve.A-10);if(e>=ve.a&&e<=ve.f)return e-(ve.a-10)},mo=e=>{const t="hex invalid";if(!vc(e))return H(t);const n=e.length,s=n/2;if(n%2)return H(t);const i=un(s);for(let a=0,o=0;aglobalThis?.crypto,bc=()=>bo()?.subtle??H("crypto.subtle must be defined, consider polyfill"),St=(...e)=>{const t=un(e.reduce((s,i)=>s+_e(i).length,0));let n=0;return e.forEach(s=>{t.set(s,n),n+=s.length}),t},yc=(e=Be)=>bo().getRandomValues(un(e)),Zt=BigInt,Re=(e,t,n,s="bad number: out of range")=>gc(e)&&t<=e&&e{const n=e%t;return n>=0n?n:t+n},yo=e=>A(e,Vt),wc=(e,t)=>{(e===0n||t<=0n)&&H("no inverse n="+e+" mod="+t);let n=A(e,t),s=t,i=0n,a=1n;for(;n!==0n;){const o=s/n,c=s%n,l=i-a*o;s=n,n=c,i=a,a=l}return s===1n?A(i,t):H("no inverse")},$c=e=>{const t=ko[e];return typeof t!="function"&&H("hashes."+e+" not set"),t},zn=e=>e instanceof ee?e:H("Point expected"),cs=2n**256n;class ee{static BASE;static ZERO;X;Y;Z;T;constructor(t,n,s,i){const a=cs;this.X=Re(t,0n,a),this.Y=Re(n,0n,a),this.Z=Re(s,1n,a),this.T=Re(i,0n,a),Object.freeze(this)}static CURVE(){return fo}static fromAffine(t){return new ee(t.x,t.y,1n,A(t.x*t.y))}static fromBytes(t,n=!1){const s=Hn,i=ho(_e(t,Be)),a=t[31];i[31]=a&-129;const o=$o(i);Re(o,0n,n?cs:V);const l=A(o*o),p=A(l-1n),d=A(s*l+1n);let{isValid:u,value:h}=kc(p,d);u||H("bad point: y not sqrt");const v=(h&1n)===1n,w=(a&128)!==0;return!n&&h===0n&&w&&H("bad point: x==0, isLastByteOdd"),w!==v&&(h=A(-h)),new ee(h,o,1n,A(h*o))}static fromHex(t,n){return ee.fromBytes(mo(t),n)}get x(){return this.toAffine().x}get y(){return this.toAffine().y}assertValidity(){const t=Kn,n=Hn,s=this;if(s.is0())return H("bad point: ZERO");const{X:i,Y:a,Z:o,T:c}=s,l=A(i*i),p=A(a*a),d=A(o*o),u=A(d*d),h=A(l*t),v=A(d*A(h+p)),w=A(u+A(n*A(l*p)));if(v!==w)return H("bad point: equation left != right (1)");const $=A(i*a),k=A(o*c);return $!==k?H("bad point: equation left != right (2)"):this}equals(t){const{X:n,Y:s,Z:i}=this,{X:a,Y:o,Z:c}=zn(t),l=A(n*c),p=A(a*i),d=A(s*c),u=A(o*i);return l===p&&d===u}is0(){return this.equals(Qe)}negate(){return new ee(A(-this.X),this.Y,this.Z,A(-this.T))}double(){const{X:t,Y:n,Z:s}=this,i=Kn,a=A(t*t),o=A(n*n),c=A(2n*A(s*s)),l=A(i*a),p=t+n,d=A(A(p*p)-a-o),u=l+o,h=u-c,v=l-o,w=A(d*h),$=A(u*v),k=A(d*v),T=A(h*u);return new ee(w,$,T,k)}add(t){const{X:n,Y:s,Z:i,T:a}=this,{X:o,Y:c,Z:l,T:p}=zn(t),d=Kn,u=Hn,h=A(n*o),v=A(s*c),w=A(a*u*p),$=A(i*l),k=A((n+s)*(o+c)-h-v),T=A($-w),M=A($+w),P=A(v-d*h),L=A(k*T),C=A(M*P),E=A(k*P),pe=A(T*M);return new ee(L,C,pe,E)}subtract(t){return this.add(zn(t).negate())}multiply(t,n=!0){if(!n&&(t===0n||this.is0()))return Qe;if(Re(t,1n,Vt),t===1n)return this;if(this.equals(Fe))return Pc(t).p;let s=Qe,i=Fe;for(let a=this;t>0n;a=a.double(),t>>=1n)t&1n?s=s.add(a):n&&(i=i.add(a));return s}multiplyUnsafe(t){return this.multiply(t,!1)}toAffine(){const{X:t,Y:n,Z:s}=this;if(this.equals(Qe))return{x:0n,y:1n};const i=wc(s,V);A(s*i)!==1n&&H("invalid inverse");const a=A(t*i),o=A(n*i);return{x:a,y:o}}toBytes(){const{x:t,y:n}=this.assertValidity().toAffine(),s=wo(n);return s[31]|=t&1n?128:0,s}toHex(){return vo(this.toBytes())}clearCofactor(){return this.multiply(Zt(fc),!1)}isSmallOrder(){return this.clearCofactor().is0()}isTorsionFree(){let t=this.multiply(Vt/2n,!1).double();return Vt%2n&&(t=t.add(this)),t.is0()}}const Fe=new ee(Ji,Xi,1n,A(Ji*Xi)),Qe=new ee(0n,1n,1n,0n);ee.BASE=Fe;ee.ZERO=Qe;const wo=e=>mo(go(Re(e,0n,cs),Ds)).reverse(),$o=e=>Zt("0x"+vo(ho(_e(e)).reverse())),ce=(e,t)=>{let n=e;for(;t-- >0n;)n*=n,n%=V;return n},xc=e=>{const n=e*e%V*e%V,s=ce(n,2n)*n%V,i=ce(s,1n)*e%V,a=ce(i,5n)*i%V,o=ce(a,10n)*a%V,c=ce(o,20n)*o%V,l=ce(c,40n)*c%V,p=ce(l,80n)*l%V,d=ce(p,80n)*l%V,u=ce(d,10n)*a%V;return{pow_p_5_8:ce(u,2n)*e%V,b2:n}},ta=0x2b8324804fc1df0b2b4d00993dfbd7a72f431806ad2fe478c4ee1b274a0ea0b0n,kc=(e,t)=>{const n=A(t*t*t),s=A(n*n*t),i=xc(e*s).pow_p_5_8;let a=A(e*n*i);const o=A(t*a*a),c=a,l=A(a*ta),p=o===e,d=o===A(-e),u=o===A(-e*ta);return p&&(a=c),(d||u)&&(a=l),(A(a)&1n)===1n&&(a=A(-a)),{isValid:p||d,value:a}},ds=e=>yo($o(e)),Bs=(...e)=>ko.sha512Async(St(...e)),Ac=(...e)=>$c("sha512")(St(...e)),xo=e=>{const t=e.slice(0,Be);t[0]&=248,t[31]&=127,t[31]|=64;const n=e.slice(Be,Ds),s=ds(t),i=Fe.multiply(s),a=i.toBytes();return{head:t,prefix:n,scalar:s,point:i,pointBytes:a}},Fs=e=>Bs(_e(e,Be)).then(xo),Sc=e=>xo(Ac(_e(e,Be))),_c=e=>Fs(e).then(t=>t.pointBytes),Tc=e=>Bs(e.hashable).then(e.finish),Cc=(e,t,n)=>{const{pointBytes:s,scalar:i}=e,a=ds(t),o=Fe.multiply(a).toBytes();return{hashable:St(o,s,n),finish:p=>{const d=yo(a+ds(p)*i);return _e(St(o,wo(d)),Ds)}}},Ec=async(e,t)=>{const n=_e(e),s=await Fs(t),i=await Bs(s.prefix,n);return Tc(Cc(s,i,n))},ko={sha512Async:async e=>{const t=bc(),n=St(e);return un(await t.digest("SHA-512",n.buffer))},sha512:void 0},Lc=(e=yc(Be))=>e,Mc={getExtendedPublicKeyAsync:Fs,getExtendedPublicKey:Sc,randomSecretKey:Lc},Jt=8,Ic=256,Ao=Math.ceil(Ic/Jt)+1,us=2**(Jt-1),Rc=()=>{const e=[];let t=Fe,n=t;for(let s=0;s{const n=t.negate();return e?n:t},Pc=e=>{const t=na||(na=Rc());let n=Qe,s=Fe;const i=2**Jt,a=i,o=Zt(i-1),c=Zt(Jt);for(let l=0;l>=c,p>us&&(p-=a,e+=1n);const d=l*us,u=d,h=d+Math.abs(p)-1,v=l%2!==0,w=p<0;p===0?s=s.add(sa(v,t[u])):n=n.add(sa(w,t[h]))}return e!==0n&&H("invalid wnaf"),{p:n,f:s}},jn="clawdbot-device-identity-v1";function ps(e){let t="";for(const n of e)t+=String.fromCharCode(n);return btoa(t).replaceAll("+","-").replaceAll("/","_").replace(/=+$/g,"")}function So(e){const t=e.replaceAll("-","+").replaceAll("_","/"),n=t+"=".repeat((4-t.length%4)%4),s=atob(n),i=new Uint8Array(s.length);for(let a=0;at.toString(16).padStart(2,"0")).join("")}async function _o(e){const t=await crypto.subtle.digest("SHA-256",e);return Nc(new Uint8Array(t))}async function Oc(){const e=Mc.randomSecretKey(),t=await _c(e);return{deviceId:await _o(t),publicKey:ps(t),privateKey:ps(e)}}async function Us(){try{const n=localStorage.getItem(jn);if(n){const s=JSON.parse(n);if(s?.version===1&&typeof s.deviceId=="string"&&typeof s.publicKey=="string"&&typeof s.privateKey=="string"){const i=await _o(So(s.publicKey));if(i!==s.deviceId){const a={...s,deviceId:i};return localStorage.setItem(jn,JSON.stringify(a)),{deviceId:i,publicKey:s.publicKey,privateKey:s.privateKey}}return{deviceId:s.deviceId,publicKey:s.publicKey,privateKey:s.privateKey}}}}catch{}const e=await Oc(),t={version:1,deviceId:e.deviceId,publicKey:e.publicKey,privateKey:e.privateKey,createdAtMs:Date.now()};return localStorage.setItem(jn,JSON.stringify(t)),e}async function Dc(e,t){const n=So(e),s=new TextEncoder().encode(t),i=await Ec(s,n);return ps(i)}const To="clawdbot.device.auth.v1";function Ks(e){return e.trim()}function Bc(e){if(!Array.isArray(e))return[];const t=new Set;for(const n of e){const s=n.trim();s&&t.add(s)}return[...t].sort()}function Hs(){try{const e=window.localStorage.getItem(To);if(!e)return null;const t=JSON.parse(e);return!t||t.version!==1||!t.deviceId||typeof t.deviceId!="string"||!t.tokens||typeof t.tokens!="object"?null:t}catch{return null}}function Co(e){try{window.localStorage.setItem(To,JSON.stringify(e))}catch{}}function Fc(e){const t=Hs();if(!t||t.deviceId!==e.deviceId)return null;const n=Ks(e.role),s=t.tokens[n];return!s||typeof s.token!="string"?null:s}function Eo(e){const t=Ks(e.role),n={version:1,deviceId:e.deviceId,tokens:{}},s=Hs();s&&s.deviceId===e.deviceId&&(n.tokens={...s.tokens});const i={token:e.token,role:t,scopes:Bc(e.scopes),updatedAtMs:Date.now()};return n.tokens[t]=i,Co(n),i}function Lo(e){const t=Hs();if(!t||t.deviceId!==e.deviceId)return;const n=Ks(e.role);if(!t.tokens[n])return;const s={...t,tokens:{...t.tokens}};delete s.tokens[n],Co(s)}async function Te(e,t){if(!(!e.client||!e.connected)&&!e.devicesLoading){e.devicesLoading=!0,t?.quiet||(e.devicesError=null);try{const n=await e.client.request("device.pair.list",{});e.devicesList={pending:Array.isArray(n?.pending)?n.pending:[],paired:Array.isArray(n?.paired)?n.paired:[]}}catch(n){t?.quiet||(e.devicesError=String(n))}finally{e.devicesLoading=!1}}}async function Uc(e,t){if(!(!e.client||!e.connected))try{await e.client.request("device.pair.approve",{requestId:t}),await Te(e)}catch(n){e.devicesError=String(n)}}async function Kc(e,t){if(!(!e.client||!e.connected||!window.confirm("Reject this device pairing request?")))try{await e.client.request("device.pair.reject",{requestId:t}),await Te(e)}catch(s){e.devicesError=String(s)}}async function Hc(e,t){if(!(!e.client||!e.connected))try{const n=await e.client.request("device.token.rotate",t);if(n?.token){const s=await Us(),i=n.role??t.role;(n.deviceId===s.deviceId||t.deviceId===s.deviceId)&&Eo({deviceId:s.deviceId,role:i,token:n.token,scopes:n.scopes??t.scopes??[]}),window.prompt("New device token (copy and store securely):",n.token)}await Te(e)}catch(n){e.devicesError=String(n)}}async function zc(e,t){if(!(!e.client||!e.connected||!window.confirm(`Revoke token for ${t.deviceId} (${t.role})?`)))try{await e.client.request("device.token.revoke",t);const s=await Us();t.deviceId===s.deviceId&&Lo({deviceId:s.deviceId,role:t.role}),await Te(e)}catch(s){e.devicesError=String(s)}}async function pn(e,t){if(!(!e.client||!e.connected)&&!e.nodesLoading){e.nodesLoading=!0,t?.quiet||(e.lastError=null);try{const n=await e.client.request("node.list",{});e.nodes=Array.isArray(n.nodes)?n.nodes:[]}catch(n){t?.quiet||(e.lastError=String(n))}finally{e.nodesLoading=!1}}}function jc(e){if(!e||e.kind==="gateway")return{method:"exec.approvals.get",params:{}};const t=e.nodeId.trim();return t?{method:"exec.approvals.node.get",params:{nodeId:t}}:null}function qc(e,t){if(!e||e.kind==="gateway")return{method:"exec.approvals.set",params:t};const n=e.nodeId.trim();return n?{method:"exec.approvals.node.set",params:{...t,nodeId:n}}:null}async function zs(e,t){if(!(!e.client||!e.connected)&&!e.execApprovalsLoading){e.execApprovalsLoading=!0,e.lastError=null;try{const n=jc(t);if(!n){e.lastError="Select a node before loading exec approvals.";return}const s=await e.client.request(n.method,n.params);Vc(e,s)}catch(n){e.lastError=String(n)}finally{e.execApprovalsLoading=!1}}}function Vc(e,t){e.execApprovalsSnapshot=t,e.execApprovalsDirty||(e.execApprovalsForm=De(t.file??{}))}async function Wc(e,t){if(!(!e.client||!e.connected)){e.execApprovalsSaving=!0,e.lastError=null;try{const n=e.execApprovalsSnapshot?.hash;if(!n){e.lastError="Exec approvals hash missing; reload and retry.";return}const s=e.execApprovalsForm??e.execApprovalsSnapshot?.file??{},i=qc(t,{file:s,baseHash:n});if(!i){e.lastError="Select a node before saving exec approvals.";return}await e.client.request(i.method,i.params),e.execApprovalsDirty=!1,await zs(e,t)}catch(n){e.lastError=String(n)}finally{e.execApprovalsSaving=!1}}}function Gc(e,t,n){const s=De(e.execApprovalsForm??e.execApprovalsSnapshot?.file??{});lo(s,t,n),e.execApprovalsForm=s,e.execApprovalsDirty=!0}function Yc(e,t){const n=De(e.execApprovalsForm??e.execApprovalsSnapshot?.file??{});co(n,t),e.execApprovalsForm=n,e.execApprovalsDirty=!0}async function js(e){if(!(!e.client||!e.connected)&&!e.presenceLoading){e.presenceLoading=!0,e.presenceError=null,e.presenceStatus=null;try{const t=await e.client.request("system-presence",{});Array.isArray(t)?(e.presenceEntries=t,e.presenceStatus=t.length===0?"No instances yet.":null):(e.presenceEntries=[],e.presenceStatus="No presence payload.")}catch(t){e.presenceError=String(t)}finally{e.presenceLoading=!1}}}function tt(e,t,n){if(!t.trim())return;const s={...e.skillMessages};n?s[t]=n:delete s[t],e.skillMessages=s}function fn(e){return e instanceof Error?e.message:String(e)}async function Ct(e,t){if(t?.clearMessages&&Object.keys(e.skillMessages).length>0&&(e.skillMessages={}),!(!e.client||!e.connected)&&!e.skillsLoading){e.skillsLoading=!0,e.skillsError=null;try{const n=await e.client.request("skills.status",{});n&&(e.skillsReport=n)}catch(n){e.skillsError=fn(n)}finally{e.skillsLoading=!1}}}function Qc(e,t,n){e.skillEdits={...e.skillEdits,[t]:n}}async function Zc(e,t,n){if(!(!e.client||!e.connected)){e.skillsBusyKey=t,e.skillsError=null;try{await e.client.request("skills.update",{skillKey:t,enabled:n}),await Ct(e),tt(e,t,{kind:"success",message:n?"Skill enabled":"Skill disabled"})}catch(s){const i=fn(s);e.skillsError=i,tt(e,t,{kind:"error",message:i})}finally{e.skillsBusyKey=null}}}async function Jc(e,t){if(!(!e.client||!e.connected)){e.skillsBusyKey=t,e.skillsError=null;try{const n=e.skillEdits[t]??"";await e.client.request("skills.update",{skillKey:t,apiKey:n}),await Ct(e),tt(e,t,{kind:"success",message:"API key saved"})}catch(n){const s=fn(n);e.skillsError=s,tt(e,t,{kind:"error",message:s})}finally{e.skillsBusyKey=null}}}async function Xc(e,t,n,s){if(!(!e.client||!e.connected)){e.skillsBusyKey=t,e.skillsError=null;try{const i=await e.client.request("skills.install",{name:n,installId:s,timeoutMs:12e4});await Ct(e),tt(e,t,{kind:"success",message:i?.message??"Installed"})}catch(i){const a=fn(i);e.skillsError=a,tt(e,t,{kind:"error",message:a})}finally{e.skillsBusyKey=null}}}function ed(){return typeof window>"u"||typeof window.matchMedia!="function"||window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function qs(e){return e==="system"?ed():e}const Ft=e=>Number.isNaN(e)?.5:e<=0?0:e>=1?1:e,td=()=>typeof window>"u"||typeof window.matchMedia!="function"?!1:window.matchMedia("(prefers-reduced-motion: reduce)").matches??!1,Ut=e=>{e.classList.remove("theme-transition"),e.style.removeProperty("--theme-switch-x"),e.style.removeProperty("--theme-switch-y")},nd=({nextTheme:e,applyTheme:t,context:n,currentTheme:s})=>{if(s===e)return;const i=globalThis.document??null;if(!i){t();return}const a=i.documentElement,o=i,c=td();if(!!o.startViewTransition&&!c){let p=.5,d=.5;if(n?.pointerClientX!==void 0&&n?.pointerClientY!==void 0&&typeof window<"u")p=Ft(n.pointerClientX/window.innerWidth),d=Ft(n.pointerClientY/window.innerHeight);else if(n?.element){const u=n.element.getBoundingClientRect();u.width>0&&u.height>0&&typeof window<"u"&&(p=Ft((u.left+u.width/2)/window.innerWidth),d=Ft((u.top+u.height/2)/window.innerHeight))}a.style.setProperty("--theme-switch-x",`${p*100}%`),a.style.setProperty("--theme-switch-y",`${d*100}%`),a.classList.add("theme-transition");try{const u=o.startViewTransition?.(()=>{t()});u?.finished?u.finished.finally(()=>Ut(a)):Ut(a)}catch{Ut(a),t()}return}t(),Ut(a)};function sd(e){e.nodesPollInterval==null&&(e.nodesPollInterval=window.setInterval(()=>{pn(e,{quiet:!0})},5e3))}function id(e){e.nodesPollInterval!=null&&(clearInterval(e.nodesPollInterval),e.nodesPollInterval=null)}function Vs(e){e.logsPollInterval==null&&(e.logsPollInterval=window.setInterval(()=>{e.tab==="logs"&&Os(e,{quiet:!0})},2e3))}function Ws(e){e.logsPollInterval!=null&&(clearInterval(e.logsPollInterval),e.logsPollInterval=null)}function Gs(e){e.debugPollInterval==null&&(e.debugPollInterval=window.setInterval(()=>{e.tab==="debug"&&dn(e)},3e3))}function Ys(e){e.debugPollInterval!=null&&(clearInterval(e.debugPollInterval),e.debugPollInterval=null)}function ke(e,t){const n={...t,lastActiveSessionKey:t.lastActiveSessionKey?.trim()||t.sessionKey.trim()||"main"};e.settings=n,fl(n),t.theme!==e.theme&&(e.theme=t.theme,hn(e,qs(t.theme))),e.applySessionKey=e.settings.lastActiveSessionKey}function Mo(e,t){const n=t.trim();n&&e.settings.lastActiveSessionKey!==n&&ke(e,{...e.settings,lastActiveSessionKey:n})}function ad(e){if(!window.location.search)return;const t=new URLSearchParams(window.location.search),n=t.get("token"),s=t.get("password"),i=t.get("session"),a=t.get("gatewayUrl");let o=!1;if(n!=null){const l=n.trim();l&&l!==e.settings.token&&ke(e,{...e.settings,token:l}),t.delete("token"),o=!0}if(s!=null){const l=s.trim();l&&(e.password=l),t.delete("password"),o=!0}if(i!=null){const l=i.trim();l&&(e.sessionKey=l,ke(e,{...e.settings,sessionKey:l,lastActiveSessionKey:l}))}if(a!=null){const l=a.trim();l&&l!==e.settings.gatewayUrl&&ke(e,{...e.settings,gatewayUrl:l}),t.delete("gatewayUrl"),o=!0}if(!o)return;const c=new URL(window.location.href);c.search=t.toString(),window.history.replaceState({},"",c.toString())}function od(e,t){e.tab!==t&&(e.tab=t),t==="chat"&&(e.chatHasAutoScrolled=!1),t==="logs"?Vs(e):Ws(e),t==="debug"?Gs(e):Ys(e),Qs(e),Ro(e,t,!1)}function rd(e,t,n){nd({nextTheme:t,applyTheme:()=>{e.theme=t,ke(e,{...e.settings,theme:t}),hn(e,qs(t))},context:n,currentTheme:e.theme})}async function Qs(e){e.tab==="overview"&&await Po(e),e.tab==="channels"&&await gd(e),e.tab==="instances"&&await js(e),e.tab==="sessions"&&await st(e),e.tab==="cron"&&await Zs(e),e.tab==="skills"&&await Ct(e),e.tab==="nodes"&&(await pn(e),await Te(e),await be(e),await zs(e)),e.tab==="chat"&&(await wd(e),ln(e,!e.chatHasAutoScrolled)),e.tab==="config"&&(await uo(e),await be(e)),e.tab==="debug"&&(await dn(e),e.eventLog=e.eventLogBuffer),e.tab==="logs"&&(e.logsAtBottom=!0,await Os(e,{reset:!0}),ro(e,!0))}function ld(){if(typeof window>"u")return"";const e=window.__CLAWDBOT_CONTROL_UI_BASE_PATH__;return typeof e=="string"&&e.trim()?rn(e):gl(window.location.pathname)}function cd(e){e.theme=e.settings.theme??"system",hn(e,qs(e.theme))}function hn(e,t){if(e.themeResolved=t,typeof document>"u")return;const n=document.documentElement;n.dataset.theme=t,n.style.colorScheme=t}function dd(e){if(typeof window>"u"||typeof window.matchMedia!="function")return;if(e.themeMedia=window.matchMedia("(prefers-color-scheme: dark)"),e.themeMediaHandler=n=>{e.theme==="system"&&hn(e,n.matches?"dark":"light")},typeof e.themeMedia.addEventListener=="function"){e.themeMedia.addEventListener("change",e.themeMediaHandler);return}e.themeMedia.addListener(e.themeMediaHandler)}function ud(e){if(!e.themeMedia||!e.themeMediaHandler)return;if(typeof e.themeMedia.removeEventListener=="function"){e.themeMedia.removeEventListener("change",e.themeMediaHandler);return}e.themeMedia.removeListener(e.themeMediaHandler),e.themeMedia=null,e.themeMediaHandler=null}function pd(e,t){if(typeof window>"u")return;const n=so(window.location.pathname,e.basePath)??"chat";Io(e,n),Ro(e,n,t)}function fd(e){if(typeof window>"u")return;const t=so(window.location.pathname,e.basePath);if(!t)return;const s=new URL(window.location.href).searchParams.get("session")?.trim();s&&(e.sessionKey=s,ke(e,{...e.settings,sessionKey:s,lastActiveSessionKey:s})),Io(e,t)}function Io(e,t){e.tab!==t&&(e.tab=t),t==="chat"&&(e.chatHasAutoScrolled=!1),t==="logs"?Vs(e):Ws(e),t==="debug"?Gs(e):Ys(e),e.connected&&Qs(e)}function Ro(e,t,n){if(typeof window>"u")return;const s=kt(Rs(t,e.basePath)),i=kt(window.location.pathname),a=new URL(window.location.href);t==="chat"&&e.sessionKey?a.searchParams.set("session",e.sessionKey):a.searchParams.delete("session"),i!==s&&(a.pathname=s),n?window.history.replaceState({},"",a.toString()):window.history.pushState({},"",a.toString())}function hd(e,t,n){if(typeof window>"u")return;const s=new URL(window.location.href);s.searchParams.set("session",t),window.history.replaceState({},"",s.toString())}async function Po(e){await Promise.all([oe(e,!1),js(e),st(e),Tt(e),dn(e)])}async function gd(e){await Promise.all([oe(e,!0),uo(e),be(e)])}async function Zs(e){await Promise.all([oe(e,!1),Tt(e),cn(e)])}function No(e){return e.chatSending||!!e.chatRunId}function vd(e){const t=e.trim();if(!t)return!1;const n=t.toLowerCase();return n==="/stop"?!0:n==="stop"||n==="esc"||n==="abort"||n==="wait"||n==="exit"}async function Oo(e){e.connected&&(e.chatMessage="",await El(e))}function md(e,t){const n=t.trim();n&&(e.chatQueue=[...e.chatQueue,{id:Ps(),text:n,createdAt:Date.now()}])}async function Do(e,t,n){Ns(e);const s=await Cl(e,t);return!s&&n?.previousDraft!=null&&(e.chatMessage=n.previousDraft),s&&Mo(e,e.sessionKey),s&&n?.restoreDraft&&n.previousDraft?.trim()&&(e.chatMessage=n.previousDraft),ln(e),s&&!e.chatRunId&&Bo(e),s}async function Bo(e){if(!e.connected||No(e))return;const[t,...n]=e.chatQueue;if(!t)return;e.chatQueue=n,await Do(e,t.text)||(e.chatQueue=[t,...e.chatQueue])}function bd(e,t){e.chatQueue=e.chatQueue.filter(n=>n.id!==t)}async function yd(e,t,n){if(!e.connected)return;const s=e.chatMessage,i=(t??e.chatMessage).trim();if(i){if(vd(i)){await Oo(e);return}if(t==null&&(e.chatMessage=""),No(e)){md(e,i);return}await Do(e,i,{previousDraft:t==null?s:void 0,restoreDraft:!!(t&&n?.restoreDraft)})}}async function wd(e){await Promise.all([Xe(e),st(e),fs(e)]),ln(e,!0)}const $d=Bo;function xd(e){const t=eo(e.sessionKey);return t?.agentId?t.agentId:e.hello?.snapshot?.sessionDefaults?.defaultAgentId?.trim()||"main"}function kd(e,t){const n=rn(e),s=encodeURIComponent(t);return n?`${n}/avatar/${s}?meta=1`:`/avatar/${s}?meta=1`}async function fs(e){if(!e.connected){e.chatAvatarUrl=null;return}const t=xd(e);if(!t){e.chatAvatarUrl=null;return}e.chatAvatarUrl=null;const n=kd(e.basePath,t);try{const s=await fetch(n,{method:"GET"});if(!s.ok){e.chatAvatarUrl=null;return}const i=await s.json(),a=typeof i.avatarUrl=="string"?i.avatarUrl.trim():"";e.chatAvatarUrl=a||null}catch{e.chatAvatarUrl=null}}const Fo={CHILD:2},Uo=e=>(...t)=>({_$litDirective$:e,values:t});let Ko=class{constructor(t){}get _$AU(){return this._$AM._$AU}_$AT(t,n,s){this._$Ct=t,this._$AM=n,this._$Ci=s}_$AS(t,n){return this.update(t,n)}update(t,n){return this.render(...n)}};const{I:Ad}=nl,ia=e=>e,aa=()=>document.createComment(""),lt=(e,t,n)=>{const s=e._$AA.parentNode,i=t===void 0?e._$AB:t._$AA;if(n===void 0){const a=s.insertBefore(aa(),i),o=s.insertBefore(aa(),i);n=new Ad(a,o,e,e.options)}else{const a=n._$AB.nextSibling,o=n._$AM,c=o!==e;if(c){let l;n._$AQ?.(e),n._$AM=e,n._$AP!==void 0&&(l=e._$AU)!==o._$AU&&n._$AP(l)}if(a!==i||c){let l=n._$AA;for(;l!==a;){const p=ia(l).nextSibling;ia(s).insertBefore(l,i),l=p}}}return n},Me=(e,t,n=e)=>(e._$AI(t,n),e),Sd={},_d=(e,t=Sd)=>e._$AH=t,Td=e=>e._$AH,qn=e=>{e._$AR(),e._$AA.remove()};const oa=(e,t,n)=>{const s=new Map;for(let i=t;i<=n;i++)s.set(e[i],i);return s},Ho=Uo(class extends Ko{constructor(e){if(super(e),e.type!==Fo.CHILD)throw Error("repeat() can only be used in text expressions")}dt(e,t,n){let s;n===void 0?n=t:t!==void 0&&(s=t);const i=[],a=[];let o=0;for(const c of e)i[o]=s?s(c,o):o,a[o]=n(c,o),o++;return{values:a,keys:i}}render(e,t,n){return this.dt(e,t,n).values}update(e,[t,n,s]){const i=Td(e),{values:a,keys:o}=this.dt(t,n,s);if(!Array.isArray(i))return this.ut=o,a;const c=this.ut??=[],l=[];let p,d,u=0,h=i.length-1,v=0,w=a.length-1;for(;u<=h&&v<=w;)if(i[u]===null)u++;else if(i[h]===null)h--;else if(c[u]===o[v])l[v]=Me(i[u],a[v]),u++,v++;else if(c[h]===o[w])l[w]=Me(i[h],a[w]),h--,w--;else if(c[u]===o[w])l[w]=Me(i[u],a[w]),lt(e,l[w+1],i[u]),u++,w--;else if(c[h]===o[v])l[v]=Me(i[h],a[v]),lt(e,i[u],i[h]),h--,v++;else if(p===void 0&&(p=oa(o,v,w),d=oa(c,u,h)),p.has(c[u]))if(p.has(c[h])){const $=d.get(o[v]),k=$!==void 0?i[$]:null;if(k===null){const T=lt(e,i[u]);Me(T,a[v]),l[v]=T}else l[v]=Me(k,a[v]),lt(e,i[u],k),i[$]=null;v++}else qn(i[h]),h--;else qn(i[u]),u++;for(;v<=w;){const $=lt(e,l[w+1]);Me($,a[v]),l[v++]=$}for(;u<=h;){const $=i[u++];$!==null&&qn($)}return this.ut=o,_d(e,l),Se}});function zo(e){const t=e;let n=typeof t.role=="string"?t.role:"unknown";const s=typeof t.toolCallId=="string"||typeof t.tool_call_id=="string",i=t.content,a=Array.isArray(i)?i:null,o=Array.isArray(a)&&a.some(u=>{const v=String(u.type??"").toLowerCase();return v==="toolresult"||v==="tool_result"}),c=typeof t.toolName=="string"||typeof t.tool_name=="string";(s||o||c)&&(n="toolResult");let l=[];typeof t.content=="string"?l=[{type:"text",text:t.content}]:Array.isArray(t.content)?l=t.content.map(u=>({type:u.type||"text",text:u.text,name:u.name,args:u.args||u.arguments})):typeof t.text=="string"&&(l=[{type:"text",text:t.text}]);const p=typeof t.timestamp=="number"?t.timestamp:Date.now(),d=typeof t.id=="string"?t.id:void 0;return{role:n,content:l,timestamp:p,id:d}}function Js(e){const t=e.toLowerCase();return e==="user"||e==="User"?e:e==="assistant"?"assistant":e==="system"?"system":t==="toolresult"||t==="tool_result"||t==="tool"||t==="function"?"tool":e}function jo(e){const t=e,n=typeof t.role=="string"?t.role.toLowerCase():"";return n==="toolresult"||n==="tool_result"}class hs extends Ko{constructor(t){if(super(t),this.it=g,t.type!==Fo.CHILD)throw Error(this.constructor.directiveName+"() can only be used in child bindings")}render(t){if(t===g||t==null)return this._t=void 0,this.it=t;if(t===Se)return t;if(typeof t!="string")throw Error(this.constructor.directiveName+"() called with a non-string value");if(t===this.it)return this._t;this.it=t;const n=[t];return n.raw=n,this._t={_$litType$:this.constructor.resultType,strings:n,values:[]}}}hs.directiveName="unsafeHTML",hs.resultType=1;const gs=Uo(hs);const{entries:qo,setPrototypeOf:ra,isFrozen:Cd,getPrototypeOf:Ed,getOwnPropertyDescriptor:Ld}=Object;let{freeze:Z,seal:ne,create:vs}=Object,{apply:ms,construct:bs}=typeof Reflect<"u"&&Reflect;Z||(Z=function(t){return t});ne||(ne=function(t){return t});ms||(ms=function(t,n){for(var s=arguments.length,i=new Array(s>2?s-2:0),a=2;a1?n-1:0),i=1;i1?n-1:0),i=1;i2&&arguments[2]!==void 0?arguments[2]:Wt;ra&&ra(e,null);let s=t.length;for(;s--;){let i=t[s];if(typeof i=="string"){const a=n(i);a!==i&&(Cd(t)||(t[s]=a),i=a)}e[i]=!0}return e}function Od(e){for(let t=0;t/gm),Kd=ne(/\$\{[\w\W]*/gm),Hd=ne(/^data-[\-\w.\u00B7-\uFFFF]+$/),zd=ne(/^aria-[\-\w]+$/),Vo=ne(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),jd=ne(/^(?:\w+script|data):/i),qd=ne(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),Wo=ne(/^html$/i),Vd=ne(/^[a-z][.\w]*(-[.\w]+)+$/i);var fa=Object.freeze({__proto__:null,ARIA_ATTR:zd,ATTR_WHITESPACE:qd,CUSTOM_ELEMENT:Vd,DATA_ATTR:Hd,DOCTYPE_NAME:Wo,ERB_EXPR:Ud,IS_ALLOWED_URI:Vo,IS_SCRIPT_OR_DATA:jd,MUSTACHE_EXPR:Fd,TMPLIT_EXPR:Kd});const ft={element:1,text:3,progressingInstruction:7,comment:8,document:9},Wd=function(){return typeof window>"u"?null:window},Gd=function(t,n){if(typeof t!="object"||typeof t.createPolicy!="function")return null;let s=null;const i="data-tt-policy-suffix";n&&n.hasAttribute(i)&&(s=n.getAttribute(i));const a="dompurify"+(s?"#"+s:"");try{return t.createPolicy(a,{createHTML(o){return o},createScriptURL(o){return o}})}catch{return console.warn("TrustedTypes policy "+a+" could not be created."),null}},ha=function(){return{afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}};function Go(){let e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:Wd();const t=_=>Go(_);if(t.version="3.3.1",t.removed=[],!e||!e.document||e.document.nodeType!==ft.document||!e.Element)return t.isSupported=!1,t;let{document:n}=e;const s=n,i=s.currentScript,{DocumentFragment:a,HTMLTemplateElement:o,Node:c,Element:l,NodeFilter:p,NamedNodeMap:d=e.NamedNodeMap||e.MozNamedAttrMap,HTMLFormElement:u,DOMParser:h,trustedTypes:v}=e,w=l.prototype,$=pt(w,"cloneNode"),k=pt(w,"remove"),T=pt(w,"nextSibling"),M=pt(w,"childNodes"),P=pt(w,"parentNode");if(typeof o=="function"){const _=n.createElement("template");_.content&&_.content.ownerDocument&&(n=_.content.ownerDocument)}let L,C="";const{implementation:E,createNodeIterator:pe,createDocumentFragment:yn,getElementsByTagName:wn}=n,{importNode:kr}=s;let W=ha();t.isSupported=typeof qo=="function"&&typeof P=="function"&&E&&E.createHTMLDocument!==void 0;const{MUSTACHE_EXPR:$n,ERB_EXPR:xn,TMPLIT_EXPR:kn,DATA_ATTR:Ar,ARIA_ATTR:Sr,IS_SCRIPT_OR_DATA:_r,ATTR_WHITESPACE:di,CUSTOM_ELEMENT:Tr}=fa;let{IS_ALLOWED_URI:ui}=fa,K=null;const pi=I({},[...ca,...Gn,...Yn,...Qn,...da]);let z=null;const fi=I({},[...ua,...Zn,...pa,...Ht]);let B=Object.seal(vs(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),it=null,An=null;const He=Object.seal(vs(null,{tagCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeCheck:{writable:!0,configurable:!1,enumerable:!0,value:null}}));let hi=!0,Sn=!0,gi=!1,vi=!0,ze=!1,Lt=!0,Ce=!1,_n=!1,Tn=!1,je=!1,Mt=!1,It=!1,mi=!0,bi=!1;const Cr="user-content-";let Cn=!0,at=!1,qe={},re=null;const En=I({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let yi=null;const wi=I({},["audio","video","img","source","image","track"]);let Ln=null;const $i=I({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Rt="http://www.w3.org/1998/Math/MathML",Pt="http://www.w3.org/2000/svg",fe="http://www.w3.org/1999/xhtml";let Ve=fe,Mn=!1,In=null;const Er=I({},[Rt,Pt,fe],Vn);let Nt=I({},["mi","mo","mn","ms","mtext"]),Ot=I({},["annotation-xml"]);const Lr=I({},["title","style","font","a","script"]);let ot=null;const Mr=["application/xhtml+xml","text/html"],Ir="text/html";let U=null,We=null;const Rr=n.createElement("form"),xi=function(f){return f instanceof RegExp||f instanceof Function},Rn=function(){let f=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};if(!(We&&We===f)){if((!f||typeof f!="object")&&(f={}),f=de(f),ot=Mr.indexOf(f.PARSER_MEDIA_TYPE)===-1?Ir:f.PARSER_MEDIA_TYPE,U=ot==="application/xhtml+xml"?Vn:Wt,K=se(f,"ALLOWED_TAGS")?I({},f.ALLOWED_TAGS,U):pi,z=se(f,"ALLOWED_ATTR")?I({},f.ALLOWED_ATTR,U):fi,In=se(f,"ALLOWED_NAMESPACES")?I({},f.ALLOWED_NAMESPACES,Vn):Er,Ln=se(f,"ADD_URI_SAFE_ATTR")?I(de($i),f.ADD_URI_SAFE_ATTR,U):$i,yi=se(f,"ADD_DATA_URI_TAGS")?I(de(wi),f.ADD_DATA_URI_TAGS,U):wi,re=se(f,"FORBID_CONTENTS")?I({},f.FORBID_CONTENTS,U):En,it=se(f,"FORBID_TAGS")?I({},f.FORBID_TAGS,U):de({}),An=se(f,"FORBID_ATTR")?I({},f.FORBID_ATTR,U):de({}),qe=se(f,"USE_PROFILES")?f.USE_PROFILES:!1,hi=f.ALLOW_ARIA_ATTR!==!1,Sn=f.ALLOW_DATA_ATTR!==!1,gi=f.ALLOW_UNKNOWN_PROTOCOLS||!1,vi=f.ALLOW_SELF_CLOSE_IN_ATTR!==!1,ze=f.SAFE_FOR_TEMPLATES||!1,Lt=f.SAFE_FOR_XML!==!1,Ce=f.WHOLE_DOCUMENT||!1,je=f.RETURN_DOM||!1,Mt=f.RETURN_DOM_FRAGMENT||!1,It=f.RETURN_TRUSTED_TYPE||!1,Tn=f.FORCE_BODY||!1,mi=f.SANITIZE_DOM!==!1,bi=f.SANITIZE_NAMED_PROPS||!1,Cn=f.KEEP_CONTENT!==!1,at=f.IN_PLACE||!1,ui=f.ALLOWED_URI_REGEXP||Vo,Ve=f.NAMESPACE||fe,Nt=f.MATHML_TEXT_INTEGRATION_POINTS||Nt,Ot=f.HTML_INTEGRATION_POINTS||Ot,B=f.CUSTOM_ELEMENT_HANDLING||{},f.CUSTOM_ELEMENT_HANDLING&&xi(f.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(B.tagNameCheck=f.CUSTOM_ELEMENT_HANDLING.tagNameCheck),f.CUSTOM_ELEMENT_HANDLING&&xi(f.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(B.attributeNameCheck=f.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),f.CUSTOM_ELEMENT_HANDLING&&typeof f.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements=="boolean"&&(B.allowCustomizedBuiltInElements=f.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),ze&&(Sn=!1),Mt&&(je=!0),qe&&(K=I({},da),z=[],qe.html===!0&&(I(K,ca),I(z,ua)),qe.svg===!0&&(I(K,Gn),I(z,Zn),I(z,Ht)),qe.svgFilters===!0&&(I(K,Yn),I(z,Zn),I(z,Ht)),qe.mathMl===!0&&(I(K,Qn),I(z,pa),I(z,Ht))),f.ADD_TAGS&&(typeof f.ADD_TAGS=="function"?He.tagCheck=f.ADD_TAGS:(K===pi&&(K=de(K)),I(K,f.ADD_TAGS,U))),f.ADD_ATTR&&(typeof f.ADD_ATTR=="function"?He.attributeCheck=f.ADD_ATTR:(z===fi&&(z=de(z)),I(z,f.ADD_ATTR,U))),f.ADD_URI_SAFE_ATTR&&I(Ln,f.ADD_URI_SAFE_ATTR,U),f.FORBID_CONTENTS&&(re===En&&(re=de(re)),I(re,f.FORBID_CONTENTS,U)),f.ADD_FORBID_CONTENTS&&(re===En&&(re=de(re)),I(re,f.ADD_FORBID_CONTENTS,U)),Cn&&(K["#text"]=!0),Ce&&I(K,["html","head","body"]),K.table&&(I(K,["tbody"]),delete it.tbody),f.TRUSTED_TYPES_POLICY){if(typeof f.TRUSTED_TYPES_POLICY.createHTML!="function")throw ut('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if(typeof f.TRUSTED_TYPES_POLICY.createScriptURL!="function")throw ut('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');L=f.TRUSTED_TYPES_POLICY,C=L.createHTML("")}else L===void 0&&(L=Gd(v,i)),L!==null&&typeof C=="string"&&(C=L.createHTML(""));Z&&Z(f),We=f}},ki=I({},[...Gn,...Yn,...Dd]),Ai=I({},[...Qn,...Bd]),Pr=function(f){let x=P(f);(!x||!x.tagName)&&(x={namespaceURI:Ve,tagName:"template"});const S=Wt(f.tagName),D=Wt(x.tagName);return In[f.namespaceURI]?f.namespaceURI===Pt?x.namespaceURI===fe?S==="svg":x.namespaceURI===Rt?S==="svg"&&(D==="annotation-xml"||Nt[D]):!!ki[S]:f.namespaceURI===Rt?x.namespaceURI===fe?S==="math":x.namespaceURI===Pt?S==="math"&&Ot[D]:!!Ai[S]:f.namespaceURI===fe?x.namespaceURI===Pt&&!Ot[D]||x.namespaceURI===Rt&&!Nt[D]?!1:!Ai[S]&&(Lr[S]||!ki[S]):!!(ot==="application/xhtml+xml"&&In[f.namespaceURI]):!1},le=function(f){ct(t.removed,{element:f});try{P(f).removeChild(f)}catch{k(f)}},Ee=function(f,x){try{ct(t.removed,{attribute:x.getAttributeNode(f),from:x})}catch{ct(t.removed,{attribute:null,from:x})}if(x.removeAttribute(f),f==="is")if(je||Mt)try{le(x)}catch{}else try{x.setAttribute(f,"")}catch{}},Si=function(f){let x=null,S=null;if(Tn)f=""+f;else{const F=Wn(f,/^[\r\n\t ]+/);S=F&&F[0]}ot==="application/xhtml+xml"&&Ve===fe&&(f=''+f+"");const D=L?L.createHTML(f):f;if(Ve===fe)try{x=new h().parseFromString(D,ot)}catch{}if(!x||!x.documentElement){x=E.createDocument(Ve,"template",null);try{x.documentElement.innerHTML=Mn?C:D}catch{}}const q=x.body||x.documentElement;return f&&S&&q.insertBefore(n.createTextNode(S),q.childNodes[0]||null),Ve===fe?wn.call(x,Ce?"html":"body")[0]:Ce?x.documentElement:q},_i=function(f){return pe.call(f.ownerDocument||f,f,p.SHOW_ELEMENT|p.SHOW_COMMENT|p.SHOW_TEXT|p.SHOW_PROCESSING_INSTRUCTION|p.SHOW_CDATA_SECTION,null)},Pn=function(f){return f instanceof u&&(typeof f.nodeName!="string"||typeof f.textContent!="string"||typeof f.removeChild!="function"||!(f.attributes instanceof d)||typeof f.removeAttribute!="function"||typeof f.setAttribute!="function"||typeof f.namespaceURI!="string"||typeof f.insertBefore!="function"||typeof f.hasChildNodes!="function")},Ti=function(f){return typeof c=="function"&&f instanceof c};function he(_,f,x){Kt(_,S=>{S.call(t,f,x,We)})}const Ci=function(f){let x=null;if(he(W.beforeSanitizeElements,f,null),Pn(f))return le(f),!0;const S=U(f.nodeName);if(he(W.uponSanitizeElement,f,{tagName:S,allowedTags:K}),Lt&&f.hasChildNodes()&&!Ti(f.firstElementChild)&&G(/<[/\w!]/g,f.innerHTML)&&G(/<[/\w!]/g,f.textContent)||f.nodeType===ft.progressingInstruction||Lt&&f.nodeType===ft.comment&&G(/<[/\w]/g,f.data))return le(f),!0;if(!(He.tagCheck instanceof Function&&He.tagCheck(S))&&(!K[S]||it[S])){if(!it[S]&&Li(S)&&(B.tagNameCheck instanceof RegExp&&G(B.tagNameCheck,S)||B.tagNameCheck instanceof Function&&B.tagNameCheck(S)))return!1;if(Cn&&!re[S]){const D=P(f)||f.parentNode,q=M(f)||f.childNodes;if(q&&D){const F=q.length;for(let X=F-1;X>=0;--X){const ge=$(q[X],!0);ge.__removalCount=(f.__removalCount||0)+1,D.insertBefore(ge,T(f))}}}return le(f),!0}return f instanceof l&&!Pr(f)||(S==="noscript"||S==="noembed"||S==="noframes")&&G(/<\/no(script|embed|frames)/i,f.innerHTML)?(le(f),!0):(ze&&f.nodeType===ft.text&&(x=f.textContent,Kt([$n,xn,kn],D=>{x=dt(x,D," ")}),f.textContent!==x&&(ct(t.removed,{element:f.cloneNode()}),f.textContent=x)),he(W.afterSanitizeElements,f,null),!1)},Ei=function(f,x,S){if(mi&&(x==="id"||x==="name")&&(S in n||S in Rr))return!1;if(!(Sn&&!An[x]&&G(Ar,x))){if(!(hi&&G(Sr,x))){if(!(He.attributeCheck instanceof Function&&He.attributeCheck(x,f))){if(!z[x]||An[x]){if(!(Li(f)&&(B.tagNameCheck instanceof RegExp&&G(B.tagNameCheck,f)||B.tagNameCheck instanceof Function&&B.tagNameCheck(f))&&(B.attributeNameCheck instanceof RegExp&&G(B.attributeNameCheck,x)||B.attributeNameCheck instanceof Function&&B.attributeNameCheck(x,f))||x==="is"&&B.allowCustomizedBuiltInElements&&(B.tagNameCheck instanceof RegExp&&G(B.tagNameCheck,S)||B.tagNameCheck instanceof Function&&B.tagNameCheck(S))))return!1}else if(!Ln[x]){if(!G(ui,dt(S,di,""))){if(!((x==="src"||x==="xlink:href"||x==="href")&&f!=="script"&&Rd(S,"data:")===0&&yi[f])){if(!(gi&&!G(_r,dt(S,di,"")))){if(S)return!1}}}}}}}return!0},Li=function(f){return f!=="annotation-xml"&&Wn(f,Tr)},Mi=function(f){he(W.beforeSanitizeAttributes,f,null);const{attributes:x}=f;if(!x||Pn(f))return;const S={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:z,forceKeepAttr:void 0};let D=x.length;for(;D--;){const q=x[D],{name:F,namespaceURI:X,value:ge}=q,Ge=U(F),Nn=ge;let j=F==="value"?Nn:Pd(Nn);if(S.attrName=Ge,S.attrValue=j,S.keepAttr=!0,S.forceKeepAttr=void 0,he(W.uponSanitizeAttribute,f,S),j=S.attrValue,bi&&(Ge==="id"||Ge==="name")&&(Ee(F,f),j=Cr+j),Lt&&G(/((--!?|])>)|<\/(style|title|textarea)/i,j)){Ee(F,f);continue}if(Ge==="attributename"&&Wn(j,"href")){Ee(F,f);continue}if(S.forceKeepAttr)continue;if(!S.keepAttr){Ee(F,f);continue}if(!vi&&G(/\/>/i,j)){Ee(F,f);continue}ze&&Kt([$n,xn,kn],Ri=>{j=dt(j,Ri," ")});const Ii=U(f.nodeName);if(!Ei(Ii,Ge,j)){Ee(F,f);continue}if(L&&typeof v=="object"&&typeof v.getAttributeType=="function"&&!X)switch(v.getAttributeType(Ii,Ge)){case"TrustedHTML":{j=L.createHTML(j);break}case"TrustedScriptURL":{j=L.createScriptURL(j);break}}if(j!==Nn)try{X?f.setAttributeNS(X,F,j):f.setAttribute(F,j),Pn(f)?le(f):la(t.removed)}catch{Ee(F,f)}}he(W.afterSanitizeAttributes,f,null)},Nr=function _(f){let x=null;const S=_i(f);for(he(W.beforeSanitizeShadowDOM,f,null);x=S.nextNode();)he(W.uponSanitizeShadowNode,x,null),Ci(x),Mi(x),x.content instanceof a&&_(x.content);he(W.afterSanitizeShadowDOM,f,null)};return t.sanitize=function(_){let f=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},x=null,S=null,D=null,q=null;if(Mn=!_,Mn&&(_=""),typeof _!="string"&&!Ti(_))if(typeof _.toString=="function"){if(_=_.toString(),typeof _!="string")throw ut("dirty is not a string, aborting")}else throw ut("toString is not a function");if(!t.isSupported)return _;if(_n||Rn(f),t.removed=[],typeof _=="string"&&(at=!1),at){if(_.nodeName){const ge=U(_.nodeName);if(!K[ge]||it[ge])throw ut("root node is forbidden and cannot be sanitized in-place")}}else if(_ instanceof c)x=Si(""),S=x.ownerDocument.importNode(_,!0),S.nodeType===ft.element&&S.nodeName==="BODY"||S.nodeName==="HTML"?x=S:x.appendChild(S);else{if(!je&&!ze&&!Ce&&_.indexOf("<")===-1)return L&&It?L.createHTML(_):_;if(x=Si(_),!x)return je?null:It?C:""}x&&Tn&&le(x.firstChild);const F=_i(at?_:x);for(;D=F.nextNode();)Ci(D),Mi(D),D.content instanceof a&&Nr(D.content);if(at)return _;if(je){if(Mt)for(q=yn.call(x.ownerDocument);x.firstChild;)q.appendChild(x.firstChild);else q=x;return(z.shadowroot||z.shadowrootmode)&&(q=kr.call(s,q,!0)),q}let X=Ce?x.outerHTML:x.innerHTML;return Ce&&K["!doctype"]&&x.ownerDocument&&x.ownerDocument.doctype&&x.ownerDocument.doctype.name&&G(Wo,x.ownerDocument.doctype.name)&&(X=" -`+X),ze&&Kt([$n,xn,kn],ge=>{X=dt(X,ge," ")}),L&&It?L.createHTML(X):X},t.setConfig=function(){let _=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};Rn(_),_n=!0},t.clearConfig=function(){We=null,_n=!1},t.isValidAttribute=function(_,f,x){We||Rn({});const S=U(_),D=U(f);return Ei(S,D,x)},t.addHook=function(_,f){typeof f=="function"&&ct(W[_],f)},t.removeHook=function(_,f){if(f!==void 0){const x=Md(W[_],f);return x===-1?void 0:Id(W[_],x,1)[0]}return la(W[_])},t.removeHooks=function(_){W[_]=[]},t.removeAllHooks=function(){W=ha()},t}var ys=Go();function Xs(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}var Ke=Xs();function Yo(e){Ke=e}var yt={exec:()=>null};function R(e,t=""){let n=typeof e=="string"?e:e.source,s={replace:(i,a)=>{let o=typeof a=="string"?a:a.source;return o=o.replace(Y.caret,"$1"),n=n.replace(i,o),s},getRegex:()=>new RegExp(n,t)};return s}var Yd=(()=>{try{return!!new RegExp("(?<=1)(?/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceTabs:/^\t+/,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] +\S/,listReplaceTask:/^\[[ xX]\] +/,listTaskCheckbox:/\[[ xX]\]/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^/i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^$/,pedanticHrefTitle:/^([^'"]*[^\s])\s+(['"])(.*)\2/,unicodeAlphaNumeric:/[\p{L}\p{N}]/u,escapeTest:/[&<>"']/,escapeReplace:/[&<>"']/g,escapeTestNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,escapeReplaceNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,unescapeTest:/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig,caret:/(^|[^\[])\^/g,percentDecode:/%25/g,findPipe:/\|/g,splitPipe:/ \|/,slashPipe:/\\\|/g,carriageReturn:/\r\n|\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\S*/,endingNewline:/\n$/,listItemRegex:e=>new RegExp(`^( {0,3}${e})((?:[ ][^\\n]*)?(?:\\n|$))`),nextBulletRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`),hrRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),fencesBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}(?:\`\`\`|~~~)`),headingBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}#`),htmlBeginRegex:e=>new RegExp(`^ {0,${Math.min(3,e-1)}}<(?:[a-z].*>|!--)`,"i")},Qd=/^(?:[ \t]*(?:\n|$))+/,Zd=/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,Jd=/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,Et=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,Xd=/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,ei=/(?:[*+-]|\d{1,9}[.)])/,Qo=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,Zo=R(Qo).replace(/bull/g,ei).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/\|table/g,"").getRegex(),eu=R(Qo).replace(/bull/g,ei).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/table/g,/ {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/).getRegex(),ti=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,tu=/^[^\n]+/,ni=/(?!\s*\])(?:\\[\s\S]|[^\[\]\\])+/,nu=R(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",ni).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),su=R(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,ei).getRegex(),gn="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",si=/|$))/,iu=R("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$))","i").replace("comment",si).replace("tag",gn).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),Jo=R(ti).replace("hr",Et).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",gn).getRegex(),au=R(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",Jo).getRegex(),ii={blockquote:au,code:Zd,def:nu,fences:Jd,heading:Xd,hr:Et,html:iu,lheading:Zo,list:su,newline:Qd,paragraph:Jo,table:yt,text:tu},ga=R("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",Et).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code","(?: {4}| {0,3} )[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",gn).getRegex(),ou={...ii,lheading:eu,table:ga,paragraph:R(ti).replace("hr",Et).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",ga).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",gn).getRegex()},ru={...ii,html:R(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment",si).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:yt,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:R(ti).replace("hr",Et).replace("heading",` *#{1,6} *[^ -]`).replace("lheading",Zo).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},lu=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,cu=/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,Xo=/^( {2,}|\\)\n(?!\s*$)/,du=/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\`+)[^`]+\k(?!`))*?\]\((?:\\[\s\S]|[^\\\(\)]|\((?:\\[\s\S]|[^\\\(\)])*\))*\)/).replace("precode-",Yd?"(?`+)[^`]+\k(?!`)/).replace("html",/<(?! )[^<>]*?>/).getRegex(),nr=/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/,gu=R(nr,"u").replace(/punct/g,vn).getRegex(),vu=R(nr,"u").replace(/punct/g,tr).getRegex(),sr="^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)",mu=R(sr,"gu").replace(/notPunctSpace/g,er).replace(/punctSpace/g,ai).replace(/punct/g,vn).getRegex(),bu=R(sr,"gu").replace(/notPunctSpace/g,fu).replace(/punctSpace/g,pu).replace(/punct/g,tr).getRegex(),yu=R("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,er).replace(/punctSpace/g,ai).replace(/punct/g,vn).getRegex(),wu=R(/\\(punct)/,"gu").replace(/punct/g,vn).getRegex(),$u=R(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),xu=R(si).replace("(?:-->|$)","-->").getRegex(),ku=R("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",xu).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),Xt=/(?:\[(?:\\[\s\S]|[^\[\]\\])*\]|\\[\s\S]|`+[^`]*?`+(?!`)|[^\[\]\\`])*?/,Au=R(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]*(?:\n[ \t]*)?)(title))?\s*\)/).replace("label",Xt).replace("href",/<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),ir=R(/^!?\[(label)\]\[(ref)\]/).replace("label",Xt).replace("ref",ni).getRegex(),ar=R(/^!?\[(ref)\](?:\[\])?/).replace("ref",ni).getRegex(),Su=R("reflink|nolink(?!\\()","g").replace("reflink",ir).replace("nolink",ar).getRegex(),va=/[hH][tT][tT][pP][sS]?|[fF][tT][pP]/,oi={_backpedal:yt,anyPunctuation:wu,autolink:$u,blockSkip:hu,br:Xo,code:cu,del:yt,emStrongLDelim:gu,emStrongRDelimAst:mu,emStrongRDelimUnd:yu,escape:lu,link:Au,nolink:ar,punctuation:uu,reflink:ir,reflinkSearch:Su,tag:ku,text:du,url:yt},_u={...oi,link:R(/^!?\[(label)\]\((.*?)\)/).replace("label",Xt).getRegex(),reflink:R(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",Xt).getRegex()},ws={...oi,emStrongRDelimAst:bu,emStrongLDelim:vu,url:R(/^((?:protocol):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/).replace("protocol",va).replace("email",/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(),_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])((?:\\[\s\S]|[^\\])*?(?:\\[\s\S]|[^\s~\\]))\1(?=[^~]|$)/,text:R(/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\":">",'"':""","'":"'"},ma=e=>Cu[e];function me(e,t){if(t){if(Y.escapeTest.test(e))return e.replace(Y.escapeReplace,ma)}else if(Y.escapeTestNoEncode.test(e))return e.replace(Y.escapeReplaceNoEncode,ma);return e}function ba(e){try{e=encodeURI(e).replace(Y.percentDecode,"%")}catch{return null}return e}function ya(e,t){let n=e.replace(Y.findPipe,(a,o,c)=>{let l=!1,p=o;for(;--p>=0&&c[p]==="\\";)l=!l;return l?"|":" |"}),s=n.split(Y.splitPipe),i=0;if(s[0].trim()||s.shift(),s.length>0&&!s.at(-1)?.trim()&&s.pop(),t)if(s.length>t)s.splice(t);else for(;s.length0?-2:-1}function wa(e,t,n,s,i){let a=t.href,o=t.title||null,c=e[1].replace(i.other.outputLinkReplace,"$1");s.state.inLink=!0;let l={type:e[0].charAt(0)==="!"?"image":"link",raw:n,href:a,title:o,text:c,tokens:s.inlineTokens(c)};return s.state.inLink=!1,l}function Lu(e,t,n){let s=e.match(n.other.indentCodeCompensation);if(s===null)return t;let i=s[1];return t.split(` -`).map(a=>{let o=a.match(n.other.beginningSpace);if(o===null)return a;let[c]=o;return c.length>=i.length?a.slice(i.length):a}).join(` -`)}var en=class{options;rules;lexer;constructor(e){this.options=e||Ke}space(e){let t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){let t=this.rules.block.code.exec(e);if(t){let n=t[0].replace(this.rules.other.codeRemoveIndent,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?n:gt(n,` -`)}}}fences(e){let t=this.rules.block.fences.exec(e);if(t){let n=t[0],s=Lu(n,t[3]||"",this.rules);return{type:"code",raw:n,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:s}}}heading(e){let t=this.rules.block.heading.exec(e);if(t){let n=t[2].trim();if(this.rules.other.endingHash.test(n)){let s=gt(n,"#");(this.options.pedantic||!s||this.rules.other.endingSpaceChar.test(s))&&(n=s.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:n,tokens:this.lexer.inline(n)}}}hr(e){let t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:gt(t[0],` -`)}}blockquote(e){let t=this.rules.block.blockquote.exec(e);if(t){let n=gt(t[0],` -`).split(` -`),s="",i="",a=[];for(;n.length>0;){let o=!1,c=[],l;for(l=0;l1,i={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");let a=this.rules.other.listItemRegex(n),o=!1;for(;e;){let l=!1,p="",d="";if(!(t=a.exec(e))||this.rules.block.hr.test(e))break;p=t[0],e=e.substring(p.length);let u=t[2].split(` -`,1)[0].replace(this.rules.other.listReplaceTabs,$=>" ".repeat(3*$.length)),h=e.split(` -`,1)[0],v=!u.trim(),w=0;if(this.options.pedantic?(w=2,d=u.trimStart()):v?w=t[1].length+1:(w=t[2].search(this.rules.other.nonSpaceChar),w=w>4?1:w,d=u.slice(w),w+=t[1].length),v&&this.rules.other.blankLine.test(h)&&(p+=h+` -`,e=e.substring(h.length+1),l=!0),!l){let $=this.rules.other.nextBulletRegex(w),k=this.rules.other.hrRegex(w),T=this.rules.other.fencesBeginRegex(w),M=this.rules.other.headingBeginRegex(w),P=this.rules.other.htmlBeginRegex(w);for(;e;){let L=e.split(` -`,1)[0],C;if(h=L,this.options.pedantic?(h=h.replace(this.rules.other.listReplaceNesting," "),C=h):C=h.replace(this.rules.other.tabCharGlobal," "),T.test(h)||M.test(h)||P.test(h)||$.test(h)||k.test(h))break;if(C.search(this.rules.other.nonSpaceChar)>=w||!h.trim())d+=` -`+C.slice(w);else{if(v||u.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4||T.test(u)||M.test(u)||k.test(u))break;d+=` -`+h}!v&&!h.trim()&&(v=!0),p+=L+` -`,e=e.substring(L.length+1),u=C.slice(w)}}i.loose||(o?i.loose=!0:this.rules.other.doubleBlankLine.test(p)&&(o=!0)),i.items.push({type:"list_item",raw:p,task:!!this.options.gfm&&this.rules.other.listIsTask.test(d),loose:!1,text:d,tokens:[]}),i.raw+=p}let c=i.items.at(-1);if(c)c.raw=c.raw.trimEnd(),c.text=c.text.trimEnd();else return;i.raw=i.raw.trimEnd();for(let l of i.items){if(this.lexer.state.top=!1,l.tokens=this.lexer.blockTokens(l.text,[]),l.task){if(l.text=l.text.replace(this.rules.other.listReplaceTask,""),l.tokens[0]?.type==="text"||l.tokens[0]?.type==="paragraph"){l.tokens[0].raw=l.tokens[0].raw.replace(this.rules.other.listReplaceTask,""),l.tokens[0].text=l.tokens[0].text.replace(this.rules.other.listReplaceTask,"");for(let d=this.lexer.inlineQueue.length-1;d>=0;d--)if(this.rules.other.listIsTask.test(this.lexer.inlineQueue[d].src)){this.lexer.inlineQueue[d].src=this.lexer.inlineQueue[d].src.replace(this.rules.other.listReplaceTask,"");break}}let p=this.rules.other.listTaskCheckbox.exec(l.raw);if(p){let d={type:"checkbox",raw:p[0]+" ",checked:p[0]!=="[ ]"};l.checked=d.checked,i.loose?l.tokens[0]&&["paragraph","text"].includes(l.tokens[0].type)&&"tokens"in l.tokens[0]&&l.tokens[0].tokens?(l.tokens[0].raw=d.raw+l.tokens[0].raw,l.tokens[0].text=d.raw+l.tokens[0].text,l.tokens[0].tokens.unshift(d)):l.tokens.unshift({type:"paragraph",raw:d.raw,text:d.raw,tokens:[d]}):l.tokens.unshift(d)}}if(!i.loose){let p=l.tokens.filter(u=>u.type==="space"),d=p.length>0&&p.some(u=>this.rules.other.anyLine.test(u.raw));i.loose=d}}if(i.loose)for(let l of i.items){l.loose=!0;for(let p of l.tokens)p.type==="text"&&(p.type="paragraph")}return i}}html(e){let t=this.rules.block.html.exec(e);if(t)return{type:"html",block:!0,raw:t[0],pre:t[1]==="pre"||t[1]==="script"||t[1]==="style",text:t[0]}}def(e){let t=this.rules.block.def.exec(e);if(t){let n=t[1].toLowerCase().replace(this.rules.other.multipleSpaceGlobal," "),s=t[2]?t[2].replace(this.rules.other.hrefBrackets,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",i=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:n,raw:t[0],href:s,title:i}}}table(e){let t=this.rules.block.table.exec(e);if(!t||!this.rules.other.tableDelimiter.test(t[2]))return;let n=ya(t[1]),s=t[2].replace(this.rules.other.tableAlignChars,"").split("|"),i=t[3]?.trim()?t[3].replace(this.rules.other.tableRowBlankLine,"").split(` -`):[],a={type:"table",raw:t[0],header:[],align:[],rows:[]};if(n.length===s.length){for(let o of s)this.rules.other.tableAlignRight.test(o)?a.align.push("right"):this.rules.other.tableAlignCenter.test(o)?a.align.push("center"):this.rules.other.tableAlignLeft.test(o)?a.align.push("left"):a.align.push(null);for(let o=0;o({text:c,tokens:this.lexer.inline(c),header:!1,align:a.align[l]})));return a}}lheading(e){let t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:t[2].charAt(0)==="="?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){let t=this.rules.block.paragraph.exec(e);if(t){let n=t[1].charAt(t[1].length-1)===` -`?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:n,tokens:this.lexer.inline(n)}}}text(e){let t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){let t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:t[1]}}tag(e){let t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){let t=this.rules.inline.link.exec(e);if(t){let n=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(n)){if(!this.rules.other.endAngleBracket.test(n))return;let a=gt(n.slice(0,-1),"\\");if((n.length-a.length)%2===0)return}else{let a=Eu(t[2],"()");if(a===-2)return;if(a>-1){let o=(t[0].indexOf("!")===0?5:4)+t[1].length+a;t[2]=t[2].substring(0,a),t[0]=t[0].substring(0,o).trim(),t[3]=""}}let s=t[2],i="";if(this.options.pedantic){let a=this.rules.other.pedanticHrefTitle.exec(s);a&&(s=a[1],i=a[3])}else i=t[3]?t[3].slice(1,-1):"";return s=s.trim(),this.rules.other.startAngleBracket.test(s)&&(this.options.pedantic&&!this.rules.other.endAngleBracket.test(n)?s=s.slice(1):s=s.slice(1,-1)),wa(t,{href:s&&s.replace(this.rules.inline.anyPunctuation,"$1"),title:i&&i.replace(this.rules.inline.anyPunctuation,"$1")},t[0],this.lexer,this.rules)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){let s=(n[2]||n[1]).replace(this.rules.other.multipleSpaceGlobal," "),i=t[s.toLowerCase()];if(!i){let a=n[0].charAt(0);return{type:"text",raw:a,text:a}}return wa(n,i,n[0],this.lexer,this.rules)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!(!s||s[3]&&n.match(this.rules.other.unicodeAlphaNumeric))&&(!(s[1]||s[2])||!n||this.rules.inline.punctuation.exec(n))){let i=[...s[0]].length-1,a,o,c=i,l=0,p=s[0][0]==="*"?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(p.lastIndex=0,t=t.slice(-1*e.length+i);(s=p.exec(t))!=null;){if(a=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!a)continue;if(o=[...a].length,s[3]||s[4]){c+=o;continue}else if((s[5]||s[6])&&i%3&&!((i+o)%3)){l+=o;continue}if(c-=o,c>0)continue;o=Math.min(o,o+c+l);let d=[...s[0]][0].length,u=e.slice(0,i+s.index+d+o);if(Math.min(i,o)%2){let v=u.slice(1,-1);return{type:"em",raw:u,text:v,tokens:this.lexer.inlineTokens(v)}}let h=u.slice(2,-2);return{type:"strong",raw:u,text:h,tokens:this.lexer.inlineTokens(h)}}}}codespan(e){let t=this.rules.inline.code.exec(e);if(t){let n=t[2].replace(this.rules.other.newLineCharGlobal," "),s=this.rules.other.nonSpaceChar.test(n),i=this.rules.other.startingSpaceChar.test(n)&&this.rules.other.endingSpaceChar.test(n);return s&&i&&(n=n.substring(1,n.length-1)),{type:"codespan",raw:t[0],text:n}}}br(e){let t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){let t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){let t=this.rules.inline.autolink.exec(e);if(t){let n,s;return t[2]==="@"?(n=t[1],s="mailto:"+n):(n=t[1],s=n),{type:"link",raw:t[0],text:n,href:s,tokens:[{type:"text",raw:n,text:n}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let n,s;if(t[2]==="@")n=t[0],s="mailto:"+n;else{let i;do i=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??"";while(i!==t[0]);n=t[0],t[1]==="www."?s="http://"+t[0]:s=t[0]}return{type:"link",raw:t[0],text:n,href:s,tokens:[{type:"text",raw:n,text:n}]}}}inlineText(e){let t=this.rules.inline.text.exec(e);if(t){let n=this.lexer.state.inRawBlock;return{type:"text",raw:t[0],text:t[0],escaped:n}}}},ie=class $s{tokens;options;state;inlineQueue;tokenizer;constructor(t){this.tokens=[],this.tokens.links=Object.create(null),this.options=t||Ke,this.options.tokenizer=this.options.tokenizer||new en,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};let n={other:Y,block:zt.normal,inline:ht.normal};this.options.pedantic?(n.block=zt.pedantic,n.inline=ht.pedantic):this.options.gfm&&(n.block=zt.gfm,this.options.breaks?n.inline=ht.breaks:n.inline=ht.gfm),this.tokenizer.rules=n}static get rules(){return{block:zt,inline:ht}}static lex(t,n){return new $s(n).lex(t)}static lexInline(t,n){return new $s(n).inlineTokens(t)}lex(t){t=t.replace(Y.carriageReturn,` -`),this.blockTokens(t,this.tokens);for(let n=0;n(i=o.call({lexer:this},t,n))?(t=t.substring(i.raw.length),n.push(i),!0):!1))continue;if(i=this.tokenizer.space(t)){t=t.substring(i.raw.length);let o=n.at(-1);i.raw.length===1&&o!==void 0?o.raw+=` -`:n.push(i);continue}if(i=this.tokenizer.code(t)){t=t.substring(i.raw.length);let o=n.at(-1);o?.type==="paragraph"||o?.type==="text"?(o.raw+=(o.raw.endsWith(` -`)?"":` -`)+i.raw,o.text+=` -`+i.text,this.inlineQueue.at(-1).src=o.text):n.push(i);continue}if(i=this.tokenizer.fences(t)){t=t.substring(i.raw.length),n.push(i);continue}if(i=this.tokenizer.heading(t)){t=t.substring(i.raw.length),n.push(i);continue}if(i=this.tokenizer.hr(t)){t=t.substring(i.raw.length),n.push(i);continue}if(i=this.tokenizer.blockquote(t)){t=t.substring(i.raw.length),n.push(i);continue}if(i=this.tokenizer.list(t)){t=t.substring(i.raw.length),n.push(i);continue}if(i=this.tokenizer.html(t)){t=t.substring(i.raw.length),n.push(i);continue}if(i=this.tokenizer.def(t)){t=t.substring(i.raw.length);let o=n.at(-1);o?.type==="paragraph"||o?.type==="text"?(o.raw+=(o.raw.endsWith(` -`)?"":` -`)+i.raw,o.text+=` -`+i.raw,this.inlineQueue.at(-1).src=o.text):this.tokens.links[i.tag]||(this.tokens.links[i.tag]={href:i.href,title:i.title},n.push(i));continue}if(i=this.tokenizer.table(t)){t=t.substring(i.raw.length),n.push(i);continue}if(i=this.tokenizer.lheading(t)){t=t.substring(i.raw.length),n.push(i);continue}let a=t;if(this.options.extensions?.startBlock){let o=1/0,c=t.slice(1),l;this.options.extensions.startBlock.forEach(p=>{l=p.call({lexer:this},c),typeof l=="number"&&l>=0&&(o=Math.min(o,l))}),o<1/0&&o>=0&&(a=t.substring(0,o+1))}if(this.state.top&&(i=this.tokenizer.paragraph(a))){let o=n.at(-1);s&&o?.type==="paragraph"?(o.raw+=(o.raw.endsWith(` -`)?"":` -`)+i.raw,o.text+=` -`+i.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=o.text):n.push(i),s=a.length!==t.length,t=t.substring(i.raw.length);continue}if(i=this.tokenizer.text(t)){t=t.substring(i.raw.length);let o=n.at(-1);o?.type==="text"?(o.raw+=(o.raw.endsWith(` -`)?"":` -`)+i.raw,o.text+=` -`+i.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=o.text):n.push(i);continue}if(t){let o="Infinite loop on byte: "+t.charCodeAt(0);if(this.options.silent){console.error(o);break}else throw new Error(o)}}return this.state.top=!0,n}inline(t,n=[]){return this.inlineQueue.push({src:t,tokens:n}),n}inlineTokens(t,n=[]){let s=t,i=null;if(this.tokens.links){let l=Object.keys(this.tokens.links);if(l.length>0)for(;(i=this.tokenizer.rules.inline.reflinkSearch.exec(s))!=null;)l.includes(i[0].slice(i[0].lastIndexOf("[")+1,-1))&&(s=s.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+s.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;(i=this.tokenizer.rules.inline.anyPunctuation.exec(s))!=null;)s=s.slice(0,i.index)+"++"+s.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);let a;for(;(i=this.tokenizer.rules.inline.blockSkip.exec(s))!=null;)a=i[2]?i[2].length:0,s=s.slice(0,i.index+a)+"["+"a".repeat(i[0].length-a-2)+"]"+s.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);s=this.options.hooks?.emStrongMask?.call({lexer:this},s)??s;let o=!1,c="";for(;t;){o||(c=""),o=!1;let l;if(this.options.extensions?.inline?.some(d=>(l=d.call({lexer:this},t,n))?(t=t.substring(l.raw.length),n.push(l),!0):!1))continue;if(l=this.tokenizer.escape(t)){t=t.substring(l.raw.length),n.push(l);continue}if(l=this.tokenizer.tag(t)){t=t.substring(l.raw.length),n.push(l);continue}if(l=this.tokenizer.link(t)){t=t.substring(l.raw.length),n.push(l);continue}if(l=this.tokenizer.reflink(t,this.tokens.links)){t=t.substring(l.raw.length);let d=n.at(-1);l.type==="text"&&d?.type==="text"?(d.raw+=l.raw,d.text+=l.text):n.push(l);continue}if(l=this.tokenizer.emStrong(t,s,c)){t=t.substring(l.raw.length),n.push(l);continue}if(l=this.tokenizer.codespan(t)){t=t.substring(l.raw.length),n.push(l);continue}if(l=this.tokenizer.br(t)){t=t.substring(l.raw.length),n.push(l);continue}if(l=this.tokenizer.del(t)){t=t.substring(l.raw.length),n.push(l);continue}if(l=this.tokenizer.autolink(t)){t=t.substring(l.raw.length),n.push(l);continue}if(!this.state.inLink&&(l=this.tokenizer.url(t))){t=t.substring(l.raw.length),n.push(l);continue}let p=t;if(this.options.extensions?.startInline){let d=1/0,u=t.slice(1),h;this.options.extensions.startInline.forEach(v=>{h=v.call({lexer:this},u),typeof h=="number"&&h>=0&&(d=Math.min(d,h))}),d<1/0&&d>=0&&(p=t.substring(0,d+1))}if(l=this.tokenizer.inlineText(p)){t=t.substring(l.raw.length),l.raw.slice(-1)!=="_"&&(c=l.raw.slice(-1)),o=!0;let d=n.at(-1);d?.type==="text"?(d.raw+=l.raw,d.text+=l.text):n.push(l);continue}if(t){let d="Infinite loop on byte: "+t.charCodeAt(0);if(this.options.silent){console.error(d);break}else throw new Error(d)}}return n}},tn=class{options;parser;constructor(e){this.options=e||Ke}space(e){return""}code({text:e,lang:t,escaped:n}){let s=(t||"").match(Y.notSpaceStart)?.[0],i=e.replace(Y.endingNewline,"")+` -`;return s?'

'+(n?i:me(i,!0))+`
-`:"
"+(n?i:me(i,!0))+`
-`}blockquote({tokens:e}){return`
-${this.parser.parse(e)}
-`}html({text:e}){return e}def(e){return""}heading({tokens:e,depth:t}){return`${this.parser.parseInline(e)} -`}hr(e){return`
-`}list(e){let t=e.ordered,n=e.start,s="";for(let o=0;o -`+s+" -`}listitem(e){return`
  • ${this.parser.parse(e.tokens)}
  • -`}checkbox({checked:e}){return" '}paragraph({tokens:e}){return`

    ${this.parser.parseInline(e)}

    -`}table(e){let t="",n="";for(let i=0;i${s}`),` - -`+t+` -`+s+`
    -`}tablerow({text:e}){return` -${e} -`}tablecell(e){let t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+` -`}strong({tokens:e}){return`${this.parser.parseInline(e)}`}em({tokens:e}){return`${this.parser.parseInline(e)}`}codespan({text:e}){return`${me(e,!0)}`}br(e){return"
    "}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:t,tokens:n}){let s=this.parser.parseInline(n),i=ba(e);if(i===null)return s;e=i;let a='
    ",a}image({href:e,title:t,text:n,tokens:s}){s&&(n=this.parser.parseInline(s,this.parser.textRenderer));let i=ba(e);if(i===null)return me(n);e=i;let a=`${n}{let o=i[a].flat(1/0);n=n.concat(this.walkTokens(o,t))}):i.tokens&&(n=n.concat(this.walkTokens(i.tokens,t)))}}return n}use(...e){let t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach(n=>{let s={...n};if(s.async=this.defaults.async||s.async||!1,n.extensions&&(n.extensions.forEach(i=>{if(!i.name)throw new Error("extension name required");if("renderer"in i){let a=t.renderers[i.name];a?t.renderers[i.name]=function(...o){let c=i.renderer.apply(this,o);return c===!1&&(c=a.apply(this,o)),c}:t.renderers[i.name]=i.renderer}if("tokenizer"in i){if(!i.level||i.level!=="block"&&i.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");let a=t[i.level];a?a.unshift(i.tokenizer):t[i.level]=[i.tokenizer],i.start&&(i.level==="block"?t.startBlock?t.startBlock.push(i.start):t.startBlock=[i.start]:i.level==="inline"&&(t.startInline?t.startInline.push(i.start):t.startInline=[i.start]))}"childTokens"in i&&i.childTokens&&(t.childTokens[i.name]=i.childTokens)}),s.extensions=t),n.renderer){let i=this.defaults.renderer||new tn(this.defaults);for(let a in n.renderer){if(!(a in i))throw new Error(`renderer '${a}' does not exist`);if(["options","parser"].includes(a))continue;let o=a,c=n.renderer[o],l=i[o];i[o]=(...p)=>{let d=c.apply(i,p);return d===!1&&(d=l.apply(i,p)),d||""}}s.renderer=i}if(n.tokenizer){let i=this.defaults.tokenizer||new en(this.defaults);for(let a in n.tokenizer){if(!(a in i))throw new Error(`tokenizer '${a}' does not exist`);if(["options","rules","lexer"].includes(a))continue;let o=a,c=n.tokenizer[o],l=i[o];i[o]=(...p)=>{let d=c.apply(i,p);return d===!1&&(d=l.apply(i,p)),d}}s.tokenizer=i}if(n.hooks){let i=this.defaults.hooks||new vt;for(let a in n.hooks){if(!(a in i))throw new Error(`hook '${a}' does not exist`);if(["options","block"].includes(a))continue;let o=a,c=n.hooks[o],l=i[o];vt.passThroughHooks.has(a)?i[o]=p=>{if(this.defaults.async&&vt.passThroughHooksRespectAsync.has(a))return(async()=>{let u=await c.call(i,p);return l.call(i,u)})();let d=c.call(i,p);return l.call(i,d)}:i[o]=(...p)=>{if(this.defaults.async)return(async()=>{let u=await c.apply(i,p);return u===!1&&(u=await l.apply(i,p)),u})();let d=c.apply(i,p);return d===!1&&(d=l.apply(i,p)),d}}s.hooks=i}if(n.walkTokens){let i=this.defaults.walkTokens,a=n.walkTokens;s.walkTokens=function(o){let c=[];return c.push(a.call(this,o)),i&&(c=c.concat(i.call(this,o))),c}}this.defaults={...this.defaults,...s}}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return ie.lex(e,t??this.defaults)}parser(e,t){return ae.parse(e,t??this.defaults)}parseMarkdown(e){return(t,n)=>{let s={...n},i={...this.defaults,...s},a=this.onError(!!i.silent,!!i.async);if(this.defaults.async===!0&&s.async===!1)return a(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(typeof t>"u"||t===null)return a(new Error("marked(): input parameter is undefined or null"));if(typeof t!="string")return a(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(t)+", string expected"));if(i.hooks&&(i.hooks.options=i,i.hooks.block=e),i.async)return(async()=>{let o=i.hooks?await i.hooks.preprocess(t):t,c=await(i.hooks?await i.hooks.provideLexer():e?ie.lex:ie.lexInline)(o,i),l=i.hooks?await i.hooks.processAllTokens(c):c;i.walkTokens&&await Promise.all(this.walkTokens(l,i.walkTokens));let p=await(i.hooks?await i.hooks.provideParser():e?ae.parse:ae.parseInline)(l,i);return i.hooks?await i.hooks.postprocess(p):p})().catch(a);try{i.hooks&&(t=i.hooks.preprocess(t));let o=(i.hooks?i.hooks.provideLexer():e?ie.lex:ie.lexInline)(t,i);i.hooks&&(o=i.hooks.processAllTokens(o)),i.walkTokens&&this.walkTokens(o,i.walkTokens);let c=(i.hooks?i.hooks.provideParser():e?ae.parse:ae.parseInline)(o,i);return i.hooks&&(c=i.hooks.postprocess(c)),c}catch(o){return a(o)}}}onError(e,t){return n=>{if(n.message+=` -Please report this to https://github.com/markedjs/marked.`,e){let s="

    An error occurred:

    "+me(n.message+"",!0)+"
    ";return t?Promise.resolve(s):s}if(t)return Promise.reject(n);throw n}}},Ue=new Mu;function N(e,t){return Ue.parse(e,t)}N.options=N.setOptions=function(e){return Ue.setOptions(e),N.defaults=Ue.defaults,Yo(N.defaults),N};N.getDefaults=Xs;N.defaults=Ke;N.use=function(...e){return Ue.use(...e),N.defaults=Ue.defaults,Yo(N.defaults),N};N.walkTokens=function(e,t){return Ue.walkTokens(e,t)};N.parseInline=Ue.parseInline;N.Parser=ae;N.parser=ae.parse;N.Renderer=tn;N.TextRenderer=ri;N.Lexer=ie;N.lexer=ie.lex;N.Tokenizer=en;N.Hooks=vt;N.parse=N;N.options;N.setOptions;N.use;N.walkTokens;N.parseInline;ae.parse;ie.lex;N.setOptions({gfm:!0,breaks:!0,mangle:!1});const $a=["a","b","blockquote","br","code","del","em","h1","h2","h3","h4","hr","i","li","ol","p","pre","strong","table","tbody","td","th","thead","tr","ul"],xa=["class","href","rel","target","title","start"];let ka=!1;const Iu=14e4,Ru=4e4,Pu=200,Jn=5e4,Ne=new Map;function Nu(e){const t=Ne.get(e);return t===void 0?null:(Ne.delete(e),Ne.set(e,t),t)}function Aa(e,t){if(Ne.set(e,t),Ne.size<=Pu)return;const n=Ne.keys().next().value;n&&Ne.delete(n)}function Ou(){ka||(ka=!0,ys.addHook("afterSanitizeAttributes",e=>{!(e instanceof HTMLAnchorElement)||!e.getAttribute("href")||(e.setAttribute("rel","noreferrer noopener"),e.setAttribute("target","_blank"))}))}function ks(e){const t=e.trim();if(!t)return"";if(Ou(),t.length<=Jn){const o=Nu(t);if(o!==null)return o}const n=ao(t,Iu),s=n.truncated?` - -… truncated (${n.total} chars, showing first ${n.text.length}).`:"";if(n.text.length>Ru){const c=`
    ${Du(`${n.text}${s}`)}
    `,l=ys.sanitize(c,{ALLOWED_TAGS:$a,ALLOWED_ATTR:xa});return t.length<=Jn&&Aa(t,l),l}const i=N.parse(`${n.text}${s}`),a=ys.sanitize(i,{ALLOWED_TAGS:$a,ALLOWED_ATTR:xa});return t.length<=Jn&&Aa(t,a),a}function Du(e){return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}const Bu=1500,Fu=2e3,or="Copy as markdown",Uu="Copied",Ku="Copy failed";async function Hu(e){if(!e)return!1;try{return await navigator.clipboard.writeText(e),!0}catch{return!1}}function jt(e,t){e.title=t,e.setAttribute("aria-label",t)}function zu(e){const t=e.label??or;return r` - - `}function ju(e){return zu({text:()=>e,label:or})}const qu={icon:"puzzle",detailKeys:["command","path","url","targetUrl","targetId","ref","element","node","nodeId","id","requestId","to","channelId","guildId","userId","name","query","pattern","messageId"]},Vu={bash:{icon:"wrench",title:"Bash",detailKeys:["command"]},process:{icon:"wrench",title:"Process",detailKeys:["sessionId"]},read:{icon:"fileText",title:"Read",detailKeys:["path"]},write:{icon:"edit",title:"Write",detailKeys:["path"]},edit:{icon:"penLine",title:"Edit",detailKeys:["path"]},attach:{icon:"paperclip",title:"Attach",detailKeys:["path","url","fileName"]},browser:{icon:"globe",title:"Browser",actions:{status:{label:"status"},start:{label:"start"},stop:{label:"stop"},tabs:{label:"tabs"},open:{label:"open",detailKeys:["targetUrl"]},focus:{label:"focus",detailKeys:["targetId"]},close:{label:"close",detailKeys:["targetId"]},snapshot:{label:"snapshot",detailKeys:["targetUrl","targetId","ref","element","format"]},screenshot:{label:"screenshot",detailKeys:["targetUrl","targetId","ref","element"]},navigate:{label:"navigate",detailKeys:["targetUrl","targetId"]},console:{label:"console",detailKeys:["level","targetId"]},pdf:{label:"pdf",detailKeys:["targetId"]},upload:{label:"upload",detailKeys:["paths","ref","inputRef","element","targetId"]},dialog:{label:"dialog",detailKeys:["accept","promptText","targetId"]},act:{label:"act",detailKeys:["request.kind","request.ref","request.selector","request.text","request.value"]}}},canvas:{icon:"image",title:"Canvas",actions:{present:{label:"present",detailKeys:["target","node","nodeId"]},hide:{label:"hide",detailKeys:["node","nodeId"]},navigate:{label:"navigate",detailKeys:["url","node","nodeId"]},eval:{label:"eval",detailKeys:["javaScript","node","nodeId"]},snapshot:{label:"snapshot",detailKeys:["format","node","nodeId"]},a2ui_push:{label:"A2UI push",detailKeys:["jsonlPath","node","nodeId"]},a2ui_reset:{label:"A2UI reset",detailKeys:["node","nodeId"]}}},nodes:{icon:"smartphone",title:"Nodes",actions:{status:{label:"status"},describe:{label:"describe",detailKeys:["node","nodeId"]},pending:{label:"pending"},approve:{label:"approve",detailKeys:["requestId"]},reject:{label:"reject",detailKeys:["requestId"]},notify:{label:"notify",detailKeys:["node","nodeId","title","body"]},camera_snap:{label:"camera snap",detailKeys:["node","nodeId","facing","deviceId"]},camera_list:{label:"camera list",detailKeys:["node","nodeId"]},camera_clip:{label:"camera clip",detailKeys:["node","nodeId","facing","duration","durationMs"]},screen_record:{label:"screen record",detailKeys:["node","nodeId","duration","durationMs","fps","screenIndex"]}}},cron:{icon:"loader",title:"Cron",actions:{status:{label:"status"},list:{label:"list"},add:{label:"add",detailKeys:["job.name","job.id","job.schedule","job.cron"]},update:{label:"update",detailKeys:["id"]},remove:{label:"remove",detailKeys:["id"]},run:{label:"run",detailKeys:["id"]},runs:{label:"runs",detailKeys:["id"]},wake:{label:"wake",detailKeys:["text","mode"]}}},gateway:{icon:"plug",title:"Gateway",actions:{restart:{label:"restart",detailKeys:["reason","delayMs"]},"config.get":{label:"config get"},"config.schema":{label:"config schema"},"config.apply":{label:"config apply",detailKeys:["restartDelayMs"]},"update.run":{label:"update run",detailKeys:["restartDelayMs"]}}},whatsapp_login:{icon:"circle",title:"WhatsApp Login",actions:{start:{label:"start"},wait:{label:"wait"}}},discord:{icon:"messageSquare",title:"Discord",actions:{react:{label:"react",detailKeys:["channelId","messageId","emoji"]},reactions:{label:"reactions",detailKeys:["channelId","messageId"]},sticker:{label:"sticker",detailKeys:["to","stickerIds"]},poll:{label:"poll",detailKeys:["question","to"]},permissions:{label:"permissions",detailKeys:["channelId"]},readMessages:{label:"read messages",detailKeys:["channelId","limit"]},sendMessage:{label:"send",detailKeys:["to","content"]},editMessage:{label:"edit",detailKeys:["channelId","messageId"]},deleteMessage:{label:"delete",detailKeys:["channelId","messageId"]},threadCreate:{label:"thread create",detailKeys:["channelId","name"]},threadList:{label:"thread list",detailKeys:["guildId","channelId"]},threadReply:{label:"thread reply",detailKeys:["channelId","content"]},pinMessage:{label:"pin",detailKeys:["channelId","messageId"]},unpinMessage:{label:"unpin",detailKeys:["channelId","messageId"]},listPins:{label:"list pins",detailKeys:["channelId"]},searchMessages:{label:"search",detailKeys:["guildId","content"]},memberInfo:{label:"member",detailKeys:["guildId","userId"]},roleInfo:{label:"roles",detailKeys:["guildId"]},emojiList:{label:"emoji list",detailKeys:["guildId"]},roleAdd:{label:"role add",detailKeys:["guildId","userId","roleId"]},roleRemove:{label:"role remove",detailKeys:["guildId","userId","roleId"]},channelInfo:{label:"channel",detailKeys:["channelId"]},channelList:{label:"channels",detailKeys:["guildId"]},voiceStatus:{label:"voice",detailKeys:["guildId","userId"]},eventList:{label:"events",detailKeys:["guildId"]},eventCreate:{label:"event create",detailKeys:["guildId","name"]},timeout:{label:"timeout",detailKeys:["guildId","userId"]},kick:{label:"kick",detailKeys:["guildId","userId"]},ban:{label:"ban",detailKeys:["guildId","userId"]}}},slack:{icon:"messageSquare",title:"Slack",actions:{react:{label:"react",detailKeys:["channelId","messageId","emoji"]},reactions:{label:"reactions",detailKeys:["channelId","messageId"]},sendMessage:{label:"send",detailKeys:["to","content"]},editMessage:{label:"edit",detailKeys:["channelId","messageId"]},deleteMessage:{label:"delete",detailKeys:["channelId","messageId"]},readMessages:{label:"read messages",detailKeys:["channelId","limit"]},pinMessage:{label:"pin",detailKeys:["channelId","messageId"]},unpinMessage:{label:"unpin",detailKeys:["channelId","messageId"]},listPins:{label:"list pins",detailKeys:["channelId"]},memberInfo:{label:"member",detailKeys:["userId"]},emojiList:{label:"emoji list"}}}},Wu={fallback:qu,tools:Vu},rr=Wu,Sa=rr.fallback??{icon:"puzzle"},Gu=rr.tools??{};function Yu(e){return(e??"tool").trim()}function Qu(e){const t=e.replace(/_/g," ").trim();return t?t.split(/\s+/).map(n=>n.length<=2&&n.toUpperCase()===n?n:`${n.at(0)?.toUpperCase()??""}${n.slice(1)}`).join(" "):"Tool"}function Zu(e){const t=e?.trim();if(t)return t.replace(/_/g," ")}function lr(e){if(e!=null){if(typeof e=="string"){const t=e.trim();if(!t)return;const n=t.split(/\r?\n/)[0]?.trim()??"";return n?n.length>160?`${n.slice(0,157)}…`:n:void 0}if(typeof e=="number"||typeof e=="boolean")return String(e);if(Array.isArray(e)){const t=e.map(s=>lr(s)).filter(s=>!!s);if(t.length===0)return;const n=t.slice(0,3).join(", ");return t.length>3?`${n}…`:n}}}function Ju(e,t){if(!e||typeof e!="object")return;let n=e;for(const s of t.split(".")){if(!s||!n||typeof n!="object")return;n=n[s]}return n}function Xu(e,t){for(const n of t){const s=Ju(e,n),i=lr(s);if(i)return i}}function ep(e){if(!e||typeof e!="object")return;const t=e,n=typeof t.path=="string"?t.path:void 0;if(!n)return;const s=typeof t.offset=="number"?t.offset:void 0,i=typeof t.limit=="number"?t.limit:void 0;return s!==void 0&&i!==void 0?`${n}:${s}-${s+i}`:n}function tp(e){if(!e||typeof e!="object")return;const t=e;return typeof t.path=="string"?t.path:void 0}function np(e,t){if(!(!e||!t))return e.actions?.[t]??void 0}function sp(e){const t=Yu(e.name),n=t.toLowerCase(),s=Gu[n],i=s?.icon??Sa.icon??"puzzle",a=s?.title??Qu(t),o=s?.label??t,c=e.args&&typeof e.args=="object"?e.args.action:void 0,l=typeof c=="string"?c.trim():void 0,p=np(s,l),d=Zu(p?.label??l);let u;n==="read"&&(u=ep(e.args)),!u&&(n==="write"||n==="edit"||n==="attach")&&(u=tp(e.args));const h=p?.detailKeys??s?.detailKeys??Sa.detailKeys??[];return!u&&h.length>0&&(u=Xu(e.args,h)),!u&&e.meta&&(u=e.meta),u&&(u=ap(u)),{name:t,icon:i,title:a,label:o,verb:d,detail:u}}function ip(e){const t=[];if(e.verb&&t.push(e.verb),e.detail&&t.push(e.detail),t.length!==0)return t.join(" · ")}function ap(e){return e&&e.replace(/\/Users\/[^/]+/g,"~").replace(/\/home\/[^/]+/g,"~")}const op=80,rp=2,_a=100;function lp(e){const t=e.trim();if(t.startsWith("{")||t.startsWith("["))try{const n=JSON.parse(t);return"```json\n"+JSON.stringify(n,null,2)+"\n```"}catch{}return e}function cp(e){const t=e.split(` -`),n=t.slice(0,rp),s=n.join(` -`);return s.length>_a?s.slice(0,_a)+"…":n.lengthi.kind==="result")){const i=typeof t.toolName=="string"&&t.toolName||typeof t.tool_name=="string"&&t.tool_name||"tool",a=oo(e)??void 0;s.push({kind:"result",name:i,text:a})}return s}function Ta(e,t){const n=sp({name:e.name,args:e.args}),s=ip(n),i=!!e.text?.trim(),a=!!t,o=a?()=>{if(i){t(lp(e.text));return}const u=`## ${n.label} - -${s?`**Command:** \`${s}\` - -`:""}*No output — tool completed successfully.*`;t(u)}:void 0,c=i&&(e.text?.length??0)<=op,l=i&&!c,p=i&&c,d=!i;return r` -
    {u.key!=="Enter"&&u.key!==" "||(u.preventDefault(),o?.())}:g} - > -
    -
    - ${Q[n.icon]} - ${n.label} -
    - ${a?r`${i?"View":""} ${Q.check}`:g} - ${d&&!a?r`${Q.check}`:g} -
    - ${s?r`
    ${s}
    `:g} - ${d?r`
    Completed
    `:g} - ${l?r`
    ${cp(e.text)}
    `:g} - ${p?r`
    ${e.text}
    `:g} -
    - `}function up(e){return Array.isArray(e)?e.filter(Boolean):[]}function pp(e){if(typeof e!="string")return e;const t=e.trim();if(!t||!t.startsWith("{")&&!t.startsWith("["))return e;try{return JSON.parse(t)}catch{return e}}function fp(e){if(typeof e.text=="string")return e.text;if(typeof e.content=="string")return e.content}function hp(e){return r` -
    - ${li("assistant",e)} -
    - -
    -
    - `}function gp(e,t,n,s){const i=new Date(t).toLocaleTimeString([],{hour:"numeric",minute:"2-digit"}),a=s?.name??"Assistant";return r` -
    - ${li("assistant",s)} -
    - ${cr({role:"assistant",content:[{type:"text",text:e}],timestamp:t},{isStreaming:!0,showReasoning:!1},n)} - -
    -
    - `}function vp(e,t){const n=Js(e.role),s=t.assistantName??"Assistant",i=n==="user"?"You":n==="assistant"?s:n,a=n==="user"?"user":n==="assistant"?"assistant":"other",o=new Date(e.timestamp).toLocaleTimeString([],{hour:"numeric",minute:"2-digit"});return r` -
    - ${li(e.role,{name:s,avatar:t.assistantAvatar??null})} -
    - ${e.messages.map((c,l)=>cr(c.message,{isStreaming:e.isStreaming&&l===e.messages.length-1,showReasoning:t.showReasoning},t.onOpenSidebar))} - -
    -
    - `}function li(e,t){const n=Js(e),s=t?.name?.trim()||"Assistant",i=t?.avatar?.trim()||"",a=n==="user"?"U":n==="assistant"?s.charAt(0).toUpperCase()||"A":n==="tool"?"⚙":"?",o=n==="user"?"user":n==="assistant"?"assistant":n==="tool"?"tool":"other";return i&&n==="assistant"?mp(i)?r`${s}`:r`
    ${i}
    `:r`
    ${a}
    `}function mp(e){return/^https?:\/\//i.test(e)||/^data:image\//i.test(e)||/^\//.test(e)}function cr(e,t,n){const s=e,i=typeof s.role=="string"?s.role:"unknown",a=jo(e)||i.toLowerCase()==="toolresult"||i.toLowerCase()==="tool_result"||typeof s.toolCallId=="string"||typeof s.tool_call_id=="string",o=dp(e),c=o.length>0,l=oo(e),p=t.showReasoning&&i==="assistant"?Al(e):null,d=l?.trim()?l:null,u=p?_l(p):null,h=d,v=i==="assistant"&&!!h?.trim(),w=["chat-bubble",v?"has-copy":"",t.isStreaming?"streaming":"","fade-in"].filter(Boolean).join(" ");return!h&&c&&a?r`${o.map($=>Ta($,n))}`:!h&&!c?g:r` -
    - ${v?ju(h):g} - ${u?r`
    ${gs(ks(u))}
    `:g} - ${h?r`
    ${gs(ks(h))}
    `:g} - ${o.map($=>Ta($,n))} -
    - `}function bp(e){return r` - - `}var yp=Object.defineProperty,wp=Object.getOwnPropertyDescriptor,mn=(e,t,n,s)=>{for(var i=s>1?void 0:s?wp(t,n):t,a=e.length-1,o;a>=0;a--)(o=e[a])&&(i=(s?o(t,n,i):o(i))||i);return s&&i&&yp(t,n,i),i};let nt=class extends Ze{constructor(){super(...arguments),this.splitRatio=.6,this.minRatio=.4,this.maxRatio=.7,this.isDragging=!1,this.startX=0,this.startRatio=0,this.handleMouseDown=e=>{this.isDragging=!0,this.startX=e.clientX,this.startRatio=this.splitRatio,this.classList.add("dragging"),document.addEventListener("mousemove",this.handleMouseMove),document.addEventListener("mouseup",this.handleMouseUp),e.preventDefault()},this.handleMouseMove=e=>{if(!this.isDragging)return;const t=this.parentElement;if(!t)return;const n=t.getBoundingClientRect().width,i=(e.clientX-this.startX)/n;let a=this.startRatio+i;a=Math.max(this.minRatio,Math.min(this.maxRatio,a)),this.dispatchEvent(new CustomEvent("resize",{detail:{splitRatio:a},bubbles:!0,composed:!0}))},this.handleMouseUp=()=>{this.isDragging=!1,this.classList.remove("dragging"),document.removeEventListener("mousemove",this.handleMouseMove),document.removeEventListener("mouseup",this.handleMouseUp)}}render(){return r``}connectedCallback(){super.connectedCallback(),this.addEventListener("mousedown",this.handleMouseDown)}disconnectedCallback(){super.disconnectedCallback(),this.removeEventListener("mousedown",this.handleMouseDown),document.removeEventListener("mousemove",this.handleMouseMove),document.removeEventListener("mouseup",this.handleMouseUp)}};nt.styles=Dr` - :host { - width: 4px; - cursor: col-resize; - background: var(--border, #333); - transition: background 150ms ease-out; - flex-shrink: 0; - position: relative; - } - - :host::before { - content: ""; - position: absolute; - top: 0; - left: -4px; - right: -4px; - bottom: 0; - } - - :host(:hover) { - background: var(--accent, #007bff); - } - - :host(.dragging) { - background: var(--accent, #007bff); - } - `;mn([on({type:Number})],nt.prototype,"splitRatio",2);mn([on({type:Number})],nt.prototype,"minRatio",2);mn([on({type:Number})],nt.prototype,"maxRatio",2);nt=mn([Ja("resizable-divider")],nt);const $p=5e3;function xp(e){return e?e.active?r` -
    - ${Q.loader} Compacting context... -
    - `:e.completedAt&&Date.now()-e.completedAt<$p?r` -
    - ${Q.check} Context compacted -
    - `:g:g}function kp(e){const t=e.connected,n=e.sending||e.stream!==null,s=!!(e.canAbort&&e.onAbort),a=e.sessions?.sessions?.find(h=>h.key===e.sessionKey)?.reasoningLevel??"off",o=e.showThinking&&a!=="off",c={name:e.assistantName,avatar:e.assistantAvatar??e.assistantAvatarUrl??null},l=e.connected?"Message (↩ to send, Shift+↩ for line breaks)":"Connect to the gateway to start chatting…",p=e.splitRatio??.6,d=!!(e.sidebarOpen&&e.onCloseSidebar),u=r` -
    - ${e.loading?r`
    Loading chat…
    `:g} - ${Ho(Sp(e),h=>h.key,h=>h.kind==="reading-indicator"?hp(c):h.kind==="stream"?gp(h.text,h.startedAt,e.onOpenSidebar,c):h.kind==="group"?vp(h,{onOpenSidebar:e.onOpenSidebar,showReasoning:o,assistantName:e.assistantName,assistantAvatar:c.avatar}):g)} -
    - `;return r` -
    - ${e.disabledReason?r`
    ${e.disabledReason}
    `:g} - - ${e.error?r`
    ${e.error}
    `:g} - - ${xp(e.compactionStatus)} - - ${e.focusMode?r` - - `:g} - -
    -
    - ${u} -
    - - ${d?r` - e.onSplitRatioChange?.(h.detail.splitRatio)} - > -
    - ${bp({content:e.sidebarContent??null,error:e.sidebarError??null,onClose:e.onCloseSidebar,onViewRawText:()=>{!e.sidebarContent||!e.onOpenSidebar||e.onOpenSidebar(`\`\`\` -${e.sidebarContent} -\`\`\``)}})} -
    - `:g} -
    - - ${e.queue.length?r` -
    -
    Queued (${e.queue.length})
    -
    - ${e.queue.map(h=>r` -
    -
    ${h.text}
    - -
    - `)} -
    -
    - `:g} - -
    - -
    - - -
    -
    -
    - `}const Ca=200;function Ap(e){const t=[];let n=null;for(const s of e){if(s.kind!=="message"){n&&(t.push(n),n=null),t.push(s);continue}const i=zo(s.message),a=Js(i.role),o=i.timestamp||Date.now();!n||n.role!==a?(n&&t.push(n),n={kind:"group",key:`group:${a}:${s.key}`,role:a,messages:[{message:s.message,key:s.key}],timestamp:o,isStreaming:!1}):n.messages.push({message:s.message,key:s.key})}return n&&t.push(n),t}function Sp(e){const t=[],n=Array.isArray(e.messages)?e.messages:[],s=Array.isArray(e.toolMessages)?e.toolMessages:[],i=Math.max(0,n.length-Ca);i>0&&t.push({kind:"message",key:"chat:history:notice",message:{role:"system",content:`Showing last ${Ca} messages (${i} hidden).`,timestamp:Date.now()}});for(let a=i;a0?t.push({kind:"stream",key:a,text:e.stream,startedAt:e.streamStartedAt??Date.now()}):t.push({kind:"reading-indicator",key:a})}return Ap(t)}function Ea(e,t){const n=e,s=typeof n.toolCallId=="string"?n.toolCallId:"";if(s)return`tool:${s}`;const i=typeof n.id=="string"?n.id:"";if(i)return`msg:${i}`;const a=typeof n.messageId=="string"?n.messageId:"";if(a)return`msg:${a}`;const o=typeof n.timestamp=="number"?n.timestamp:null,c=typeof n.role=="string"?n.role:"unknown";return o!=null?`msg:${c}:${o}:${t}`:`msg:${c}:${t}`}function ue(e){if(e)return Array.isArray(e.type)?e.type.filter(n=>n!=="null")[0]??e.type[0]:e.type}function dr(e){if(!e)return"";if(e.default!==void 0)return e.default;switch(ue(e)){case"object":return{};case"array":return[];case"boolean":return!1;case"number":case"integer":return 0;case"string":return"";default:return""}}function bn(e){return e.filter(t=>typeof t=="string").join(".")}function te(e,t){const n=bn(e),s=t[n];if(s)return s;const i=n.split(".");for(const[a,o]of Object.entries(t)){if(!a.includes("*"))continue;const c=a.split(".");if(c.length!==i.length)continue;let l=!0;for(let p=0;pt.toUpperCase())}function _p(e){const t=bn(e).toLowerCase();return t.includes("token")||t.includes("password")||t.includes("secret")||t.includes("apikey")||t.endsWith("key")}const Tp=new Set(["title","description","default","nullable"]);function Cp(e){return Object.keys(e??{}).filter(n=>!Tp.has(n)).length===0}function Ep(e){if(e===void 0)return"";try{return JSON.stringify(e,null,2)??""}catch{return""}}const _t={chevronDown:r``,plus:r``,minus:r``,trash:r``,edit:r``};function ye(e){const{schema:t,value:n,path:s,hints:i,unsupported:a,disabled:o,onPatch:c}=e,l=e.showLabel??!0,p=ue(t),d=te(s,i),u=d?.label??t.title??we(String(s.at(-1))),h=d?.help??t.description,v=bn(s);if(a.has(v))return r`
    -
    ${u}
    -
    Unsupported schema node. Use Raw mode.
    -
    `;if(t.anyOf||t.oneOf){const $=(t.anyOf??t.oneOf??[]).filter(C=>!(C.type==="null"||Array.isArray(C.type)&&C.type.includes("null")));if($.length===1)return ye({...e,schema:$[0]});const k=C=>{if(C.const!==void 0)return C.const;if(C.enum&&C.enum.length===1)return C.enum[0]},T=$.map(k),M=T.every(C=>C!==void 0);if(M&&T.length>0&&T.length<=5){const C=n??t.default;return r` -
    - ${l?r``:g} - ${h?r`
    ${h}
    `:g} -
    - ${T.map((E,pe)=>r` - - `)} -
    -
    - `}if(M&&T.length>5)return Ma({...e,options:T,value:n??t.default});const P=new Set($.map(C=>ue(C)).filter(Boolean)),L=new Set([...P].map(C=>C==="integer"?"number":C));if([...L].every(C=>["string","number","boolean"].includes(C))){const C=L.has("string"),E=L.has("number");if(L.has("boolean")&&L.size===1)return ye({...e,schema:{...t,type:"boolean",anyOf:void 0,oneOf:void 0}});if(C||E)return La({...e,inputType:E&&!C?"number":"text"})}}if(t.enum){const w=t.enum;if(w.length<=5){const $=n??t.default;return r` -
    - ${l?r``:g} - ${h?r`
    ${h}
    `:g} -
    - ${w.map(k=>r` - - `)} -
    -
    - `}return Ma({...e,options:w,value:n??t.default})}if(p==="object")return Mp(e);if(p==="array")return Ip(e);if(p==="boolean"){const w=typeof n=="boolean"?n:typeof t.default=="boolean"?t.default:!1;return r` - - `}return p==="number"||p==="integer"?Lp(e):p==="string"?La({...e,inputType:"text"}):r` -
    -
    ${u}
    -
    Unsupported type: ${p}. Use Raw mode.
    -
    - `}function La(e){const{schema:t,value:n,path:s,hints:i,disabled:a,onPatch:o,inputType:c}=e,l=e.showLabel??!0,p=te(s,i),d=p?.label??t.title??we(String(s.at(-1))),u=p?.help??t.description,h=p?.sensitive??_p(s),v=p?.placeholder??(h?"••••":t.default!==void 0?`Default: ${t.default}`:""),w=n??"";return r` -
    - ${l?r``:g} - ${u?r`
    ${u}
    `:g} -
    - {const k=$.target.value;if(c==="number"){if(k.trim()===""){o(s,void 0);return}const T=Number(k);o(s,Number.isNaN(T)?k:T);return}o(s,k)}} - /> - ${t.default!==void 0?r` - - `:g} -
    -
    - `}function Lp(e){const{schema:t,value:n,path:s,hints:i,disabled:a,onPatch:o}=e,c=e.showLabel??!0,l=te(s,i),p=l?.label??t.title??we(String(s.at(-1))),d=l?.help??t.description,u=n??t.default??"",h=typeof u=="number"?u:0;return r` -
    - ${c?r``:g} - ${d?r`
    ${d}
    `:g} -
    - - {const w=v.target.value,$=w===""?void 0:Number(w);o(s,$)}} - /> - -
    -
    - `}function Ma(e){const{schema:t,value:n,path:s,hints:i,disabled:a,options:o,onPatch:c}=e,l=e.showLabel??!0,p=te(s,i),d=p?.label??t.title??we(String(s.at(-1))),u=p?.help??t.description,h=n??t.default,v=o.findIndex($=>$===h||String($)===String(h)),w="__unset__";return r` -
    - ${l?r``:g} - ${u?r`
    ${u}
    `:g} - -
    - `}function Mp(e){const{schema:t,value:n,path:s,hints:i,unsupported:a,disabled:o,onPatch:c}=e;e.showLabel;const l=te(s,i),p=l?.label??t.title??we(String(s.at(-1))),d=l?.help??t.description,u=n??t.default,h=u&&typeof u=="object"&&!Array.isArray(u)?u:{},v=t.properties??{},$=Object.entries(v).sort((P,L)=>{const C=te([...s,P[0]],i)?.order??0,E=te([...s,L[0]],i)?.order??0;return C!==E?C-E:P[0].localeCompare(L[0])}),k=new Set(Object.keys(v)),T=t.additionalProperties,M=!!T&&typeof T=="object";return s.length===1?r` -
    - ${$.map(([P,L])=>ye({schema:L,value:h[P],path:[...s,P],hints:i,unsupported:a,disabled:o,onPatch:c}))} - ${M?Ia({schema:T,value:h,path:s,hints:i,unsupported:a,disabled:o,reservedKeys:k,onPatch:c}):g} -
    - `:r` -
    - - ${p} - ${_t.chevronDown} - - ${d?r`
    ${d}
    `:g} -
    - ${$.map(([P,L])=>ye({schema:L,value:h[P],path:[...s,P],hints:i,unsupported:a,disabled:o,onPatch:c}))} - ${M?Ia({schema:T,value:h,path:s,hints:i,unsupported:a,disabled:o,reservedKeys:k,onPatch:c}):g} -
    -
    - `}function Ip(e){const{schema:t,value:n,path:s,hints:i,unsupported:a,disabled:o,onPatch:c}=e,l=e.showLabel??!0,p=te(s,i),d=p?.label??t.title??we(String(s.at(-1))),u=p?.help??t.description,h=Array.isArray(t.items)?t.items[0]:t.items;if(!h)return r` -
    -
    ${d}
    -
    Unsupported array schema. Use Raw mode.
    -
    - `;const v=Array.isArray(n)?n:Array.isArray(t.default)?t.default:[];return r` -
    -
    - ${l?r`${d}`:g} - ${v.length} item${v.length!==1?"s":""} - -
    - ${u?r`
    ${u}
    `:g} - - ${v.length===0?r` -
    - No items yet. Click "Add" to create one. -
    - `:r` -
    - ${v.map((w,$)=>r` -
    -
    - #${$+1} - -
    -
    - ${ye({schema:h,value:w,path:[...s,$],hints:i,unsupported:a,disabled:o,showLabel:!1,onPatch:c})} -
    -
    - `)} -
    - `} -
    - `}function Ia(e){const{schema:t,value:n,path:s,hints:i,unsupported:a,disabled:o,reservedKeys:c,onPatch:l}=e,p=Cp(t),d=Object.entries(n??{}).filter(([u])=>!c.has(u));return r` -
    -
    - Custom entries - -
    - - ${d.length===0?r` -
    No custom entries.
    - `:r` -
    - ${d.map(([u,h])=>{const v=[...s,u],w=Ep(h);return r` -
    -
    - {const k=$.target.value.trim();if(!k||k===u)return;const T={...n??{}};k in T||(T[k]=T[u],delete T[u],l(s,T))}} - /> -
    -
    - ${p?r` - - `:ye({schema:t,value:h,path:v,hints:i,unsupported:a,disabled:o,showLabel:!1,onPatch:l})} -
    - -
    - `})} -
    - `} -
    - `}const Ra={env:r``,update:r``,agents:r``,auth:r``,channels:r``,messages:r``,commands:r``,hooks:r``,skills:r``,tools:r``,gateway:r``,wizard:r``,meta:r``,logging:r``,browser:r``,ui:r``,models:r``,bindings:r``,broadcast:r``,audio:r``,session:r``,cron:r``,web:r``,discovery:r``,canvasHost:r``,talk:r``,plugins:r``,default:r``},ci={env:{label:"Environment Variables",description:"Environment variables passed to the gateway process"},update:{label:"Updates",description:"Auto-update settings and release channel"},agents:{label:"Agents",description:"Agent configurations, models, and identities"},auth:{label:"Authentication",description:"API keys and authentication profiles"},channels:{label:"Channels",description:"Messaging channels (Telegram, Discord, Slack, etc.)"},messages:{label:"Messages",description:"Message handling and routing settings"},commands:{label:"Commands",description:"Custom slash commands"},hooks:{label:"Hooks",description:"Webhooks and event hooks"},skills:{label:"Skills",description:"Skill packs and capabilities"},tools:{label:"Tools",description:"Tool configurations (browser, search, etc.)"},gateway:{label:"Gateway",description:"Gateway server settings (port, auth, binding)"},wizard:{label:"Setup Wizard",description:"Setup wizard state and history"},meta:{label:"Metadata",description:"Gateway metadata and version information"},logging:{label:"Logging",description:"Log levels and output configuration"},browser:{label:"Browser",description:"Browser automation settings"},ui:{label:"UI",description:"User interface preferences"},models:{label:"Models",description:"AI model configurations and providers"},bindings:{label:"Bindings",description:"Key bindings and shortcuts"},broadcast:{label:"Broadcast",description:"Broadcast and notification settings"},audio:{label:"Audio",description:"Audio input/output settings"},session:{label:"Session",description:"Session management and persistence"},cron:{label:"Cron",description:"Scheduled tasks and automation"},web:{label:"Web",description:"Web server and API settings"},discovery:{label:"Discovery",description:"Service discovery and networking"},canvasHost:{label:"Canvas Host",description:"Canvas rendering and display"},talk:{label:"Talk",description:"Voice and speech settings"},plugins:{label:"Plugins",description:"Plugin management and extensions"}};function Pa(e){return Ra[e]??Ra.default}function Rp(e,t,n){if(!n)return!0;const s=n.toLowerCase(),i=ci[e];return e.toLowerCase().includes(s)||i&&(i.label.toLowerCase().includes(s)||i.description.toLowerCase().includes(s))?!0:mt(t,s)}function mt(e,t){if(e.title?.toLowerCase().includes(t)||e.description?.toLowerCase().includes(t)||e.enum?.some(s=>String(s).toLowerCase().includes(t)))return!0;if(e.properties){for(const[s,i]of Object.entries(e.properties))if(s.toLowerCase().includes(t)||mt(i,t))return!0}if(e.items){const s=Array.isArray(e.items)?e.items:[e.items];for(const i of s)if(i&&mt(i,t))return!0}if(e.additionalProperties&&typeof e.additionalProperties=="object"&&mt(e.additionalProperties,t))return!0;const n=e.anyOf??e.oneOf??e.allOf;if(n){for(const s of n)if(s&&mt(s,t))return!0}return!1}function Pp(e){if(!e.schema)return r`
    Schema unavailable.
    `;const t=e.schema,n=e.value??{};if(ue(t)!=="object"||!t.properties)return r`
    Unsupported schema. Use Raw.
    `;const s=new Set(e.unsupportedPaths??[]),i=t.properties,a=e.searchQuery??"",o=e.activeSection,c=e.activeSubsection??null,p=Object.entries(i).sort((u,h)=>{const v=te([u[0]],e.uiHints)?.order??50,w=te([h[0]],e.uiHints)?.order??50;return v!==w?v-w:u[0].localeCompare(h[0])}).filter(([u,h])=>!(o&&u!==o||a&&!Rp(u,h,a)));let d=null;if(o&&c&&p.length===1){const u=p[0]?.[1];u&&ue(u)==="object"&&u.properties&&u.properties[c]&&(d={sectionKey:o,subsectionKey:c,schema:u.properties[c]})}return p.length===0?r` -
    -
    ${Q.search}
    -
    - ${a?`No settings match "${a}"`:"No settings in this section"} -
    -
    - `:r` -
    - ${d?(()=>{const{sectionKey:u,subsectionKey:h,schema:v}=d,w=te([u,h],e.uiHints),$=w?.label??v.title??we(h),k=w?.help??v.description??"",T=n[u],M=T&&typeof T=="object"?T[h]:void 0,P=`config-section-${u}-${h}`;return r` -
    -
    - ${Pa(u)} -
    -

    ${$}

    - ${k?r`

    ${k}

    `:g} -
    -
    -
    - ${ye({schema:v,value:M,path:[u,h],hints:e.uiHints,unsupported:s,disabled:e.disabled??!1,showLabel:!1,onPatch:e.onPatch})} -
    -
    - `})():p.map(([u,h])=>{const v=ci[u]??{label:u.charAt(0).toUpperCase()+u.slice(1),description:h.description??""};return r` -
    -
    - ${Pa(u)} -
    -

    ${v.label}

    - ${v.description?r`

    ${v.description}

    `:g} -
    -
    -
    - ${ye({schema:h,value:n[u],path:[u],hints:e.uiHints,unsupported:s,disabled:e.disabled??!1,showLabel:!1,onPatch:e.onPatch})} -
    -
    - `})} -
    - `}const Np=new Set(["title","description","default","nullable"]);function Op(e){return Object.keys(e??{}).filter(n=>!Np.has(n)).length===0}function ur(e){const t=e.filter(i=>i!=null),n=t.length!==e.length,s=[];for(const i of t)s.some(a=>Object.is(a,i))||s.push(i);return{enumValues:s,nullable:n}}function pr(e){return!e||typeof e!="object"?{schema:null,unsupportedPaths:[""]}:wt(e,[])}function wt(e,t){const n=new Set,s={...e},i=bn(t)||"";if(e.anyOf||e.oneOf||e.allOf){const c=Dp(e,t);return c||{schema:e,unsupportedPaths:[i]}}const a=Array.isArray(e.type)&&e.type.includes("null"),o=ue(e)??(e.properties||e.additionalProperties?"object":void 0);if(s.type=o??e.type,s.nullable=a||e.nullable,s.enum){const{enumValues:c,nullable:l}=ur(s.enum);s.enum=c,l&&(s.nullable=!0),c.length===0&&n.add(i)}if(o==="object"){const c=e.properties??{},l={};for(const[p,d]of Object.entries(c)){const u=wt(d,[...t,p]);u.schema&&(l[p]=u.schema);for(const h of u.unsupportedPaths)n.add(h)}if(s.properties=l,e.additionalProperties===!0)n.add(i);else if(e.additionalProperties===!1)s.additionalProperties=!1;else if(e.additionalProperties&&typeof e.additionalProperties=="object"&&!Op(e.additionalProperties)){const p=wt(e.additionalProperties,[...t,"*"]);s.additionalProperties=p.schema??e.additionalProperties,p.unsupportedPaths.length>0&&n.add(i)}}else if(o==="array"){const c=Array.isArray(e.items)?e.items[0]:e.items;if(!c)n.add(i);else{const l=wt(c,[...t,"*"]);s.items=l.schema??c,l.unsupportedPaths.length>0&&n.add(i)}}else o!=="string"&&o!=="number"&&o!=="integer"&&o!=="boolean"&&!s.enum&&n.add(i);return{schema:s,unsupportedPaths:Array.from(n)}}function Dp(e,t){if(e.allOf)return null;const n=e.anyOf??e.oneOf;if(!n)return null;const s=[],i=[];let a=!1;for(const c of n){if(!c||typeof c!="object")return null;if(Array.isArray(c.enum)){const{enumValues:l,nullable:p}=ur(c.enum);s.push(...l),p&&(a=!0);continue}if("const"in c){if(c.const==null){a=!0;continue}s.push(c.const);continue}if(ue(c)==="null"){a=!0;continue}i.push(c)}if(s.length>0&&i.length===0){const c=[];for(const l of s)c.some(p=>Object.is(p,l))||c.push(l);return{schema:{...e,enum:c,nullable:a,anyOf:void 0,oneOf:void 0,allOf:void 0},unsupportedPaths:[]}}if(i.length===1){const c=wt(i[0],t);return c.schema&&(c.schema.nullable=a||c.schema.nullable),c}const o=["string","number","integer","boolean"];return i.length>0&&s.length===0&&i.every(c=>c.type&&o.includes(String(c.type)))?{schema:{...e,nullable:a},unsupportedPaths:[]}:null}const As={all:r``,env:r``,update:r``,agents:r``,auth:r``,channels:r``,messages:r``,commands:r``,hooks:r``,skills:r``,tools:r``,gateway:r``,wizard:r``,meta:r``,logging:r``,browser:r``,ui:r``,models:r``,bindings:r``,broadcast:r``,audio:r``,session:r``,cron:r``,web:r``,discovery:r``,canvasHost:r``,talk:r``,plugins:r``,default:r``},Na=[{key:"env",label:"Environment"},{key:"update",label:"Updates"},{key:"agents",label:"Agents"},{key:"auth",label:"Authentication"},{key:"channels",label:"Channels"},{key:"messages",label:"Messages"},{key:"commands",label:"Commands"},{key:"hooks",label:"Hooks"},{key:"skills",label:"Skills"},{key:"tools",label:"Tools"},{key:"gateway",label:"Gateway"},{key:"wizard",label:"Setup Wizard"}],Oa="__all__";function Da(e){return As[e]??As.default}function Bp(e,t){const n=ci[e];return n||{label:t?.title??we(e),description:t?.description??""}}function Fp(e){const{key:t,schema:n,uiHints:s}=e;if(!n||ue(n)!=="object"||!n.properties)return[];const i=Object.entries(n.properties).map(([a,o])=>{const c=te([t,a],s),l=c?.label??o.title??we(a),p=c?.help??o.description??"",d=c?.order??50;return{key:a,label:l,description:p,order:d}});return i.sort((a,o)=>a.order!==o.order?a.order-o.order:a.key.localeCompare(o.key)),i}function Up(e,t){if(!e||!t)return[];const n=[];function s(i,a,o){if(i===a)return;if(typeof i!=typeof a){n.push({path:o,from:i,to:a});return}if(typeof i!="object"||i===null||a===null){i!==a&&n.push({path:o,from:i,to:a});return}if(Array.isArray(i)&&Array.isArray(a)){JSON.stringify(i)!==JSON.stringify(a)&&n.push({path:o,from:i,to:a});return}const c=i,l=a,p=new Set([...Object.keys(c),...Object.keys(l)]);for(const d of p)s(c[d],l[d],o?`${o}.${d}`:d)}return s(e,t,""),n}function Ba(e,t=40){let n;try{n=JSON.stringify(e)??String(e)}catch{n=String(e)}return n.length<=t?n:n.slice(0,t-3)+"..."}function Kp(e){const t=e.valid==null?"unknown":e.valid?"valid":"invalid",n=pr(e.schema),s=n.schema?n.unsupportedPaths.length>0:!1,i=n.schema?.properties??{},a=Na.filter(E=>E.key in i),o=new Set(Na.map(E=>E.key)),c=Object.keys(i).filter(E=>!o.has(E)).map(E=>({key:E,label:E.charAt(0).toUpperCase()+E.slice(1)})),l=[...a,...c],p=e.activeSection&&n.schema&&ue(n.schema)==="object"?n.schema.properties?.[e.activeSection]:void 0,d=e.activeSection?Bp(e.activeSection,p):null,u=e.activeSection?Fp({key:e.activeSection,schema:p,uiHints:e.uiHints}):[],h=e.formMode==="form"&&!!e.activeSection&&u.length>0,v=e.activeSubsection===Oa,w=e.searchQuery||v?null:e.activeSubsection??u[0]?.key??null,$=e.formMode==="form"?Up(e.originalValue,e.formValue):[],k=e.formMode==="raw"&&e.raw!==e.originalRaw,T=e.formMode==="form"?$.length>0:k,M=!!e.formValue&&!e.loading&&!!n.schema,P=e.connected&&!e.saving&&T&&(e.formMode==="raw"?!0:M),L=e.connected&&!e.applying&&!e.updating&&T&&(e.formMode==="raw"?!0:M),C=e.connected&&!e.applying&&!e.updating;return r` -
    - - - - -
    - -
    -
    - ${T?r` - ${e.formMode==="raw"?"Unsaved changes":`${$.length} unsaved change${$.length!==1?"s":""}`} - `:r` - No changes - `} -
    -
    - - - - -
    -
    - - - ${T&&e.formMode==="form"?r` -
    - - View ${$.length} pending change${$.length!==1?"s":""} - - - - -
    - ${$.map(E=>r` -
    -
    ${E.path}
    -
    - ${Ba(E.from)} - - ${Ba(E.to)} -
    -
    - `)} -
    -
    - `:g} - - ${d&&e.formMode==="form"?r` -
    -
    ${Da(e.activeSection??"")}
    -
    -
    ${d.label}
    - ${d.description?r`
    ${d.description}
    `:g} -
    -
    - `:g} - - ${h?r` -
    - - ${u.map(E=>r` - - `)} -
    - `:g} - - -
    - ${e.formMode==="form"?r` - ${e.schemaLoading?r`
    -
    - Loading schema… -
    `:Pp({schema:n.schema,uiHints:e.uiHints,value:e.formValue,disabled:e.loading||!e.formValue,unsupportedPaths:n.unsupportedPaths,onPatch:e.onFormPatch,searchQuery:e.searchQuery,activeSection:e.activeSection,activeSubsection:w})} - ${s?r`
    - Form view can't safely edit some fields. - Use Raw to avoid losing config entries. -
    `:g} - `:r` - - `} -
    - - ${e.issues.length>0?r`
    -
    ${JSON.stringify(e.issues,null,2)}
    -
    `:g} -
    -
    - `}function Hp(e){if(!e&&e!==0)return"n/a";const t=Math.round(e/1e3);if(t<60)return`${t}s`;const n=Math.round(t/60);return n<60?`${n}m`:`${Math.round(n/60)}h`}function zp(e,t){const n=t.snapshot,s=n?.channels;if(!n||!s)return!1;const i=s[e],a=typeof i?.configured=="boolean"&&i.configured,o=typeof i?.running=="boolean"&&i.running,c=typeof i?.connected=="boolean"&&i.connected,p=(n.channelAccounts?.[e]??[]).some(d=>d.configured||d.running||d.connected);return a||o||c||p}function jp(e,t){return t?.[e]?.length??0}function fr(e,t){const n=jp(e,t);return n<2?g:r``}function qp(e,t){let n=e;for(const s of t){if(!n)return null;const i=ue(n);if(i==="object"){const a=n.properties??{};if(typeof s=="string"&&a[s]){n=a[s];continue}const o=n.additionalProperties;if(typeof s=="string"&&o&&typeof o=="object"){n=o;continue}return null}if(i==="array"){if(typeof s!="number")return null;n=(Array.isArray(n.items)?n.items[0]:n.items)??null;continue}return null}return n}function Vp(e,t){const s=(e.channels??{})[t],i=e[t];return(s&&typeof s=="object"?s:null)??(i&&typeof i=="object"?i:null)??{}}function Wp(e){const t=pr(e.schema),n=t.schema;if(!n)return r`
    Schema unavailable. Use Raw.
    `;const s=qp(n,["channels",e.channelId]);if(!s)return r`
    Channel config schema unavailable.
    `;const i=e.configValue??{},a=Vp(i,e.channelId);return r` -
    - ${ye({schema:s,value:a,path:["channels",e.channelId],hints:e.uiHints,unsupported:new Set(t.unsupportedPaths),disabled:e.disabled,showLabel:!1,onPatch:e.onPatch})} -
    - `}function $e(e){const{channelId:t,props:n}=e,s=n.configSaving||n.configSchemaLoading;return r` -
    - ${n.configSchemaLoading?r`
    Loading config schema…
    `:Wp({channelId:t,configValue:n.configForm,schema:n.configSchema,uiHints:n.configUiHints,disabled:s,onPatch:n.onConfigPatch})} -
    - - -
    -
    - `}function Gp(e){const{props:t,discord:n,accountCountLabel:s}=e;return r` -
    -
    Discord
    -
    Bot status and channel configuration.
    - ${s} - -
    -
    - Configured - ${n?.configured?"Yes":"No"} -
    -
    - Running - ${n?.running?"Yes":"No"} -
    -
    - Last start - ${n?.lastStartAt?O(n.lastStartAt):"n/a"} -
    -
    - Last probe - ${n?.lastProbeAt?O(n.lastProbeAt):"n/a"} -
    -
    - - ${n?.lastError?r`
    - ${n.lastError} -
    `:g} - - ${n?.probe?r`
    - Probe ${n.probe.ok?"ok":"failed"} · - ${n.probe.status??""} ${n.probe.error??""} -
    `:g} - - ${$e({channelId:"discord",props:t})} - -
    - -
    -
    - `}function Yp(e){const{props:t,googleChat:n,accountCountLabel:s}=e;return r` -
    -
    Google Chat
    -
    Chat API webhook status and channel configuration.
    - ${s} - -
    -
    - Configured - ${n?n.configured?"Yes":"No":"n/a"} -
    -
    - Running - ${n?n.running?"Yes":"No":"n/a"} -
    -
    - Credential - ${n?.credentialSource??"n/a"} -
    -
    - Audience - - ${n?.audienceType?`${n.audienceType}${n.audience?` · ${n.audience}`:""}`:"n/a"} - -
    -
    - Last start - ${n?.lastStartAt?O(n.lastStartAt):"n/a"} -
    -
    - Last probe - ${n?.lastProbeAt?O(n.lastProbeAt):"n/a"} -
    -
    - - ${n?.lastError?r`
    - ${n.lastError} -
    `:g} - - ${n?.probe?r`
    - Probe ${n.probe.ok?"ok":"failed"} · - ${n.probe.status??""} ${n.probe.error??""} -
    `:g} - - ${$e({channelId:"googlechat",props:t})} - -
    - -
    -
    - `}function Qp(e){const{props:t,imessage:n,accountCountLabel:s}=e;return r` -
    -
    iMessage
    -
    macOS bridge status and channel configuration.
    - ${s} - -
    -
    - Configured - ${n?.configured?"Yes":"No"} -
    -
    - Running - ${n?.running?"Yes":"No"} -
    -
    - Last start - ${n?.lastStartAt?O(n.lastStartAt):"n/a"} -
    -
    - Last probe - ${n?.lastProbeAt?O(n.lastProbeAt):"n/a"} -
    -
    - - ${n?.lastError?r`
    - ${n.lastError} -
    `:g} - - ${n?.probe?r`
    - Probe ${n.probe.ok?"ok":"failed"} · - ${n.probe.error??""} -
    `:g} - - ${$e({channelId:"imessage",props:t})} - -
    - -
    -
    - `}function Zp(e){const{values:t,original:n}=e;return t.name!==n.name||t.displayName!==n.displayName||t.about!==n.about||t.picture!==n.picture||t.banner!==n.banner||t.website!==n.website||t.nip05!==n.nip05||t.lud16!==n.lud16}function Jp(e){const{state:t,callbacks:n,accountId:s}=e,i=Zp(t),a=(c,l,p={})=>{const{type:d="text",placeholder:u,maxLength:h,help:v}=p,w=t.values[c]??"",$=t.fieldErrors[c],k=`nostr-profile-${c}`;return d==="textarea"?r` -
    - - - ${v?r`
    ${v}
    `:g} - ${$?r`
    ${$}
    `:g} -
    - `:r` -
    - - {const M=T.target;n.onFieldChange(c,M.value)}} - ?disabled=${t.saving} - /> - ${v?r`
    ${v}
    `:g} - ${$?r`
    ${$}
    `:g} -
    - `},o=()=>{const c=t.values.picture;return c?r` -
    - Profile picture preview{const p=l.target;p.style.display="none"}} - @load=${l=>{const p=l.target;p.style.display="block"}} - /> -
    - `:g};return r` -
    -
    -
    Edit Profile
    -
    Account: ${s}
    -
    - - ${t.error?r`
    ${t.error}
    `:g} - - ${t.success?r`
    ${t.success}
    `:g} - - ${o()} - - ${a("name","Username",{placeholder:"satoshi",maxLength:256,help:"Short username (e.g., satoshi)"})} - - ${a("displayName","Display Name",{placeholder:"Satoshi Nakamoto",maxLength:256,help:"Your full display name"})} - - ${a("about","Bio",{type:"textarea",placeholder:"Tell people about yourself...",maxLength:2e3,help:"A brief bio or description"})} - - ${a("picture","Avatar URL",{type:"url",placeholder:"https://example.com/avatar.jpg",help:"HTTPS URL to your profile picture"})} - - ${t.showAdvanced?r` -
    -
    Advanced
    - - ${a("banner","Banner URL",{type:"url",placeholder:"https://example.com/banner.jpg",help:"HTTPS URL to a banner image"})} - - ${a("website","Website",{type:"url",placeholder:"https://example.com",help:"Your personal website"})} - - ${a("nip05","NIP-05 Identifier",{placeholder:"you@example.com",help:"Verifiable identifier (e.g., you@domain.com)"})} - - ${a("lud16","Lightning Address",{placeholder:"you@getalby.com",help:"Lightning address for tips (LUD-16)"})} -
    - `:g} - -
    - - - - - - - -
    - - ${i?r`
    - You have unsaved changes -
    `:g} -
    - `}function Xp(e){const t={name:e?.name??"",displayName:e?.displayName??"",about:e?.about??"",picture:e?.picture??"",banner:e?.banner??"",website:e?.website??"",nip05:e?.nip05??"",lud16:e?.lud16??""};return{values:t,original:{...t},saving:!1,importing:!1,error:null,success:null,fieldErrors:{},showAdvanced:!!(e?.banner||e?.website||e?.nip05||e?.lud16)}}function Fa(e){return e?e.length<=20?e:`${e.slice(0,8)}...${e.slice(-8)}`:"n/a"}function ef(e){const{props:t,nostr:n,nostrAccounts:s,accountCountLabel:i,profileFormState:a,profileFormCallbacks:o,onEditProfile:c}=e,l=s[0],p=n?.configured??l?.configured??!1,d=n?.running??l?.running??!1,u=n?.publicKey??l?.publicKey,h=n?.lastStartAt??l?.lastStartAt??null,v=n?.lastError??l?.lastError??null,w=s.length>1,$=a!=null,k=M=>{const P=M.publicKey,L=M.profile,C=L?.displayName??L?.name??M.name??M.accountId;return r` - - `},T=()=>{if($&&o)return Jp({state:a,callbacks:o,accountId:s[0]?.accountId??"default"});const M=l?.profile??n?.profile,{name:P,displayName:L,about:C,picture:E,nip05:pe}=M??{},yn=P||L||C||E||pe;return r` -
    -
    -
    Profile
    - ${p?r` - - `:g} -
    - ${yn?r` -
    - ${E?r` -
    - Profile picture{wn.target.style.display="none"}} - /> -
    - `:g} - ${P?r`
    Name${P}
    `:g} - ${L?r`
    Display Name${L}
    `:g} - ${C?r`
    About${C}
    `:g} - ${pe?r`
    NIP-05${pe}
    `:g} -
    - `:r` -
    - No profile set. Click "Edit Profile" to add your name, bio, and avatar. -
    - `} -
    - `};return r` -
    -
    Nostr
    -
    Decentralized DMs via Nostr relays (NIP-04).
    - ${i} - - ${w?r` - - `:r` -
    -
    - Configured - ${p?"Yes":"No"} -
    -
    - Running - ${d?"Yes":"No"} -
    -
    - Public Key - ${Fa(u)} -
    -
    - Last start - ${h?O(h):"n/a"} -
    -
    - `} - - ${v?r`
    ${v}
    `:g} - - ${T()} - - ${$e({channelId:"nostr",props:t})} - -
    - -
    -
    - `}function tf(e){const{props:t,signal:n,accountCountLabel:s}=e;return r` -
    -
    Signal
    -
    signal-cli status and channel configuration.
    - ${s} - -
    -
    - Configured - ${n?.configured?"Yes":"No"} -
    -
    - Running - ${n?.running?"Yes":"No"} -
    -
    - Base URL - ${n?.baseUrl??"n/a"} -
    -
    - Last start - ${n?.lastStartAt?O(n.lastStartAt):"n/a"} -
    -
    - Last probe - ${n?.lastProbeAt?O(n.lastProbeAt):"n/a"} -
    -
    - - ${n?.lastError?r`
    - ${n.lastError} -
    `:g} - - ${n?.probe?r`
    - Probe ${n.probe.ok?"ok":"failed"} · - ${n.probe.status??""} ${n.probe.error??""} -
    `:g} - - ${$e({channelId:"signal",props:t})} - -
    - -
    -
    - `}function nf(e){const{props:t,slack:n,accountCountLabel:s}=e;return r` -
    -
    Slack
    -
    Socket mode status and channel configuration.
    - ${s} - -
    -
    - Configured - ${n?.configured?"Yes":"No"} -
    -
    - Running - ${n?.running?"Yes":"No"} -
    -
    - Last start - ${n?.lastStartAt?O(n.lastStartAt):"n/a"} -
    -
    - Last probe - ${n?.lastProbeAt?O(n.lastProbeAt):"n/a"} -
    -
    - - ${n?.lastError?r`
    - ${n.lastError} -
    `:g} - - ${n?.probe?r`
    - Probe ${n.probe.ok?"ok":"failed"} · - ${n.probe.status??""} ${n.probe.error??""} -
    `:g} - - ${$e({channelId:"slack",props:t})} - -
    - -
    -
    - `}function sf(e){const{props:t,telegram:n,telegramAccounts:s,accountCountLabel:i}=e,a=s.length>1,o=c=>{const p=c.probe?.bot?.username,d=c.name||c.accountId;return r` - - `};return r` -
    -
    Telegram
    -
    Bot status and channel configuration.
    - ${i} - - ${a?r` - - `:r` -
    -
    - Configured - ${n?.configured?"Yes":"No"} -
    -
    - Running - ${n?.running?"Yes":"No"} -
    -
    - Mode - ${n?.mode??"n/a"} -
    -
    - Last start - ${n?.lastStartAt?O(n.lastStartAt):"n/a"} -
    -
    - Last probe - ${n?.lastProbeAt?O(n.lastProbeAt):"n/a"} -
    -
    - `} - - ${n?.lastError?r`
    - ${n.lastError} -
    `:g} - - ${n?.probe?r`
    - Probe ${n.probe.ok?"ok":"failed"} · - ${n.probe.status??""} ${n.probe.error??""} -
    `:g} - - ${$e({channelId:"telegram",props:t})} - -
    - -
    -
    - `}function af(e){const{props:t,whatsapp:n,accountCountLabel:s}=e;return r` -
    -
    WhatsApp
    -
    Link WhatsApp Web and monitor connection health.
    - ${s} - -
    -
    - Configured - ${n?.configured?"Yes":"No"} -
    -
    - Linked - ${n?.linked?"Yes":"No"} -
    -
    - Running - ${n?.running?"Yes":"No"} -
    -
    - Connected - ${n?.connected?"Yes":"No"} -
    -
    - Last connect - - ${n?.lastConnectedAt?O(n.lastConnectedAt):"n/a"} - -
    -
    - Last message - - ${n?.lastMessageAt?O(n.lastMessageAt):"n/a"} - -
    -
    - Auth age - - ${n?.authAgeMs!=null?Hp(n.authAgeMs):"n/a"} - -
    -
    - - ${n?.lastError?r`
    - ${n.lastError} -
    `:g} - - ${t.whatsappMessage?r`
    - ${t.whatsappMessage} -
    `:g} - - ${t.whatsappQrDataUrl?r`
    - WhatsApp QR -
    `:g} - -
    - - - - - -
    - - ${$e({channelId:"whatsapp",props:t})} -
    - `}function of(e){const t=e.snapshot?.channels,n=t?.whatsapp??void 0,s=t?.telegram??void 0,i=t?.discord??null;t?.googlechat;const a=t?.slack??null,o=t?.signal??null,c=t?.imessage??null,l=t?.nostr??null,d=rf(e.snapshot).map((u,h)=>({key:u,enabled:zp(u,e),order:h})).sort((u,h)=>u.enabled!==h.enabled?u.enabled?-1:1:u.order-h.order);return r` -
    - ${d.map(u=>lf(u.key,e,{whatsapp:n,telegram:s,discord:i,slack:a,signal:o,imessage:c,nostr:l,channelAccounts:e.snapshot?.channelAccounts??null}))} -
    - -
    -
    -
    -
    Channel health
    -
    Channel status snapshots from the gateway.
    -
    -
    ${e.lastSuccessAt?O(e.lastSuccessAt):"n/a"}
    -
    - ${e.lastError?r`
    - ${e.lastError} -
    `:g} -
    -${e.snapshot?JSON.stringify(e.snapshot,null,2):"No snapshot yet."}
    -      
    -
    - `}function rf(e){return e?.channelMeta?.length?e.channelMeta.map(t=>t.id):e?.channelOrder?.length?e.channelOrder:["whatsapp","telegram","discord","googlechat","slack","signal","imessage","nostr"]}function lf(e,t,n){const s=fr(e,n.channelAccounts);switch(e){case"whatsapp":return af({props:t,whatsapp:n.whatsapp,accountCountLabel:s});case"telegram":return sf({props:t,telegram:n.telegram,telegramAccounts:n.channelAccounts?.telegram??[],accountCountLabel:s});case"discord":return Gp({props:t,discord:n.discord,accountCountLabel:s});case"googlechat":return Yp({props:t,accountCountLabel:s});case"slack":return nf({props:t,slack:n.slack,accountCountLabel:s});case"signal":return tf({props:t,signal:n.signal,accountCountLabel:s});case"imessage":return Qp({props:t,imessage:n.imessage,accountCountLabel:s});case"nostr":{const i=n.channelAccounts?.nostr??[],a=i[0],o=a?.accountId??"default",c=a?.profile??null,l=t.nostrProfileAccountId===o?t.nostrProfileFormState:null,p=l?{onFieldChange:t.onNostrProfileFieldChange,onSave:t.onNostrProfileSave,onImport:t.onNostrProfileImport,onCancel:t.onNostrProfileCancel,onToggleAdvanced:t.onNostrProfileToggleAdvanced}:null;return ef({props:t,nostr:n.nostr,nostrAccounts:i,accountCountLabel:s,profileFormState:l,profileFormCallbacks:p,onEditProfile:()=>t.onNostrProfileEdit(o,c)})}default:return cf(e,t,n.channelAccounts??{})}}function cf(e,t,n){const s=uf(t.snapshot,e),i=t.snapshot?.channels?.[e],a=typeof i?.configured=="boolean"?i.configured:void 0,o=typeof i?.running=="boolean"?i.running:void 0,c=typeof i?.connected=="boolean"?i.connected:void 0,l=typeof i?.lastError=="string"?i.lastError:void 0,p=n[e]??[],d=fr(e,n);return r` -
    -
    ${s}
    -
    Channel status and configuration.
    - ${d} - - ${p.length>0?r` - - `:r` -
    -
    - Configured - ${a==null?"n/a":a?"Yes":"No"} -
    -
    - Running - ${o==null?"n/a":o?"Yes":"No"} -
    -
    - Connected - ${c==null?"n/a":c?"Yes":"No"} -
    -
    - `} - - ${l?r`
    - ${l} -
    `:g} - - ${$e({channelId:e,props:t})} -
    - `}function df(e){return e?.channelMeta?.length?Object.fromEntries(e.channelMeta.map(t=>[t.id,t])):{}}function uf(e,t){return df(e)[t]?.label??e?.channelLabels?.[t]??t}const pf=600*1e3;function hr(e){return e.lastInboundAt?Date.now()-e.lastInboundAt
    - `:r` -
    - Auth failed. Re-copy a tokenized URL with - clawdbot dashboard --no-open, or update the token, - then click Connect. -
    - Docs: Control UI auth -
    -
    - `})(),a=(()=>{if(e.connected||!e.lastError||(typeof window<"u"?window.isSecureContext:!0)!==!1)return null;const c=e.lastError.toLowerCase();return!c.includes("secure context")&&!c.includes("device identity required")?null:r` -
    - This page is HTTP, so the browser blocks device identity. Use HTTPS (Tailscale Serve) or - open http://127.0.0.1:18789 on the gateway host. -
    - If you must stay on HTTP, set - gateway.controlUi.allowInsecureAuth: true (token-only). -
    -
    - Docs: Tailscale Serve - · - Docs: Insecure HTTP -
    -
    - `})();return r` -
    -
    -
    Gateway Access
    -
    Where the dashboard connects and how it authenticates.
    -
    - - - - -
    -
    - - - Click Connect to apply connection changes. -
    -
    - -
    -
    Snapshot
    -
    Latest gateway handshake information.
    -
    -
    -
    Status
    -
    - ${e.connected?"Connected":"Disconnected"} -
    -
    -
    -
    Uptime
    -
    ${n}
    -
    -
    -
    Tick Interval
    -
    ${s}
    -
    -
    -
    Last Channels Refresh
    -
    - ${e.lastChannelsRefresh?O(e.lastChannelsRefresh):"n/a"} -
    -
    -
    - ${e.lastError?r`
    -
    ${e.lastError}
    - ${i??""} - ${a??""} -
    `:r`
    - Use Channels to link WhatsApp, Telegram, Discord, Signal, or iMessage. -
    `} -
    -
    - -
    -
    -
    Instances
    -
    ${e.presenceCount}
    -
    Presence beacons in the last 5 minutes.
    -
    -
    -
    Sessions
    -
    ${e.sessionsCount??"n/a"}
    -
    Recent session keys tracked by the gateway.
    -
    -
    -
    Cron
    -
    - ${e.cronEnabled==null?"n/a":e.cronEnabled?"Enabled":"Disabled"} -
    -
    Next wake ${gr(e.cronNext)}
    -
    -
    - -
    -
    Notes
    -
    Quick reminders for remote control setups.
    -
    -
    -
    Tailscale serve
    -
    - Prefer serve mode to keep the gateway on loopback with tailnet auth. -
    -
    -
    -
    Session hygiene
    -
    Use /new or sessions.patch to reset context.
    -
    -
    -
    Cron reminders
    -
    Use isolated sessions for recurring runs.
    -
    -
    -
    - `}const rh=["","off","minimal","low","medium","high"],lh=["","off","on"],ch=[{value:"",label:"inherit"},{value:"off",label:"off (explicit)"},{value:"on",label:"on"}],dh=["","off","on","stream"];function uh(e){if(!e)return"";const t=e.trim().toLowerCase();return t==="z.ai"||t==="z-ai"?"zai":t}function vr(e){return uh(e)==="zai"}function ph(e){return vr(e)?lh:rh}function fh(e,t){return!t||!e||e==="off"?e:"on"}function hh(e,t){return e?t&&e==="on"?"low":e:null}function gh(e){const t=e.result?.sessions??[];return r` -
    -
    -
    -
    Sessions
    -
    Active session keys and per-session overrides.
    -
    - -
    - -
    - - - - -
    - - ${e.error?r`
    ${e.error}
    `:g} - -
    - ${e.result?`Store: ${e.result.path}`:""} -
    - -
    -
    -
    Key
    -
    Label
    -
    Kind
    -
    Updated
    -
    Tokens
    -
    Thinking
    -
    Verbose
    -
    Reasoning
    -
    Actions
    -
    - ${t.length===0?r`
    No sessions found.
    `:t.map(n=>vh(n,e.basePath,e.onPatch,e.onDelete,e.loading))} -
    -
    - `}function vh(e,t,n,s,i){const a=e.updatedAt?O(e.updatedAt):"n/a",o=e.thinkingLevel??"",c=vr(e.modelProvider),l=fh(o,c),p=ph(e.modelProvider),d=e.verboseLevel??"",u=e.reasoningLevel??"",h=e.displayName??e.key,v=e.kind!=="global",w=v?`${Rs("chat",t)}?session=${encodeURIComponent(e.key)}`:null;return r` -
    -
    ${v?r`${h}`:h}
    -
    - {const k=$.target.value.trim();n(e.key,{label:k||null})}} - /> -
    -
    ${e.kind}
    -
    ${a}
    -
    ${bf(e)}
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - `}function mh(e){const t=Math.max(0,e),n=Math.floor(t/1e3);if(n<60)return`${n}s`;const s=Math.floor(n/60);return s<60?`${s}m`:`${Math.floor(s/60)}h`}function Ie(e,t){return t?r`
    ${e}${t}
    `:g}function bh(e){const t=e.execApprovalQueue[0];if(!t)return g;const n=t.request,s=t.expiresAtMs-Date.now(),i=s>0?`expires in ${mh(s)}`:"expired",a=e.execApprovalQueue.length;return r` - - `}function yh(e){const t=e.report?.skills??[],n=e.filter.trim().toLowerCase(),s=n?t.filter(i=>[i.name,i.description,i.source].join(" ").toLowerCase().includes(n)):t;return r` -
    -
    -
    -
    Skills
    -
    Bundled, managed, and workspace skills.
    -
    - -
    - -
    - -
    ${s.length} shown
    -
    - - ${e.error?r`
    ${e.error}
    `:g} - - ${s.length===0?r`
    No skills found.
    `:r` -
    - ${s.map(i=>wh(i,e))} -
    - `} -
    - `}function wh(e,t){const n=t.busyKey===e.skillKey,s=t.edits[e.skillKey]??"",i=t.messages[e.skillKey]??null,a=e.install.length>0&&e.missing.bins.length>0,o=[...e.missing.bins.map(l=>`bin:${l}`),...e.missing.env.map(l=>`env:${l}`),...e.missing.config.map(l=>`config:${l}`),...e.missing.os.map(l=>`os:${l}`)],c=[];return e.disabled&&c.push("disabled"),e.blockedByAllowlist&&c.push("blocked by allowlist"),r` -
    -
    -
    - ${e.emoji?`${e.emoji} `:""}${e.name} -
    -
    ${as(e.description,140)}
    -
    - ${e.source} - - ${e.eligible?"eligible":"blocked"} - - ${e.disabled?r`disabled`:g} -
    - ${o.length>0?r` -
    - Missing: ${o.join(", ")} -
    - `:g} - ${c.length>0?r` -
    - Reason: ${c.join(", ")} -
    - `:g} -
    -
    -
    - - ${a?r``:g} -
    - ${i?r`
    - ${i.message} -
    `:g} - ${e.primaryEnv?r` -
    - API key - t.onEdit(e.skillKey,l.target.value)} - /> -
    - - `:g} -
    -
    - `}function $h(e,t){const n=Rs(t,e.basePath);return r` - {s.defaultPrevented||s.button!==0||s.metaKey||s.ctrlKey||s.shiftKey||s.altKey||(s.preventDefault(),e.setTab(t))}} - title=${ss(t)} - > - - ${ss(t)} - - `}function xh(e){const t=kh(e.sessionKey,e.sessionsResult),n=e.onboarding,s=e.onboarding,i=e.onboarding?!1:e.settings.chatShowThinking,a=e.onboarding?!0:e.settings.chatFocusMode,o=r``,c=r``;return r` -
    - - - | - - -
    - `}function kh(e,t){const n=new Set,s=[],i=t?.sessions?.find(a=>a.key===e);if(n.add(e),s.push({key:e,displayName:i?.displayName}),t?.sessions)for(const a of t.sessions)n.has(a.key)||(n.add(a.key),s.push({key:a.key,displayName:a.displayName}));return s}const Ah=["system","light","dark"];function Sh(e){const t=Math.max(0,Ah.indexOf(e.theme)),n=s=>i=>{const o={element:i.currentTarget};(i.clientX||i.clientY)&&(o.pointerClientX=i.clientX,o.pointerClientY=i.clientY),e.setTheme(s,o)};return r` -
    -
    - - - - -
    -
    - `}function _h(){return r` - - `}function Th(){return r` - - `}function Ch(){return r` - - `}const Eh=/^data:/i,Lh=/^https?:\/\//i;function Mh(e){const t=e.agentsList?.agents??[],s=eo(e.sessionKey)?.agentId??e.agentsList?.defaultId??"main",a=t.find(c=>c.id===s)?.identity,o=a?.avatarUrl??a?.avatar;if(o)return Eh.test(o)||Lh.test(o)?o:a?.avatarUrl}function Ih(e){const t=e.presenceEntries.length,n=e.sessionsResult?.count??null,s=e.cronStatus?.nextWakeAtMs??null,i=e.connected?null:"Disconnected from gateway.",a=e.tab==="chat",o=a&&(e.settings.chatFocusMode||e.onboarding),c=e.onboarding?!1:e.settings.chatShowThinking,l=Mh(e),p=e.chatAvatarUrl??l??null;return r` -
    -
    -
    - -
    - -
    -
    CLAWDBOT
    -
    Gateway Dashboard
    -
    -
    -
    -
    -
    - - Health - ${e.connected?"OK":"Offline"} -
    - ${Sh(e)} -
    -
    - -
    -
    -
    -
    ${ss(e.tab)}
    -
    ${ml(e.tab)}
    -
    -
    - ${e.lastError?r`
    ${e.lastError}
    `:g} - ${a?xh(e):g} -
    -
    - - ${e.tab==="overview"?oh({connected:e.connected,hello:e.hello,settings:e.settings,password:e.password,lastError:e.lastError,presenceCount:t,sessionsCount:n,cronEnabled:e.cronStatus?.enabled??null,cronNext:s,lastChannelsRefresh:e.channelsLastSuccess,onSettingsChange:d=>e.applySettings(d),onPasswordChange:d=>e.password=d,onSessionKeyChange:d=>{e.sessionKey=d,e.chatMessage="",e.resetToolStream(),e.applySettings({...e.settings,sessionKey:d,lastActiveSessionKey:d}),e.loadAssistantIdentity()},onConnect:()=>e.connect(),onRefresh:()=>e.loadOverview()}):g} - - ${e.tab==="channels"?of({connected:e.connected,loading:e.channelsLoading,snapshot:e.channelsSnapshot,lastError:e.channelsError,lastSuccessAt:e.channelsLastSuccess,whatsappMessage:e.whatsappLoginMessage,whatsappQrDataUrl:e.whatsappLoginQrDataUrl,whatsappConnected:e.whatsappLoginConnected,whatsappBusy:e.whatsappBusy,configSchema:e.configSchema,configSchemaLoading:e.configSchemaLoading,configForm:e.configForm,configUiHints:e.configUiHints,configSaving:e.configSaving,configFormDirty:e.configFormDirty,nostrProfileFormState:e.nostrProfileFormState,nostrProfileAccountId:e.nostrProfileAccountId,onRefresh:d=>oe(e,d),onWhatsAppStart:d=>e.handleWhatsAppStart(d),onWhatsAppWait:()=>e.handleWhatsAppWait(),onWhatsAppLogout:()=>e.handleWhatsAppLogout(),onConfigPatch:(d,u)=>Bt(e,d,u),onConfigSave:()=>e.handleChannelConfigSave(),onConfigReload:()=>e.handleChannelConfigReload(),onNostrProfileEdit:(d,u)=>e.handleNostrProfileEdit(d,u),onNostrProfileCancel:()=>e.handleNostrProfileCancel(),onNostrProfileFieldChange:(d,u)=>e.handleNostrProfileFieldChange(d,u),onNostrProfileSave:()=>e.handleNostrProfileSave(),onNostrProfileImport:()=>e.handleNostrProfileImport(),onNostrProfileToggleAdvanced:()=>e.handleNostrProfileToggleAdvanced()}):g} - - ${e.tab==="instances"?Lf({loading:e.presenceLoading,entries:e.presenceEntries,lastError:e.presenceError,statusMessage:e.presenceStatus,onRefresh:()=>js(e)}):g} - - ${e.tab==="sessions"?gh({loading:e.sessionsLoading,result:e.sessionsResult,error:e.sessionsError,activeMinutes:e.sessionsFilterActive,limit:e.sessionsFilterLimit,includeGlobal:e.sessionsIncludeGlobal,includeUnknown:e.sessionsIncludeUnknown,basePath:e.basePath,onFiltersChange:d=>{e.sessionsFilterActive=d.activeMinutes,e.sessionsFilterLimit=d.limit,e.sessionsIncludeGlobal=d.includeGlobal,e.sessionsIncludeUnknown=d.includeUnknown},onRefresh:()=>st(e),onPatch:(d,u)=>Ml(e,d,u),onDelete:d=>Il(e,d)}):g} - - ${e.tab==="cron"?Sf({loading:e.cronLoading,status:e.cronStatus,jobs:e.cronJobs,error:e.cronError,busy:e.cronBusy,form:e.cronForm,channels:e.channelsSnapshot?.channelMeta?.length?e.channelsSnapshot.channelMeta.map(d=>d.id):e.channelsSnapshot?.channelOrder??[],channelLabels:e.channelsSnapshot?.channelLabels??{},channelMeta:e.channelsSnapshot?.channelMeta??[],runsJobId:e.cronRunsJobId,runs:e.cronRuns,onFormChange:d=>e.cronForm={...e.cronForm,...d},onRefresh:()=>e.loadCron(),onAdd:()=>ec(e),onToggle:(d,u)=>tc(e,d,u),onRun:d=>nc(e,d),onRemove:d=>sc(e,d),onLoadRuns:d=>po(e,d)}):g} - - ${e.tab==="skills"?yh({loading:e.skillsLoading,report:e.skillsReport,error:e.skillsError,filter:e.skillsFilter,edits:e.skillEdits,messages:e.skillMessages,busyKey:e.skillsBusyKey,onFilterChange:d=>e.skillsFilter=d,onRefresh:()=>Ct(e,{clearMessages:!0}),onToggle:(d,u)=>Zc(e,d,u),onEdit:(d,u)=>Qc(e,d,u),onSaveKey:d=>Jc(e,d),onInstall:(d,u,h)=>Xc(e,d,u,h)}):g} - - ${e.tab==="nodes"?Nf({loading:e.nodesLoading,nodes:e.nodes,devicesLoading:e.devicesLoading,devicesError:e.devicesError,devicesList:e.devicesList,configForm:e.configForm??e.configSnapshot?.config,configLoading:e.configLoading,configSaving:e.configSaving,configDirty:e.configFormDirty,configFormMode:e.configFormMode,execApprovalsLoading:e.execApprovalsLoading,execApprovalsSaving:e.execApprovalsSaving,execApprovalsDirty:e.execApprovalsDirty,execApprovalsSnapshot:e.execApprovalsSnapshot,execApprovalsForm:e.execApprovalsForm,execApprovalsSelectedAgent:e.execApprovalsSelectedAgent,execApprovalsTarget:e.execApprovalsTarget,execApprovalsTargetNodeId:e.execApprovalsTargetNodeId,onRefresh:()=>pn(e),onDevicesRefresh:()=>Te(e),onDeviceApprove:d=>Uc(e,d),onDeviceReject:d=>Kc(e,d),onDeviceRotate:(d,u,h)=>Hc(e,{deviceId:d,role:u,scopes:h}),onDeviceRevoke:(d,u)=>zc(e,{deviceId:d,role:u}),onLoadConfig:()=>be(e),onLoadExecApprovals:()=>{const d=e.execApprovalsTarget==="node"&&e.execApprovalsTargetNodeId?{kind:"node",nodeId:e.execApprovalsTargetNodeId}:{kind:"gateway"};return zs(e,d)},onBindDefault:d=>{d?Bt(e,["tools","exec","node"],d):Zi(e,["tools","exec","node"])},onBindAgent:(d,u)=>{const h=["agents","list",d,"tools","exec","node"];u?Bt(e,h,u):Zi(e,h)},onSaveBindings:()=>ls(e),onExecApprovalsTargetChange:(d,u)=>{e.execApprovalsTarget=d,e.execApprovalsTargetNodeId=u,e.execApprovalsSnapshot=null,e.execApprovalsForm=null,e.execApprovalsDirty=!1,e.execApprovalsSelectedAgent=null},onExecApprovalsSelectAgent:d=>{e.execApprovalsSelectedAgent=d},onExecApprovalsPatch:(d,u)=>Gc(e,d,u),onExecApprovalsRemove:d=>Yc(e,d),onSaveExecApprovals:()=>{const d=e.execApprovalsTarget==="node"&&e.execApprovalsTargetNodeId?{kind:"node",nodeId:e.execApprovalsTargetNodeId}:{kind:"gateway"};return Wc(e,d)}}):g} - - ${e.tab==="chat"?kp({sessionKey:e.sessionKey,onSessionKeyChange:d=>{e.sessionKey=d,e.chatMessage="",e.chatStream=null,e.chatStreamStartedAt=null,e.chatRunId=null,e.chatQueue=[],e.resetToolStream(),e.resetChatScroll(),e.applySettings({...e.settings,sessionKey:d,lastActiveSessionKey:d}),e.loadAssistantIdentity(),Xe(e),fs(e)},thinkingLevel:e.chatThinkingLevel,showThinking:c,loading:e.chatLoading,sending:e.chatSending,compactionStatus:e.compactionStatus,assistantAvatarUrl:p,messages:e.chatMessages,toolMessages:e.chatToolMessages,stream:e.chatStream,streamStartedAt:e.chatStreamStartedAt,draft:e.chatMessage,queue:e.chatQueue,connected:e.connected,canSend:e.connected,disabledReason:i,error:e.lastError,sessions:e.sessionsResult,focusMode:o,onRefresh:()=>(e.resetToolStream(),Promise.all([Xe(e),fs(e)])),onToggleFocusMode:()=>{e.onboarding||e.applySettings({...e.settings,chatFocusMode:!e.settings.chatFocusMode})},onChatScroll:d=>e.handleChatScroll(d),onDraftChange:d=>e.chatMessage=d,onSend:()=>e.handleSendChat(),canAbort:!!e.chatRunId,onAbort:()=>{e.handleAbortChat()},onQueueRemove:d=>e.removeQueuedMessage(d),onNewSession:()=>e.handleSendChat("/new",{restoreDraft:!0}),sidebarOpen:e.sidebarOpen,sidebarContent:e.sidebarContent,sidebarError:e.sidebarError,splitRatio:e.splitRatio,onOpenSidebar:d=>e.handleOpenSidebar(d),onCloseSidebar:()=>e.handleCloseSidebar(),onSplitRatioChange:d=>e.handleSplitRatioChange(d),assistantName:e.assistantName,assistantAvatar:e.assistantAvatar}):g} - - ${e.tab==="config"?Kp({raw:e.configRaw,originalRaw:e.configRawOriginal,valid:e.configValid,issues:e.configIssues,loading:e.configLoading,saving:e.configSaving,applying:e.configApplying,updating:e.updateRunning,connected:e.connected,schema:e.configSchema,schemaLoading:e.configSchemaLoading,uiHints:e.configUiHints,formMode:e.configFormMode,formValue:e.configForm,originalValue:e.configFormOriginal,searchQuery:e.configSearchQuery,activeSection:e.configActiveSection,activeSubsection:e.configActiveSubsection,onRawChange:d=>{e.configRaw=d},onFormModeChange:d=>e.configFormMode=d,onFormPatch:(d,u)=>Bt(e,d,u),onSearchChange:d=>e.configSearchQuery=d,onSectionChange:d=>{e.configActiveSection=d,e.configActiveSubsection=null},onSubsectionChange:d=>e.configActiveSubsection=d,onReload:()=>be(e),onSave:()=>ls(e),onApply:()=>Ql(e),onUpdate:()=>Zl(e)}):g} - - ${e.tab==="debug"?Ef({loading:e.debugLoading,status:e.debugStatus,health:e.debugHealth,models:e.debugModels,heartbeat:e.debugHeartbeat,eventLog:e.eventLog,callMethod:e.debugCallMethod,callParams:e.debugCallParams,callResult:e.debugCallResult,callError:e.debugCallError,onCallMethodChange:d=>e.debugCallMethod=d,onCallParamsChange:d=>e.debugCallParams=d,onRefresh:()=>dn(e),onCall:()=>rc(e)}):g} - - ${e.tab==="logs"?Pf({loading:e.logsLoading,error:e.logsError,file:e.logsFile,entries:e.logsEntries,filterText:e.logsFilterText,levelFilters:e.logsLevelFilters,autoFollow:e.logsAutoFollow,truncated:e.logsTruncated,onFilterTextChange:d=>e.logsFilterText=d,onLevelToggle:(d,u)=>{e.logsLevelFilters={...e.logsLevelFilters,[d]:u}},onToggleAutoFollow:d=>e.logsAutoFollow=d,onRefresh:()=>Os(e,{reset:!0}),onExport:(d,u)=>e.exportLogs(d,u),onScroll:d=>e.handleLogsScroll(d)}):g} -
    - ${bh(e)} -
    - `}const Rh={trace:!0,debug:!0,info:!0,warn:!0,error:!0,fatal:!0},Ph={name:"",description:"",agentId:"",enabled:!0,scheduleKind:"every",scheduleAt:"",everyAmount:"30",everyUnit:"minutes",cronExpr:"0 7 * * *",cronTz:"",sessionTarget:"main",wakeMode:"next-heartbeat",payloadKind:"systemEvent",payloadText:"",deliver:!1,channel:"last",to:"",timeoutSeconds:"",postToMainPrefix:""};async function Nh(e){if(!(!e.client||!e.connected)&&!e.agentsLoading){e.agentsLoading=!0,e.agentsError=null;try{const t=await e.client.request("agents.list",{});t&&(e.agentsList=t)}catch(t){e.agentsError=String(t)}finally{e.agentsLoading=!1}}}const mr={WEBCHAT_UI:"webchat-ui",CONTROL_UI:"clawdbot-control-ui",WEBCHAT:"webchat",CLI:"cli",GATEWAY_CLIENT:"gateway-client",MACOS_APP:"clawdbot-macos",IOS_APP:"clawdbot-ios",ANDROID_APP:"clawdbot-android",NODE_HOST:"node-host",TEST:"test",FINGERPRINT:"fingerprint",PROBE:"clawdbot-probe"},za=mr,Ss={WEBCHAT:"webchat",CLI:"cli",UI:"ui",BACKEND:"backend",NODE:"node",PROBE:"probe",TEST:"test"};new Set(Object.values(mr));new Set(Object.values(Ss));function Oh(e){const t=e.version??(e.nonce?"v2":"v1"),n=e.scopes.join(","),s=e.token??"",i=[t,e.deviceId,e.clientId,e.clientMode,e.role,n,String(e.signedAtMs),s];return t==="v2"&&i.push(e.nonce??""),i.join("|")}const Dh=4008;class Bh{constructor(t){this.opts=t,this.ws=null,this.pending=new Map,this.closed=!1,this.lastSeq=null,this.connectNonce=null,this.connectSent=!1,this.connectTimer=null,this.backoffMs=800}start(){this.closed=!1,this.connect()}stop(){this.closed=!0,this.ws?.close(),this.ws=null,this.flushPending(new Error("gateway client stopped"))}get connected(){return this.ws?.readyState===WebSocket.OPEN}connect(){this.closed||(this.ws=new WebSocket(this.opts.url),this.ws.onopen=()=>this.queueConnect(),this.ws.onmessage=t=>this.handleMessage(String(t.data??"")),this.ws.onclose=t=>{const n=String(t.reason??"");this.ws=null,this.flushPending(new Error(`gateway closed (${t.code}): ${n}`)),this.opts.onClose?.({code:t.code,reason:n}),this.scheduleReconnect()},this.ws.onerror=()=>{})}scheduleReconnect(){if(this.closed)return;const t=this.backoffMs;this.backoffMs=Math.min(this.backoffMs*1.7,15e3),window.setTimeout(()=>this.connect(),t)}flushPending(t){for(const[,n]of this.pending)n.reject(t);this.pending.clear()}async sendConnect(){if(this.connectSent)return;this.connectSent=!0,this.connectTimer!==null&&(window.clearTimeout(this.connectTimer),this.connectTimer=null);const t=typeof crypto<"u"&&!!crypto.subtle,n=["operator.admin","operator.approvals","operator.pairing"],s="operator";let i=null,a=!1,o=this.opts.token;if(t){i=await Us();const d=Fc({deviceId:i.deviceId,role:s})?.token;o=d??this.opts.token,a=!!(d&&this.opts.token)}const c=o||this.opts.password?{token:o,password:this.opts.password}:void 0;let l;if(t&&i){const d=Date.now(),u=this.connectNonce??void 0,h=Oh({deviceId:i.deviceId,clientId:this.opts.clientName??za.CONTROL_UI,clientMode:this.opts.mode??Ss.WEBCHAT,role:s,scopes:n,signedAtMs:d,token:o??null,nonce:u}),v=await Dc(i.privateKey,h);l={id:i.deviceId,publicKey:i.publicKey,signature:v,signedAt:d,nonce:u}}const p={minProtocol:3,maxProtocol:3,client:{id:this.opts.clientName??za.CONTROL_UI,version:this.opts.clientVersion??"dev",platform:this.opts.platform??navigator.platform??"web",mode:this.opts.mode??Ss.WEBCHAT,instanceId:this.opts.instanceId},role:s,scopes:n,device:l,caps:[],auth:c,userAgent:navigator.userAgent,locale:navigator.language};this.request("connect",p).then(d=>{d?.auth?.deviceToken&&i&&Eo({deviceId:i.deviceId,role:d.auth.role??s,token:d.auth.deviceToken,scopes:d.auth.scopes??[]}),this.backoffMs=800,this.opts.onHello?.(d)}).catch(()=>{a&&i&&Lo({deviceId:i.deviceId,role:s}),this.ws?.close(Dh,"connect failed")})}handleMessage(t){let n;try{n=JSON.parse(t)}catch{return}const s=n;if(s.type==="event"){const i=n;if(i.event==="connect.challenge"){const o=i.payload,c=o&&typeof o.nonce=="string"?o.nonce:null;c&&(this.connectNonce=c,this.sendConnect());return}const a=typeof i.seq=="number"?i.seq:null;a!==null&&(this.lastSeq!==null&&a>this.lastSeq+1&&this.opts.onGap?.({expected:this.lastSeq+1,received:a}),this.lastSeq=a);try{this.opts.onEvent?.(i)}catch(o){console.error("[gateway] event handler error:",o)}return}if(s.type==="res"){const i=n,a=this.pending.get(i.id);if(!a)return;this.pending.delete(i.id),i.ok?a.resolve(i.payload):a.reject(new Error(i.error?.message??"request failed"));return}}request(t,n){if(!this.ws||this.ws.readyState!==WebSocket.OPEN)return Promise.reject(new Error("gateway not connected"));const s=Ps(),i={type:"req",id:s,method:t,params:n},a=new Promise((o,c)=>{this.pending.set(s,{resolve:l=>o(l),reject:c})});return this.ws.send(JSON.stringify(i)),a}queueConnect(){this.connectNonce=null,this.connectSent=!1,this.connectTimer!==null&&window.clearTimeout(this.connectTimer),this.connectTimer=window.setTimeout(()=>{this.sendConnect()},750)}}function _s(e){return typeof e=="object"&&e!==null}function Fh(e){if(!_s(e))return null;const t=typeof e.id=="string"?e.id.trim():"",n=e.request;if(!t||!_s(n))return null;const s=typeof n.command=="string"?n.command.trim():"";if(!s)return null;const i=typeof e.createdAtMs=="number"?e.createdAtMs:0,a=typeof e.expiresAtMs=="number"?e.expiresAtMs:0;return!i||!a?null:{id:t,request:{command:s,cwd:typeof n.cwd=="string"?n.cwd:null,host:typeof n.host=="string"?n.host:null,security:typeof n.security=="string"?n.security:null,ask:typeof n.ask=="string"?n.ask:null,agentId:typeof n.agentId=="string"?n.agentId:null,resolvedPath:typeof n.resolvedPath=="string"?n.resolvedPath:null,sessionKey:typeof n.sessionKey=="string"?n.sessionKey:null},createdAtMs:i,expiresAtMs:a}}function Uh(e){if(!_s(e))return null;const t=typeof e.id=="string"?e.id.trim():"";return t?{id:t,decision:typeof e.decision=="string"?e.decision:null,resolvedBy:typeof e.resolvedBy=="string"?e.resolvedBy:null,ts:typeof e.ts=="number"?e.ts:null}:null}function br(e){const t=Date.now();return e.filter(n=>n.expiresAtMs>t)}function Kh(e,t){const n=br(e).filter(s=>s.id!==t.id);return n.push(t),n}function ja(e,t){return br(e).filter(n=>n.id!==t)}async function yr(e,t){if(!e.client||!e.connected)return;const n=e.sessionKey.trim(),s=n?{sessionKey:n}:{};try{const i=await e.client.request("agent.identity.get",s);if(!i)return;const a=ns(i);e.assistantName=a.name,e.assistantAvatar=a.avatar,e.assistantAgentId=a.agentId??null}catch{}}function Xn(e,t){const n=(e??"").trim(),s=t.mainSessionKey?.trim();if(!s)return n;if(!n)return s;const i=t.mainKey?.trim()||"main",a=t.defaultAgentId?.trim();return n==="main"||n===i||a&&(n===`agent:${a}:main`||n===`agent:${a}:${i}`)?s:n}function Hh(e,t){if(!t?.mainSessionKey)return;const n=Xn(e.sessionKey,t),s=Xn(e.settings.sessionKey,t),i=Xn(e.settings.lastActiveSessionKey,t),a=n||s||e.sessionKey,o={...e.settings,sessionKey:s||a,lastActiveSessionKey:i||a},c=o.sessionKey!==e.settings.sessionKey||o.lastActiveSessionKey!==e.settings.lastActiveSessionKey;a!==e.sessionKey&&(e.sessionKey=a),c&&ke(e,o)}function wr(e){e.lastError=null,e.hello=null,e.connected=!1,e.execApprovalQueue=[],e.execApprovalError=null,e.client?.stop(),e.client=new Bh({url:e.settings.gatewayUrl,token:e.settings.token.trim()?e.settings.token:void 0,password:e.password.trim()?e.password:void 0,clientName:"clawdbot-control-ui",mode:"webchat",onHello:t=>{e.connected=!0,e.lastError=null,e.hello=t,qh(e,t),yr(e),Nh(e),pn(e,{quiet:!0}),Te(e,{quiet:!0}),Qs(e)},onClose:({code:t,reason:n})=>{e.connected=!1,t!==1012&&(e.lastError=`disconnected (${t}): ${n||"no reason"}`)},onEvent:t=>zh(e,t),onGap:({expected:t,received:n})=>{e.lastError=`event gap detected (expected seq ${t}, got ${n}); refresh recommended`}}),e.client.start()}function zh(e,t){try{jh(e,t)}catch(n){console.error("[gateway] handleGatewayEvent error:",t.event,n)}}function jh(e,t){if(e.eventLogBuffer=[{ts:Date.now(),event:t.event,payload:t.payload},...e.eventLogBuffer].slice(0,250),e.tab==="debug"&&(e.eventLog=e.eventLogBuffer),t.event==="agent"){if(e.onboarding)return;Hl(e,t.payload);return}if(t.event==="chat"){const n=t.payload;n?.sessionKey&&Mo(e,n.sessionKey);const s=Ll(e,n);(s==="final"||s==="error"||s==="aborted")&&(Ns(e),$d(e)),s==="final"&&Xe(e);return}if(t.event==="presence"){const n=t.payload;n?.presence&&Array.isArray(n.presence)&&(e.presenceEntries=n.presence,e.presenceError=null,e.presenceStatus=null);return}if(t.event==="cron"&&e.tab==="cron"&&Zs(e),(t.event==="device.pair.requested"||t.event==="device.pair.resolved")&&Te(e,{quiet:!0}),t.event==="exec.approval.requested"){const n=Fh(t.payload);if(n){e.execApprovalQueue=Kh(e.execApprovalQueue,n),e.execApprovalError=null;const s=Math.max(0,n.expiresAtMs-Date.now()+500);window.setTimeout(()=>{e.execApprovalQueue=ja(e.execApprovalQueue,n.id)},s)}return}if(t.event==="exec.approval.resolved"){const n=Uh(t.payload);n&&(e.execApprovalQueue=ja(e.execApprovalQueue,n.id))}}function qh(e,t){const n=t.snapshot;n?.presence&&Array.isArray(n.presence)&&(e.presenceEntries=n.presence),n?.health&&(e.debugHealth=n.health),n?.sessionDefaults&&Hh(e,n.sessionDefaults)}function Vh(e){e.basePath=ld(),pd(e,!0),cd(e),dd(e),window.addEventListener("popstate",e.popStateHandler),ad(e),wr(e),sd(e),e.tab==="logs"&&Vs(e),e.tab==="debug"&&Gs(e)}function Wh(e){Wl(e)}function Gh(e){window.removeEventListener("popstate",e.popStateHandler),id(e),Ws(e),Ys(e),ud(e),e.topbarObserver?.disconnect(),e.topbarObserver=null}function Yh(e,t){if(e.tab==="chat"&&(t.has("chatMessages")||t.has("chatToolMessages")||t.has("chatStream")||t.has("chatLoading")||t.has("tab"))){const n=t.has("tab"),s=t.has("chatLoading")&&t.get("chatLoading")===!0&&e.chatLoading===!1;ln(e,n||s||!e.chatHasAutoScrolled)}e.tab==="logs"&&(t.has("logsEntries")||t.has("logsAutoFollow")||t.has("tab"))&&e.logsAutoFollow&&e.logsAtBottom&&ro(e,t.has("tab")||t.has("logsAutoFollow"))}async function Qh(e,t){await ic(e,t),await oe(e,!0)}async function Zh(e){await ac(e),await oe(e,!0)}async function Jh(e){await oc(e),await oe(e,!0)}async function Xh(e){await ls(e),await be(e),await oe(e,!0)}async function eg(e){await be(e),await oe(e,!0)}function tg(e){if(!Array.isArray(e))return{};const t={};for(const n of e){if(typeof n!="string")continue;const[s,...i]=n.split(":");if(!s||i.length===0)continue;const a=s.trim(),o=i.join(":").trim();a&&o&&(t[a]=o)}return t}function $r(e){return(e.channelsSnapshot?.channelAccounts?.nostr??[])[0]?.accountId??e.nostrProfileAccountId??"default"}function xr(e,t=""){return`/api/channels/nostr/${encodeURIComponent(e)}/profile${t}`}function ng(e,t,n){e.nostrProfileAccountId=t,e.nostrProfileFormState=Xp(n??void 0)}function sg(e){e.nostrProfileFormState=null,e.nostrProfileAccountId=null}function ig(e,t,n){const s=e.nostrProfileFormState;s&&(e.nostrProfileFormState={...s,values:{...s.values,[t]:n},fieldErrors:{...s.fieldErrors,[t]:""}})}function ag(e){const t=e.nostrProfileFormState;t&&(e.nostrProfileFormState={...t,showAdvanced:!t.showAdvanced})}async function og(e){const t=e.nostrProfileFormState;if(!t||t.saving)return;const n=$r(e);e.nostrProfileFormState={...t,saving:!0,error:null,success:null,fieldErrors:{}};try{const s=await fetch(xr(n),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(t.values)}),i=await s.json().catch(()=>null);if(!s.ok||i?.ok===!1||!i){const a=i?.error??`Profile update failed (${s.status})`;e.nostrProfileFormState={...t,saving:!1,error:a,success:null,fieldErrors:tg(i?.details)};return}if(!i.persisted){e.nostrProfileFormState={...t,saving:!1,error:"Profile publish failed on all relays.",success:null};return}e.nostrProfileFormState={...t,saving:!1,error:null,success:"Profile published to relays.",fieldErrors:{},original:{...t.values}},await oe(e,!0)}catch(s){e.nostrProfileFormState={...t,saving:!1,error:`Profile update failed: ${String(s)}`,success:null}}}async function rg(e){const t=e.nostrProfileFormState;if(!t||t.importing)return;const n=$r(e);e.nostrProfileFormState={...t,importing:!0,error:null,success:null};try{const s=await fetch(xr(n,"/import"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({autoMerge:!0})}),i=await s.json().catch(()=>null);if(!s.ok||i?.ok===!1||!i){const l=i?.error??`Profile import failed (${s.status})`;e.nostrProfileFormState={...t,importing:!1,error:l,success:null};return}const a=i.merged??i.imported??null,o=a?{...t.values,...a}:t.values,c=!!(o.banner||o.website||o.nip05||o.lud16);e.nostrProfileFormState={...t,importing:!1,values:o,error:null,success:i.saved?"Profile imported from relays. Review and publish.":"Profile imported. Review and publish.",showAdvanced:c},i.saved&&await oe(e,!0)}catch(s){e.nostrProfileFormState={...t,importing:!1,error:`Profile import failed: ${String(s)}`,success:null}}}var lg=Object.defineProperty,cg=Object.getOwnPropertyDescriptor,b=(e,t,n,s)=>{for(var i=s>1?void 0:s?cg(t,n):t,a=e.length-1,o;a>=0;a--)(o=e[a])&&(i=(s?o(t,n,i):o(i))||i);return s&&i&&lg(t,n,i),i};const es=ul();function dg(){if(!window.location.search)return!1;const t=new URLSearchParams(window.location.search).get("onboarding");if(!t)return!1;const n=t.trim().toLowerCase();return n==="1"||n==="true"||n==="yes"||n==="on"}let m=class extends Ze{constructor(){super(...arguments),this.settings=pl(),this.password="",this.tab="chat",this.onboarding=dg(),this.connected=!1,this.theme=this.settings.theme??"system",this.themeResolved="dark",this.hello=null,this.lastError=null,this.eventLog=[],this.eventLogBuffer=[],this.toolStreamSyncTimer=null,this.sidebarCloseTimer=null,this.assistantName=es.name,this.assistantAvatar=es.avatar,this.assistantAgentId=es.agentId??null,this.sessionKey=this.settings.sessionKey,this.chatLoading=!1,this.chatSending=!1,this.chatMessage="",this.chatMessages=[],this.chatToolMessages=[],this.chatStream=null,this.chatStreamStartedAt=null,this.chatRunId=null,this.compactionStatus=null,this.chatAvatarUrl=null,this.chatThinkingLevel=null,this.chatQueue=[],this.sidebarOpen=!1,this.sidebarContent=null,this.sidebarError=null,this.splitRatio=this.settings.splitRatio,this.nodesLoading=!1,this.nodes=[],this.devicesLoading=!1,this.devicesError=null,this.devicesList=null,this.execApprovalsLoading=!1,this.execApprovalsSaving=!1,this.execApprovalsDirty=!1,this.execApprovalsSnapshot=null,this.execApprovalsForm=null,this.execApprovalsSelectedAgent=null,this.execApprovalsTarget="gateway",this.execApprovalsTargetNodeId=null,this.execApprovalQueue=[],this.execApprovalBusy=!1,this.execApprovalError=null,this.configLoading=!1,this.configRaw=`{ -} -`,this.configRawOriginal="",this.configValid=null,this.configIssues=[],this.configSaving=!1,this.configApplying=!1,this.updateRunning=!1,this.applySessionKey=this.settings.lastActiveSessionKey,this.configSnapshot=null,this.configSchema=null,this.configSchemaVersion=null,this.configSchemaLoading=!1,this.configUiHints={},this.configForm=null,this.configFormOriginal=null,this.configFormDirty=!1,this.configFormMode="form",this.configSearchQuery="",this.configActiveSection=null,this.configActiveSubsection=null,this.channelsLoading=!1,this.channelsSnapshot=null,this.channelsError=null,this.channelsLastSuccess=null,this.whatsappLoginMessage=null,this.whatsappLoginQrDataUrl=null,this.whatsappLoginConnected=null,this.whatsappBusy=!1,this.nostrProfileFormState=null,this.nostrProfileAccountId=null,this.presenceLoading=!1,this.presenceEntries=[],this.presenceError=null,this.presenceStatus=null,this.agentsLoading=!1,this.agentsList=null,this.agentsError=null,this.sessionsLoading=!1,this.sessionsResult=null,this.sessionsError=null,this.sessionsFilterActive="",this.sessionsFilterLimit="120",this.sessionsIncludeGlobal=!0,this.sessionsIncludeUnknown=!1,this.cronLoading=!1,this.cronJobs=[],this.cronStatus=null,this.cronError=null,this.cronForm={...Ph},this.cronRunsJobId=null,this.cronRuns=[],this.cronBusy=!1,this.skillsLoading=!1,this.skillsReport=null,this.skillsError=null,this.skillsFilter="",this.skillEdits={},this.skillsBusyKey=null,this.skillMessages={},this.debugLoading=!1,this.debugStatus=null,this.debugHealth=null,this.debugModels=[],this.debugHeartbeat=null,this.debugCallMethod="",this.debugCallParams="{}",this.debugCallResult=null,this.debugCallError=null,this.logsLoading=!1,this.logsError=null,this.logsFile=null,this.logsEntries=[],this.logsFilterText="",this.logsLevelFilters={...Rh},this.logsAutoFollow=!0,this.logsTruncated=!1,this.logsCursor=null,this.logsLastFetchAt=null,this.logsLimit=500,this.logsMaxBytes=25e4,this.logsAtBottom=!0,this.client=null,this.chatScrollFrame=null,this.chatScrollTimeout=null,this.chatHasAutoScrolled=!1,this.chatUserNearBottom=!0,this.nodesPollInterval=null,this.logsPollInterval=null,this.debugPollInterval=null,this.logsScrollFrame=null,this.toolStreamById=new Map,this.toolStreamOrder=[],this.basePath="",this.popStateHandler=()=>fd(this),this.themeMedia=null,this.themeMediaHandler=null,this.topbarObserver=null}createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),Vh(this)}firstUpdated(){Wh(this)}disconnectedCallback(){Gh(this),super.disconnectedCallback()}updated(e){Yh(this,e)}connect(){wr(this)}handleChatScroll(e){zl(this,e)}handleLogsScroll(e){jl(this,e)}exportLogs(e,t){Vl(e,t)}resetToolStream(){Ns(this)}resetChatScroll(){ql(this)}async loadAssistantIdentity(){await yr(this)}applySettings(e){ke(this,e)}setTab(e){od(this,e)}setTheme(e,t){rd(this,e,t)}async loadOverview(){await Po(this)}async loadCron(){await Zs(this)}async handleAbortChat(){await Oo(this)}removeQueuedMessage(e){bd(this,e)}async handleSendChat(e,t){await yd(this,e,t)}async handleWhatsAppStart(e){await Qh(this,e)}async handleWhatsAppWait(){await Zh(this)}async handleWhatsAppLogout(){await Jh(this)}async handleChannelConfigSave(){await Xh(this)}async handleChannelConfigReload(){await eg(this)}handleNostrProfileEdit(e,t){ng(this,e,t)}handleNostrProfileCancel(){sg(this)}handleNostrProfileFieldChange(e,t){ig(this,e,t)}async handleNostrProfileSave(){await og(this)}async handleNostrProfileImport(){await rg(this)}handleNostrProfileToggleAdvanced(){ag(this)}async handleExecApprovalDecision(e){const t=this.execApprovalQueue[0];if(!(!t||!this.client||this.execApprovalBusy)){this.execApprovalBusy=!0,this.execApprovalError=null;try{await this.client.request("exec.approval.resolve",{id:t.id,decision:e}),this.execApprovalQueue=this.execApprovalQueue.filter(n=>n.id!==t.id)}catch(n){this.execApprovalError=`Exec approval failed: ${String(n)}`}finally{this.execApprovalBusy=!1}}}handleOpenSidebar(e){this.sidebarCloseTimer!=null&&(window.clearTimeout(this.sidebarCloseTimer),this.sidebarCloseTimer=null),this.sidebarContent=e,this.sidebarError=null,this.sidebarOpen=!0}handleCloseSidebar(){this.sidebarOpen=!1,this.sidebarCloseTimer!=null&&window.clearTimeout(this.sidebarCloseTimer),this.sidebarCloseTimer=window.setTimeout(()=>{this.sidebarOpen||(this.sidebarContent=null,this.sidebarError=null,this.sidebarCloseTimer=null)},200)}handleSplitRatioChange(e){const t=Math.max(.4,Math.min(.7,e));this.splitRatio=t,this.applySettings({...this.settings,splitRatio:t})}render(){return Ih(this)}};b([y()],m.prototype,"settings",2);b([y()],m.prototype,"password",2);b([y()],m.prototype,"tab",2);b([y()],m.prototype,"onboarding",2);b([y()],m.prototype,"connected",2);b([y()],m.prototype,"theme",2);b([y()],m.prototype,"themeResolved",2);b([y()],m.prototype,"hello",2);b([y()],m.prototype,"lastError",2);b([y()],m.prototype,"eventLog",2);b([y()],m.prototype,"assistantName",2);b([y()],m.prototype,"assistantAvatar",2);b([y()],m.prototype,"assistantAgentId",2);b([y()],m.prototype,"sessionKey",2);b([y()],m.prototype,"chatLoading",2);b([y()],m.prototype,"chatSending",2);b([y()],m.prototype,"chatMessage",2);b([y()],m.prototype,"chatMessages",2);b([y()],m.prototype,"chatToolMessages",2);b([y()],m.prototype,"chatStream",2);b([y()],m.prototype,"chatStreamStartedAt",2);b([y()],m.prototype,"chatRunId",2);b([y()],m.prototype,"compactionStatus",2);b([y()],m.prototype,"chatAvatarUrl",2);b([y()],m.prototype,"chatThinkingLevel",2);b([y()],m.prototype,"chatQueue",2);b([y()],m.prototype,"sidebarOpen",2);b([y()],m.prototype,"sidebarContent",2);b([y()],m.prototype,"sidebarError",2);b([y()],m.prototype,"splitRatio",2);b([y()],m.prototype,"nodesLoading",2);b([y()],m.prototype,"nodes",2);b([y()],m.prototype,"devicesLoading",2);b([y()],m.prototype,"devicesError",2);b([y()],m.prototype,"devicesList",2);b([y()],m.prototype,"execApprovalsLoading",2);b([y()],m.prototype,"execApprovalsSaving",2);b([y()],m.prototype,"execApprovalsDirty",2);b([y()],m.prototype,"execApprovalsSnapshot",2);b([y()],m.prototype,"execApprovalsForm",2);b([y()],m.prototype,"execApprovalsSelectedAgent",2);b([y()],m.prototype,"execApprovalsTarget",2);b([y()],m.prototype,"execApprovalsTargetNodeId",2);b([y()],m.prototype,"execApprovalQueue",2);b([y()],m.prototype,"execApprovalBusy",2);b([y()],m.prototype,"execApprovalError",2);b([y()],m.prototype,"configLoading",2);b([y()],m.prototype,"configRaw",2);b([y()],m.prototype,"configRawOriginal",2);b([y()],m.prototype,"configValid",2);b([y()],m.prototype,"configIssues",2);b([y()],m.prototype,"configSaving",2);b([y()],m.prototype,"configApplying",2);b([y()],m.prototype,"updateRunning",2);b([y()],m.prototype,"applySessionKey",2);b([y()],m.prototype,"configSnapshot",2);b([y()],m.prototype,"configSchema",2);b([y()],m.prototype,"configSchemaVersion",2);b([y()],m.prototype,"configSchemaLoading",2);b([y()],m.prototype,"configUiHints",2);b([y()],m.prototype,"configForm",2);b([y()],m.prototype,"configFormOriginal",2);b([y()],m.prototype,"configFormDirty",2);b([y()],m.prototype,"configFormMode",2);b([y()],m.prototype,"configSearchQuery",2);b([y()],m.prototype,"configActiveSection",2);b([y()],m.prototype,"configActiveSubsection",2);b([y()],m.prototype,"channelsLoading",2);b([y()],m.prototype,"channelsSnapshot",2);b([y()],m.prototype,"channelsError",2);b([y()],m.prototype,"channelsLastSuccess",2);b([y()],m.prototype,"whatsappLoginMessage",2);b([y()],m.prototype,"whatsappLoginQrDataUrl",2);b([y()],m.prototype,"whatsappLoginConnected",2);b([y()],m.prototype,"whatsappBusy",2);b([y()],m.prototype,"nostrProfileFormState",2);b([y()],m.prototype,"nostrProfileAccountId",2);b([y()],m.prototype,"presenceLoading",2);b([y()],m.prototype,"presenceEntries",2);b([y()],m.prototype,"presenceError",2);b([y()],m.prototype,"presenceStatus",2);b([y()],m.prototype,"agentsLoading",2);b([y()],m.prototype,"agentsList",2);b([y()],m.prototype,"agentsError",2);b([y()],m.prototype,"sessionsLoading",2);b([y()],m.prototype,"sessionsResult",2);b([y()],m.prototype,"sessionsError",2);b([y()],m.prototype,"sessionsFilterActive",2);b([y()],m.prototype,"sessionsFilterLimit",2);b([y()],m.prototype,"sessionsIncludeGlobal",2);b([y()],m.prototype,"sessionsIncludeUnknown",2);b([y()],m.prototype,"cronLoading",2);b([y()],m.prototype,"cronJobs",2);b([y()],m.prototype,"cronStatus",2);b([y()],m.prototype,"cronError",2);b([y()],m.prototype,"cronForm",2);b([y()],m.prototype,"cronRunsJobId",2);b([y()],m.prototype,"cronRuns",2);b([y()],m.prototype,"cronBusy",2);b([y()],m.prototype,"skillsLoading",2);b([y()],m.prototype,"skillsReport",2);b([y()],m.prototype,"skillsError",2);b([y()],m.prototype,"skillsFilter",2);b([y()],m.prototype,"skillEdits",2);b([y()],m.prototype,"skillsBusyKey",2);b([y()],m.prototype,"skillMessages",2);b([y()],m.prototype,"debugLoading",2);b([y()],m.prototype,"debugStatus",2);b([y()],m.prototype,"debugHealth",2);b([y()],m.prototype,"debugModels",2);b([y()],m.prototype,"debugHeartbeat",2);b([y()],m.prototype,"debugCallMethod",2);b([y()],m.prototype,"debugCallParams",2);b([y()],m.prototype,"debugCallResult",2);b([y()],m.prototype,"debugCallError",2);b([y()],m.prototype,"logsLoading",2);b([y()],m.prototype,"logsError",2);b([y()],m.prototype,"logsFile",2);b([y()],m.prototype,"logsEntries",2);b([y()],m.prototype,"logsFilterText",2);b([y()],m.prototype,"logsLevelFilters",2);b([y()],m.prototype,"logsAutoFollow",2);b([y()],m.prototype,"logsTruncated",2);b([y()],m.prototype,"logsCursor",2);b([y()],m.prototype,"logsLastFetchAt",2);b([y()],m.prototype,"logsLimit",2);b([y()],m.prototype,"logsMaxBytes",2);b([y()],m.prototype,"logsAtBottom",2);m=b([Ja("clawdbot-app")],m); -//# sourceMappingURL=index-DQcOTEYz.js.map diff --git a/dist/control-ui/assets/index-DQcOTEYz.js.map b/dist/control-ui/assets/index-DQcOTEYz.js.map deleted file mode 100644 index d48222472..000000000 --- a/dist/control-ui/assets/index-DQcOTEYz.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"index-DQcOTEYz.js","sources":["../../../node_modules/.pnpm/@lit+reactive-element@2.1.2/node_modules/@lit/reactive-element/css-tag.js","../../../node_modules/.pnpm/@lit+reactive-element@2.1.2/node_modules/@lit/reactive-element/reactive-element.js","../../../node_modules/.pnpm/lit-html@3.3.2/node_modules/lit-html/lit-html.js","../../../node_modules/.pnpm/lit-element@4.2.2/node_modules/lit-element/lit-element.js","../../../node_modules/.pnpm/@lit+reactive-element@2.1.2/node_modules/@lit/reactive-element/decorators/custom-element.js","../../../node_modules/.pnpm/@lit+reactive-element@2.1.2/node_modules/@lit/reactive-element/decorators/property.js","../../../node_modules/.pnpm/@lit+reactive-element@2.1.2/node_modules/@lit/reactive-element/decorators/state.js","../../../ui/src/ui/assistant-identity.ts","../../../ui/src/ui/storage.ts","../../../src/sessions/session-key-utils.ts","../../../ui/src/ui/navigation.ts","../../../ui/src/ui/icons.ts","../../../src/shared/text/reasoning-tags.ts","../../../ui/src/ui/format.ts","../../../ui/src/ui/chat/message-extract.ts","../../../ui/src/ui/uuid.ts","../../../ui/src/ui/controllers/chat.ts","../../../ui/src/ui/controllers/sessions.ts","../../../ui/src/ui/app-tool-stream.ts","../../../ui/src/ui/app-scroll.ts","../../../ui/src/ui/controllers/config/form-utils.ts","../../../ui/src/ui/controllers/config.ts","../../../ui/src/ui/controllers/cron.ts","../../../ui/src/ui/controllers/channels.ts","../../../ui/src/ui/controllers/debug.ts","../../../ui/src/ui/controllers/logs.ts","../../../node_modules/.pnpm/@noble+ed25519@3.0.0/node_modules/@noble/ed25519/index.js","../../../ui/src/ui/device-identity.ts","../../../ui/src/ui/device-auth.ts","../../../ui/src/ui/controllers/devices.ts","../../../ui/src/ui/controllers/nodes.ts","../../../ui/src/ui/controllers/exec-approvals.ts","../../../ui/src/ui/controllers/presence.ts","../../../ui/src/ui/controllers/skills.ts","../../../ui/src/ui/theme.ts","../../../ui/src/ui/theme-transition.ts","../../../ui/src/ui/app-polling.ts","../../../ui/src/ui/app-settings.ts","../../../ui/src/ui/app-chat.ts","../../../node_modules/.pnpm/lit-html@3.3.2/node_modules/lit-html/directive.js","../../../node_modules/.pnpm/lit-html@3.3.2/node_modules/lit-html/directive-helpers.js","../../../node_modules/.pnpm/lit-html@3.3.2/node_modules/lit-html/directives/repeat.js","../../../ui/src/ui/chat/message-normalizer.ts","../../../node_modules/.pnpm/lit-html@3.3.2/node_modules/lit-html/directives/unsafe-html.js","../../../node_modules/.pnpm/dompurify@3.3.1/node_modules/dompurify/dist/purify.es.mjs","../../../node_modules/.pnpm/marked@17.0.1/node_modules/marked/lib/marked.esm.js","../../../ui/src/ui/markdown.ts","../../../ui/src/ui/chat/copy-as-markdown.ts","../../../ui/src/ui/tool-display.ts","../../../ui/src/ui/chat/constants.ts","../../../ui/src/ui/chat/tool-helpers.ts","../../../ui/src/ui/chat/tool-cards.ts","../../../ui/src/ui/chat/grouped-render.ts","../../../ui/src/ui/views/markdown-sidebar.ts","../../../ui/src/ui/components/resizable-divider.ts","../../../ui/src/ui/views/chat.ts","../../../ui/src/ui/views/config-form.shared.ts","../../../ui/src/ui/views/config-form.node.ts","../../../ui/src/ui/views/config-form.render.ts","../../../ui/src/ui/views/config-form.analyze.ts","../../../ui/src/ui/views/config.ts","../../../ui/src/ui/views/channels.shared.ts","../../../ui/src/ui/views/channels.config.ts","../../../ui/src/ui/views/channels.discord.ts","../../../ui/src/ui/views/channels.googlechat.ts","../../../ui/src/ui/views/channels.imessage.ts","../../../ui/src/ui/views/channels.nostr-profile-form.ts","../../../ui/src/ui/views/channels.nostr.ts","../../../ui/src/ui/views/channels.signal.ts","../../../ui/src/ui/views/channels.slack.ts","../../../ui/src/ui/views/channels.telegram.ts","../../../ui/src/ui/views/channels.whatsapp.ts","../../../ui/src/ui/views/channels.ts","../../../ui/src/ui/presenter.ts","../../../ui/src/ui/views/cron.ts","../../../ui/src/ui/views/debug.ts","../../../ui/src/ui/views/instances.ts","../../../ui/src/ui/views/logs.ts","../../../ui/src/ui/views/nodes.ts","../../../ui/src/ui/views/overview.ts","../../../ui/src/ui/views/sessions.ts","../../../ui/src/ui/views/exec-approval.ts","../../../ui/src/ui/views/skills.ts","../../../ui/src/ui/app-render.helpers.ts","../../../ui/src/ui/app-render.ts","../../../ui/src/ui/app-defaults.ts","../../../ui/src/ui/controllers/agents.ts","../../../src/gateway/protocol/client-info.ts","../../../src/gateway/device-auth.ts","../../../ui/src/ui/gateway.ts","../../../ui/src/ui/controllers/exec-approval.ts","../../../ui/src/ui/controllers/assistant-identity.ts","../../../ui/src/ui/app-gateway.ts","../../../ui/src/ui/app-lifecycle.ts","../../../ui/src/ui/app-channels.ts","../../../ui/src/ui/app.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */\nconst t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow)&&\"adoptedStyleSheets\"in Document.prototype&&\"replace\"in CSSStyleSheet.prototype,s=Symbol(),o=new WeakMap;class n{constructor(t,e,o){if(this._$cssResult$=!0,o!==s)throw Error(\"CSSResult is not constructable. Use `unsafeCSS` or `css` instead.\");this.cssText=t,this.t=e}get styleSheet(){let t=this.o;const s=this.t;if(e&&void 0===t){const e=void 0!==s&&1===s.length;e&&(t=o.get(s)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),e&&o.set(s,t))}return t}toString(){return this.cssText}}const r=t=>new n(\"string\"==typeof t?t:t+\"\",void 0,s),i=(t,...e)=>{const o=1===t.length?t[0]:e.reduce((e,s,o)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if(\"number\"==typeof t)return t;throw Error(\"Value passed to 'css' function must be a 'css' function result: \"+t+\". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.\")})(s)+t[o+1],t[0]);return new n(o,t,s)},S=(s,o)=>{if(e)s.adoptedStyleSheets=o.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(const e of o){const o=document.createElement(\"style\"),n=t.litNonce;void 0!==n&&o.setAttribute(\"nonce\",n),o.textContent=e.cssText,s.appendChild(o)}},c=e?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e=\"\";for(const s of t.cssRules)e+=s.cssText;return r(e)})(t):t;export{n as CSSResult,S as adoptStyles,i as css,c as getCompatibleStyle,e as supportsAdoptingStyleSheets,r as unsafeCSS};\n//# sourceMappingURL=css-tag.js.map\n","import{getCompatibleStyle as t,adoptStyles as s}from\"./css-tag.js\";export{CSSResult,css,supportsAdoptingStyleSheets,unsafeCSS}from\"./css-tag.js\";\n/**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */const{is:i,defineProperty:e,getOwnPropertyDescriptor:h,getOwnPropertyNames:r,getOwnPropertySymbols:o,getPrototypeOf:n}=Object,a=globalThis,c=a.trustedTypes,l=c?c.emptyScript:\"\",p=a.reactiveElementPolyfillSupport,d=(t,s)=>t,u={toAttribute(t,s){switch(s){case Boolean:t=t?l:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,s){let i=t;switch(s){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t)}catch(t){i=null}}return i}},f=(t,s)=>!i(t,s),b={attribute:!0,type:String,converter:u,reflect:!1,useDefault:!1,hasChanged:f};Symbol.metadata??=Symbol(\"metadata\"),a.litPropertyMetadata??=new WeakMap;class y extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,s=b){if(s.state&&(s.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(t)&&((s=Object.create(s)).wrapped=!0),this.elementProperties.set(t,s),!s.noAccessor){const i=Symbol(),h=this.getPropertyDescriptor(t,i,s);void 0!==h&&e(this.prototype,t,h)}}static getPropertyDescriptor(t,s,i){const{get:e,set:r}=h(this.prototype,t)??{get(){return this[s]},set(t){this[s]=t}};return{get:e,set(s){const h=e?.call(this);r?.call(this,s),this.requestUpdate(t,h,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)??b}static _$Ei(){if(this.hasOwnProperty(d(\"elementProperties\")))return;const t=n(this);t.finalize(),void 0!==t.l&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties)}static finalize(){if(this.hasOwnProperty(d(\"finalized\")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(d(\"properties\"))){const t=this.properties,s=[...r(t),...o(t)];for(const i of s)this.createProperty(i,t[i])}const t=this[Symbol.metadata];if(null!==t){const s=litPropertyMetadata.get(t);if(void 0!==s)for(const[t,i]of s)this.elementProperties.set(t,i)}this._$Eh=new Map;for(const[t,s]of this.elementProperties){const i=this._$Eu(t,s);void 0!==i&&this._$Eh.set(i,t)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(s){const i=[];if(Array.isArray(s)){const e=new Set(s.flat(1/0).reverse());for(const s of e)i.unshift(t(s))}else void 0!==s&&i.push(t(s));return i}static _$Eu(t,s){const i=s.attribute;return!1===i?void 0:\"string\"==typeof i?i:\"string\"==typeof t?t.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(t=>this.enableUpdating=t),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(t=>t(this))}addController(t){(this._$EO??=new Set).add(t),void 0!==this.renderRoot&&this.isConnected&&t.hostConnected?.()}removeController(t){this._$EO?.delete(t)}_$E_(){const t=new Map,s=this.constructor.elementProperties;for(const i of s.keys())this.hasOwnProperty(i)&&(t.set(i,this[i]),delete this[i]);t.size>0&&(this._$Ep=t)}createRenderRoot(){const t=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return s(t,this.constructor.elementStyles),t}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(t=>t.hostConnected?.())}enableUpdating(t){}disconnectedCallback(){this._$EO?.forEach(t=>t.hostDisconnected?.())}attributeChangedCallback(t,s,i){this._$AK(t,i)}_$ET(t,s){const i=this.constructor.elementProperties.get(t),e=this.constructor._$Eu(t,i);if(void 0!==e&&!0===i.reflect){const h=(void 0!==i.converter?.toAttribute?i.converter:u).toAttribute(s,i.type);this._$Em=t,null==h?this.removeAttribute(e):this.setAttribute(e,h),this._$Em=null}}_$AK(t,s){const i=this.constructor,e=i._$Eh.get(t);if(void 0!==e&&this._$Em!==e){const t=i.getPropertyOptions(e),h=\"function\"==typeof t.converter?{fromAttribute:t.converter}:void 0!==t.converter?.fromAttribute?t.converter:u;this._$Em=e;const r=h.fromAttribute(s,t.type);this[e]=r??this._$Ej?.get(e)??r,this._$Em=null}}requestUpdate(t,s,i,e=!1,h){if(void 0!==t){const r=this.constructor;if(!1===e&&(h=this[t]),i??=r.getPropertyOptions(t),!((i.hasChanged??f)(h,s)||i.useDefault&&i.reflect&&h===this._$Ej?.get(t)&&!this.hasAttribute(r._$Eu(t,i))))return;this.C(t,s,i)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(t,s,{useDefault:i,reflect:e,wrapped:h},r){i&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,r??s??this[t]),!0!==h||void 0!==r)||(this._$AL.has(t)||(this.hasUpdated||i||(s=void 0),this._$AL.set(t,s)),!0===e&&this._$Em!==t&&(this._$Eq??=new Set).add(t))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[t,s]of this._$Ep)this[t]=s;this._$Ep=void 0}const t=this.constructor.elementProperties;if(t.size>0)for(const[s,i]of t){const{wrapped:t}=i,e=this[s];!0!==t||this._$AL.has(s)||void 0===e||this.C(s,void 0,i,e)}}let t=!1;const s=this._$AL;try{t=this.shouldUpdate(s),t?(this.willUpdate(s),this._$EO?.forEach(t=>t.hostUpdate?.()),this.update(s)):this._$EM()}catch(s){throw t=!1,this._$EM(),s}t&&this._$AE(s)}willUpdate(t){}_$AE(t){this._$EO?.forEach(t=>t.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(t){return!0}update(t){this._$Eq&&=this._$Eq.forEach(t=>this._$ET(t,this[t])),this._$EM()}updated(t){}firstUpdated(t){}}y.elementStyles=[],y.shadowRootOptions={mode:\"open\"},y[d(\"elementProperties\")]=new Map,y[d(\"finalized\")]=new Map,p?.({ReactiveElement:y}),(a.reactiveElementVersions??=[]).push(\"2.1.2\");export{y as ReactiveElement,s as adoptStyles,u as defaultConverter,t as getCompatibleStyle,f as notEqual};\n//# sourceMappingURL=reactive-element.js.map\n","/**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */\nconst t=globalThis,i=t=>t,s=t.trustedTypes,e=s?s.createPolicy(\"lit-html\",{createHTML:t=>t}):void 0,h=\"$lit$\",o=`lit$${Math.random().toFixed(9).slice(2)}$`,n=\"?\"+o,r=`<${n}>`,l=document,c=()=>l.createComment(\"\"),a=t=>null===t||\"object\"!=typeof t&&\"function\"!=typeof t,u=Array.isArray,d=t=>u(t)||\"function\"==typeof t?.[Symbol.iterator],f=\"[ \\t\\n\\f\\r]\",v=/<(?:(!--|\\/[^a-zA-Z])|(\\/?[a-zA-Z][^>\\s]*)|(\\/?$))/g,_=/-->/g,m=/>/g,p=RegExp(`>|${f}(?:([^\\\\s\"'>=/]+)(${f}*=${f}*(?:[^ \\t\\n\\f\\r\"'\\`<>=]|(\"|')|))|$)`,\"g\"),g=/'/g,$=/\"/g,y=/^(?:script|style|textarea|title)$/i,x=t=>(i,...s)=>({_$litType$:t,strings:i,values:s}),b=x(1),w=x(2),T=x(3),E=Symbol.for(\"lit-noChange\"),A=Symbol.for(\"lit-nothing\"),C=new WeakMap,P=l.createTreeWalker(l,129);function V(t,i){if(!u(t)||!t.hasOwnProperty(\"raw\"))throw Error(\"invalid template strings array\");return void 0!==e?e.createHTML(i):i}const N=(t,i)=>{const s=t.length-1,e=[];let n,l=2===i?\"\":3===i?\"\":\"\",c=v;for(let i=0;i\"===u[0]?(c=n??v,d=-1):void 0===u[1]?d=-2:(d=c.lastIndex-u[2].length,a=u[1],c=void 0===u[3]?p:'\"'===u[3]?$:g):c===$||c===g?c=p:c===_||c===m?c=v:(c=p,n=void 0);const x=c===p&&t[i+1].startsWith(\"/>\")?\" \":\"\";l+=c===v?s+r:d>=0?(e.push(a),s.slice(0,d)+h+s.slice(d)+o+x):s+o+(-2===d?i:x)}return[V(t,l+(t[s]||\"\")+(2===i?\"\":3===i?\"\":\"\")),e]};class S{constructor({strings:t,_$litType$:i},e){let r;this.parts=[];let l=0,a=0;const u=t.length-1,d=this.parts,[f,v]=N(t,i);if(this.el=S.createElement(f,e),P.currentNode=this.el.content,2===i||3===i){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes)}for(;null!==(r=P.nextNode())&&d.length0){r.textContent=s?s.emptyScript:\"\";for(let s=0;s2||\"\"!==s[0]||\"\"!==s[1]?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=A}_$AI(t,i=this,s,e){const h=this.strings;let o=!1;if(void 0===h)t=M(this,t,i,0),o=!a(t)||t!==this._$AH&&t!==E,o&&(this._$AH=t);else{const e=t;let n,r;for(t=h[0],n=0;n{const e=s?.renderBefore??i;let h=e._$litPart$;if(void 0===h){const t=s?.renderBefore??null;e._$litPart$=h=new k(i.insertBefore(c(),t),t,void 0,s??{})}return h._$AI(t),h};export{j as _$LH,b as html,T as mathml,E as noChange,A as nothing,D as render,w as svg};\n//# sourceMappingURL=lit-html.js.map\n","import{ReactiveElement as t}from\"@lit/reactive-element\";export*from\"@lit/reactive-element\";import{render as e,noChange as r}from\"lit-html\";export*from\"lit-html\";\n/**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */const s=globalThis;class i extends t{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){const t=super.createRenderRoot();return this.renderOptions.renderBefore??=t.firstChild,t}update(t){const r=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(t),this._$Do=e(r,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return r}}i._$litElement$=!0,i[\"finalized\"]=!0,s.litElementHydrateSupport?.({LitElement:i});const o=s.litElementPolyfillSupport;o?.({LitElement:i});const n={_$AK:(t,e,r)=>{t._$AK(e,r)},_$AL:t=>t._$AL};(s.litElementVersions??=[]).push(\"4.2.2\");export{i as LitElement,n as _$LE};\n//# sourceMappingURL=lit-element.js.map\n","/**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */\nconst t=t=>(e,o)=>{void 0!==o?o.addInitializer(()=>{customElements.define(t,e)}):customElements.define(t,e)};export{t as customElement};\n//# sourceMappingURL=custom-element.js.map\n","import{notEqual as t,defaultConverter as e}from\"../reactive-element.js\";\n/**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */const o={attribute:!0,type:String,converter:e,reflect:!1,hasChanged:t},r=(t=o,e,r)=>{const{kind:n,metadata:i}=r;let s=globalThis.litPropertyMetadata.get(i);if(void 0===s&&globalThis.litPropertyMetadata.set(i,s=new Map),\"setter\"===n&&((t=Object.create(t)).wrapped=!0),s.set(r.name,t),\"accessor\"===n){const{name:o}=r;return{set(r){const n=e.get.call(this);e.set.call(this,r),this.requestUpdate(o,n,t,!0,r)},init(e){return void 0!==e&&this.C(o,void 0,t,e),e}}}if(\"setter\"===n){const{name:o}=r;return function(r){const n=this[o];e.call(this,r),this.requestUpdate(o,n,t,!0,r)}}throw Error(\"Unsupported decorator location: \"+n)};function n(t){return(e,o)=>\"object\"==typeof o?r(t,e,o):((t,e,o)=>{const r=e.hasOwnProperty(o);return e.constructor.createProperty(o,t),r?Object.getOwnPropertyDescriptor(e,o):void 0})(t,e,o)}export{n as property,r as standardProperty};\n//# sourceMappingURL=property.js.map\n","import{property as t}from\"./property.js\";\n/**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */function r(r){return t({...r,state:!0,attribute:!1})}export{r as state};\n//# sourceMappingURL=state.js.map\n","const MAX_ASSISTANT_NAME = 50;\nconst MAX_ASSISTANT_AVATAR = 200;\n\nexport const DEFAULT_ASSISTANT_NAME = \"Assistant\";\nexport const DEFAULT_ASSISTANT_AVATAR = \"A\";\n\nexport type AssistantIdentity = {\n agentId?: string | null;\n name: string;\n avatar: string | null;\n};\n\ndeclare global {\n interface Window {\n __CLAWDBOT_ASSISTANT_NAME__?: string;\n __CLAWDBOT_ASSISTANT_AVATAR__?: string;\n }\n}\n\nfunction coerceIdentityValue(value: string | undefined, maxLength: number): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.trim();\n if (!trimmed) return undefined;\n if (trimmed.length <= maxLength) return trimmed;\n return trimmed.slice(0, maxLength);\n}\n\nexport function normalizeAssistantIdentity(\n input?: Partial | null,\n): AssistantIdentity {\n const name =\n coerceIdentityValue(input?.name, MAX_ASSISTANT_NAME) ?? DEFAULT_ASSISTANT_NAME;\n const avatar = coerceIdentityValue(input?.avatar ?? undefined, MAX_ASSISTANT_AVATAR) ?? null;\n const agentId =\n typeof input?.agentId === \"string\" && input.agentId.trim()\n ? input.agentId.trim()\n : null;\n return { agentId, name, avatar };\n}\n\nexport function resolveInjectedAssistantIdentity(): AssistantIdentity {\n if (typeof window === \"undefined\") {\n return normalizeAssistantIdentity({});\n }\n return normalizeAssistantIdentity({\n name: window.__CLAWDBOT_ASSISTANT_NAME__,\n avatar: window.__CLAWDBOT_ASSISTANT_AVATAR__,\n });\n}\n","const KEY = \"clawdbot.control.settings.v1\";\n\nimport type { ThemeMode } from \"./theme\";\n\nexport type UiSettings = {\n gatewayUrl: string;\n token: string;\n sessionKey: string;\n lastActiveSessionKey: string;\n theme: ThemeMode;\n chatFocusMode: boolean;\n chatShowThinking: boolean;\n splitRatio: number; // Sidebar split ratio (0.4 to 0.7, default 0.6)\n navCollapsed: boolean; // Collapsible sidebar state\n navGroupsCollapsed: Record; // Which nav groups are collapsed\n};\n\nexport function loadSettings(): UiSettings {\n const defaultUrl = (() => {\n const proto = location.protocol === \"https:\" ? \"wss\" : \"ws\";\n return `${proto}://${location.host}`;\n })();\n\n const defaults: UiSettings = {\n gatewayUrl: defaultUrl,\n token: \"\",\n sessionKey: \"main\",\n lastActiveSessionKey: \"main\",\n theme: \"system\",\n chatFocusMode: false,\n chatShowThinking: true,\n splitRatio: 0.6,\n navCollapsed: false,\n navGroupsCollapsed: {},\n };\n\n try {\n const raw = localStorage.getItem(KEY);\n if (!raw) return defaults;\n const parsed = JSON.parse(raw) as Partial;\n return {\n gatewayUrl:\n typeof parsed.gatewayUrl === \"string\" && parsed.gatewayUrl.trim()\n ? parsed.gatewayUrl.trim()\n : defaults.gatewayUrl,\n token: typeof parsed.token === \"string\" ? parsed.token : defaults.token,\n sessionKey:\n typeof parsed.sessionKey === \"string\" && parsed.sessionKey.trim()\n ? parsed.sessionKey.trim()\n : defaults.sessionKey,\n lastActiveSessionKey:\n typeof parsed.lastActiveSessionKey === \"string\" &&\n parsed.lastActiveSessionKey.trim()\n ? parsed.lastActiveSessionKey.trim()\n : (typeof parsed.sessionKey === \"string\" &&\n parsed.sessionKey.trim()) ||\n defaults.lastActiveSessionKey,\n theme:\n parsed.theme === \"light\" ||\n parsed.theme === \"dark\" ||\n parsed.theme === \"system\"\n ? parsed.theme\n : defaults.theme,\n chatFocusMode:\n typeof parsed.chatFocusMode === \"boolean\"\n ? parsed.chatFocusMode\n : defaults.chatFocusMode,\n chatShowThinking:\n typeof parsed.chatShowThinking === \"boolean\"\n ? parsed.chatShowThinking\n : defaults.chatShowThinking,\n splitRatio:\n typeof parsed.splitRatio === \"number\" &&\n parsed.splitRatio >= 0.4 &&\n parsed.splitRatio <= 0.7\n ? parsed.splitRatio\n : defaults.splitRatio,\n navCollapsed:\n typeof parsed.navCollapsed === \"boolean\"\n ? parsed.navCollapsed\n : defaults.navCollapsed,\n navGroupsCollapsed:\n typeof parsed.navGroupsCollapsed === \"object\" &&\n parsed.navGroupsCollapsed !== null\n ? parsed.navGroupsCollapsed\n : defaults.navGroupsCollapsed,\n };\n } catch {\n return defaults;\n }\n}\n\nexport function saveSettings(next: UiSettings) {\n localStorage.setItem(KEY, JSON.stringify(next));\n}\n","export type ParsedAgentSessionKey = {\n agentId: string;\n rest: string;\n};\n\nexport function parseAgentSessionKey(\n sessionKey: string | undefined | null,\n): ParsedAgentSessionKey | null {\n const raw = (sessionKey ?? \"\").trim();\n if (!raw) return null;\n const parts = raw.split(\":\").filter(Boolean);\n if (parts.length < 3) return null;\n if (parts[0] !== \"agent\") return null;\n const agentId = parts[1]?.trim();\n const rest = parts.slice(2).join(\":\");\n if (!agentId || !rest) return null;\n return { agentId, rest };\n}\n\nexport function isSubagentSessionKey(sessionKey: string | undefined | null): boolean {\n const raw = (sessionKey ?? \"\").trim();\n if (!raw) return false;\n if (raw.toLowerCase().startsWith(\"subagent:\")) return true;\n const parsed = parseAgentSessionKey(raw);\n return Boolean((parsed?.rest ?? \"\").toLowerCase().startsWith(\"subagent:\"));\n}\n\nexport function isAcpSessionKey(sessionKey: string | undefined | null): boolean {\n const raw = (sessionKey ?? \"\").trim();\n if (!raw) return false;\n const normalized = raw.toLowerCase();\n if (normalized.startsWith(\"acp:\")) return true;\n const parsed = parseAgentSessionKey(raw);\n return Boolean((parsed?.rest ?? \"\").toLowerCase().startsWith(\"acp:\"));\n}\n\nconst THREAD_SESSION_MARKERS = [\":thread:\", \":topic:\"];\n\nexport function resolveThreadParentSessionKey(\n sessionKey: string | undefined | null,\n): string | null {\n const raw = (sessionKey ?? \"\").trim();\n if (!raw) return null;\n const normalized = raw.toLowerCase();\n let idx = -1;\n for (const marker of THREAD_SESSION_MARKERS) {\n const candidate = normalized.lastIndexOf(marker);\n if (candidate > idx) idx = candidate;\n }\n if (idx <= 0) return null;\n const parent = raw.slice(0, idx).trim();\n return parent ? parent : null;\n}\n","import type { IconName } from \"./icons.js\";\n\nexport const TAB_GROUPS = [\n { label: \"Chat\", tabs: [\"chat\"] },\n {\n label: \"Control\",\n tabs: [\"overview\", \"channels\", \"instances\", \"sessions\", \"cron\"],\n },\n { label: \"Agent\", tabs: [\"skills\", \"nodes\"] },\n { label: \"Settings\", tabs: [\"config\", \"debug\", \"logs\"] },\n] as const;\n\nexport type Tab =\n | \"overview\"\n | \"channels\"\n | \"instances\"\n | \"sessions\"\n | \"cron\"\n | \"skills\"\n | \"nodes\"\n | \"chat\"\n | \"config\"\n | \"debug\"\n | \"logs\";\n\nconst TAB_PATHS: Record = {\n overview: \"/overview\",\n channels: \"/channels\",\n instances: \"/instances\",\n sessions: \"/sessions\",\n cron: \"/cron\",\n skills: \"/skills\",\n nodes: \"/nodes\",\n chat: \"/chat\",\n config: \"/config\",\n debug: \"/debug\",\n logs: \"/logs\",\n};\n\nconst PATH_TO_TAB = new Map(\n Object.entries(TAB_PATHS).map(([tab, path]) => [path, tab as Tab]),\n);\n\nexport function normalizeBasePath(basePath: string): string {\n if (!basePath) return \"\";\n let base = basePath.trim();\n if (!base.startsWith(\"/\")) base = `/${base}`;\n if (base === \"/\") return \"\";\n if (base.endsWith(\"/\")) base = base.slice(0, -1);\n return base;\n}\n\nexport function normalizePath(path: string): string {\n if (!path) return \"/\";\n let normalized = path.trim();\n if (!normalized.startsWith(\"/\")) normalized = `/${normalized}`;\n if (normalized.length > 1 && normalized.endsWith(\"/\")) {\n normalized = normalized.slice(0, -1);\n }\n return normalized;\n}\n\nexport function pathForTab(tab: Tab, basePath = \"\"): string {\n const base = normalizeBasePath(basePath);\n const path = TAB_PATHS[tab];\n return base ? `${base}${path}` : path;\n}\n\nexport function tabFromPath(pathname: string, basePath = \"\"): Tab | null {\n const base = normalizeBasePath(basePath);\n let path = pathname || \"/\";\n if (base) {\n if (path === base) {\n path = \"/\";\n } else if (path.startsWith(`${base}/`)) {\n path = path.slice(base.length);\n }\n }\n let normalized = normalizePath(path).toLowerCase();\n if (normalized.endsWith(\"/index.html\")) normalized = \"/\";\n if (normalized === \"/\") return \"chat\";\n return PATH_TO_TAB.get(normalized) ?? null;\n}\n\nexport function inferBasePathFromPathname(pathname: string): string {\n let normalized = normalizePath(pathname);\n if (normalized.endsWith(\"/index.html\")) {\n normalized = normalizePath(normalized.slice(0, -\"/index.html\".length));\n }\n if (normalized === \"/\") return \"\";\n const segments = normalized.split(\"/\").filter(Boolean);\n if (segments.length === 0) return \"\";\n for (let i = 0; i < segments.length; i++) {\n const candidate = `/${segments.slice(i).join(\"/\")}`.toLowerCase();\n if (PATH_TO_TAB.has(candidate)) {\n const prefix = segments.slice(0, i);\n return prefix.length ? `/${prefix.join(\"/\")}` : \"\";\n }\n }\n return `/${segments.join(\"/\")}`;\n}\n\nexport function iconForTab(tab: Tab): IconName {\n switch (tab) {\n case \"chat\":\n return \"messageSquare\";\n case \"overview\":\n return \"barChart\";\n case \"channels\":\n return \"link\";\n case \"instances\":\n return \"radio\";\n case \"sessions\":\n return \"fileText\";\n case \"cron\":\n return \"loader\";\n case \"skills\":\n return \"zap\";\n case \"nodes\":\n return \"monitor\";\n case \"config\":\n return \"settings\";\n case \"debug\":\n return \"bug\";\n case \"logs\":\n return \"scrollText\";\n default:\n return \"folder\";\n }\n}\n\nexport function titleForTab(tab: Tab) {\n switch (tab) {\n case \"overview\":\n return \"Overview\";\n case \"channels\":\n return \"Channels\";\n case \"instances\":\n return \"Instances\";\n case \"sessions\":\n return \"Sessions\";\n case \"cron\":\n return \"Cron Jobs\";\n case \"skills\":\n return \"Skills\";\n case \"nodes\":\n return \"Nodes\";\n case \"chat\":\n return \"Chat\";\n case \"config\":\n return \"Config\";\n case \"debug\":\n return \"Debug\";\n case \"logs\":\n return \"Logs\";\n default:\n return \"Control\";\n }\n}\n\nexport function subtitleForTab(tab: Tab) {\n switch (tab) {\n case \"overview\":\n return \"Gateway status, entry points, and a fast health read.\";\n case \"channels\":\n return \"Manage channels and settings.\";\n case \"instances\":\n return \"Presence beacons from connected clients and nodes.\";\n case \"sessions\":\n return \"Inspect active sessions and adjust per-session defaults.\";\n case \"cron\":\n return \"Schedule wakeups and recurring agent runs.\";\n case \"skills\":\n return \"Manage skill availability and API key injection.\";\n case \"nodes\":\n return \"Paired devices, capabilities, and command exposure.\";\n case \"chat\":\n return \"Direct gateway chat session for quick interventions.\";\n case \"config\":\n return \"Edit ~/.clawdbot/clawdbot.json safely.\";\n case \"debug\":\n return \"Gateway snapshots, events, and manual RPC calls.\";\n case \"logs\":\n return \"Live tail of the gateway file logs.\";\n default:\n return \"\";\n }\n}\n","import { html, type TemplateResult } from \"lit\";\n\n// Lucide-style SVG icons\n// All icons use currentColor for stroke\n\nexport const icons = {\n // Navigation icons\n messageSquare: html``,\n barChart: html``,\n link: html``,\n radio: html``,\n fileText: html``,\n zap: html``,\n monitor: html``,\n settings: html``,\n bug: html``,\n scrollText: html``,\n folder: html``,\n\n // UI icons\n menu: html``,\n x: html``,\n check: html``,\n copy: html``,\n search: html``,\n brain: html``,\n book: html``,\n loader: html``,\n\n // Tool icons\n wrench: html``,\n fileCode: html``,\n edit: html``,\n penLine: html``,\n paperclip: html``,\n globe: html``,\n image: html``,\n smartphone: html``,\n plug: html``,\n circle: html``,\n puzzle: html``,\n} as const;\n\nexport type IconName = keyof typeof icons;\n\nexport function icon(name: IconName): TemplateResult {\n return icons[name];\n}\n\nexport function renderIcon(name: IconName, className = \"nav-item__icon\"): TemplateResult {\n return html`${icons[name]}`;\n}\n\n// Legacy function for compatibility\nexport function renderEmojiIcon(iconContent: string | TemplateResult, className: string): TemplateResult {\n return html`${iconContent}`;\n}\n\nexport function setEmojiIcon(target: HTMLElement | null, icon: string): void {\n if (!target) return;\n target.textContent = icon;\n}\n","export type ReasoningTagMode = \"strict\" | \"preserve\";\nexport type ReasoningTagTrim = \"none\" | \"start\" | \"both\";\n\nconst QUICK_TAG_RE = /<\\s*\\/?\\s*(?:think(?:ing)?|thought|antthinking|final)\\b/i;\nconst FINAL_TAG_RE = /<\\s*\\/?\\s*final\\b[^>]*>/gi;\nconst THINKING_TAG_RE = /<\\s*(\\/?)\\s*(?:think(?:ing)?|thought|antthinking)\\b[^>]*>/gi;\n\nfunction applyTrim(value: string, mode: ReasoningTagTrim): string {\n if (mode === \"none\") return value;\n if (mode === \"start\") return value.trimStart();\n return value.trim();\n}\n\nexport function stripReasoningTagsFromText(\n text: string,\n options?: {\n mode?: ReasoningTagMode;\n trim?: ReasoningTagTrim;\n },\n): string {\n if (!text) return text;\n if (!QUICK_TAG_RE.test(text)) return text;\n\n const mode = options?.mode ?? \"strict\";\n const trimMode = options?.trim ?? \"both\";\n\n let cleaned = text;\n if (FINAL_TAG_RE.test(cleaned)) {\n FINAL_TAG_RE.lastIndex = 0;\n cleaned = cleaned.replace(FINAL_TAG_RE, \"\");\n } else {\n FINAL_TAG_RE.lastIndex = 0;\n }\n\n THINKING_TAG_RE.lastIndex = 0;\n let result = \"\";\n let lastIndex = 0;\n let inThinking = false;\n\n for (const match of cleaned.matchAll(THINKING_TAG_RE)) {\n const idx = match.index ?? 0;\n const isClose = match[1] === \"/\";\n\n if (!inThinking) {\n result += cleaned.slice(lastIndex, idx);\n if (!isClose) {\n inThinking = true;\n }\n } else if (isClose) {\n inThinking = false;\n }\n\n lastIndex = idx + match[0].length;\n }\n\n if (!inThinking || mode === \"preserve\") {\n result += cleaned.slice(lastIndex);\n }\n\n return applyTrim(result, trimMode);\n}\n","import { stripReasoningTagsFromText } from \"../../../src/shared/text/reasoning-tags.js\";\n\nexport function formatMs(ms?: number | null): string {\n if (!ms && ms !== 0) return \"n/a\";\n return new Date(ms).toLocaleString();\n}\n\nexport function formatAgo(ms?: number | null): string {\n if (!ms && ms !== 0) return \"n/a\";\n const diff = Date.now() - ms;\n if (diff < 0) return \"just now\";\n const sec = Math.round(diff / 1000);\n if (sec < 60) return `${sec}s ago`;\n const min = Math.round(sec / 60);\n if (min < 60) return `${min}m ago`;\n const hr = Math.round(min / 60);\n if (hr < 48) return `${hr}h ago`;\n const day = Math.round(hr / 24);\n return `${day}d ago`;\n}\n\nexport function formatDurationMs(ms?: number | null): string {\n if (!ms && ms !== 0) return \"n/a\";\n if (ms < 1000) return `${ms}ms`;\n const sec = Math.round(ms / 1000);\n if (sec < 60) return `${sec}s`;\n const min = Math.round(sec / 60);\n if (min < 60) return `${min}m`;\n const hr = Math.round(min / 60);\n if (hr < 48) return `${hr}h`;\n const day = Math.round(hr / 24);\n return `${day}d`;\n}\n\nexport function formatList(values?: Array): string {\n if (!values || values.length === 0) return \"none\";\n return values.filter((v): v is string => Boolean(v && v.trim())).join(\", \");\n}\n\nexport function clampText(value: string, max = 120): string {\n if (value.length <= max) return value;\n return `${value.slice(0, Math.max(0, max - 1))}…`;\n}\n\nexport function truncateText(value: string, max: number): {\n text: string;\n truncated: boolean;\n total: number;\n} {\n if (value.length <= max) {\n return { text: value, truncated: false, total: value.length };\n }\n return {\n text: value.slice(0, Math.max(0, max)),\n truncated: true,\n total: value.length,\n };\n}\n\nexport function toNumber(value: string, fallback: number): number {\n const n = Number(value);\n return Number.isFinite(n) ? n : fallback;\n}\n\nexport function parseList(input: string): string[] {\n return input\n .split(/[,\\n]/)\n .map((v) => v.trim())\n .filter((v) => v.length > 0);\n}\n\nexport function stripThinkingTags(value: string): string {\n return stripReasoningTagsFromText(value, { mode: \"preserve\", trim: \"start\" });\n}\n","import { stripThinkingTags } from \"../format\";\n\nconst ENVELOPE_PREFIX = /^\\[([^\\]]+)\\]\\s*/;\nconst ENVELOPE_CHANNELS = [\n \"WebChat\",\n \"WhatsApp\",\n \"Telegram\",\n \"Signal\",\n \"Slack\",\n \"Discord\",\n \"iMessage\",\n \"Teams\",\n \"Matrix\",\n \"Zalo\",\n \"Zalo Personal\",\n \"BlueBubbles\",\n];\n\nconst textCache = new WeakMap();\nconst thinkingCache = new WeakMap();\n\nfunction looksLikeEnvelopeHeader(header: string): boolean {\n if (/\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}Z\\b/.test(header)) return true;\n if (/\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}\\b/.test(header)) return true;\n return ENVELOPE_CHANNELS.some((label) => header.startsWith(`${label} `));\n}\n\nexport function stripEnvelope(text: string): string {\n const match = text.match(ENVELOPE_PREFIX);\n if (!match) return text;\n const header = match[1] ?? \"\";\n if (!looksLikeEnvelopeHeader(header)) return text;\n return text.slice(match[0].length);\n}\n\nexport function extractText(message: unknown): string | null {\n const m = message as Record;\n const role = typeof m.role === \"string\" ? m.role : \"\";\n const content = m.content;\n if (typeof content === \"string\") {\n const processed = role === \"assistant\" ? stripThinkingTags(content) : stripEnvelope(content);\n return processed;\n }\n if (Array.isArray(content)) {\n const parts = content\n .map((p) => {\n const item = p as Record;\n if (item.type === \"text\" && typeof item.text === \"string\") return item.text;\n return null;\n })\n .filter((v): v is string => typeof v === \"string\");\n if (parts.length > 0) {\n const joined = parts.join(\"\\n\");\n const processed = role === \"assistant\" ? stripThinkingTags(joined) : stripEnvelope(joined);\n return processed;\n }\n }\n if (typeof m.text === \"string\") {\n const processed = role === \"assistant\" ? stripThinkingTags(m.text) : stripEnvelope(m.text);\n return processed;\n }\n return null;\n}\n\nexport function extractTextCached(message: unknown): string | null {\n if (!message || typeof message !== \"object\") return extractText(message);\n const obj = message as object;\n if (textCache.has(obj)) return textCache.get(obj) ?? null;\n const value = extractText(message);\n textCache.set(obj, value);\n return value;\n}\n\nexport function extractThinking(message: unknown): string | null {\n const m = message as Record;\n const content = m.content;\n const parts: string[] = [];\n if (Array.isArray(content)) {\n for (const p of content) {\n const item = p as Record;\n if (item.type === \"thinking\" && typeof item.thinking === \"string\") {\n const cleaned = item.thinking.trim();\n if (cleaned) parts.push(cleaned);\n }\n }\n }\n if (parts.length > 0) return parts.join(\"\\n\");\n\n // Back-compat: older logs may still have tags inside text blocks.\n const rawText = extractRawText(message);\n if (!rawText) return null;\n const matches = [\n ...rawText.matchAll(\n /<\\s*think(?:ing)?\\s*>([\\s\\S]*?)<\\s*\\/\\s*think(?:ing)?\\s*>/gi,\n ),\n ];\n const extracted = matches\n .map((m) => (m[1] ?? \"\").trim())\n .filter(Boolean);\n return extracted.length > 0 ? extracted.join(\"\\n\") : null;\n}\n\nexport function extractThinkingCached(message: unknown): string | null {\n if (!message || typeof message !== \"object\") return extractThinking(message);\n const obj = message as object;\n if (thinkingCache.has(obj)) return thinkingCache.get(obj) ?? null;\n const value = extractThinking(message);\n thinkingCache.set(obj, value);\n return value;\n}\n\nexport function extractRawText(message: unknown): string | null {\n const m = message as Record;\n const content = m.content;\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n const parts = content\n .map((p) => {\n const item = p as Record;\n if (item.type === \"text\" && typeof item.text === \"string\") return item.text;\n return null;\n })\n .filter((v): v is string => typeof v === \"string\");\n if (parts.length > 0) return parts.join(\"\\n\");\n }\n if (typeof m.text === \"string\") return m.text;\n return null;\n}\n\nexport function formatReasoningMarkdown(text: string): string {\n const trimmed = text.trim();\n if (!trimmed) return \"\";\n const lines = trimmed\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter(Boolean)\n .map((line) => `_${line}_`);\n return lines.length ? [\"_Reasoning:_\", ...lines].join(\"\\n\") : \"\";\n}\n","export type CryptoLike = {\n randomUUID?: (() => string) | undefined;\n getRandomValues?: ((array: Uint8Array) => Uint8Array) | undefined;\n};\n\nfunction uuidFromBytes(bytes: Uint8Array): string {\n bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4\n bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 1\n\n let hex = \"\";\n for (let i = 0; i < bytes.length; i++) {\n hex += bytes[i]!.toString(16).padStart(2, \"0\");\n }\n\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(\n 16,\n 20,\n )}-${hex.slice(20)}`;\n}\n\nfunction weakRandomBytes(): Uint8Array {\n const bytes = new Uint8Array(16);\n const now = Date.now();\n for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);\n bytes[0] ^= now & 0xff;\n bytes[1] ^= (now >>> 8) & 0xff;\n bytes[2] ^= (now >>> 16) & 0xff;\n bytes[3] ^= (now >>> 24) & 0xff;\n return bytes;\n}\n\nexport function generateUUID(cryptoLike: CryptoLike | null = globalThis.crypto): string {\n if (cryptoLike && typeof cryptoLike.randomUUID === \"function\") return cryptoLike.randomUUID();\n\n if (cryptoLike && typeof cryptoLike.getRandomValues === \"function\") {\n const bytes = new Uint8Array(16);\n cryptoLike.getRandomValues(bytes);\n return uuidFromBytes(bytes);\n }\n\n return uuidFromBytes(weakRandomBytes());\n}\n\n","import type { GatewayBrowserClient } from \"../gateway\";\nimport { extractText } from \"../chat/message-extract\";\nimport { generateUUID } from \"../uuid\";\n\nexport type ChatState = {\n client: GatewayBrowserClient | null;\n connected: boolean;\n sessionKey: string;\n chatLoading: boolean;\n chatMessages: unknown[];\n chatThinkingLevel: string | null;\n chatSending: boolean;\n chatMessage: string;\n chatRunId: string | null;\n chatStream: string | null;\n chatStreamStartedAt: number | null;\n lastError: string | null;\n};\n\nexport type ChatEventPayload = {\n runId: string;\n sessionKey: string;\n state: \"delta\" | \"final\" | \"aborted\" | \"error\";\n message?: unknown;\n errorMessage?: string;\n};\n\nexport async function loadChatHistory(state: ChatState) {\n if (!state.client || !state.connected) return;\n state.chatLoading = true;\n state.lastError = null;\n try {\n const res = (await state.client.request(\"chat.history\", {\n sessionKey: state.sessionKey,\n limit: 200,\n })) as { messages?: unknown[]; thinkingLevel?: string | null };\n state.chatMessages = Array.isArray(res.messages) ? res.messages : [];\n state.chatThinkingLevel = res.thinkingLevel ?? null;\n } catch (err) {\n state.lastError = String(err);\n } finally {\n state.chatLoading = false;\n }\n}\n\nexport async function sendChatMessage(state: ChatState, message: string): Promise {\n if (!state.client || !state.connected) return false;\n const msg = message.trim();\n if (!msg) return false;\n\n const now = Date.now();\n state.chatMessages = [\n ...state.chatMessages,\n {\n role: \"user\",\n content: [{ type: \"text\", text: msg }],\n timestamp: now,\n },\n ];\n\n state.chatSending = true;\n state.lastError = null;\n const runId = generateUUID();\n state.chatRunId = runId;\n state.chatStream = \"\";\n state.chatStreamStartedAt = now;\n try {\n await state.client.request(\"chat.send\", {\n sessionKey: state.sessionKey,\n message: msg,\n deliver: false,\n idempotencyKey: runId,\n });\n return true;\n } catch (err) {\n const error = String(err);\n state.chatRunId = null;\n state.chatStream = null;\n state.chatStreamStartedAt = null;\n state.lastError = error;\n state.chatMessages = [\n ...state.chatMessages,\n {\n role: \"assistant\",\n content: [{ type: \"text\", text: \"Error: \" + error }],\n timestamp: Date.now(),\n },\n ];\n return false;\n } finally {\n state.chatSending = false;\n }\n}\n\nexport async function abortChatRun(state: ChatState): Promise {\n if (!state.client || !state.connected) return false;\n const runId = state.chatRunId;\n try {\n await state.client.request(\n \"chat.abort\",\n runId\n ? { sessionKey: state.sessionKey, runId }\n : { sessionKey: state.sessionKey },\n );\n return true;\n } catch (err) {\n state.lastError = String(err);\n return false;\n }\n}\n\nexport function handleChatEvent(\n state: ChatState,\n payload?: ChatEventPayload,\n) {\n if (!payload) return null;\n if (payload.sessionKey !== state.sessionKey) return null;\n if (payload.runId && state.chatRunId && payload.runId !== state.chatRunId)\n return null;\n\n if (payload.state === \"delta\") {\n const next = extractText(payload.message);\n if (typeof next === \"string\") {\n const current = state.chatStream ?? \"\";\n if (!current || next.length >= current.length) {\n state.chatStream = next;\n }\n }\n } else if (payload.state === \"final\") {\n state.chatStream = null;\n state.chatRunId = null;\n state.chatStreamStartedAt = null;\n } else if (payload.state === \"aborted\") {\n state.chatStream = null;\n state.chatRunId = null;\n state.chatStreamStartedAt = null;\n } else if (payload.state === \"error\") {\n state.chatStream = null;\n state.chatRunId = null;\n state.chatStreamStartedAt = null;\n state.lastError = payload.errorMessage ?? \"chat error\";\n }\n return payload.state;\n}\n","import type { GatewayBrowserClient } from \"../gateway\";\nimport { toNumber } from \"../format\";\nimport type { SessionsListResult } from \"../types\";\n\nexport type SessionsState = {\n client: GatewayBrowserClient | null;\n connected: boolean;\n sessionsLoading: boolean;\n sessionsResult: SessionsListResult | null;\n sessionsError: string | null;\n sessionsFilterActive: string;\n sessionsFilterLimit: string;\n sessionsIncludeGlobal: boolean;\n sessionsIncludeUnknown: boolean;\n};\n\nexport async function loadSessions(state: SessionsState) {\n if (!state.client || !state.connected) return;\n if (state.sessionsLoading) return;\n state.sessionsLoading = true;\n state.sessionsError = null;\n try {\n const params: Record = {\n includeGlobal: state.sessionsIncludeGlobal,\n includeUnknown: state.sessionsIncludeUnknown,\n };\n const activeMinutes = toNumber(state.sessionsFilterActive, 0);\n const limit = toNumber(state.sessionsFilterLimit, 0);\n if (activeMinutes > 0) params.activeMinutes = activeMinutes;\n if (limit > 0) params.limit = limit;\n const res = (await state.client.request(\"sessions.list\", params)) as\n | SessionsListResult\n | undefined;\n if (res) state.sessionsResult = res;\n } catch (err) {\n state.sessionsError = String(err);\n } finally {\n state.sessionsLoading = false;\n }\n}\n\nexport async function patchSession(\n state: SessionsState,\n key: string,\n patch: {\n label?: string | null;\n thinkingLevel?: string | null;\n verboseLevel?: string | null;\n reasoningLevel?: string | null;\n },\n) {\n if (!state.client || !state.connected) return;\n const params: Record = { key };\n if (\"label\" in patch) params.label = patch.label;\n if (\"thinkingLevel\" in patch) params.thinkingLevel = patch.thinkingLevel;\n if (\"verboseLevel\" in patch) params.verboseLevel = patch.verboseLevel;\n if (\"reasoningLevel\" in patch) params.reasoningLevel = patch.reasoningLevel;\n try {\n await state.client.request(\"sessions.patch\", params);\n await loadSessions(state);\n } catch (err) {\n state.sessionsError = String(err);\n }\n}\n\nexport async function deleteSession(state: SessionsState, key: string) {\n if (!state.client || !state.connected) return;\n if (state.sessionsLoading) return;\n const confirmed = window.confirm(\n `Delete session \"${key}\"?\\n\\nDeletes the session entry and archives its transcript.`,\n );\n if (!confirmed) return;\n state.sessionsLoading = true;\n state.sessionsError = null;\n try {\n await state.client.request(\"sessions.delete\", { key, deleteTranscript: true });\n await loadSessions(state);\n } catch (err) {\n state.sessionsError = String(err);\n } finally {\n state.sessionsLoading = false;\n }\n}\n","import { truncateText } from \"./format\";\n\nconst TOOL_STREAM_LIMIT = 50;\nconst TOOL_STREAM_THROTTLE_MS = 80;\nconst TOOL_OUTPUT_CHAR_LIMIT = 120_000;\n\nexport type AgentEventPayload = {\n runId: string;\n seq: number;\n stream: string;\n ts: number;\n sessionKey?: string;\n data: Record;\n};\n\nexport type ToolStreamEntry = {\n toolCallId: string;\n runId: string;\n sessionKey?: string;\n name: string;\n args?: unknown;\n output?: string;\n startedAt: number;\n updatedAt: number;\n message: Record;\n};\n\ntype ToolStreamHost = {\n sessionKey: string;\n chatRunId: string | null;\n toolStreamById: Map;\n toolStreamOrder: string[];\n chatToolMessages: Record[];\n toolStreamSyncTimer: number | null;\n};\n\nfunction extractToolOutputText(value: unknown): string | null {\n if (!value || typeof value !== \"object\") return null;\n const record = value as Record;\n if (typeof record.text === \"string\") return record.text;\n const content = record.content;\n if (!Array.isArray(content)) return null;\n const parts = content\n .map((item) => {\n if (!item || typeof item !== \"object\") return null;\n const entry = item as Record;\n if (entry.type === \"text\" && typeof entry.text === \"string\") return entry.text;\n return null;\n })\n .filter((part): part is string => Boolean(part));\n if (parts.length === 0) return null;\n return parts.join(\"\\n\");\n}\n\nfunction formatToolOutput(value: unknown): string | null {\n if (value === null || value === undefined) return null;\n if (typeof value === \"number\" || typeof value === \"boolean\") {\n return String(value);\n }\n const contentText = extractToolOutputText(value);\n let text: string;\n if (typeof value === \"string\") {\n text = value;\n } else if (contentText) {\n text = contentText;\n } else {\n try {\n text = JSON.stringify(value, null, 2);\n } catch {\n text = String(value);\n }\n }\n const truncated = truncateText(text, TOOL_OUTPUT_CHAR_LIMIT);\n if (!truncated.truncated) return truncated.text;\n return `${truncated.text}\\n\\n… truncated (${truncated.total} chars, showing first ${truncated.text.length}).`;\n}\n\nfunction buildToolStreamMessage(entry: ToolStreamEntry): Record {\n const content: Array> = [];\n content.push({\n type: \"toolcall\",\n name: entry.name,\n arguments: entry.args ?? {},\n });\n if (entry.output) {\n content.push({\n type: \"toolresult\",\n name: entry.name,\n text: entry.output,\n });\n }\n return {\n role: \"assistant\",\n toolCallId: entry.toolCallId,\n runId: entry.runId,\n content,\n timestamp: entry.startedAt,\n };\n}\n\nfunction trimToolStream(host: ToolStreamHost) {\n if (host.toolStreamOrder.length <= TOOL_STREAM_LIMIT) return;\n const overflow = host.toolStreamOrder.length - TOOL_STREAM_LIMIT;\n const removed = host.toolStreamOrder.splice(0, overflow);\n for (const id of removed) host.toolStreamById.delete(id);\n}\n\nfunction syncToolStreamMessages(host: ToolStreamHost) {\n host.chatToolMessages = host.toolStreamOrder\n .map((id) => host.toolStreamById.get(id)?.message)\n .filter((msg): msg is Record => Boolean(msg));\n}\n\nexport function flushToolStreamSync(host: ToolStreamHost) {\n if (host.toolStreamSyncTimer != null) {\n clearTimeout(host.toolStreamSyncTimer);\n host.toolStreamSyncTimer = null;\n }\n syncToolStreamMessages(host);\n}\n\nexport function scheduleToolStreamSync(host: ToolStreamHost, force = false) {\n if (force) {\n flushToolStreamSync(host);\n return;\n }\n if (host.toolStreamSyncTimer != null) return;\n host.toolStreamSyncTimer = window.setTimeout(\n () => flushToolStreamSync(host),\n TOOL_STREAM_THROTTLE_MS,\n );\n}\n\nexport function resetToolStream(host: ToolStreamHost) {\n host.toolStreamById.clear();\n host.toolStreamOrder = [];\n host.chatToolMessages = [];\n flushToolStreamSync(host);\n}\n\nexport type CompactionStatus = {\n active: boolean;\n startedAt: number | null;\n completedAt: number | null;\n};\n\ntype CompactionHost = ToolStreamHost & {\n compactionStatus?: CompactionStatus | null;\n compactionClearTimer?: number | null;\n};\n\nconst COMPACTION_TOAST_DURATION_MS = 5000;\n\nexport function handleCompactionEvent(host: CompactionHost, payload: AgentEventPayload) {\n const data = payload.data ?? {};\n const phase = typeof data.phase === \"string\" ? data.phase : \"\";\n \n // Clear any existing timer\n if (host.compactionClearTimer != null) {\n window.clearTimeout(host.compactionClearTimer);\n host.compactionClearTimer = null;\n }\n \n if (phase === \"start\") {\n host.compactionStatus = {\n active: true,\n startedAt: Date.now(),\n completedAt: null,\n };\n } else if (phase === \"end\") {\n host.compactionStatus = {\n active: false,\n startedAt: host.compactionStatus?.startedAt ?? null,\n completedAt: Date.now(),\n };\n // Auto-clear the toast after duration\n host.compactionClearTimer = window.setTimeout(() => {\n host.compactionStatus = null;\n host.compactionClearTimer = null;\n }, COMPACTION_TOAST_DURATION_MS);\n }\n}\n\nexport function handleAgentEvent(host: ToolStreamHost, payload?: AgentEventPayload) {\n if (!payload) return;\n \n // Handle compaction events\n if (payload.stream === \"compaction\") {\n handleCompactionEvent(host as CompactionHost, payload);\n return;\n }\n \n if (payload.stream !== \"tool\") return;\n const sessionKey =\n typeof payload.sessionKey === \"string\" ? payload.sessionKey : undefined;\n if (sessionKey && sessionKey !== host.sessionKey) return;\n // Fallback: only accept session-less events for the active run.\n if (!sessionKey && host.chatRunId && payload.runId !== host.chatRunId) return;\n if (host.chatRunId && payload.runId !== host.chatRunId) return;\n if (!host.chatRunId) return;\n\n const data = payload.data ?? {};\n const toolCallId = typeof data.toolCallId === \"string\" ? data.toolCallId : \"\";\n if (!toolCallId) return;\n const name = typeof data.name === \"string\" ? data.name : \"tool\";\n const phase = typeof data.phase === \"string\" ? data.phase : \"\";\n const args = phase === \"start\" ? data.args : undefined;\n const output =\n phase === \"update\"\n ? formatToolOutput(data.partialResult)\n : phase === \"result\"\n ? formatToolOutput(data.result)\n : undefined;\n\n const now = Date.now();\n let entry = host.toolStreamById.get(toolCallId);\n if (!entry) {\n entry = {\n toolCallId,\n runId: payload.runId,\n sessionKey,\n name,\n args,\n output,\n startedAt: typeof payload.ts === \"number\" ? payload.ts : now,\n updatedAt: now,\n message: {},\n };\n host.toolStreamById.set(toolCallId, entry);\n host.toolStreamOrder.push(toolCallId);\n } else {\n entry.name = name;\n if (args !== undefined) entry.args = args;\n if (output !== undefined) entry.output = output;\n entry.updatedAt = now;\n }\n\n entry.message = buildToolStreamMessage(entry);\n trimToolStream(host);\n scheduleToolStreamSync(host, phase === \"result\");\n}\n","type ScrollHost = {\n updateComplete: Promise;\n querySelector: (selectors: string) => Element | null;\n style: CSSStyleDeclaration;\n chatScrollFrame: number | null;\n chatScrollTimeout: number | null;\n chatHasAutoScrolled: boolean;\n chatUserNearBottom: boolean;\n logsScrollFrame: number | null;\n logsAtBottom: boolean;\n topbarObserver: ResizeObserver | null;\n};\n\nexport function scheduleChatScroll(host: ScrollHost, force = false) {\n if (host.chatScrollFrame) cancelAnimationFrame(host.chatScrollFrame);\n if (host.chatScrollTimeout != null) {\n clearTimeout(host.chatScrollTimeout);\n host.chatScrollTimeout = null;\n }\n const pickScrollTarget = () => {\n const container = host.querySelector(\".chat-thread\") as HTMLElement | null;\n if (container) {\n const overflowY = getComputedStyle(container).overflowY;\n const canScroll =\n overflowY === \"auto\" ||\n overflowY === \"scroll\" ||\n container.scrollHeight - container.clientHeight > 1;\n if (canScroll) return container;\n }\n return (document.scrollingElement ?? document.documentElement) as HTMLElement | null;\n };\n // Wait for Lit render to complete, then scroll\n void host.updateComplete.then(() => {\n host.chatScrollFrame = requestAnimationFrame(() => {\n host.chatScrollFrame = null;\n const target = pickScrollTarget();\n if (!target) return;\n const distanceFromBottom =\n target.scrollHeight - target.scrollTop - target.clientHeight;\n const shouldStick = force || host.chatUserNearBottom || distanceFromBottom < 200;\n if (!shouldStick) return;\n if (force) host.chatHasAutoScrolled = true;\n target.scrollTop = target.scrollHeight;\n host.chatUserNearBottom = true;\n const retryDelay = force ? 150 : 120;\n host.chatScrollTimeout = window.setTimeout(() => {\n host.chatScrollTimeout = null;\n const latest = pickScrollTarget();\n if (!latest) return;\n const latestDistanceFromBottom =\n latest.scrollHeight - latest.scrollTop - latest.clientHeight;\n const shouldStickRetry =\n force || host.chatUserNearBottom || latestDistanceFromBottom < 200;\n if (!shouldStickRetry) return;\n latest.scrollTop = latest.scrollHeight;\n host.chatUserNearBottom = true;\n }, retryDelay);\n });\n });\n}\n\nexport function scheduleLogsScroll(host: ScrollHost, force = false) {\n if (host.logsScrollFrame) cancelAnimationFrame(host.logsScrollFrame);\n void host.updateComplete.then(() => {\n host.logsScrollFrame = requestAnimationFrame(() => {\n host.logsScrollFrame = null;\n const container = host.querySelector(\".log-stream\") as HTMLElement | null;\n if (!container) return;\n const distanceFromBottom =\n container.scrollHeight - container.scrollTop - container.clientHeight;\n const shouldStick = force || distanceFromBottom < 80;\n if (!shouldStick) return;\n container.scrollTop = container.scrollHeight;\n });\n });\n}\n\nexport function handleChatScroll(host: ScrollHost, event: Event) {\n const container = event.currentTarget as HTMLElement | null;\n if (!container) return;\n const distanceFromBottom =\n container.scrollHeight - container.scrollTop - container.clientHeight;\n host.chatUserNearBottom = distanceFromBottom < 200;\n}\n\nexport function handleLogsScroll(host: ScrollHost, event: Event) {\n const container = event.currentTarget as HTMLElement | null;\n if (!container) return;\n const distanceFromBottom =\n container.scrollHeight - container.scrollTop - container.clientHeight;\n host.logsAtBottom = distanceFromBottom < 80;\n}\n\nexport function resetChatScroll(host: ScrollHost) {\n host.chatHasAutoScrolled = false;\n host.chatUserNearBottom = true;\n}\n\nexport function exportLogs(lines: string[], label: string) {\n if (lines.length === 0) return;\n const blob = new Blob([`${lines.join(\"\\n\")}\\n`], { type: \"text/plain\" });\n const url = URL.createObjectURL(blob);\n const anchor = document.createElement(\"a\");\n const stamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, \"-\");\n anchor.href = url;\n anchor.download = `clawdbot-logs-${label}-${stamp}.log`;\n anchor.click();\n URL.revokeObjectURL(url);\n}\n\nexport function observeTopbar(host: ScrollHost) {\n if (typeof ResizeObserver === \"undefined\") return;\n const topbar = host.querySelector(\".topbar\");\n if (!topbar) return;\n const update = () => {\n const { height } = topbar.getBoundingClientRect();\n host.style.setProperty(\"--topbar-height\", `${height}px`);\n };\n update();\n host.topbarObserver = new ResizeObserver(() => update());\n host.topbarObserver.observe(topbar);\n}\n","export function cloneConfigObject(value: T): T {\n if (typeof structuredClone === \"function\") {\n return structuredClone(value);\n }\n return JSON.parse(JSON.stringify(value)) as T;\n}\n\nexport function serializeConfigForm(form: Record): string {\n return `${JSON.stringify(form, null, 2).trimEnd()}\\n`;\n}\n\nexport function setPathValue(\n obj: Record | unknown[],\n path: Array,\n value: unknown,\n) {\n if (path.length === 0) return;\n let current: Record | unknown[] = obj;\n for (let i = 0; i < path.length - 1; i += 1) {\n const key = path[i];\n const nextKey = path[i + 1];\n if (typeof key === \"number\") {\n if (!Array.isArray(current)) return;\n if (current[key] == null) {\n current[key] =\n typeof nextKey === \"number\" ? [] : ({} as Record);\n }\n current = current[key] as Record | unknown[];\n } else {\n if (typeof current !== \"object\" || current == null) return;\n const record = current as Record;\n if (record[key] == null) {\n record[key] =\n typeof nextKey === \"number\" ? [] : ({} as Record);\n }\n current = record[key] as Record | unknown[];\n }\n }\n const lastKey = path[path.length - 1];\n if (typeof lastKey === \"number\") {\n if (Array.isArray(current)) current[lastKey] = value;\n return;\n }\n if (typeof current === \"object\" && current != null) {\n (current as Record)[lastKey] = value;\n }\n}\n\nexport function removePathValue(\n obj: Record | unknown[],\n path: Array,\n) {\n if (path.length === 0) return;\n let current: Record | unknown[] = obj;\n for (let i = 0; i < path.length - 1; i += 1) {\n const key = path[i];\n if (typeof key === \"number\") {\n if (!Array.isArray(current)) return;\n current = current[key] as Record | unknown[];\n } else {\n if (typeof current !== \"object\" || current == null) return;\n current = (current as Record)[key] as\n | Record\n | unknown[];\n }\n if (current == null) return;\n }\n const lastKey = path[path.length - 1];\n if (typeof lastKey === \"number\") {\n if (Array.isArray(current)) current.splice(lastKey, 1);\n return;\n }\n if (typeof current === \"object\" && current != null) {\n delete (current as Record)[lastKey];\n }\n}\n\n","import type { GatewayBrowserClient } from \"../gateway\";\nimport type {\n ConfigSchemaResponse,\n ConfigSnapshot,\n ConfigUiHints,\n} from \"../types\";\nimport {\n cloneConfigObject,\n removePathValue,\n serializeConfigForm,\n setPathValue,\n} from \"./config/form-utils\";\n\nexport type ConfigState = {\n client: GatewayBrowserClient | null;\n connected: boolean;\n applySessionKey: string;\n configLoading: boolean;\n configRaw: string;\n configRawOriginal: string;\n configValid: boolean | null;\n configIssues: unknown[];\n configSaving: boolean;\n configApplying: boolean;\n updateRunning: boolean;\n configSnapshot: ConfigSnapshot | null;\n configSchema: unknown | null;\n configSchemaVersion: string | null;\n configSchemaLoading: boolean;\n configUiHints: ConfigUiHints;\n configForm: Record | null;\n configFormOriginal: Record | null;\n configFormDirty: boolean;\n configFormMode: \"form\" | \"raw\";\n configSearchQuery: string;\n configActiveSection: string | null;\n configActiveSubsection: string | null;\n lastError: string | null;\n};\n\nexport async function loadConfig(state: ConfigState) {\n if (!state.client || !state.connected) return;\n state.configLoading = true;\n state.lastError = null;\n try {\n const res = (await state.client.request(\"config.get\", {})) as ConfigSnapshot;\n applyConfigSnapshot(state, res);\n } catch (err) {\n state.lastError = String(err);\n } finally {\n state.configLoading = false;\n }\n}\n\nexport async function loadConfigSchema(state: ConfigState) {\n if (!state.client || !state.connected) return;\n if (state.configSchemaLoading) return;\n state.configSchemaLoading = true;\n try {\n const res = (await state.client.request(\n \"config.schema\",\n {},\n )) as ConfigSchemaResponse;\n applyConfigSchema(state, res);\n } catch (err) {\n state.lastError = String(err);\n } finally {\n state.configSchemaLoading = false;\n }\n}\n\nexport function applyConfigSchema(\n state: ConfigState,\n res: ConfigSchemaResponse,\n) {\n state.configSchema = res.schema ?? null;\n state.configUiHints = res.uiHints ?? {};\n state.configSchemaVersion = res.version ?? null;\n}\n\nexport function applyConfigSnapshot(state: ConfigState, snapshot: ConfigSnapshot) {\n state.configSnapshot = snapshot;\n const rawFromSnapshot =\n typeof snapshot.raw === \"string\"\n ? snapshot.raw\n : snapshot.config && typeof snapshot.config === \"object\"\n ? serializeConfigForm(snapshot.config as Record)\n : state.configRaw;\n if (!state.configFormDirty || state.configFormMode === \"raw\") {\n state.configRaw = rawFromSnapshot;\n } else if (state.configForm) {\n state.configRaw = serializeConfigForm(state.configForm);\n } else {\n state.configRaw = rawFromSnapshot;\n }\n state.configValid = typeof snapshot.valid === \"boolean\" ? snapshot.valid : null;\n state.configIssues = Array.isArray(snapshot.issues) ? snapshot.issues : [];\n\n if (!state.configFormDirty) {\n state.configForm = cloneConfigObject(snapshot.config ?? {});\n state.configFormOriginal = cloneConfigObject(snapshot.config ?? {});\n state.configRawOriginal = rawFromSnapshot;\n }\n}\n\nexport async function saveConfig(state: ConfigState) {\n if (!state.client || !state.connected) return;\n state.configSaving = true;\n state.lastError = null;\n try {\n const raw =\n state.configFormMode === \"form\" && state.configForm\n ? serializeConfigForm(state.configForm)\n : state.configRaw;\n const baseHash = state.configSnapshot?.hash;\n if (!baseHash) {\n state.lastError = \"Config hash missing; reload and retry.\";\n return;\n }\n await state.client.request(\"config.set\", { raw, baseHash });\n state.configFormDirty = false;\n await loadConfig(state);\n } catch (err) {\n state.lastError = String(err);\n } finally {\n state.configSaving = false;\n }\n}\n\nexport async function applyConfig(state: ConfigState) {\n if (!state.client || !state.connected) return;\n state.configApplying = true;\n state.lastError = null;\n try {\n const raw =\n state.configFormMode === \"form\" && state.configForm\n ? serializeConfigForm(state.configForm)\n : state.configRaw;\n const baseHash = state.configSnapshot?.hash;\n if (!baseHash) {\n state.lastError = \"Config hash missing; reload and retry.\";\n return;\n }\n await state.client.request(\"config.apply\", {\n raw,\n baseHash,\n sessionKey: state.applySessionKey,\n });\n state.configFormDirty = false;\n await loadConfig(state);\n } catch (err) {\n state.lastError = String(err);\n } finally {\n state.configApplying = false;\n }\n}\n\nexport async function runUpdate(state: ConfigState) {\n if (!state.client || !state.connected) return;\n state.updateRunning = true;\n state.lastError = null;\n try {\n await state.client.request(\"update.run\", {\n sessionKey: state.applySessionKey,\n });\n } catch (err) {\n state.lastError = String(err);\n } finally {\n state.updateRunning = false;\n }\n}\n\nexport function updateConfigFormValue(\n state: ConfigState,\n path: Array,\n value: unknown,\n) {\n const base = cloneConfigObject(\n state.configForm ?? state.configSnapshot?.config ?? {},\n );\n setPathValue(base, path, value);\n state.configForm = base;\n state.configFormDirty = true;\n if (state.configFormMode === \"form\") {\n state.configRaw = serializeConfigForm(base);\n }\n}\n\nexport function removeConfigFormValue(\n state: ConfigState,\n path: Array,\n) {\n const base = cloneConfigObject(\n state.configForm ?? state.configSnapshot?.config ?? {},\n );\n removePathValue(base, path);\n state.configForm = base;\n state.configFormDirty = true;\n if (state.configFormMode === \"form\") {\n state.configRaw = serializeConfigForm(base);\n }\n}\n","import { toNumber } from \"../format\";\nimport type { GatewayBrowserClient } from \"../gateway\";\nimport type { CronJob, CronRunLogEntry, CronStatus } from \"../types\";\nimport type { CronFormState } from \"../ui-types\";\n\nexport type CronState = {\n client: GatewayBrowserClient | null;\n connected: boolean;\n cronLoading: boolean;\n cronJobs: CronJob[];\n cronStatus: CronStatus | null;\n cronError: string | null;\n cronForm: CronFormState;\n cronRunsJobId: string | null;\n cronRuns: CronRunLogEntry[];\n cronBusy: boolean;\n};\n\nexport async function loadCronStatus(state: CronState) {\n if (!state.client || !state.connected) return;\n try {\n const res = (await state.client.request(\"cron.status\", {})) as CronStatus;\n state.cronStatus = res;\n } catch (err) {\n state.cronError = String(err);\n }\n}\n\nexport async function loadCronJobs(state: CronState) {\n if (!state.client || !state.connected) return;\n if (state.cronLoading) return;\n state.cronLoading = true;\n state.cronError = null;\n try {\n const res = (await state.client.request(\"cron.list\", {\n includeDisabled: true,\n })) as { jobs?: CronJob[] };\n state.cronJobs = Array.isArray(res.jobs) ? res.jobs : [];\n } catch (err) {\n state.cronError = String(err);\n } finally {\n state.cronLoading = false;\n }\n}\n\nexport function buildCronSchedule(form: CronFormState) {\n if (form.scheduleKind === \"at\") {\n const ms = Date.parse(form.scheduleAt);\n if (!Number.isFinite(ms)) throw new Error(\"Invalid run time.\");\n return { kind: \"at\" as const, atMs: ms };\n }\n if (form.scheduleKind === \"every\") {\n const amount = toNumber(form.everyAmount, 0);\n if (amount <= 0) throw new Error(\"Invalid interval amount.\");\n const unit = form.everyUnit;\n const mult = unit === \"minutes\" ? 60_000 : unit === \"hours\" ? 3_600_000 : 86_400_000;\n return { kind: \"every\" as const, everyMs: amount * mult };\n }\n const expr = form.cronExpr.trim();\n if (!expr) throw new Error(\"Cron expression required.\");\n return { kind: \"cron\" as const, expr, tz: form.cronTz.trim() || undefined };\n}\n\nexport function buildCronPayload(form: CronFormState) {\n if (form.payloadKind === \"systemEvent\") {\n const text = form.payloadText.trim();\n if (!text) throw new Error(\"System event text required.\");\n return { kind: \"systemEvent\" as const, text };\n }\n const message = form.payloadText.trim();\n if (!message) throw new Error(\"Agent message required.\");\n const payload: {\n kind: \"agentTurn\";\n message: string;\n deliver?: boolean;\n channel?: string;\n to?: string;\n timeoutSeconds?: number;\n } = { kind: \"agentTurn\", message };\n if (form.deliver) payload.deliver = true;\n if (form.channel) payload.channel = form.channel;\n if (form.to.trim()) payload.to = form.to.trim();\n const timeoutSeconds = toNumber(form.timeoutSeconds, 0);\n if (timeoutSeconds > 0) payload.timeoutSeconds = timeoutSeconds;\n return payload;\n}\n\nexport async function addCronJob(state: CronState) {\n if (!state.client || !state.connected || state.cronBusy) return;\n state.cronBusy = true;\n state.cronError = null;\n try {\n const schedule = buildCronSchedule(state.cronForm);\n const payload = buildCronPayload(state.cronForm);\n const agentId = state.cronForm.agentId.trim();\n const job = {\n name: state.cronForm.name.trim(),\n description: state.cronForm.description.trim() || undefined,\n agentId: agentId || undefined,\n enabled: state.cronForm.enabled,\n schedule,\n sessionTarget: state.cronForm.sessionTarget,\n wakeMode: state.cronForm.wakeMode,\n payload,\n isolation:\n state.cronForm.postToMainPrefix.trim() &&\n state.cronForm.sessionTarget === \"isolated\"\n ? { postToMainPrefix: state.cronForm.postToMainPrefix.trim() }\n : undefined,\n };\n if (!job.name) throw new Error(\"Name required.\");\n await state.client.request(\"cron.add\", job);\n state.cronForm = {\n ...state.cronForm,\n name: \"\",\n description: \"\",\n payloadText: \"\",\n };\n await loadCronJobs(state);\n await loadCronStatus(state);\n } catch (err) {\n state.cronError = String(err);\n } finally {\n state.cronBusy = false;\n }\n}\n\nexport async function toggleCronJob(\n state: CronState,\n job: CronJob,\n enabled: boolean,\n) {\n if (!state.client || !state.connected || state.cronBusy) return;\n state.cronBusy = true;\n state.cronError = null;\n try {\n await state.client.request(\"cron.update\", { id: job.id, patch: { enabled } });\n await loadCronJobs(state);\n await loadCronStatus(state);\n } catch (err) {\n state.cronError = String(err);\n } finally {\n state.cronBusy = false;\n }\n}\n\nexport async function runCronJob(state: CronState, job: CronJob) {\n if (!state.client || !state.connected || state.cronBusy) return;\n state.cronBusy = true;\n state.cronError = null;\n try {\n await state.client.request(\"cron.run\", { id: job.id, mode: \"force\" });\n await loadCronRuns(state, job.id);\n } catch (err) {\n state.cronError = String(err);\n } finally {\n state.cronBusy = false;\n }\n}\n\nexport async function removeCronJob(state: CronState, job: CronJob) {\n if (!state.client || !state.connected || state.cronBusy) return;\n state.cronBusy = true;\n state.cronError = null;\n try {\n await state.client.request(\"cron.remove\", { id: job.id });\n if (state.cronRunsJobId === job.id) {\n state.cronRunsJobId = null;\n state.cronRuns = [];\n }\n await loadCronJobs(state);\n await loadCronStatus(state);\n } catch (err) {\n state.cronError = String(err);\n } finally {\n state.cronBusy = false;\n }\n}\n\nexport async function loadCronRuns(state: CronState, jobId: string) {\n if (!state.client || !state.connected) return;\n try {\n const res = (await state.client.request(\"cron.runs\", {\n id: jobId,\n limit: 50,\n })) as { entries?: CronRunLogEntry[] };\n state.cronRunsJobId = jobId;\n state.cronRuns = Array.isArray(res.entries) ? res.entries : [];\n } catch (err) {\n state.cronError = String(err);\n }\n}\n","import type { ChannelsStatusSnapshot } from \"../types\";\nimport type { ChannelsState } from \"./channels.types\";\n\nexport type { ChannelsState };\n\nexport async function loadChannels(state: ChannelsState, probe: boolean) {\n if (!state.client || !state.connected) return;\n if (state.channelsLoading) return;\n state.channelsLoading = true;\n state.channelsError = null;\n try {\n const res = (await state.client.request(\"channels.status\", {\n probe,\n timeoutMs: 8000,\n })) as ChannelsStatusSnapshot;\n state.channelsSnapshot = res;\n state.channelsLastSuccess = Date.now();\n } catch (err) {\n state.channelsError = String(err);\n } finally {\n state.channelsLoading = false;\n }\n}\n\nexport async function startWhatsAppLogin(state: ChannelsState, force: boolean) {\n if (!state.client || !state.connected || state.whatsappBusy) return;\n state.whatsappBusy = true;\n try {\n const res = (await state.client.request(\"web.login.start\", {\n force,\n timeoutMs: 30000,\n })) as { message?: string; qrDataUrl?: string };\n state.whatsappLoginMessage = res.message ?? null;\n state.whatsappLoginQrDataUrl = res.qrDataUrl ?? null;\n state.whatsappLoginConnected = null;\n } catch (err) {\n state.whatsappLoginMessage = String(err);\n state.whatsappLoginQrDataUrl = null;\n state.whatsappLoginConnected = null;\n } finally {\n state.whatsappBusy = false;\n }\n}\n\nexport async function waitWhatsAppLogin(state: ChannelsState) {\n if (!state.client || !state.connected || state.whatsappBusy) return;\n state.whatsappBusy = true;\n try {\n const res = (await state.client.request(\"web.login.wait\", {\n timeoutMs: 120000,\n })) as { connected?: boolean; message?: string };\n state.whatsappLoginMessage = res.message ?? null;\n state.whatsappLoginConnected = res.connected ?? null;\n if (res.connected) state.whatsappLoginQrDataUrl = null;\n } catch (err) {\n state.whatsappLoginMessage = String(err);\n state.whatsappLoginConnected = null;\n } finally {\n state.whatsappBusy = false;\n }\n}\n\nexport async function logoutWhatsApp(state: ChannelsState) {\n if (!state.client || !state.connected || state.whatsappBusy) return;\n state.whatsappBusy = true;\n try {\n await state.client.request(\"channels.logout\", { channel: \"whatsapp\" });\n state.whatsappLoginMessage = \"Logged out.\";\n state.whatsappLoginQrDataUrl = null;\n state.whatsappLoginConnected = null;\n } catch (err) {\n state.whatsappLoginMessage = String(err);\n } finally {\n state.whatsappBusy = false;\n }\n}\n","import type { GatewayBrowserClient } from \"../gateway\";\nimport type { HealthSnapshot, StatusSummary } from \"../types\";\n\nexport type DebugState = {\n client: GatewayBrowserClient | null;\n connected: boolean;\n debugLoading: boolean;\n debugStatus: StatusSummary | null;\n debugHealth: HealthSnapshot | null;\n debugModels: unknown[];\n debugHeartbeat: unknown | null;\n debugCallMethod: string;\n debugCallParams: string;\n debugCallResult: string | null;\n debugCallError: string | null;\n};\n\nexport async function loadDebug(state: DebugState) {\n if (!state.client || !state.connected) return;\n if (state.debugLoading) return;\n state.debugLoading = true;\n try {\n const [status, health, models, heartbeat] = await Promise.all([\n state.client.request(\"status\", {}),\n state.client.request(\"health\", {}),\n state.client.request(\"models.list\", {}),\n state.client.request(\"last-heartbeat\", {}),\n ]);\n state.debugStatus = status as StatusSummary;\n state.debugHealth = health as HealthSnapshot;\n const modelPayload = models as { models?: unknown[] } | undefined;\n state.debugModels = Array.isArray(modelPayload?.models)\n ? modelPayload?.models\n : [];\n state.debugHeartbeat = heartbeat as unknown;\n } catch (err) {\n state.debugCallError = String(err);\n } finally {\n state.debugLoading = false;\n }\n}\n\nexport async function callDebugMethod(state: DebugState) {\n if (!state.client || !state.connected) return;\n state.debugCallError = null;\n state.debugCallResult = null;\n try {\n const params = state.debugCallParams.trim()\n ? (JSON.parse(state.debugCallParams) as unknown)\n : {};\n const res = await state.client.request(state.debugCallMethod.trim(), params);\n state.debugCallResult = JSON.stringify(res, null, 2);\n } catch (err) {\n state.debugCallError = String(err);\n }\n}\n\n","import type { GatewayBrowserClient } from \"../gateway\";\nimport type { LogEntry, LogLevel } from \"../types\";\n\nexport type LogsState = {\n client: GatewayBrowserClient | null;\n connected: boolean;\n logsLoading: boolean;\n logsError: string | null;\n logsCursor: number | null;\n logsFile: string | null;\n logsEntries: LogEntry[];\n logsTruncated: boolean;\n logsLastFetchAt: number | null;\n logsLimit: number;\n logsMaxBytes: number;\n};\n\nconst LOG_BUFFER_LIMIT = 2000;\nconst LEVELS = new Set([\n \"trace\",\n \"debug\",\n \"info\",\n \"warn\",\n \"error\",\n \"fatal\",\n]);\n\nfunction parseMaybeJsonString(value: unknown) {\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n if (!trimmed.startsWith(\"{\") || !trimmed.endsWith(\"}\")) return null;\n try {\n const parsed = JSON.parse(trimmed) as unknown;\n if (!parsed || typeof parsed !== \"object\") return null;\n return parsed as Record;\n } catch {\n return null;\n }\n}\n\nfunction normalizeLevel(value: unknown): LogLevel | null {\n if (typeof value !== \"string\") return null;\n const lowered = value.toLowerCase() as LogLevel;\n return LEVELS.has(lowered) ? lowered : null;\n}\n\nexport function parseLogLine(line: string): LogEntry {\n if (!line.trim()) return { raw: line, message: line };\n try {\n const obj = JSON.parse(line) as Record;\n const meta =\n obj && typeof obj._meta === \"object\" && obj._meta !== null\n ? (obj._meta as Record)\n : null;\n const time =\n typeof obj.time === \"string\"\n ? obj.time\n : typeof meta?.date === \"string\"\n ? meta?.date\n : null;\n const level = normalizeLevel(meta?.logLevelName ?? meta?.level);\n\n const contextCandidate =\n typeof obj[\"0\"] === \"string\"\n ? (obj[\"0\"] as string)\n : typeof meta?.name === \"string\"\n ? (meta?.name as string)\n : null;\n const contextObj = parseMaybeJsonString(contextCandidate);\n let subsystem: string | null = null;\n if (contextObj) {\n if (typeof contextObj.subsystem === \"string\") subsystem = contextObj.subsystem;\n else if (typeof contextObj.module === \"string\") subsystem = contextObj.module;\n }\n if (!subsystem && contextCandidate && contextCandidate.length < 120) {\n subsystem = contextCandidate;\n }\n\n let message: string | null = null;\n if (typeof obj[\"1\"] === \"string\") message = obj[\"1\"] as string;\n else if (!contextObj && typeof obj[\"0\"] === \"string\") message = obj[\"0\"] as string;\n else if (typeof obj.message === \"string\") message = obj.message as string;\n\n return {\n raw: line,\n time,\n level,\n subsystem,\n message: message ?? line,\n meta: meta ?? undefined,\n };\n } catch {\n return { raw: line, message: line };\n }\n}\n\nexport async function loadLogs(\n state: LogsState,\n opts?: { reset?: boolean; quiet?: boolean },\n) {\n if (!state.client || !state.connected) return;\n if (state.logsLoading && !opts?.quiet) return;\n if (!opts?.quiet) state.logsLoading = true;\n state.logsError = null;\n try {\n const res = await state.client.request(\"logs.tail\", {\n cursor: opts?.reset ? undefined : state.logsCursor ?? undefined,\n limit: state.logsLimit,\n maxBytes: state.logsMaxBytes,\n });\n const payload = res as {\n file?: string;\n cursor?: number;\n size?: number;\n lines?: unknown;\n truncated?: boolean;\n reset?: boolean;\n };\n const lines = Array.isArray(payload.lines)\n ? (payload.lines.filter((line) => typeof line === \"string\") as string[])\n : [];\n const entries = lines.map(parseLogLine);\n const shouldReset = Boolean(opts?.reset || payload.reset || state.logsCursor == null);\n state.logsEntries = shouldReset\n ? entries\n : [...state.logsEntries, ...entries].slice(-LOG_BUFFER_LIMIT);\n if (typeof payload.cursor === \"number\") state.logsCursor = payload.cursor;\n if (typeof payload.file === \"string\") state.logsFile = payload.file;\n state.logsTruncated = Boolean(payload.truncated);\n state.logsLastFetchAt = Date.now();\n } catch (err) {\n state.logsError = String(err);\n } finally {\n if (!opts?.quiet) state.logsLoading = false;\n }\n}\n","/*! noble-ed25519 - MIT License (c) 2019 Paul Miller (paulmillr.com) */\n/**\n * 5KB JS implementation of ed25519 EdDSA signatures.\n * Compliant with RFC8032, FIPS 186-5 & ZIP215.\n * @module\n * @example\n * ```js\nimport * as ed from '@noble/ed25519';\n(async () => {\n const secretKey = ed.utils.randomSecretKey();\n const message = Uint8Array.from([0xab, 0xbc, 0xcd, 0xde]);\n const pubKey = await ed.getPublicKeyAsync(secretKey); // Sync methods are also present\n const signature = await ed.signAsync(message, secretKey);\n const isValid = await ed.verifyAsync(signature, message, pubKey);\n})();\n```\n */\n/**\n * Curve params. ed25519 is twisted edwards curve. Equation is −x² + y² = -a + dx²y².\n * * P = `2n**255n - 19n` // field over which calculations are done\n * * N = `2n**252n + 27742317777372353535851937790883648493n` // group order, amount of curve points\n * * h = 8 // cofactor\n * * a = `Fp.create(BigInt(-1))` // equation param\n * * d = -121665/121666 a.k.a. `Fp.neg(121665 * Fp.inv(121666))` // equation param\n * * Gx, Gy are coordinates of Generator / base point\n */\nconst ed25519_CURVE = {\n p: 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffedn,\n n: 0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3edn,\n h: 8n,\n a: 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffecn,\n d: 0x52036cee2b6ffe738cc740797779e89800700a4d4141d8ab75eb4dca135978a3n,\n Gx: 0x216936d3cd6e53fec0a4e231fdd6dc5c692cc7609525a7b2c9562d608f25d51an,\n Gy: 0x6666666666666666666666666666666666666666666666666666666666666658n,\n};\nconst { p: P, n: N, Gx, Gy, a: _a, d: _d, h } = ed25519_CURVE;\nconst L = 32; // field / group byte length\nconst L2 = 64;\n// Helpers and Precomputes sections are reused between libraries\n// ## Helpers\n// ----------\nconst captureTrace = (...args) => {\n if ('captureStackTrace' in Error && typeof Error.captureStackTrace === 'function') {\n Error.captureStackTrace(...args);\n }\n};\nconst err = (message = '') => {\n const e = new Error(message);\n captureTrace(e, err);\n throw e;\n};\nconst isBig = (n) => typeof n === 'bigint'; // is big integer\nconst isStr = (s) => typeof s === 'string'; // is string\nconst isBytes = (a) => a instanceof Uint8Array || (ArrayBuffer.isView(a) && a.constructor.name === 'Uint8Array');\n/** Asserts something is Uint8Array. */\nconst abytes = (value, length, title = '') => {\n const bytes = isBytes(value);\n const len = value?.length;\n const needsLen = length !== undefined;\n if (!bytes || (needsLen && len !== length)) {\n const prefix = title && `\"${title}\" `;\n const ofLen = needsLen ? ` of length ${length}` : '';\n const got = bytes ? `length=${len}` : `type=${typeof value}`;\n err(prefix + 'expected Uint8Array' + ofLen + ', got ' + got);\n }\n return value;\n};\n/** create Uint8Array */\nconst u8n = (len) => new Uint8Array(len);\nconst u8fr = (buf) => Uint8Array.from(buf);\nconst padh = (n, pad) => n.toString(16).padStart(pad, '0');\nconst bytesToHex = (b) => Array.from(abytes(b))\n .map((e) => padh(e, 2))\n .join('');\nconst C = { _0: 48, _9: 57, A: 65, F: 70, a: 97, f: 102 }; // ASCII characters\nconst _ch = (ch) => {\n if (ch >= C._0 && ch <= C._9)\n return ch - C._0; // '2' => 50-48\n if (ch >= C.A && ch <= C.F)\n return ch - (C.A - 10); // 'B' => 66-(65-10)\n if (ch >= C.a && ch <= C.f)\n return ch - (C.a - 10); // 'b' => 98-(97-10)\n return;\n};\nconst hexToBytes = (hex) => {\n const e = 'hex invalid';\n if (!isStr(hex))\n return err(e);\n const hl = hex.length;\n const al = hl / 2;\n if (hl % 2)\n return err(e);\n const array = u8n(al);\n for (let ai = 0, hi = 0; ai < al; ai++, hi += 2) {\n // treat each char as ASCII\n const n1 = _ch(hex.charCodeAt(hi)); // parse first char, multiply it by 16\n const n2 = _ch(hex.charCodeAt(hi + 1)); // parse second char\n if (n1 === undefined || n2 === undefined)\n return err(e);\n array[ai] = n1 * 16 + n2; // example: 'A9' => 10*16 + 9\n }\n return array;\n};\nconst cr = () => globalThis?.crypto; // WebCrypto is available in all modern environments\nconst subtle = () => cr()?.subtle ?? err('crypto.subtle must be defined, consider polyfill');\n// prettier-ignore\nconst concatBytes = (...arrs) => {\n const r = u8n(arrs.reduce((sum, a) => sum + abytes(a).length, 0)); // create u8a of summed length\n let pad = 0; // walk through each array,\n arrs.forEach(a => { r.set(a, pad); pad += a.length; }); // ensure they have proper type\n return r;\n};\n/** WebCrypto OS-level CSPRNG (random number generator). Will throw when not available. */\nconst randomBytes = (len = L) => {\n const c = cr();\n return c.getRandomValues(u8n(len));\n};\nconst big = BigInt;\nconst assertRange = (n, min, max, msg = 'bad number: out of range') => (isBig(n) && min <= n && n < max ? n : err(msg));\n/** modular division */\nconst M = (a, b = P) => {\n const r = a % b;\n return r >= 0n ? r : b + r;\n};\nconst modN = (a) => M(a, N);\n/** Modular inversion using euclidean GCD (non-CT). No negative exponent for now. */\n// prettier-ignore\nconst invert = (num, md) => {\n if (num === 0n || md <= 0n)\n err('no inverse n=' + num + ' mod=' + md);\n let a = M(num, md), b = md, x = 0n, y = 1n, u = 1n, v = 0n;\n while (a !== 0n) {\n const q = b / a, r = b % a;\n const m = x - u * q, n = y - v * q;\n b = a, a = r, x = u, y = v, u = m, v = n;\n }\n return b === 1n ? M(x, md) : err('no inverse'); // b is gcd at this point\n};\nconst callHash = (name) => {\n // @ts-ignore\n const fn = hashes[name];\n if (typeof fn !== 'function')\n err('hashes.' + name + ' not set');\n return fn;\n};\nconst hash = (msg) => callHash('sha512')(msg);\nconst apoint = (p) => (p instanceof Point ? p : err('Point expected'));\n// ## End of Helpers\n// -----------------\nconst B256 = 2n ** 256n;\n/** Point in XYZT extended coordinates. */\nclass Point {\n static BASE;\n static ZERO;\n X;\n Y;\n Z;\n T;\n constructor(X, Y, Z, T) {\n const max = B256;\n this.X = assertRange(X, 0n, max);\n this.Y = assertRange(Y, 0n, max);\n this.Z = assertRange(Z, 1n, max);\n this.T = assertRange(T, 0n, max);\n Object.freeze(this);\n }\n static CURVE() {\n return ed25519_CURVE;\n }\n static fromAffine(p) {\n return new Point(p.x, p.y, 1n, M(p.x * p.y));\n }\n /** RFC8032 5.1.3: Uint8Array to Point. */\n static fromBytes(hex, zip215 = false) {\n const d = _d;\n // Copy array to not mess it up.\n const normed = u8fr(abytes(hex, L));\n // adjust first LE byte = last BE byte\n const lastByte = hex[31];\n normed[31] = lastByte & ~0x80;\n const y = bytesToNumLE(normed);\n // zip215=true: 0 <= y < 2^256\n // zip215=false, RFC8032: 0 <= y < 2^255-19\n const max = zip215 ? B256 : P;\n assertRange(y, 0n, max);\n const y2 = M(y * y); // y²\n const u = M(y2 - 1n); // u=y²-1\n const v = M(d * y2 + 1n); // v=dy²+1\n let { isValid, value: x } = uvRatio(u, v); // (uv³)(uv⁷)^(p-5)/8; square root\n if (!isValid)\n err('bad point: y not sqrt'); // not square root: bad point\n const isXOdd = (x & 1n) === 1n; // adjust sign of x coordinate\n const isLastByteOdd = (lastByte & 0x80) !== 0; // x_0, last bit\n if (!zip215 && x === 0n && isLastByteOdd)\n err('bad point: x==0, isLastByteOdd'); // x=0, x_0=1\n if (isLastByteOdd !== isXOdd)\n x = M(-x);\n return new Point(x, y, 1n, M(x * y)); // Z=1, T=xy\n }\n static fromHex(hex, zip215) {\n return Point.fromBytes(hexToBytes(hex), zip215);\n }\n get x() {\n return this.toAffine().x;\n }\n get y() {\n return this.toAffine().y;\n }\n /** Checks if the point is valid and on-curve. */\n assertValidity() {\n const a = _a;\n const d = _d;\n const p = this;\n if (p.is0())\n return err('bad point: ZERO'); // TODO: optimize, with vars below?\n // Equation in affine coordinates: ax² + y² = 1 + dx²y²\n // Equation in projective coordinates (X/Z, Y/Z, Z): (aX² + Y²)Z² = Z⁴ + dX²Y²\n const { X, Y, Z, T } = p;\n const X2 = M(X * X); // X²\n const Y2 = M(Y * Y); // Y²\n const Z2 = M(Z * Z); // Z²\n const Z4 = M(Z2 * Z2); // Z⁴\n const aX2 = M(X2 * a); // aX²\n const left = M(Z2 * M(aX2 + Y2)); // (aX² + Y²)Z²\n const right = M(Z4 + M(d * M(X2 * Y2))); // Z⁴ + dX²Y²\n if (left !== right)\n return err('bad point: equation left != right (1)');\n // In Extended coordinates we also have T, which is x*y=T/Z: check X*Y == Z*T\n const XY = M(X * Y);\n const ZT = M(Z * T);\n if (XY !== ZT)\n return err('bad point: equation left != right (2)');\n return this;\n }\n /** Equality check: compare points P&Q. */\n equals(other) {\n const { X: X1, Y: Y1, Z: Z1 } = this;\n const { X: X2, Y: Y2, Z: Z2 } = apoint(other); // checks class equality\n const X1Z2 = M(X1 * Z2);\n const X2Z1 = M(X2 * Z1);\n const Y1Z2 = M(Y1 * Z2);\n const Y2Z1 = M(Y2 * Z1);\n return X1Z2 === X2Z1 && Y1Z2 === Y2Z1;\n }\n is0() {\n return this.equals(I);\n }\n /** Flip point over y coordinate. */\n negate() {\n return new Point(M(-this.X), this.Y, this.Z, M(-this.T));\n }\n /** Point doubling. Complete formula. Cost: `4M + 4S + 1*a + 6add + 1*2`. */\n double() {\n const { X: X1, Y: Y1, Z: Z1 } = this;\n const a = _a;\n // https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html#doubling-dbl-2008-hwcd\n const A = M(X1 * X1);\n const B = M(Y1 * Y1);\n const C = M(2n * M(Z1 * Z1));\n const D = M(a * A);\n const x1y1 = X1 + Y1;\n const E = M(M(x1y1 * x1y1) - A - B);\n const G = D + B;\n const F = G - C;\n const H = D - B;\n const X3 = M(E * F);\n const Y3 = M(G * H);\n const T3 = M(E * H);\n const Z3 = M(F * G);\n return new Point(X3, Y3, Z3, T3);\n }\n /** Point addition. Complete formula. Cost: `8M + 1*k + 8add + 1*2`. */\n add(other) {\n const { X: X1, Y: Y1, Z: Z1, T: T1 } = this;\n const { X: X2, Y: Y2, Z: Z2, T: T2 } = apoint(other); // doesn't check if other on-curve\n const a = _a;\n const d = _d;\n // https://hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html#addition-add-2008-hwcd-3\n const A = M(X1 * X2);\n const B = M(Y1 * Y2);\n const C = M(T1 * d * T2);\n const D = M(Z1 * Z2);\n const E = M((X1 + Y1) * (X2 + Y2) - A - B);\n const F = M(D - C);\n const G = M(D + C);\n const H = M(B - a * A);\n const X3 = M(E * F);\n const Y3 = M(G * H);\n const T3 = M(E * H);\n const Z3 = M(F * G);\n return new Point(X3, Y3, Z3, T3);\n }\n subtract(other) {\n return this.add(apoint(other).negate());\n }\n /**\n * Point-by-scalar multiplication. Scalar must be in range 1 <= n < CURVE.n.\n * Uses {@link wNAF} for base point.\n * Uses fake point to mitigate side-channel leakage.\n * @param n scalar by which point is multiplied\n * @param safe safe mode guards against timing attacks; unsafe mode is faster\n */\n multiply(n, safe = true) {\n if (!safe && (n === 0n || this.is0()))\n return I;\n assertRange(n, 1n, N);\n if (n === 1n)\n return this;\n if (this.equals(G))\n return wNAF(n).p;\n // init result point & fake point\n let p = I;\n let f = G;\n for (let d = this; n > 0n; d = d.double(), n >>= 1n) {\n // if bit is present, add to point\n // if not present, add to fake, for timing safety\n if (n & 1n)\n p = p.add(d);\n else if (safe)\n f = f.add(d);\n }\n return p;\n }\n multiplyUnsafe(scalar) {\n return this.multiply(scalar, false);\n }\n /** Convert point to 2d xy affine point. (X, Y, Z) ∋ (x=X/Z, y=Y/Z) */\n toAffine() {\n const { X, Y, Z } = this;\n // fast-paths for ZERO point OR Z=1\n if (this.equals(I))\n return { x: 0n, y: 1n };\n const iz = invert(Z, P);\n // (Z * Z^-1) must be 1, otherwise bad math\n if (M(Z * iz) !== 1n)\n err('invalid inverse');\n // x = X*Z^-1; y = Y*Z^-1\n const x = M(X * iz);\n const y = M(Y * iz);\n return { x, y };\n }\n toBytes() {\n const { x, y } = this.assertValidity().toAffine();\n const b = numTo32bLE(y);\n // store sign in first LE byte\n b[31] |= x & 1n ? 0x80 : 0;\n return b;\n }\n toHex() {\n return bytesToHex(this.toBytes());\n }\n clearCofactor() {\n return this.multiply(big(h), false);\n }\n isSmallOrder() {\n return this.clearCofactor().is0();\n }\n isTorsionFree() {\n // Multiply by big number N. We can't `mul(N)` because of checks. Instead, we `mul(N/2)*2+1`\n let p = this.multiply(N / 2n, false).double();\n if (N % 2n)\n p = p.add(this);\n return p.is0();\n }\n}\n/** Generator / base point */\nconst G = new Point(Gx, Gy, 1n, M(Gx * Gy));\n/** Identity / zero point */\nconst I = new Point(0n, 1n, 1n, 0n);\n// Static aliases\nPoint.BASE = G;\nPoint.ZERO = I;\nconst numTo32bLE = (num) => hexToBytes(padh(assertRange(num, 0n, B256), L2)).reverse();\nconst bytesToNumLE = (b) => big('0x' + bytesToHex(u8fr(abytes(b)).reverse()));\nconst pow2 = (x, power) => {\n // pow2(x, 4) == x^(2^4)\n let r = x;\n while (power-- > 0n) {\n r *= r;\n r %= P;\n }\n return r;\n};\n// prettier-ignore\nconst pow_2_252_3 = (x) => {\n const x2 = (x * x) % P; // x^2, bits 1\n const b2 = (x2 * x) % P; // x^3, bits 11\n const b4 = (pow2(b2, 2n) * b2) % P; // x^(2^4-1), bits 1111\n const b5 = (pow2(b4, 1n) * x) % P; // x^(2^5-1), bits 11111\n const b10 = (pow2(b5, 5n) * b5) % P; // x^(2^10)\n const b20 = (pow2(b10, 10n) * b10) % P; // x^(2^20)\n const b40 = (pow2(b20, 20n) * b20) % P; // x^(2^40)\n const b80 = (pow2(b40, 40n) * b40) % P; // x^(2^80)\n const b160 = (pow2(b80, 80n) * b80) % P; // x^(2^160)\n const b240 = (pow2(b160, 80n) * b80) % P; // x^(2^240)\n const b250 = (pow2(b240, 10n) * b10) % P; // x^(2^250)\n const pow_p_5_8 = (pow2(b250, 2n) * x) % P; // < To pow to (p+3)/8, multiply it by x.\n return { pow_p_5_8, b2 };\n};\nconst RM1 = 0x2b8324804fc1df0b2b4d00993dfbd7a72f431806ad2fe478c4ee1b274a0ea0b0n; // √-1\n// for sqrt comp\n// prettier-ignore\nconst uvRatio = (u, v) => {\n const v3 = M(v * v * v); // v³\n const v7 = M(v3 * v3 * v); // v⁷\n const pow = pow_2_252_3(u * v7).pow_p_5_8; // (uv⁷)^(p-5)/8\n let x = M(u * v3 * pow); // (uv³)(uv⁷)^(p-5)/8\n const vx2 = M(v * x * x); // vx²\n const root1 = x; // First root candidate\n const root2 = M(x * RM1); // Second root candidate; RM1 is √-1\n const useRoot1 = vx2 === u; // If vx² = u (mod p), x is a square root\n const useRoot2 = vx2 === M(-u); // If vx² = -u, set x <-- x * 2^((p-1)/4)\n const noRoot = vx2 === M(-u * RM1); // There is no valid root, vx² = -u√-1\n if (useRoot1)\n x = root1;\n if (useRoot2 || noRoot)\n x = root2; // We return root2 anyway, for const-time\n if ((M(x) & 1n) === 1n)\n x = M(-x); // edIsNegative\n return { isValid: useRoot1 || useRoot2, value: x };\n};\n// N == L, just weird naming\nconst modL_LE = (hash) => modN(bytesToNumLE(hash)); // modulo L; but little-endian\n/** hashes.sha512 should conform to the interface. */\n// TODO: rename\nconst sha512a = (...m) => hashes.sha512Async(concatBytes(...m)); // Async SHA512\nconst sha512s = (...m) => callHash('sha512')(concatBytes(...m));\n// RFC8032 5.1.5\nconst hash2extK = (hashed) => {\n // slice creates a copy, unlike subarray\n const head = hashed.slice(0, L);\n head[0] &= 248; // Clamp bits: 0b1111_1000\n head[31] &= 127; // 0b0111_1111\n head[31] |= 64; // 0b0100_0000\n const prefix = hashed.slice(L, L2); // secret key \"prefix\"\n const scalar = modL_LE(head); // modular division over curve order\n const point = G.multiply(scalar); // public key point\n const pointBytes = point.toBytes(); // point serialized to Uint8Array\n return { head, prefix, scalar, point, pointBytes };\n};\n// RFC8032 5.1.5; getPublicKey async, sync. Hash priv key and extract point.\nconst getExtendedPublicKeyAsync = (secretKey) => sha512a(abytes(secretKey, L)).then(hash2extK);\nconst getExtendedPublicKey = (secretKey) => hash2extK(sha512s(abytes(secretKey, L)));\n/** Creates 32-byte ed25519 public key from 32-byte secret key. Async. */\nconst getPublicKeyAsync = (secretKey) => getExtendedPublicKeyAsync(secretKey).then((p) => p.pointBytes);\n/** Creates 32-byte ed25519 public key from 32-byte secret key. To use, set `hashes.sha512` first. */\nconst getPublicKey = (priv) => getExtendedPublicKey(priv).pointBytes;\nconst hashFinishA = (res) => sha512a(res.hashable).then(res.finish);\nconst hashFinishS = (res) => res.finish(sha512s(res.hashable));\n// Code, shared between sync & async sign\nconst _sign = (e, rBytes, msg) => {\n const { pointBytes: P, scalar: s } = e;\n const r = modL_LE(rBytes); // r was created outside, reduce it modulo L\n const R = G.multiply(r).toBytes(); // R = [r]B\n const hashable = concatBytes(R, P, msg); // dom2(F, C) || R || A || PH(M)\n const finish = (hashed) => {\n // k = SHA512(dom2(F, C) || R || A || PH(M))\n const S = modN(r + modL_LE(hashed) * s); // S = (r + k * s) mod L; 0 <= s < l\n return abytes(concatBytes(R, numTo32bLE(S)), L2); // 64-byte sig: 32b R.x + 32b LE(S)\n };\n return { hashable, finish };\n};\n/**\n * Signs message using secret key. Async.\n * Follows RFC8032 5.1.6.\n */\nconst signAsync = async (message, secretKey) => {\n const m = abytes(message);\n const e = await getExtendedPublicKeyAsync(secretKey);\n const rBytes = await sha512a(e.prefix, m); // r = SHA512(dom2(F, C) || prefix || PH(M))\n return hashFinishA(_sign(e, rBytes, m)); // gen R, k, S, then 64-byte signature\n};\n/**\n * Signs message using secret key. To use, set `hashes.sha512` first.\n * Follows RFC8032 5.1.6.\n */\nconst sign = (message, secretKey) => {\n const m = abytes(message);\n const e = getExtendedPublicKey(secretKey);\n const rBytes = sha512s(e.prefix, m); // r = SHA512(dom2(F, C) || prefix || PH(M))\n return hashFinishS(_sign(e, rBytes, m)); // gen R, k, S, then 64-byte signature\n};\nconst defaultVerifyOpts = { zip215: true };\nconst _verify = (sig, msg, pub, opts = defaultVerifyOpts) => {\n sig = abytes(sig, L2); // Signature hex str/Bytes, must be 64 bytes\n msg = abytes(msg); // Message hex str/Bytes\n pub = abytes(pub, L);\n const { zip215 } = opts; // switch between zip215 and rfc8032 verif\n let A;\n let R;\n let s;\n let SB;\n let hashable = Uint8Array.of();\n try {\n A = Point.fromBytes(pub, zip215); // public key A decoded\n R = Point.fromBytes(sig.slice(0, L), zip215); // 0 <= R < 2^256: ZIP215 R can be >= P\n s = bytesToNumLE(sig.slice(L, L2)); // Decode second half as an integer S\n SB = G.multiply(s, false); // in the range 0 <= s < L\n hashable = concatBytes(R.toBytes(), A.toBytes(), msg); // dom2(F, C) || R || A || PH(M)\n }\n catch (error) { }\n const finish = (hashed) => {\n // k = SHA512(dom2(F, C) || R || A || PH(M))\n if (SB == null)\n return false; // false if try-catch catched an error\n if (!zip215 && A.isSmallOrder())\n return false; // false for SBS: Strongly Binding Signature\n const k = modL_LE(hashed); // decode in little-endian, modulo L\n const RkA = R.add(A.multiply(k, false)); // [8]R + [8][k]A'\n return RkA.add(SB.negate()).clearCofactor().is0(); // [8][S]B = [8]R + [8][k]A'\n };\n return { hashable, finish };\n};\n/** Verifies signature on message and public key. Async. Follows RFC8032 5.1.7. */\nconst verifyAsync = async (signature, message, publicKey, opts = defaultVerifyOpts) => hashFinishA(_verify(signature, message, publicKey, opts));\n/** Verifies signature on message and public key. To use, set `hashes.sha512` first. Follows RFC8032 5.1.7. */\nconst verify = (signature, message, publicKey, opts = defaultVerifyOpts) => hashFinishS(_verify(signature, message, publicKey, opts));\n/** Math, hex, byte helpers. Not in `utils` because utils share API with noble-curves. */\nconst etc = {\n bytesToHex: bytesToHex,\n hexToBytes: hexToBytes,\n concatBytes: concatBytes,\n mod: M,\n invert: invert,\n randomBytes: randomBytes,\n};\nconst hashes = {\n sha512Async: async (message) => {\n const s = subtle();\n const m = concatBytes(message);\n return u8n(await s.digest('SHA-512', m.buffer));\n },\n sha512: undefined,\n};\n// FIPS 186 B.4.1 compliant key generation produces private keys\n// with modulo bias being neglible. takes >N+16 bytes, returns (hash mod n-1)+1\nconst randomSecretKey = (seed = randomBytes(L)) => seed;\nconst keygen = (seed) => {\n const secretKey = randomSecretKey(seed);\n const publicKey = getPublicKey(secretKey);\n return { secretKey, publicKey };\n};\nconst keygenAsync = async (seed) => {\n const secretKey = randomSecretKey(seed);\n const publicKey = await getPublicKeyAsync(secretKey);\n return { secretKey, publicKey };\n};\n/** ed25519-specific key utilities. */\nconst utils = {\n getExtendedPublicKeyAsync: getExtendedPublicKeyAsync,\n getExtendedPublicKey: getExtendedPublicKey,\n randomSecretKey: randomSecretKey,\n};\n// ## Precomputes\n// --------------\nconst W = 8; // W is window size\nconst scalarBits = 256;\nconst pwindows = Math.ceil(scalarBits / W) + 1; // 33 for W=8, NOT 32 - see wNAF loop\nconst pwindowSize = 2 ** (W - 1); // 128 for W=8\nconst precompute = () => {\n const points = [];\n let p = G;\n let b = p;\n for (let w = 0; w < pwindows; w++) {\n b = p;\n points.push(b);\n for (let i = 1; i < pwindowSize; i++) {\n b = b.add(p);\n points.push(b);\n } // i=1, bc we skip 0\n p = b.double();\n }\n return points;\n};\nlet Gpows = undefined; // precomputes for base point G\n// const-time negate\nconst ctneg = (cnd, p) => {\n const n = p.negate();\n return cnd ? n : p;\n};\n/**\n * Precomputes give 12x faster getPublicKey(), 10x sign(), 2x verify() by\n * caching multiples of G (base point). Cache is stored in 32MB of RAM.\n * Any time `G.multiply` is done, precomputes are used.\n * Not used for getSharedSecret, which instead multiplies random pubkey `P.multiply`.\n *\n * w-ary non-adjacent form (wNAF) precomputation method is 10% slower than windowed method,\n * but takes 2x less RAM. RAM reduction is possible by utilizing `.subtract`.\n *\n * !! Precomputes can be disabled by commenting-out call of the wNAF() inside Point#multiply().\n */\nconst wNAF = (n) => {\n const comp = Gpows || (Gpows = precompute());\n let p = I;\n let f = G; // f must be G, or could become I in the end\n const pow_2_w = 2 ** W; // 256 for W=8\n const maxNum = pow_2_w; // 256 for W=8\n const mask = big(pow_2_w - 1); // 255 for W=8 == mask 0b11111111\n const shiftBy = big(W); // 8 for W=8\n for (let w = 0; w < pwindows; w++) {\n let wbits = Number(n & mask); // extract W bits.\n n >>= shiftBy; // shift number by W bits.\n // We use negative indexes to reduce size of precomputed table by 2x.\n // Instead of needing precomputes 0..256, we only calculate them for 0..128.\n // If an index > 128 is found, we do (256-index) - where 256 is next window.\n // Naive: index +127 => 127, +224 => 224\n // Optimized: index +127 => 127, +224 => 256-32\n if (wbits > pwindowSize) {\n wbits -= maxNum;\n n += 1n;\n }\n const off = w * pwindowSize;\n const offF = off; // offsets, evaluate both\n const offP = off + Math.abs(wbits) - 1;\n const isEven = w % 2 !== 0; // conditions, evaluate both\n const isNeg = wbits < 0;\n if (wbits === 0) {\n // off == I: can't add it. Adding random offF instead.\n f = f.add(ctneg(isEven, comp[offF])); // bits are 0: add garbage to fake point\n }\n else {\n p = p.add(ctneg(isNeg, comp[offP])); // bits are 1: add to result point\n }\n }\n if (n !== 0n)\n err('invalid wnaf');\n return { p, f }; // return both real and fake points for JIT\n};\n// !! Remove the export to easily use in REPL / browser console\nexport { etc, getPublicKey, getPublicKeyAsync, hash, hashes, keygen, keygenAsync, Point, sign, signAsync, utils, verify, verifyAsync, };\n","import { getPublicKeyAsync, signAsync, utils } from \"@noble/ed25519\";\n\ntype StoredIdentity = {\n version: 1;\n deviceId: string;\n publicKey: string;\n privateKey: string;\n createdAtMs: number;\n};\n\nexport type DeviceIdentity = {\n deviceId: string;\n publicKey: string;\n privateKey: string;\n};\n\nconst STORAGE_KEY = \"clawdbot-device-identity-v1\";\n\nfunction base64UrlEncode(bytes: Uint8Array): string {\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replaceAll(\"+\", \"-\").replaceAll(\"/\", \"_\").replace(/=+$/g, \"\");\n}\n\nfunction base64UrlDecode(input: string): Uint8Array {\n const normalized = input.replaceAll(\"-\", \"+\").replaceAll(\"_\", \"/\");\n const padded = normalized + \"=\".repeat((4 - (normalized.length % 4)) % 4);\n const binary = atob(padded);\n const out = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i);\n return out;\n}\n\nfunction bytesToHex(bytes: Uint8Array): string {\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n\nasync function fingerprintPublicKey(publicKey: Uint8Array): Promise {\n const hash = await crypto.subtle.digest(\"SHA-256\", publicKey);\n return bytesToHex(new Uint8Array(hash));\n}\n\nasync function generateIdentity(): Promise {\n const privateKey = utils.randomSecretKey();\n const publicKey = await getPublicKeyAsync(privateKey);\n const deviceId = await fingerprintPublicKey(publicKey);\n return {\n deviceId,\n publicKey: base64UrlEncode(publicKey),\n privateKey: base64UrlEncode(privateKey),\n };\n}\n\nexport async function loadOrCreateDeviceIdentity(): Promise {\n try {\n const raw = localStorage.getItem(STORAGE_KEY);\n if (raw) {\n const parsed = JSON.parse(raw) as StoredIdentity;\n if (\n parsed?.version === 1 &&\n typeof parsed.deviceId === \"string\" &&\n typeof parsed.publicKey === \"string\" &&\n typeof parsed.privateKey === \"string\"\n ) {\n const derivedId = await fingerprintPublicKey(base64UrlDecode(parsed.publicKey));\n if (derivedId !== parsed.deviceId) {\n const updated: StoredIdentity = {\n ...parsed,\n deviceId: derivedId,\n };\n localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));\n return {\n deviceId: derivedId,\n publicKey: parsed.publicKey,\n privateKey: parsed.privateKey,\n };\n }\n return {\n deviceId: parsed.deviceId,\n publicKey: parsed.publicKey,\n privateKey: parsed.privateKey,\n };\n }\n }\n } catch {\n // fall through to regenerate\n }\n\n const identity = await generateIdentity();\n const stored: StoredIdentity = {\n version: 1,\n deviceId: identity.deviceId,\n publicKey: identity.publicKey,\n privateKey: identity.privateKey,\n createdAtMs: Date.now(),\n };\n localStorage.setItem(STORAGE_KEY, JSON.stringify(stored));\n return identity;\n}\n\nexport async function signDevicePayload(privateKeyBase64Url: string, payload: string) {\n const key = base64UrlDecode(privateKeyBase64Url);\n const data = new TextEncoder().encode(payload);\n const sig = await signAsync(data, key);\n return base64UrlEncode(sig);\n}\n","export type DeviceAuthEntry = {\n token: string;\n role: string;\n scopes: string[];\n updatedAtMs: number;\n};\n\ntype DeviceAuthStore = {\n version: 1;\n deviceId: string;\n tokens: Record;\n};\n\nconst STORAGE_KEY = \"clawdbot.device.auth.v1\";\n\nfunction normalizeRole(role: string): string {\n return role.trim();\n}\n\nfunction normalizeScopes(scopes: string[] | undefined): string[] {\n if (!Array.isArray(scopes)) return [];\n const out = new Set();\n for (const scope of scopes) {\n const trimmed = scope.trim();\n if (trimmed) out.add(trimmed);\n }\n return [...out].sort();\n}\n\nfunction readStore(): DeviceAuthStore | null {\n try {\n const raw = window.localStorage.getItem(STORAGE_KEY);\n if (!raw) return null;\n const parsed = JSON.parse(raw) as DeviceAuthStore;\n if (!parsed || parsed.version !== 1) return null;\n if (!parsed.deviceId || typeof parsed.deviceId !== \"string\") return null;\n if (!parsed.tokens || typeof parsed.tokens !== \"object\") return null;\n return parsed;\n } catch {\n return null;\n }\n}\n\nfunction writeStore(store: DeviceAuthStore) {\n try {\n window.localStorage.setItem(STORAGE_KEY, JSON.stringify(store));\n } catch {\n // best-effort\n }\n}\n\nexport function loadDeviceAuthToken(params: {\n deviceId: string;\n role: string;\n}): DeviceAuthEntry | null {\n const store = readStore();\n if (!store || store.deviceId !== params.deviceId) return null;\n const role = normalizeRole(params.role);\n const entry = store.tokens[role];\n if (!entry || typeof entry.token !== \"string\") return null;\n return entry;\n}\n\nexport function storeDeviceAuthToken(params: {\n deviceId: string;\n role: string;\n token: string;\n scopes?: string[];\n}): DeviceAuthEntry {\n const role = normalizeRole(params.role);\n const next: DeviceAuthStore = {\n version: 1,\n deviceId: params.deviceId,\n tokens: {},\n };\n const existing = readStore();\n if (existing && existing.deviceId === params.deviceId) {\n next.tokens = { ...existing.tokens };\n }\n const entry: DeviceAuthEntry = {\n token: params.token,\n role,\n scopes: normalizeScopes(params.scopes),\n updatedAtMs: Date.now(),\n };\n next.tokens[role] = entry;\n writeStore(next);\n return entry;\n}\n\nexport function clearDeviceAuthToken(params: { deviceId: string; role: string }) {\n const store = readStore();\n if (!store || store.deviceId !== params.deviceId) return;\n const role = normalizeRole(params.role);\n if (!store.tokens[role]) return;\n const next = { ...store, tokens: { ...store.tokens } };\n delete next.tokens[role];\n writeStore(next);\n}\n","import type { GatewayBrowserClient } from \"../gateway\";\nimport { loadOrCreateDeviceIdentity } from \"../device-identity\";\nimport { clearDeviceAuthToken, storeDeviceAuthToken } from \"../device-auth\";\n\nexport type DeviceTokenSummary = {\n role: string;\n scopes?: string[];\n createdAtMs?: number;\n rotatedAtMs?: number;\n revokedAtMs?: number;\n lastUsedAtMs?: number;\n};\n\nexport type PendingDevice = {\n requestId: string;\n deviceId: string;\n displayName?: string;\n role?: string;\n remoteIp?: string;\n isRepair?: boolean;\n ts?: number;\n};\n\nexport type PairedDevice = {\n deviceId: string;\n displayName?: string;\n roles?: string[];\n scopes?: string[];\n remoteIp?: string;\n tokens?: DeviceTokenSummary[];\n createdAtMs?: number;\n approvedAtMs?: number;\n};\n\nexport type DevicePairingList = {\n pending: PendingDevice[];\n paired: PairedDevice[];\n};\n\nexport type DevicesState = {\n client: GatewayBrowserClient | null;\n connected: boolean;\n devicesLoading: boolean;\n devicesError: string | null;\n devicesList: DevicePairingList | null;\n};\n\nexport async function loadDevices(state: DevicesState, opts?: { quiet?: boolean }) {\n if (!state.client || !state.connected) return;\n if (state.devicesLoading) return;\n state.devicesLoading = true;\n if (!opts?.quiet) state.devicesError = null;\n try {\n const res = (await state.client.request(\"device.pair.list\", {})) as DevicePairingList | null;\n state.devicesList = {\n pending: Array.isArray(res?.pending) ? res!.pending : [],\n paired: Array.isArray(res?.paired) ? res!.paired : [],\n };\n } catch (err) {\n if (!opts?.quiet) state.devicesError = String(err);\n } finally {\n state.devicesLoading = false;\n }\n}\n\nexport async function approveDevicePairing(state: DevicesState, requestId: string) {\n if (!state.client || !state.connected) return;\n try {\n await state.client.request(\"device.pair.approve\", { requestId });\n await loadDevices(state);\n } catch (err) {\n state.devicesError = String(err);\n }\n}\n\nexport async function rejectDevicePairing(state: DevicesState, requestId: string) {\n if (!state.client || !state.connected) return;\n const confirmed = window.confirm(\"Reject this device pairing request?\");\n if (!confirmed) return;\n try {\n await state.client.request(\"device.pair.reject\", { requestId });\n await loadDevices(state);\n } catch (err) {\n state.devicesError = String(err);\n }\n}\n\nexport async function rotateDeviceToken(\n state: DevicesState,\n params: { deviceId: string; role: string; scopes?: string[] },\n) {\n if (!state.client || !state.connected) return;\n try {\n const res = (await state.client.request(\"device.token.rotate\", params)) as\n | { token?: string; role?: string; deviceId?: string; scopes?: string[] }\n | undefined;\n if (res?.token) {\n const identity = await loadOrCreateDeviceIdentity();\n const role = res.role ?? params.role;\n if (res.deviceId === identity.deviceId || params.deviceId === identity.deviceId) {\n storeDeviceAuthToken({\n deviceId: identity.deviceId,\n role,\n token: res.token,\n scopes: res.scopes ?? params.scopes ?? [],\n });\n }\n window.prompt(\"New device token (copy and store securely):\", res.token);\n }\n await loadDevices(state);\n } catch (err) {\n state.devicesError = String(err);\n }\n}\n\nexport async function revokeDeviceToken(\n state: DevicesState,\n params: { deviceId: string; role: string },\n) {\n if (!state.client || !state.connected) return;\n const confirmed = window.confirm(\n `Revoke token for ${params.deviceId} (${params.role})?`,\n );\n if (!confirmed) return;\n try {\n await state.client.request(\"device.token.revoke\", params);\n const identity = await loadOrCreateDeviceIdentity();\n if (params.deviceId === identity.deviceId) {\n clearDeviceAuthToken({ deviceId: identity.deviceId, role: params.role });\n }\n await loadDevices(state);\n } catch (err) {\n state.devicesError = String(err);\n }\n}\n","import type { GatewayBrowserClient } from \"../gateway\";\n\nexport type NodesState = {\n client: GatewayBrowserClient | null;\n connected: boolean;\n nodesLoading: boolean;\n nodes: Array>;\n lastError: string | null;\n};\n\nexport async function loadNodes(\n state: NodesState,\n opts?: { quiet?: boolean },\n) {\n if (!state.client || !state.connected) return;\n if (state.nodesLoading) return;\n state.nodesLoading = true;\n if (!opts?.quiet) state.lastError = null;\n try {\n const res = (await state.client.request(\"node.list\", {})) as {\n nodes?: Array>;\n };\n state.nodes = Array.isArray(res.nodes) ? res.nodes : [];\n } catch (err) {\n if (!opts?.quiet) state.lastError = String(err);\n } finally {\n state.nodesLoading = false;\n }\n}\n","import type { GatewayBrowserClient } from \"../gateway\";\nimport { cloneConfigObject, removePathValue, setPathValue } from \"./config/form-utils\";\n\nexport type ExecApprovalsDefaults = {\n security?: string;\n ask?: string;\n askFallback?: string;\n autoAllowSkills?: boolean;\n};\n\nexport type ExecApprovalsAllowlistEntry = {\n id?: string;\n pattern: string;\n lastUsedAt?: number;\n lastUsedCommand?: string;\n lastResolvedPath?: string;\n};\n\nexport type ExecApprovalsAgent = ExecApprovalsDefaults & {\n allowlist?: ExecApprovalsAllowlistEntry[];\n};\n\nexport type ExecApprovalsFile = {\n version?: number;\n socket?: { path?: string };\n defaults?: ExecApprovalsDefaults;\n agents?: Record;\n};\n\nexport type ExecApprovalsSnapshot = {\n path: string;\n exists: boolean;\n hash: string;\n file: ExecApprovalsFile;\n};\n\nexport type ExecApprovalsTarget =\n | { kind: \"gateway\" }\n | { kind: \"node\"; nodeId: string };\n\nexport type ExecApprovalsState = {\n client: GatewayBrowserClient | null;\n connected: boolean;\n execApprovalsLoading: boolean;\n execApprovalsSaving: boolean;\n execApprovalsDirty: boolean;\n execApprovalsSnapshot: ExecApprovalsSnapshot | null;\n execApprovalsForm: ExecApprovalsFile | null;\n execApprovalsSelectedAgent: string | null;\n lastError: string | null;\n};\n\nfunction resolveExecApprovalsRpc(target?: ExecApprovalsTarget | null): {\n method: string;\n params: Record;\n} | null {\n if (!target || target.kind === \"gateway\") {\n return { method: \"exec.approvals.get\", params: {} };\n }\n const nodeId = target.nodeId.trim();\n if (!nodeId) return null;\n return { method: \"exec.approvals.node.get\", params: { nodeId } };\n}\n\nfunction resolveExecApprovalsSaveRpc(\n target: ExecApprovalsTarget | null | undefined,\n params: { file: ExecApprovalsFile; baseHash: string },\n): { method: string; params: Record } | null {\n if (!target || target.kind === \"gateway\") {\n return { method: \"exec.approvals.set\", params };\n }\n const nodeId = target.nodeId.trim();\n if (!nodeId) return null;\n return { method: \"exec.approvals.node.set\", params: { ...params, nodeId } };\n}\n\nexport async function loadExecApprovals(\n state: ExecApprovalsState,\n target?: ExecApprovalsTarget | null,\n) {\n if (!state.client || !state.connected) return;\n if (state.execApprovalsLoading) return;\n state.execApprovalsLoading = true;\n state.lastError = null;\n try {\n const rpc = resolveExecApprovalsRpc(target);\n if (!rpc) {\n state.lastError = \"Select a node before loading exec approvals.\";\n return;\n }\n const res = (await state.client.request(rpc.method, rpc.params)) as ExecApprovalsSnapshot;\n applyExecApprovalsSnapshot(state, res);\n } catch (err) {\n state.lastError = String(err);\n } finally {\n state.execApprovalsLoading = false;\n }\n}\n\nexport function applyExecApprovalsSnapshot(\n state: ExecApprovalsState,\n snapshot: ExecApprovalsSnapshot,\n) {\n state.execApprovalsSnapshot = snapshot;\n if (!state.execApprovalsDirty) {\n state.execApprovalsForm = cloneConfigObject(snapshot.file ?? {});\n }\n}\n\nexport async function saveExecApprovals(\n state: ExecApprovalsState,\n target?: ExecApprovalsTarget | null,\n) {\n if (!state.client || !state.connected) return;\n state.execApprovalsSaving = true;\n state.lastError = null;\n try {\n const baseHash = state.execApprovalsSnapshot?.hash;\n if (!baseHash) {\n state.lastError = \"Exec approvals hash missing; reload and retry.\";\n return;\n }\n const file =\n state.execApprovalsForm ??\n state.execApprovalsSnapshot?.file ??\n {};\n const rpc = resolveExecApprovalsSaveRpc(target, { file, baseHash });\n if (!rpc) {\n state.lastError = \"Select a node before saving exec approvals.\";\n return;\n }\n await state.client.request(rpc.method, rpc.params);\n state.execApprovalsDirty = false;\n await loadExecApprovals(state, target);\n } catch (err) {\n state.lastError = String(err);\n } finally {\n state.execApprovalsSaving = false;\n }\n}\n\nexport function updateExecApprovalsFormValue(\n state: ExecApprovalsState,\n path: Array,\n value: unknown,\n) {\n const base = cloneConfigObject(\n state.execApprovalsForm ?? state.execApprovalsSnapshot?.file ?? {},\n );\n setPathValue(base, path, value);\n state.execApprovalsForm = base;\n state.execApprovalsDirty = true;\n}\n\nexport function removeExecApprovalsFormValue(\n state: ExecApprovalsState,\n path: Array,\n) {\n const base = cloneConfigObject(\n state.execApprovalsForm ?? state.execApprovalsSnapshot?.file ?? {},\n );\n removePathValue(base, path);\n state.execApprovalsForm = base;\n state.execApprovalsDirty = true;\n}\n","import type { GatewayBrowserClient } from \"../gateway\";\nimport type { PresenceEntry } from \"../types\";\n\nexport type PresenceState = {\n client: GatewayBrowserClient | null;\n connected: boolean;\n presenceLoading: boolean;\n presenceEntries: PresenceEntry[];\n presenceError: string | null;\n presenceStatus: string | null;\n};\n\nexport async function loadPresence(state: PresenceState) {\n if (!state.client || !state.connected) return;\n if (state.presenceLoading) return;\n state.presenceLoading = true;\n state.presenceError = null;\n state.presenceStatus = null;\n try {\n const res = (await state.client.request(\"system-presence\", {})) as\n | PresenceEntry[]\n | undefined;\n if (Array.isArray(res)) {\n state.presenceEntries = res;\n state.presenceStatus = res.length === 0 ? \"No instances yet.\" : null;\n } else {\n state.presenceEntries = [];\n state.presenceStatus = \"No presence payload.\";\n }\n } catch (err) {\n state.presenceError = String(err);\n } finally {\n state.presenceLoading = false;\n }\n}\n\n","import type { GatewayBrowserClient } from \"../gateway\";\nimport type { SkillStatusReport } from \"../types\";\n\nexport type SkillsState = {\n client: GatewayBrowserClient | null;\n connected: boolean;\n skillsLoading: boolean;\n skillsReport: SkillStatusReport | null;\n skillsError: string | null;\n skillsBusyKey: string | null;\n skillEdits: Record;\n skillMessages: SkillMessageMap;\n};\n\nexport type SkillMessage = {\n kind: \"success\" | \"error\";\n message: string;\n};\n\nexport type SkillMessageMap = Record;\n\ntype LoadSkillsOptions = {\n clearMessages?: boolean;\n};\n\nfunction setSkillMessage(state: SkillsState, key: string, message?: SkillMessage) {\n if (!key.trim()) return;\n const next = { ...state.skillMessages };\n if (message) next[key] = message;\n else delete next[key];\n state.skillMessages = next;\n}\n\nfunction getErrorMessage(err: unknown) {\n if (err instanceof Error) return err.message;\n return String(err);\n}\n\nexport async function loadSkills(state: SkillsState, options?: LoadSkillsOptions) {\n if (options?.clearMessages && Object.keys(state.skillMessages).length > 0) {\n state.skillMessages = {};\n }\n if (!state.client || !state.connected) return;\n if (state.skillsLoading) return;\n state.skillsLoading = true;\n state.skillsError = null;\n try {\n const res = (await state.client.request(\"skills.status\", {})) as\n | SkillStatusReport\n | undefined;\n if (res) state.skillsReport = res;\n } catch (err) {\n state.skillsError = getErrorMessage(err);\n } finally {\n state.skillsLoading = false;\n }\n}\n\nexport function updateSkillEdit(\n state: SkillsState,\n skillKey: string,\n value: string,\n) {\n state.skillEdits = { ...state.skillEdits, [skillKey]: value };\n}\n\nexport async function updateSkillEnabled(\n state: SkillsState,\n skillKey: string,\n enabled: boolean,\n) {\n if (!state.client || !state.connected) return;\n state.skillsBusyKey = skillKey;\n state.skillsError = null;\n try {\n await state.client.request(\"skills.update\", { skillKey, enabled });\n await loadSkills(state);\n setSkillMessage(state, skillKey, {\n kind: \"success\",\n message: enabled ? \"Skill enabled\" : \"Skill disabled\",\n });\n } catch (err) {\n const message = getErrorMessage(err);\n state.skillsError = message;\n setSkillMessage(state, skillKey, {\n kind: \"error\",\n message,\n });\n } finally {\n state.skillsBusyKey = null;\n }\n}\n\nexport async function saveSkillApiKey(state: SkillsState, skillKey: string) {\n if (!state.client || !state.connected) return;\n state.skillsBusyKey = skillKey;\n state.skillsError = null;\n try {\n const apiKey = state.skillEdits[skillKey] ?? \"\";\n await state.client.request(\"skills.update\", { skillKey, apiKey });\n await loadSkills(state);\n setSkillMessage(state, skillKey, {\n kind: \"success\",\n message: \"API key saved\",\n });\n } catch (err) {\n const message = getErrorMessage(err);\n state.skillsError = message;\n setSkillMessage(state, skillKey, {\n kind: \"error\",\n message,\n });\n } finally {\n state.skillsBusyKey = null;\n }\n}\n\nexport async function installSkill(\n state: SkillsState,\n skillKey: string,\n name: string,\n installId: string,\n) {\n if (!state.client || !state.connected) return;\n state.skillsBusyKey = skillKey;\n state.skillsError = null;\n try {\n const result = (await state.client.request(\"skills.install\", {\n name,\n installId,\n timeoutMs: 120000,\n })) as { ok?: boolean; message?: string };\n await loadSkills(state);\n setSkillMessage(state, skillKey, {\n kind: \"success\",\n message: result?.message ?? \"Installed\",\n });\n } catch (err) {\n const message = getErrorMessage(err);\n state.skillsError = message;\n setSkillMessage(state, skillKey, {\n kind: \"error\",\n message,\n });\n } finally {\n state.skillsBusyKey = null;\n }\n}\n","export type ThemeMode = \"system\" | \"light\" | \"dark\";\nexport type ResolvedTheme = \"light\" | \"dark\";\n\nexport function getSystemTheme(): ResolvedTheme {\n if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") {\n return \"dark\";\n }\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches\n ? \"dark\"\n : \"light\";\n}\n\nexport function resolveTheme(mode: ThemeMode): ResolvedTheme {\n if (mode === \"system\") return getSystemTheme();\n return mode;\n}\n","import type { ThemeMode } from \"./theme\";\n\nexport type ThemeTransitionContext = {\n element?: HTMLElement | null;\n pointerClientX?: number;\n pointerClientY?: number;\n};\n\nexport type ThemeTransitionOptions = {\n nextTheme: ThemeMode;\n applyTheme: () => void;\n context?: ThemeTransitionContext;\n currentTheme?: ThemeMode | null;\n};\n\ntype DocumentWithViewTransition = Document & {\n startViewTransition?: (callback: () => void) => { finished: Promise };\n};\n\nconst clamp01 = (value: number) => {\n if (Number.isNaN(value)) return 0.5;\n if (value <= 0) return 0;\n if (value >= 1) return 1;\n return value;\n};\n\nconst hasReducedMotionPreference = () => {\n if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") {\n return false;\n }\n return window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches ?? false;\n};\n\nconst cleanupThemeTransition = (root: HTMLElement) => {\n root.classList.remove(\"theme-transition\");\n root.style.removeProperty(\"--theme-switch-x\");\n root.style.removeProperty(\"--theme-switch-y\");\n};\n\nexport const startThemeTransition = ({\n nextTheme,\n applyTheme,\n context,\n currentTheme,\n}: ThemeTransitionOptions) => {\n if (currentTheme === nextTheme) return;\n\n const documentReference = globalThis.document ?? null;\n if (!documentReference) {\n applyTheme();\n return;\n }\n\n const root = documentReference.documentElement;\n const document_ = documentReference as DocumentWithViewTransition;\n const prefersReducedMotion = hasReducedMotionPreference();\n\n const canUseViewTransition =\n Boolean(document_.startViewTransition) && !prefersReducedMotion;\n\n if (canUseViewTransition) {\n let xPercent = 0.5;\n let yPercent = 0.5;\n\n if (\n context?.pointerClientX !== undefined &&\n context?.pointerClientY !== undefined &&\n typeof window !== \"undefined\"\n ) {\n xPercent = clamp01(context.pointerClientX / window.innerWidth);\n yPercent = clamp01(context.pointerClientY / window.innerHeight);\n } else if (context?.element) {\n const rect = context.element.getBoundingClientRect();\n if (\n rect.width > 0 &&\n rect.height > 0 &&\n typeof window !== \"undefined\"\n ) {\n xPercent = clamp01((rect.left + rect.width / 2) / window.innerWidth);\n yPercent = clamp01((rect.top + rect.height / 2) / window.innerHeight);\n }\n }\n\n root.style.setProperty(\"--theme-switch-x\", `${xPercent * 100}%`);\n root.style.setProperty(\"--theme-switch-y\", `${yPercent * 100}%`);\n root.classList.add(\"theme-transition\");\n\n try {\n const transition = document_.startViewTransition?.(() => {\n applyTheme();\n });\n if (transition?.finished) {\n void transition.finished.finally(() => cleanupThemeTransition(root));\n } else {\n cleanupThemeTransition(root);\n }\n } catch {\n cleanupThemeTransition(root);\n applyTheme();\n }\n return;\n }\n\n applyTheme();\n cleanupThemeTransition(root);\n};\n","import { loadLogs } from \"./controllers/logs\";\nimport { loadNodes } from \"./controllers/nodes\";\nimport { loadDebug } from \"./controllers/debug\";\nimport type { ClawdbotApp } from \"./app\";\n\ntype PollingHost = {\n nodesPollInterval: number | null;\n logsPollInterval: number | null;\n debugPollInterval: number | null;\n tab: string;\n};\n\nexport function startNodesPolling(host: PollingHost) {\n if (host.nodesPollInterval != null) return;\n host.nodesPollInterval = window.setInterval(\n () => void loadNodes(host as unknown as ClawdbotApp, { quiet: true }),\n 5000,\n );\n}\n\nexport function stopNodesPolling(host: PollingHost) {\n if (host.nodesPollInterval == null) return;\n clearInterval(host.nodesPollInterval);\n host.nodesPollInterval = null;\n}\n\nexport function startLogsPolling(host: PollingHost) {\n if (host.logsPollInterval != null) return;\n host.logsPollInterval = window.setInterval(() => {\n if (host.tab !== \"logs\") return;\n void loadLogs(host as unknown as ClawdbotApp, { quiet: true });\n }, 2000);\n}\n\nexport function stopLogsPolling(host: PollingHost) {\n if (host.logsPollInterval == null) return;\n clearInterval(host.logsPollInterval);\n host.logsPollInterval = null;\n}\n\nexport function startDebugPolling(host: PollingHost) {\n if (host.debugPollInterval != null) return;\n host.debugPollInterval = window.setInterval(() => {\n if (host.tab !== \"debug\") return;\n void loadDebug(host as unknown as ClawdbotApp);\n }, 3000);\n}\n\nexport function stopDebugPolling(host: PollingHost) {\n if (host.debugPollInterval == null) return;\n clearInterval(host.debugPollInterval);\n host.debugPollInterval = null;\n}\n","import { loadConfig, loadConfigSchema } from \"./controllers/config\";\nimport { loadCronJobs, loadCronStatus } from \"./controllers/cron\";\nimport { loadChannels } from \"./controllers/channels\";\nimport { loadDebug } from \"./controllers/debug\";\nimport { loadLogs } from \"./controllers/logs\";\nimport { loadDevices } from \"./controllers/devices\";\nimport { loadNodes } from \"./controllers/nodes\";\nimport { loadExecApprovals } from \"./controllers/exec-approvals\";\nimport { loadPresence } from \"./controllers/presence\";\nimport { loadSessions } from \"./controllers/sessions\";\nimport { loadSkills } from \"./controllers/skills\";\nimport { inferBasePathFromPathname, normalizeBasePath, normalizePath, pathForTab, tabFromPath, type Tab } from \"./navigation\";\nimport { saveSettings, type UiSettings } from \"./storage\";\nimport { resolveTheme, type ResolvedTheme, type ThemeMode } from \"./theme\";\nimport { startThemeTransition, type ThemeTransitionContext } from \"./theme-transition\";\nimport { scheduleChatScroll, scheduleLogsScroll } from \"./app-scroll\";\nimport { startLogsPolling, stopLogsPolling, startDebugPolling, stopDebugPolling } from \"./app-polling\";\nimport { refreshChat } from \"./app-chat\";\nimport type { ClawdbotApp } from \"./app\";\n\ntype SettingsHost = {\n settings: UiSettings;\n theme: ThemeMode;\n themeResolved: ResolvedTheme;\n applySessionKey: string;\n sessionKey: string;\n tab: Tab;\n connected: boolean;\n chatHasAutoScrolled: boolean;\n logsAtBottom: boolean;\n eventLog: unknown[];\n eventLogBuffer: unknown[];\n basePath: string;\n themeMedia: MediaQueryList | null;\n themeMediaHandler: ((event: MediaQueryListEvent) => void) | null;\n};\n\nexport function applySettings(host: SettingsHost, next: UiSettings) {\n const normalized = {\n ...next,\n lastActiveSessionKey: next.lastActiveSessionKey?.trim() || next.sessionKey.trim() || \"main\",\n };\n host.settings = normalized;\n saveSettings(normalized);\n if (next.theme !== host.theme) {\n host.theme = next.theme;\n applyResolvedTheme(host, resolveTheme(next.theme));\n }\n host.applySessionKey = host.settings.lastActiveSessionKey;\n}\n\nexport function setLastActiveSessionKey(host: SettingsHost, next: string) {\n const trimmed = next.trim();\n if (!trimmed) return;\n if (host.settings.lastActiveSessionKey === trimmed) return;\n applySettings(host, { ...host.settings, lastActiveSessionKey: trimmed });\n}\n\nexport function applySettingsFromUrl(host: SettingsHost) {\n if (!window.location.search) return;\n const params = new URLSearchParams(window.location.search);\n const tokenRaw = params.get(\"token\");\n const passwordRaw = params.get(\"password\");\n const sessionRaw = params.get(\"session\");\n const gatewayUrlRaw = params.get(\"gatewayUrl\");\n let shouldCleanUrl = false;\n\n if (tokenRaw != null) {\n const token = tokenRaw.trim();\n if (token && token !== host.settings.token) {\n applySettings(host, { ...host.settings, token });\n }\n params.delete(\"token\");\n shouldCleanUrl = true;\n }\n\n if (passwordRaw != null) {\n const password = passwordRaw.trim();\n if (password) {\n (host as { password: string }).password = password;\n }\n params.delete(\"password\");\n shouldCleanUrl = true;\n }\n\n if (sessionRaw != null) {\n const session = sessionRaw.trim();\n if (session) {\n host.sessionKey = session;\n applySettings(host, {\n ...host.settings,\n sessionKey: session,\n lastActiveSessionKey: session,\n });\n }\n }\n\n if (gatewayUrlRaw != null) {\n const gatewayUrl = gatewayUrlRaw.trim();\n if (gatewayUrl && gatewayUrl !== host.settings.gatewayUrl) {\n applySettings(host, { ...host.settings, gatewayUrl });\n }\n params.delete(\"gatewayUrl\");\n shouldCleanUrl = true;\n }\n\n if (!shouldCleanUrl) return;\n const url = new URL(window.location.href);\n url.search = params.toString();\n window.history.replaceState({}, \"\", url.toString());\n}\n\nexport function setTab(host: SettingsHost, next: Tab) {\n if (host.tab !== next) host.tab = next;\n if (next === \"chat\") host.chatHasAutoScrolled = false;\n if (next === \"logs\")\n startLogsPolling(host as unknown as Parameters[0]);\n else stopLogsPolling(host as unknown as Parameters[0]);\n if (next === \"debug\")\n startDebugPolling(host as unknown as Parameters[0]);\n else stopDebugPolling(host as unknown as Parameters[0]);\n void refreshActiveTab(host);\n syncUrlWithTab(host, next, false);\n}\n\nexport function setTheme(\n host: SettingsHost,\n next: ThemeMode,\n context?: ThemeTransitionContext,\n) {\n const applyTheme = () => {\n host.theme = next;\n applySettings(host, { ...host.settings, theme: next });\n applyResolvedTheme(host, resolveTheme(next));\n };\n startThemeTransition({\n nextTheme: next,\n applyTheme,\n context,\n currentTheme: host.theme,\n });\n}\n\nexport async function refreshActiveTab(host: SettingsHost) {\n if (host.tab === \"overview\") await loadOverview(host);\n if (host.tab === \"channels\") await loadChannelsTab(host);\n if (host.tab === \"instances\") await loadPresence(host as unknown as ClawdbotApp);\n if (host.tab === \"sessions\") await loadSessions(host as unknown as ClawdbotApp);\n if (host.tab === \"cron\") await loadCron(host);\n if (host.tab === \"skills\") await loadSkills(host as unknown as ClawdbotApp);\n if (host.tab === \"nodes\") {\n await loadNodes(host as unknown as ClawdbotApp);\n await loadDevices(host as unknown as ClawdbotApp);\n await loadConfig(host as unknown as ClawdbotApp);\n await loadExecApprovals(host as unknown as ClawdbotApp);\n }\n if (host.tab === \"chat\") {\n await refreshChat(host as unknown as Parameters[0]);\n scheduleChatScroll(\n host as unknown as Parameters[0],\n !host.chatHasAutoScrolled,\n );\n }\n if (host.tab === \"config\") {\n await loadConfigSchema(host as unknown as ClawdbotApp);\n await loadConfig(host as unknown as ClawdbotApp);\n }\n if (host.tab === \"debug\") {\n await loadDebug(host as unknown as ClawdbotApp);\n host.eventLog = host.eventLogBuffer;\n }\n if (host.tab === \"logs\") {\n host.logsAtBottom = true;\n await loadLogs(host as unknown as ClawdbotApp, { reset: true });\n scheduleLogsScroll(\n host as unknown as Parameters[0],\n true,\n );\n }\n}\n\nexport function inferBasePath() {\n if (typeof window === \"undefined\") return \"\";\n const configured = window.__CLAWDBOT_CONTROL_UI_BASE_PATH__;\n if (typeof configured === \"string\" && configured.trim()) {\n return normalizeBasePath(configured);\n }\n return inferBasePathFromPathname(window.location.pathname);\n}\n\nexport function syncThemeWithSettings(host: SettingsHost) {\n host.theme = host.settings.theme ?? \"system\";\n applyResolvedTheme(host, resolveTheme(host.theme));\n}\n\nexport function applyResolvedTheme(host: SettingsHost, resolved: ResolvedTheme) {\n host.themeResolved = resolved;\n if (typeof document === \"undefined\") return;\n const root = document.documentElement;\n root.dataset.theme = resolved;\n root.style.colorScheme = resolved;\n}\n\nexport function attachThemeListener(host: SettingsHost) {\n if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return;\n host.themeMedia = window.matchMedia(\"(prefers-color-scheme: dark)\");\n host.themeMediaHandler = (event) => {\n if (host.theme !== \"system\") return;\n applyResolvedTheme(host, event.matches ? \"dark\" : \"light\");\n };\n if (typeof host.themeMedia.addEventListener === \"function\") {\n host.themeMedia.addEventListener(\"change\", host.themeMediaHandler);\n return;\n }\n const legacy = host.themeMedia as MediaQueryList & {\n addListener: (cb: (event: MediaQueryListEvent) => void) => void;\n };\n legacy.addListener(host.themeMediaHandler);\n}\n\nexport function detachThemeListener(host: SettingsHost) {\n if (!host.themeMedia || !host.themeMediaHandler) return;\n if (typeof host.themeMedia.removeEventListener === \"function\") {\n host.themeMedia.removeEventListener(\"change\", host.themeMediaHandler);\n return;\n }\n const legacy = host.themeMedia as MediaQueryList & {\n removeListener: (cb: (event: MediaQueryListEvent) => void) => void;\n };\n legacy.removeListener(host.themeMediaHandler);\n host.themeMedia = null;\n host.themeMediaHandler = null;\n}\n\nexport function syncTabWithLocation(host: SettingsHost, replace: boolean) {\n if (typeof window === \"undefined\") return;\n const resolved = tabFromPath(window.location.pathname, host.basePath) ?? \"chat\";\n setTabFromRoute(host, resolved);\n syncUrlWithTab(host, resolved, replace);\n}\n\nexport function onPopState(host: SettingsHost) {\n if (typeof window === \"undefined\") return;\n const resolved = tabFromPath(window.location.pathname, host.basePath);\n if (!resolved) return;\n\n const url = new URL(window.location.href);\n const session = url.searchParams.get(\"session\")?.trim();\n if (session) {\n host.sessionKey = session;\n applySettings(host, {\n ...host.settings,\n sessionKey: session,\n lastActiveSessionKey: session,\n });\n }\n\n setTabFromRoute(host, resolved);\n}\n\nexport function setTabFromRoute(host: SettingsHost, next: Tab) {\n if (host.tab !== next) host.tab = next;\n if (next === \"chat\") host.chatHasAutoScrolled = false;\n if (next === \"logs\")\n startLogsPolling(host as unknown as Parameters[0]);\n else stopLogsPolling(host as unknown as Parameters[0]);\n if (next === \"debug\")\n startDebugPolling(host as unknown as Parameters[0]);\n else stopDebugPolling(host as unknown as Parameters[0]);\n if (host.connected) void refreshActiveTab(host);\n}\n\nexport function syncUrlWithTab(host: SettingsHost, tab: Tab, replace: boolean) {\n if (typeof window === \"undefined\") return;\n const targetPath = normalizePath(pathForTab(tab, host.basePath));\n const currentPath = normalizePath(window.location.pathname);\n const url = new URL(window.location.href);\n\n if (tab === \"chat\" && host.sessionKey) {\n url.searchParams.set(\"session\", host.sessionKey);\n } else {\n url.searchParams.delete(\"session\");\n }\n\n if (currentPath !== targetPath) {\n url.pathname = targetPath;\n }\n\n if (replace) {\n window.history.replaceState({}, \"\", url.toString());\n } else {\n window.history.pushState({}, \"\", url.toString());\n }\n}\n\nexport function syncUrlWithSessionKey(\n host: SettingsHost,\n sessionKey: string,\n replace: boolean,\n) {\n if (typeof window === \"undefined\") return;\n const url = new URL(window.location.href);\n url.searchParams.set(\"session\", sessionKey);\n if (replace) window.history.replaceState({}, \"\", url.toString());\n else window.history.pushState({}, \"\", url.toString());\n}\n\nexport async function loadOverview(host: SettingsHost) {\n await Promise.all([\n loadChannels(host as unknown as ClawdbotApp, false),\n loadPresence(host as unknown as ClawdbotApp),\n loadSessions(host as unknown as ClawdbotApp),\n loadCronStatus(host as unknown as ClawdbotApp),\n loadDebug(host as unknown as ClawdbotApp),\n ]);\n}\n\nexport async function loadChannelsTab(host: SettingsHost) {\n await Promise.all([\n loadChannels(host as unknown as ClawdbotApp, true),\n loadConfigSchema(host as unknown as ClawdbotApp),\n loadConfig(host as unknown as ClawdbotApp),\n ]);\n}\n\nexport async function loadCron(host: SettingsHost) {\n await Promise.all([\n loadChannels(host as unknown as ClawdbotApp, false),\n loadCronStatus(host as unknown as ClawdbotApp),\n loadCronJobs(host as unknown as ClawdbotApp),\n ]);\n}\n","import { abortChatRun, loadChatHistory, sendChatMessage } from \"./controllers/chat\";\nimport { loadSessions } from \"./controllers/sessions\";\nimport { generateUUID } from \"./uuid\";\nimport { resetToolStream } from \"./app-tool-stream\";\nimport { scheduleChatScroll } from \"./app-scroll\";\nimport { setLastActiveSessionKey } from \"./app-settings\";\nimport { normalizeBasePath } from \"./navigation\";\nimport type { GatewayHelloOk } from \"./gateway\";\nimport { parseAgentSessionKey } from \"../../../src/sessions/session-key-utils.js\";\nimport type { ClawdbotApp } from \"./app\";\n\ntype ChatHost = {\n connected: boolean;\n chatMessage: string;\n chatQueue: Array<{ id: string; text: string; createdAt: number }>;\n chatRunId: string | null;\n chatSending: boolean;\n sessionKey: string;\n basePath: string;\n hello: GatewayHelloOk | null;\n chatAvatarUrl: string | null;\n};\n\nexport function isChatBusy(host: ChatHost) {\n return host.chatSending || Boolean(host.chatRunId);\n}\n\nexport function isChatStopCommand(text: string) {\n const trimmed = text.trim();\n if (!trimmed) return false;\n const normalized = trimmed.toLowerCase();\n if (normalized === \"/stop\") return true;\n return (\n normalized === \"stop\" ||\n normalized === \"esc\" ||\n normalized === \"abort\" ||\n normalized === \"wait\" ||\n normalized === \"exit\"\n );\n}\n\nexport async function handleAbortChat(host: ChatHost) {\n if (!host.connected) return;\n host.chatMessage = \"\";\n await abortChatRun(host as unknown as ClawdbotApp);\n}\n\nfunction enqueueChatMessage(host: ChatHost, text: string) {\n const trimmed = text.trim();\n if (!trimmed) return;\n host.chatQueue = [\n ...host.chatQueue,\n {\n id: generateUUID(),\n text: trimmed,\n createdAt: Date.now(),\n },\n ];\n}\n\nasync function sendChatMessageNow(\n host: ChatHost,\n message: string,\n opts?: { previousDraft?: string; restoreDraft?: boolean },\n) {\n resetToolStream(host as unknown as Parameters[0]);\n const ok = await sendChatMessage(host as unknown as ClawdbotApp, message);\n if (!ok && opts?.previousDraft != null) {\n host.chatMessage = opts.previousDraft;\n }\n if (ok) {\n setLastActiveSessionKey(host as unknown as Parameters[0], host.sessionKey);\n }\n if (ok && opts?.restoreDraft && opts.previousDraft?.trim()) {\n host.chatMessage = opts.previousDraft;\n }\n scheduleChatScroll(host as unknown as Parameters[0]);\n if (ok && !host.chatRunId) {\n void flushChatQueue(host);\n }\n return ok;\n}\n\nasync function flushChatQueue(host: ChatHost) {\n if (!host.connected || isChatBusy(host)) return;\n const [next, ...rest] = host.chatQueue;\n if (!next) return;\n host.chatQueue = rest;\n const ok = await sendChatMessageNow(host, next.text);\n if (!ok) {\n host.chatQueue = [next, ...host.chatQueue];\n }\n}\n\nexport function removeQueuedMessage(host: ChatHost, id: string) {\n host.chatQueue = host.chatQueue.filter((item) => item.id !== id);\n}\n\nexport async function handleSendChat(\n host: ChatHost,\n messageOverride?: string,\n opts?: { restoreDraft?: boolean },\n) {\n if (!host.connected) return;\n const previousDraft = host.chatMessage;\n const message = (messageOverride ?? host.chatMessage).trim();\n if (!message) return;\n\n if (isChatStopCommand(message)) {\n await handleAbortChat(host);\n return;\n }\n\n if (messageOverride == null) {\n host.chatMessage = \"\";\n }\n\n if (isChatBusy(host)) {\n enqueueChatMessage(host, message);\n return;\n }\n\n await sendChatMessageNow(host, message, {\n previousDraft: messageOverride == null ? previousDraft : undefined,\n restoreDraft: Boolean(messageOverride && opts?.restoreDraft),\n });\n}\n\nexport async function refreshChat(host: ChatHost) {\n await Promise.all([\n loadChatHistory(host as unknown as ClawdbotApp),\n loadSessions(host as unknown as ClawdbotApp),\n refreshChatAvatar(host),\n ]);\n scheduleChatScroll(host as unknown as Parameters[0], true);\n}\n\nexport const flushChatQueueForEvent = flushChatQueue;\n\ntype SessionDefaultsSnapshot = {\n defaultAgentId?: string;\n};\n\nfunction resolveAgentIdForSession(host: ChatHost): string | null {\n const parsed = parseAgentSessionKey(host.sessionKey);\n if (parsed?.agentId) return parsed.agentId;\n const snapshot = host.hello?.snapshot as { sessionDefaults?: SessionDefaultsSnapshot } | undefined;\n const fallback = snapshot?.sessionDefaults?.defaultAgentId?.trim();\n return fallback || \"main\";\n}\n\nfunction buildAvatarMetaUrl(basePath: string, agentId: string): string {\n const base = normalizeBasePath(basePath);\n const encoded = encodeURIComponent(agentId);\n return base ? `${base}/avatar/${encoded}?meta=1` : `/avatar/${encoded}?meta=1`;\n}\n\nexport async function refreshChatAvatar(host: ChatHost) {\n if (!host.connected) {\n host.chatAvatarUrl = null;\n return;\n }\n const agentId = resolveAgentIdForSession(host);\n if (!agentId) {\n host.chatAvatarUrl = null;\n return;\n }\n host.chatAvatarUrl = null;\n const url = buildAvatarMetaUrl(host.basePath, agentId);\n try {\n const res = await fetch(url, { method: \"GET\" });\n if (!res.ok) {\n host.chatAvatarUrl = null;\n return;\n }\n const data = (await res.json()) as { avatarUrl?: unknown };\n const avatarUrl = typeof data.avatarUrl === \"string\" ? data.avatarUrl.trim() : \"\";\n host.chatAvatarUrl = avatarUrl || null;\n } catch {\n host.chatAvatarUrl = null;\n }\n}\n","/**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */\nconst t={ATTRIBUTE:1,CHILD:2,PROPERTY:3,BOOLEAN_ATTRIBUTE:4,EVENT:5,ELEMENT:6},e=t=>(...e)=>({_$litDirective$:t,values:e});class i{constructor(t){}get _$AU(){return this._$AM._$AU}_$AT(t,e,i){this._$Ct=t,this._$AM=e,this._$Ci=i}_$AS(t,e){return this.update(t,e)}update(t,e){return this.render(...e)}}export{i as Directive,t as PartType,e as directive};\n//# sourceMappingURL=directive.js.map\n","import{_$LH as o}from\"./lit-html.js\";\n/**\n * @license\n * Copyright 2020 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */const{I:t}=o,i=o=>o,n=o=>null===o||\"object\"!=typeof o&&\"function\"!=typeof o,e={HTML:1,SVG:2,MATHML:3},l=(o,t)=>void 0===t?void 0!==o?._$litType$:o?._$litType$===t,d=o=>null!=o?._$litType$?.h,c=o=>void 0!==o?._$litDirective$,f=o=>o?._$litDirective$,r=o=>void 0===o.strings,s=()=>document.createComment(\"\"),v=(o,n,e)=>{const l=o._$AA.parentNode,d=void 0===n?o._$AB:n._$AA;if(void 0===e){const i=l.insertBefore(s(),d),n=l.insertBefore(s(),d);e=new t(i,n,o,o.options)}else{const t=e._$AB.nextSibling,n=e._$AM,c=n!==o;if(c){let t;e._$AQ?.(o),e._$AM=o,void 0!==e._$AP&&(t=o._$AU)!==n._$AU&&e._$AP(t)}if(t!==d||c){let o=e._$AA;for(;o!==t;){const t=i(o).nextSibling;i(l).insertBefore(o,d),o=t}}}return e},u=(o,t,i=o)=>(o._$AI(t,i),o),m={},p=(o,t=m)=>o._$AH=t,M=o=>o._$AH,h=o=>{o._$AR(),o._$AA.remove()},j=o=>{o._$AR()};export{e as TemplateResultType,j as clearPart,M as getCommittedValue,f as getDirectiveClass,v as insertPart,d as isCompiledTemplateResult,c as isDirectiveResult,n as isPrimitive,r as isSingleExpression,l as isTemplateResult,h as removePart,u as setChildPartValue,p as setCommittedValue};\n//# sourceMappingURL=directive-helpers.js.map\n","import{noChange as e}from\"../lit-html.js\";import{directive as s,Directive as t,PartType as r}from\"../directive.js\";import{getCommittedValue as l,setChildPartValue as o,insertPart as i,removePart as n,setCommittedValue as f}from\"../directive-helpers.js\";\n/**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */\nconst u=(e,s,t)=>{const r=new Map;for(let l=s;l<=t;l++)r.set(e[l],l);return r},c=s(class extends t{constructor(e){if(super(e),e.type!==r.CHILD)throw Error(\"repeat() can only be used in text expressions\")}dt(e,s,t){let r;void 0===t?t=s:void 0!==s&&(r=s);const l=[],o=[];let i=0;for(const s of e)l[i]=r?r(s,i):i,o[i]=t(s,i),i++;return{values:o,keys:l}}render(e,s,t){return this.dt(e,s,t).values}update(s,[t,r,c]){const d=l(s),{values:p,keys:a}=this.dt(t,r,c);if(!Array.isArray(d))return this.ut=a,p;const h=this.ut??=[],v=[];let m,y,x=0,j=d.length-1,k=0,w=p.length-1;for(;x<=j&&k<=w;)if(null===d[x])x++;else if(null===d[j])j--;else if(h[x]===a[k])v[k]=o(d[x],p[k]),x++,k++;else if(h[j]===a[w])v[w]=o(d[j],p[w]),j--,w--;else if(h[x]===a[w])v[w]=o(d[x],p[w]),i(s,v[w+1],d[x]),x++,w--;else if(h[j]===a[k])v[k]=o(d[j],p[k]),i(s,d[x],d[j]),j--,k++;else if(void 0===m&&(m=u(a,k,w),y=u(h,x,j)),m.has(h[x]))if(m.has(h[j])){const e=y.get(a[k]),t=void 0!==e?d[e]:null;if(null===t){const e=i(s,d[x]);o(e,p[k]),v[k]=e}else v[k]=o(t,p[k]),i(s,d[x],t),d[e]=null;k++}else n(d[j]),j--;else n(d[x]),x++;for(;k<=w;){const e=i(s,v[w+1]);o(e,p[k]),v[k++]=e}for(;x<=j;){const e=d[x++];null!==e&&n(e)}return this.ut=a,f(s,v),e}});export{c as repeat};\n//# sourceMappingURL=repeat.js.map\n","/**\n * Message normalization utilities for chat rendering.\n */\n\nimport type {\n NormalizedMessage,\n MessageContentItem,\n} from \"../types/chat-types\";\n\n/**\n * Normalize a raw message object into a consistent structure.\n */\nexport function normalizeMessage(message: unknown): NormalizedMessage {\n const m = message as Record;\n let role = typeof m.role === \"string\" ? m.role : \"unknown\";\n\n // Detect tool messages by common gateway shapes.\n // Some tool events come through as assistant role with tool_* items in the content array.\n const hasToolId =\n typeof m.toolCallId === \"string\" || typeof m.tool_call_id === \"string\";\n\n const contentRaw = m.content;\n const contentItems = Array.isArray(contentRaw) ? contentRaw : null;\n const hasToolContent =\n Array.isArray(contentItems) &&\n contentItems.some((item) => {\n const x = item as Record;\n const t = String(x.type ?? \"\").toLowerCase();\n return t === \"toolresult\" || t === \"tool_result\";\n });\n\n const hasToolName =\n typeof (m as Record).toolName === \"string\" ||\n typeof (m as Record).tool_name === \"string\";\n\n if (hasToolId || hasToolContent || hasToolName) {\n role = \"toolResult\";\n }\n\n // Extract content\n let content: MessageContentItem[] = [];\n\n if (typeof m.content === \"string\") {\n content = [{ type: \"text\", text: m.content }];\n } else if (Array.isArray(m.content)) {\n content = m.content.map((item: Record) => ({\n type: (item.type as MessageContentItem[\"type\"]) || \"text\",\n text: item.text as string | undefined,\n name: item.name as string | undefined,\n args: item.args || item.arguments,\n }));\n } else if (typeof m.text === \"string\") {\n content = [{ type: \"text\", text: m.text }];\n }\n\n const timestamp = typeof m.timestamp === \"number\" ? m.timestamp : Date.now();\n const id = typeof m.id === \"string\" ? m.id : undefined;\n\n return { role, content, timestamp, id };\n}\n\n/**\n * Normalize role for grouping purposes.\n */\nexport function normalizeRoleForGrouping(role: string): string {\n const lower = role.toLowerCase();\n // Preserve original casing when it's already a core role.\n if (role === \"user\" || role === \"User\") return role;\n if (role === \"assistant\") return \"assistant\";\n if (role === \"system\") return \"system\";\n // Keep tool-related roles distinct so the UI can style/toggle them.\n if (\n lower === \"toolresult\" ||\n lower === \"tool_result\" ||\n lower === \"tool\" ||\n lower === \"function\"\n ) {\n return \"tool\";\n }\n return role;\n}\n\n/**\n * Check if a message is a tool result message based on its role.\n */\nexport function isToolResultMessage(message: unknown): boolean {\n const m = message as Record;\n const role = typeof m.role === \"string\" ? m.role.toLowerCase() : \"\";\n return role === \"toolresult\" || role === \"tool_result\";\n}\n","import{nothing as t,noChange as i}from\"../lit-html.js\";import{directive as r,Directive as s,PartType as n}from\"../directive.js\";\n/**\n * @license\n * Copyright 2017 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */class e extends s{constructor(i){if(super(i),this.it=t,i.type!==n.CHILD)throw Error(this.constructor.directiveName+\"() can only be used in child bindings\")}render(r){if(r===t||null==r)return this._t=void 0,this.it=r;if(r===i)return r;if(\"string\"!=typeof r)throw Error(this.constructor.directiveName+\"() called with a non-string value\");if(r===this.it)return this._t;this.it=r;const s=[r];return s.raw=s,this._t={_$litType$:this.constructor.resultType,strings:s,values:[]}}}e.directiveName=\"unsafeHTML\",e.resultType=1;const o=r(e);export{e as UnsafeHTMLDirective,o as unsafeHTML};\n//# sourceMappingURL=unsafe-html.js.map\n","/*! @license DOMPurify 3.3.1 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.3.1/LICENSE */\n\nconst {\n entries,\n setPrototypeOf,\n isFrozen,\n getPrototypeOf,\n getOwnPropertyDescriptor\n} = Object;\nlet {\n freeze,\n seal,\n create\n} = Object; // eslint-disable-line import/no-mutable-exports\nlet {\n apply,\n construct\n} = typeof Reflect !== 'undefined' && Reflect;\nif (!freeze) {\n freeze = function freeze(x) {\n return x;\n };\n}\nif (!seal) {\n seal = function seal(x) {\n return x;\n };\n}\nif (!apply) {\n apply = function apply(func, thisArg) {\n for (var _len = arguments.length, args = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {\n args[_key - 2] = arguments[_key];\n }\n return func.apply(thisArg, args);\n };\n}\nif (!construct) {\n construct = function construct(Func) {\n for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {\n args[_key2 - 1] = arguments[_key2];\n }\n return new Func(...args);\n };\n}\nconst arrayForEach = unapply(Array.prototype.forEach);\nconst arrayLastIndexOf = unapply(Array.prototype.lastIndexOf);\nconst arrayPop = unapply(Array.prototype.pop);\nconst arrayPush = unapply(Array.prototype.push);\nconst arraySplice = unapply(Array.prototype.splice);\nconst stringToLowerCase = unapply(String.prototype.toLowerCase);\nconst stringToString = unapply(String.prototype.toString);\nconst stringMatch = unapply(String.prototype.match);\nconst stringReplace = unapply(String.prototype.replace);\nconst stringIndexOf = unapply(String.prototype.indexOf);\nconst stringTrim = unapply(String.prototype.trim);\nconst objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty);\nconst regExpTest = unapply(RegExp.prototype.test);\nconst typeErrorCreate = unconstruct(TypeError);\n/**\n * Creates a new function that calls the given function with a specified thisArg and arguments.\n *\n * @param func - The function to be wrapped and called.\n * @returns A new function that calls the given function with a specified thisArg and arguments.\n */\nfunction unapply(func) {\n return function (thisArg) {\n if (thisArg instanceof RegExp) {\n thisArg.lastIndex = 0;\n }\n for (var _len3 = arguments.length, args = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {\n args[_key3 - 1] = arguments[_key3];\n }\n return apply(func, thisArg, args);\n };\n}\n/**\n * Creates a new function that constructs an instance of the given constructor function with the provided arguments.\n *\n * @param func - The constructor function to be wrapped and called.\n * @returns A new function that constructs an instance of the given constructor function with the provided arguments.\n */\nfunction unconstruct(Func) {\n return function () {\n for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {\n args[_key4] = arguments[_key4];\n }\n return construct(Func, args);\n };\n}\n/**\n * Add properties to a lookup table\n *\n * @param set - The set to which elements will be added.\n * @param array - The array containing elements to be added to the set.\n * @param transformCaseFunc - An optional function to transform the case of each element before adding to the set.\n * @returns The modified set with added elements.\n */\nfunction addToSet(set, array) {\n let transformCaseFunc = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : stringToLowerCase;\n if (setPrototypeOf) {\n // Make 'in' and truthy checks like Boolean(set.constructor)\n // independent of any properties defined on Object.prototype.\n // Prevent prototype setters from intercepting set as a this value.\n setPrototypeOf(set, null);\n }\n let l = array.length;\n while (l--) {\n let element = array[l];\n if (typeof element === 'string') {\n const lcElement = transformCaseFunc(element);\n if (lcElement !== element) {\n // Config presets (e.g. tags.js, attrs.js) are immutable.\n if (!isFrozen(array)) {\n array[l] = lcElement;\n }\n element = lcElement;\n }\n }\n set[element] = true;\n }\n return set;\n}\n/**\n * Clean up an array to harden against CSPP\n *\n * @param array - The array to be cleaned.\n * @returns The cleaned version of the array\n */\nfunction cleanArray(array) {\n for (let index = 0; index < array.length; index++) {\n const isPropertyExist = objectHasOwnProperty(array, index);\n if (!isPropertyExist) {\n array[index] = null;\n }\n }\n return array;\n}\n/**\n * Shallow clone an object\n *\n * @param object - The object to be cloned.\n * @returns A new object that copies the original.\n */\nfunction clone(object) {\n const newObject = create(null);\n for (const [property, value] of entries(object)) {\n const isPropertyExist = objectHasOwnProperty(object, property);\n if (isPropertyExist) {\n if (Array.isArray(value)) {\n newObject[property] = cleanArray(value);\n } else if (value && typeof value === 'object' && value.constructor === Object) {\n newObject[property] = clone(value);\n } else {\n newObject[property] = value;\n }\n }\n }\n return newObject;\n}\n/**\n * This method automatically checks if the prop is function or getter and behaves accordingly.\n *\n * @param object - The object to look up the getter function in its prototype chain.\n * @param prop - The property name for which to find the getter function.\n * @returns The getter function found in the prototype chain or a fallback function.\n */\nfunction lookupGetter(object, prop) {\n while (object !== null) {\n const desc = getOwnPropertyDescriptor(object, prop);\n if (desc) {\n if (desc.get) {\n return unapply(desc.get);\n }\n if (typeof desc.value === 'function') {\n return unapply(desc.value);\n }\n }\n object = getPrototypeOf(object);\n }\n function fallbackValue() {\n return null;\n }\n return fallbackValue;\n}\n\nconst html$1 = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'search', 'section', 'select', 'shadow', 'slot', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']);\nconst svg$1 = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'enterkeyhint', 'exportparts', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'inputmode', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'part', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'view', 'vkern']);\nconst svgFilters = freeze(['feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence']);\n// List of SVG elements that are disallowed by default.\n// We still need to know them so that we can do namespace\n// checks properly in case one wants to add them to\n// allow-list.\nconst svgDisallowed = freeze(['animate', 'color-profile', 'cursor', 'discard', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'foreignobject', 'hatch', 'hatchpath', 'mesh', 'meshgradient', 'meshpatch', 'meshrow', 'missing-glyph', 'script', 'set', 'solidcolor', 'unknown', 'use']);\nconst mathMl$1 = freeze(['math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mglyph', 'mi', 'mlabeledtr', 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', 'mroot', 'mrow', 'ms', 'mspace', 'msqrt', 'mstyle', 'msub', 'msup', 'msubsup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', 'munderover', 'mprescripts']);\n// Similarly to SVG, we want to know all MathML elements,\n// even those that we disallow by default.\nconst mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']);\nconst text = freeze(['#text']);\n\nconst html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'exportparts', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inert', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'part', 'pattern', 'placeholder', 'playsinline', 'popover', 'popovertarget', 'popovertargetaction', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'slot', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wrap', 'xmlns', 'slot']);\nconst svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'amplitude', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'exponent', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'mask-type', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'slope', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'tablevalues', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']);\nconst mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnsalign', 'columnlines', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lspace', 'lquote', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']);\nconst xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);\n\n// eslint-disable-next-line unicorn/better-regex\nconst MUSTACHE_EXPR = seal(/\\{\\{[\\w\\W]*|[\\w\\W]*\\}\\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode\nconst ERB_EXPR = seal(/<%[\\w\\W]*|[\\w\\W]*%>/gm);\nconst TMPLIT_EXPR = seal(/\\$\\{[\\w\\W]*/gm); // eslint-disable-line unicorn/better-regex\nconst DATA_ATTR = seal(/^data-[\\-\\w.\\u00B7-\\uFFFF]+$/); // eslint-disable-line no-useless-escape\nconst ARIA_ATTR = seal(/^aria-[\\-\\w]+$/); // eslint-disable-line no-useless-escape\nconst IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\\-]+(?:[^a-z+.\\-:]|$))/i // eslint-disable-line no-useless-escape\n);\nconst IS_SCRIPT_OR_DATA = seal(/^(?:\\w+script|data):/i);\nconst ATTR_WHITESPACE = seal(/[\\u0000-\\u0020\\u00A0\\u1680\\u180E\\u2000-\\u2029\\u205F\\u3000]/g // eslint-disable-line no-control-regex\n);\nconst DOCTYPE_NAME = seal(/^html$/i);\nconst CUSTOM_ELEMENT = seal(/^[a-z][.\\w]*(-[.\\w]+)+$/i);\n\nvar EXPRESSIONS = /*#__PURE__*/Object.freeze({\n __proto__: null,\n ARIA_ATTR: ARIA_ATTR,\n ATTR_WHITESPACE: ATTR_WHITESPACE,\n CUSTOM_ELEMENT: CUSTOM_ELEMENT,\n DATA_ATTR: DATA_ATTR,\n DOCTYPE_NAME: DOCTYPE_NAME,\n ERB_EXPR: ERB_EXPR,\n IS_ALLOWED_URI: IS_ALLOWED_URI,\n IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA,\n MUSTACHE_EXPR: MUSTACHE_EXPR,\n TMPLIT_EXPR: TMPLIT_EXPR\n});\n\n/* eslint-disable @typescript-eslint/indent */\n// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType\nconst NODE_TYPE = {\n element: 1,\n attribute: 2,\n text: 3,\n cdataSection: 4,\n entityReference: 5,\n // Deprecated\n entityNode: 6,\n // Deprecated\n progressingInstruction: 7,\n comment: 8,\n document: 9,\n documentType: 10,\n documentFragment: 11,\n notation: 12 // Deprecated\n};\nconst getGlobal = function getGlobal() {\n return typeof window === 'undefined' ? null : window;\n};\n/**\n * Creates a no-op policy for internal use only.\n * Don't export this function outside this module!\n * @param trustedTypes The policy factory.\n * @param purifyHostElement The Script element used to load DOMPurify (to determine policy name suffix).\n * @return The policy created (or null, if Trusted Types\n * are not supported or creating the policy failed).\n */\nconst _createTrustedTypesPolicy = function _createTrustedTypesPolicy(trustedTypes, purifyHostElement) {\n if (typeof trustedTypes !== 'object' || typeof trustedTypes.createPolicy !== 'function') {\n return null;\n }\n // Allow the callers to control the unique policy name\n // by adding a data-tt-policy-suffix to the script element with the DOMPurify.\n // Policy creation with duplicate names throws in Trusted Types.\n let suffix = null;\n const ATTR_NAME = 'data-tt-policy-suffix';\n if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) {\n suffix = purifyHostElement.getAttribute(ATTR_NAME);\n }\n const policyName = 'dompurify' + (suffix ? '#' + suffix : '');\n try {\n return trustedTypes.createPolicy(policyName, {\n createHTML(html) {\n return html;\n },\n createScriptURL(scriptUrl) {\n return scriptUrl;\n }\n });\n } catch (_) {\n // Policy creation failed (most likely another DOMPurify script has\n // already run). Skip creating the policy, as this will only cause errors\n // if TT are enforced.\n console.warn('TrustedTypes policy ' + policyName + ' could not be created.');\n return null;\n }\n};\nconst _createHooksMap = function _createHooksMap() {\n return {\n afterSanitizeAttributes: [],\n afterSanitizeElements: [],\n afterSanitizeShadowDOM: [],\n beforeSanitizeAttributes: [],\n beforeSanitizeElements: [],\n beforeSanitizeShadowDOM: [],\n uponSanitizeAttribute: [],\n uponSanitizeElement: [],\n uponSanitizeShadowNode: []\n };\n};\nfunction createDOMPurify() {\n let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();\n const DOMPurify = root => createDOMPurify(root);\n DOMPurify.version = '3.3.1';\n DOMPurify.removed = [];\n if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {\n // Not running in a browser, provide a factory function\n // so that you can pass your own Window\n DOMPurify.isSupported = false;\n return DOMPurify;\n }\n let {\n document\n } = window;\n const originalDocument = document;\n const currentScript = originalDocument.currentScript;\n const {\n DocumentFragment,\n HTMLTemplateElement,\n Node,\n Element,\n NodeFilter,\n NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap,\n HTMLFormElement,\n DOMParser,\n trustedTypes\n } = window;\n const ElementPrototype = Element.prototype;\n const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');\n const remove = lookupGetter(ElementPrototype, 'remove');\n const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');\n const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');\n const getParentNode = lookupGetter(ElementPrototype, 'parentNode');\n // As per issue #47, the web-components registry is inherited by a\n // new document created via createHTMLDocument. As per the spec\n // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)\n // a new empty registry is used when creating a template contents owner\n // document, so we use that as our parent document to ensure nothing\n // is inherited.\n if (typeof HTMLTemplateElement === 'function') {\n const template = document.createElement('template');\n if (template.content && template.content.ownerDocument) {\n document = template.content.ownerDocument;\n }\n }\n let trustedTypesPolicy;\n let emptyHTML = '';\n const {\n implementation,\n createNodeIterator,\n createDocumentFragment,\n getElementsByTagName\n } = document;\n const {\n importNode\n } = originalDocument;\n let hooks = _createHooksMap();\n /**\n * Expose whether this browser supports running the full DOMPurify.\n */\n DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined;\n const {\n MUSTACHE_EXPR,\n ERB_EXPR,\n TMPLIT_EXPR,\n DATA_ATTR,\n ARIA_ATTR,\n IS_SCRIPT_OR_DATA,\n ATTR_WHITESPACE,\n CUSTOM_ELEMENT\n } = EXPRESSIONS;\n let {\n IS_ALLOWED_URI: IS_ALLOWED_URI$1\n } = EXPRESSIONS;\n /**\n * We consider the elements and attributes below to be safe. Ideally\n * don't add any new ones but feel free to remove unwanted ones.\n */\n /* allowed element names */\n let ALLOWED_TAGS = null;\n const DEFAULT_ALLOWED_TAGS = addToSet({}, [...html$1, ...svg$1, ...svgFilters, ...mathMl$1, ...text]);\n /* Allowed attribute names */\n let ALLOWED_ATTR = null;\n const DEFAULT_ALLOWED_ATTR = addToSet({}, [...html, ...svg, ...mathMl, ...xml]);\n /*\n * Configure how DOMPurify should handle custom elements and their attributes as well as customized built-in elements.\n * @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements)\n * @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list)\n * @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`.\n */\n let CUSTOM_ELEMENT_HANDLING = Object.seal(create(null, {\n tagNameCheck: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: null\n },\n attributeNameCheck: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: null\n },\n allowCustomizedBuiltInElements: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: false\n }\n }));\n /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */\n let FORBID_TAGS = null;\n /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */\n let FORBID_ATTR = null;\n /* Config object to store ADD_TAGS/ADD_ATTR functions (when used as functions) */\n const EXTRA_ELEMENT_HANDLING = Object.seal(create(null, {\n tagCheck: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: null\n },\n attributeCheck: {\n writable: true,\n configurable: false,\n enumerable: true,\n value: null\n }\n }));\n /* Decide if ARIA attributes are okay */\n let ALLOW_ARIA_ATTR = true;\n /* Decide if custom data attributes are okay */\n let ALLOW_DATA_ATTR = true;\n /* Decide if unknown protocols are okay */\n let ALLOW_UNKNOWN_PROTOCOLS = false;\n /* Decide if self-closing tags in attributes are allowed.\n * Usually removed due to a mXSS issue in jQuery 3.0 */\n let ALLOW_SELF_CLOSE_IN_ATTR = true;\n /* Output should be safe for common template engines.\n * This means, DOMPurify removes data attributes, mustaches and ERB\n */\n let SAFE_FOR_TEMPLATES = false;\n /* Output should be safe even for XML used within HTML and alike.\n * This means, DOMPurify removes comments when containing risky content.\n */\n let SAFE_FOR_XML = true;\n /* Decide if document with ... should be returned */\n let WHOLE_DOCUMENT = false;\n /* Track whether config is already set on this instance of DOMPurify. */\n let SET_CONFIG = false;\n /* Decide if all elements (e.g. style, script) must be children of\n * document.body. By default, browsers might move them to document.head */\n let FORCE_BODY = false;\n /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html\n * string (or a TrustedHTML object if Trusted Types are supported).\n * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead\n */\n let RETURN_DOM = false;\n /* Decide if a DOM `DocumentFragment` should be returned, instead of a html\n * string (or a TrustedHTML object if Trusted Types are supported) */\n let RETURN_DOM_FRAGMENT = false;\n /* Try to return a Trusted Type object instead of a string, return a string in\n * case Trusted Types are not supported */\n let RETURN_TRUSTED_TYPE = false;\n /* Output should be free from DOM clobbering attacks?\n * This sanitizes markups named with colliding, clobberable built-in DOM APIs.\n */\n let SANITIZE_DOM = true;\n /* Achieve full DOM Clobbering protection by isolating the namespace of named\n * properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules.\n *\n * HTML/DOM spec rules that enable DOM Clobbering:\n * - Named Access on Window (§7.3.3)\n * - DOM Tree Accessors (§3.1.5)\n * - Form Element Parent-Child Relations (§4.10.3)\n * - Iframe srcdoc / Nested WindowProxies (§4.8.5)\n * - HTMLCollection (§4.2.10.2)\n *\n * Namespace isolation is implemented by prefixing `id` and `name` attributes\n * with a constant string, i.e., `user-content-`\n */\n let SANITIZE_NAMED_PROPS = false;\n const SANITIZE_NAMED_PROPS_PREFIX = 'user-content-';\n /* Keep element content when removing element? */\n let KEEP_CONTENT = true;\n /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead\n * of importing it into a new Document and returning a sanitized copy */\n let IN_PLACE = false;\n /* Allow usage of profiles like html, svg and mathMl */\n let USE_PROFILES = {};\n /* Tags to ignore content of when KEEP_CONTENT is true */\n let FORBID_CONTENTS = null;\n const DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']);\n /* Tags that are safe for data: URIs */\n let DATA_URI_TAGS = null;\n const DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']);\n /* Attributes safe for values like \"javascript:\" */\n let URI_SAFE_ATTRIBUTES = null;\n const DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'role', 'summary', 'title', 'value', 'style', 'xmlns']);\n const MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';\n const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';\n const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';\n /* Document namespace */\n let NAMESPACE = HTML_NAMESPACE;\n let IS_EMPTY_INPUT = false;\n /* Allowed XHTML+XML namespaces */\n let ALLOWED_NAMESPACES = null;\n const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString);\n let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']);\n let HTML_INTEGRATION_POINTS = addToSet({}, ['annotation-xml']);\n // Certain elements are allowed in both SVG and HTML\n // namespace. We need to specify them explicitly\n // so that they don't get erroneously deleted from\n // HTML namespace.\n const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, ['title', 'style', 'font', 'a', 'script']);\n /* Parsing of strict XHTML documents */\n let PARSER_MEDIA_TYPE = null;\n const SUPPORTED_PARSER_MEDIA_TYPES = ['application/xhtml+xml', 'text/html'];\n const DEFAULT_PARSER_MEDIA_TYPE = 'text/html';\n let transformCaseFunc = null;\n /* Keep a reference to config to pass to hooks */\n let CONFIG = null;\n /* Ideally, do not touch anything below this line */\n /* ______________________________________________ */\n const formElement = document.createElement('form');\n const isRegexOrFunction = function isRegexOrFunction(testValue) {\n return testValue instanceof RegExp || testValue instanceof Function;\n };\n /**\n * _parseConfig\n *\n * @param cfg optional config literal\n */\n // eslint-disable-next-line complexity\n const _parseConfig = function _parseConfig() {\n let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n if (CONFIG && CONFIG === cfg) {\n return;\n }\n /* Shield configuration object from tampering */\n if (!cfg || typeof cfg !== 'object') {\n cfg = {};\n }\n /* Shield configuration object from prototype pollution */\n cfg = clone(cfg);\n PARSER_MEDIA_TYPE =\n // eslint-disable-next-line unicorn/prefer-includes\n SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1 ? DEFAULT_PARSER_MEDIA_TYPE : cfg.PARSER_MEDIA_TYPE;\n // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is.\n transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase;\n /* Set configuration parameters */\n ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS') ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS;\n ALLOWED_ATTR = objectHasOwnProperty(cfg, 'ALLOWED_ATTR') ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR;\n ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES') ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES;\n URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR, transformCaseFunc) : DEFAULT_URI_SAFE_ATTRIBUTES;\n DATA_URI_TAGS = objectHasOwnProperty(cfg, 'ADD_DATA_URI_TAGS') ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS, transformCaseFunc) : DEFAULT_DATA_URI_TAGS;\n FORBID_CONTENTS = objectHasOwnProperty(cfg, 'FORBID_CONTENTS') ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS;\n FORBID_TAGS = objectHasOwnProperty(cfg, 'FORBID_TAGS') ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : clone({});\n FORBID_ATTR = objectHasOwnProperty(cfg, 'FORBID_ATTR') ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : clone({});\n USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES : false;\n ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true\n ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true\n ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false\n ALLOW_SELF_CLOSE_IN_ATTR = cfg.ALLOW_SELF_CLOSE_IN_ATTR !== false; // Default true\n SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false\n SAFE_FOR_XML = cfg.SAFE_FOR_XML !== false; // Default true\n WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false\n RETURN_DOM = cfg.RETURN_DOM || false; // Default false\n RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false\n RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false\n FORCE_BODY = cfg.FORCE_BODY || false; // Default false\n SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true\n SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false\n KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true\n IN_PLACE = cfg.IN_PLACE || false; // Default false\n IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI;\n NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE;\n MATHML_TEXT_INTEGRATION_POINTS = cfg.MATHML_TEXT_INTEGRATION_POINTS || MATHML_TEXT_INTEGRATION_POINTS;\n HTML_INTEGRATION_POINTS = cfg.HTML_INTEGRATION_POINTS || HTML_INTEGRATION_POINTS;\n CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {};\n if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) {\n CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck;\n }\n if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) {\n CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck;\n }\n if (cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === 'boolean') {\n CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements;\n }\n if (SAFE_FOR_TEMPLATES) {\n ALLOW_DATA_ATTR = false;\n }\n if (RETURN_DOM_FRAGMENT) {\n RETURN_DOM = true;\n }\n /* Parse profile info */\n if (USE_PROFILES) {\n ALLOWED_TAGS = addToSet({}, text);\n ALLOWED_ATTR = [];\n if (USE_PROFILES.html === true) {\n addToSet(ALLOWED_TAGS, html$1);\n addToSet(ALLOWED_ATTR, html);\n }\n if (USE_PROFILES.svg === true) {\n addToSet(ALLOWED_TAGS, svg$1);\n addToSet(ALLOWED_ATTR, svg);\n addToSet(ALLOWED_ATTR, xml);\n }\n if (USE_PROFILES.svgFilters === true) {\n addToSet(ALLOWED_TAGS, svgFilters);\n addToSet(ALLOWED_ATTR, svg);\n addToSet(ALLOWED_ATTR, xml);\n }\n if (USE_PROFILES.mathMl === true) {\n addToSet(ALLOWED_TAGS, mathMl$1);\n addToSet(ALLOWED_ATTR, mathMl);\n addToSet(ALLOWED_ATTR, xml);\n }\n }\n /* Merge configuration parameters */\n if (cfg.ADD_TAGS) {\n if (typeof cfg.ADD_TAGS === 'function') {\n EXTRA_ELEMENT_HANDLING.tagCheck = cfg.ADD_TAGS;\n } else {\n if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {\n ALLOWED_TAGS = clone(ALLOWED_TAGS);\n }\n addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);\n }\n }\n if (cfg.ADD_ATTR) {\n if (typeof cfg.ADD_ATTR === 'function') {\n EXTRA_ELEMENT_HANDLING.attributeCheck = cfg.ADD_ATTR;\n } else {\n if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {\n ALLOWED_ATTR = clone(ALLOWED_ATTR);\n }\n addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);\n }\n }\n if (cfg.ADD_URI_SAFE_ATTR) {\n addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);\n }\n if (cfg.FORBID_CONTENTS) {\n if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {\n FORBID_CONTENTS = clone(FORBID_CONTENTS);\n }\n addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);\n }\n if (cfg.ADD_FORBID_CONTENTS) {\n if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {\n FORBID_CONTENTS = clone(FORBID_CONTENTS);\n }\n addToSet(FORBID_CONTENTS, cfg.ADD_FORBID_CONTENTS, transformCaseFunc);\n }\n /* Add #text in case KEEP_CONTENT is set to true */\n if (KEEP_CONTENT) {\n ALLOWED_TAGS['#text'] = true;\n }\n /* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */\n if (WHOLE_DOCUMENT) {\n addToSet(ALLOWED_TAGS, ['html', 'head', 'body']);\n }\n /* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */\n if (ALLOWED_TAGS.table) {\n addToSet(ALLOWED_TAGS, ['tbody']);\n delete FORBID_TAGS.tbody;\n }\n if (cfg.TRUSTED_TYPES_POLICY) {\n if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') {\n throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a \"createHTML\" hook.');\n }\n if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') {\n throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a \"createScriptURL\" hook.');\n }\n // Overwrite existing TrustedTypes policy.\n trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;\n // Sign local variables required by `sanitize`.\n emptyHTML = trustedTypesPolicy.createHTML('');\n } else {\n // Uninitialized policy, attempt to initialize the internal dompurify policy.\n if (trustedTypesPolicy === undefined) {\n trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);\n }\n // If creating the internal policy succeeded sign internal variables.\n if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') {\n emptyHTML = trustedTypesPolicy.createHTML('');\n }\n }\n // Prevent further manipulation of configuration.\n // Not available in IE8, Safari 5, etc.\n if (freeze) {\n freeze(cfg);\n }\n CONFIG = cfg;\n };\n /* Keep track of all possible SVG and MathML tags\n * so that we can perform the namespace checks\n * correctly. */\n const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]);\n const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]);\n /**\n * @param element a DOM element whose namespace is being checked\n * @returns Return false if the element has a\n * namespace that a spec-compliant parser would never\n * return. Return true otherwise.\n */\n const _checkValidNamespace = function _checkValidNamespace(element) {\n let parent = getParentNode(element);\n // In JSDOM, if we're inside shadow DOM, then parentNode\n // can be null. We just simulate parent in this case.\n if (!parent || !parent.tagName) {\n parent = {\n namespaceURI: NAMESPACE,\n tagName: 'template'\n };\n }\n const tagName = stringToLowerCase(element.tagName);\n const parentTagName = stringToLowerCase(parent.tagName);\n if (!ALLOWED_NAMESPACES[element.namespaceURI]) {\n return false;\n }\n if (element.namespaceURI === SVG_NAMESPACE) {\n // The only way to switch from HTML namespace to SVG\n // is via . If it happens via any other tag, then\n // it should be killed.\n if (parent.namespaceURI === HTML_NAMESPACE) {\n return tagName === 'svg';\n }\n // The only way to switch from MathML to SVG is via`\n // svg if parent is either or MathML\n // text integration points.\n if (parent.namespaceURI === MATHML_NAMESPACE) {\n return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);\n }\n // We only allow elements that are defined in SVG\n // spec. All others are disallowed in SVG namespace.\n return Boolean(ALL_SVG_TAGS[tagName]);\n }\n if (element.namespaceURI === MATHML_NAMESPACE) {\n // The only way to switch from HTML namespace to MathML\n // is via . If it happens via any other tag, then\n // it should be killed.\n if (parent.namespaceURI === HTML_NAMESPACE) {\n return tagName === 'math';\n }\n // The only way to switch from SVG to MathML is via\n // and HTML integration points\n if (parent.namespaceURI === SVG_NAMESPACE) {\n return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];\n }\n // We only allow elements that are defined in MathML\n // spec. All others are disallowed in MathML namespace.\n return Boolean(ALL_MATHML_TAGS[tagName]);\n }\n if (element.namespaceURI === HTML_NAMESPACE) {\n // The only way to switch from SVG to HTML is via\n // HTML integration points, and from MathML to HTML\n // is via MathML text integration points\n if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {\n return false;\n }\n if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {\n return false;\n }\n // We disallow tags that are specific for MathML\n // or SVG and should never appear in HTML namespace\n return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);\n }\n // For XHTML and XML documents that support custom namespaces\n if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) {\n return true;\n }\n // The code should never reach this place (this means\n // that the element somehow got namespace that is not\n // HTML, SVG, MathML or allowed via ALLOWED_NAMESPACES).\n // Return false just in case.\n return false;\n };\n /**\n * _forceRemove\n *\n * @param node a DOM node\n */\n const _forceRemove = function _forceRemove(node) {\n arrayPush(DOMPurify.removed, {\n element: node\n });\n try {\n // eslint-disable-next-line unicorn/prefer-dom-node-remove\n getParentNode(node).removeChild(node);\n } catch (_) {\n remove(node);\n }\n };\n /**\n * _removeAttribute\n *\n * @param name an Attribute name\n * @param element a DOM node\n */\n const _removeAttribute = function _removeAttribute(name, element) {\n try {\n arrayPush(DOMPurify.removed, {\n attribute: element.getAttributeNode(name),\n from: element\n });\n } catch (_) {\n arrayPush(DOMPurify.removed, {\n attribute: null,\n from: element\n });\n }\n element.removeAttribute(name);\n // We void attribute values for unremovable \"is\" attributes\n if (name === 'is') {\n if (RETURN_DOM || RETURN_DOM_FRAGMENT) {\n try {\n _forceRemove(element);\n } catch (_) {}\n } else {\n try {\n element.setAttribute(name, '');\n } catch (_) {}\n }\n }\n };\n /**\n * _initDocument\n *\n * @param dirty - a string of dirty markup\n * @return a DOM, filled with the dirty markup\n */\n const _initDocument = function _initDocument(dirty) {\n /* Create a HTML document */\n let doc = null;\n let leadingWhitespace = null;\n if (FORCE_BODY) {\n dirty = '' + dirty;\n } else {\n /* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */\n const matches = stringMatch(dirty, /^[\\r\\n\\t ]+/);\n leadingWhitespace = matches && matches[0];\n }\n if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && NAMESPACE === HTML_NAMESPACE) {\n // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict)\n dirty = '' + dirty + '';\n }\n const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty;\n /*\n * Use the DOMParser API by default, fallback later if needs be\n * DOMParser not work for svg when has multiple root element.\n */\n if (NAMESPACE === HTML_NAMESPACE) {\n try {\n doc = new DOMParser().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE);\n } catch (_) {}\n }\n /* Use createHTMLDocument in case DOMParser is not available */\n if (!doc || !doc.documentElement) {\n doc = implementation.createDocument(NAMESPACE, 'template', null);\n try {\n doc.documentElement.innerHTML = IS_EMPTY_INPUT ? emptyHTML : dirtyPayload;\n } catch (_) {\n // Syntax error if dirtyPayload is invalid xml\n }\n }\n const body = doc.body || doc.documentElement;\n if (dirty && leadingWhitespace) {\n body.insertBefore(document.createTextNode(leadingWhitespace), body.childNodes[0] || null);\n }\n /* Work on whole document or just its body */\n if (NAMESPACE === HTML_NAMESPACE) {\n return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? 'html' : 'body')[0];\n }\n return WHOLE_DOCUMENT ? doc.documentElement : body;\n };\n /**\n * Creates a NodeIterator object that you can use to traverse filtered lists of nodes or elements in a document.\n *\n * @param root The root element or node to start traversing on.\n * @return The created NodeIterator\n */\n const _createNodeIterator = function _createNodeIterator(root) {\n return createNodeIterator.call(root.ownerDocument || root, root,\n // eslint-disable-next-line no-bitwise\n NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);\n };\n /**\n * _isClobbered\n *\n * @param element element to check for clobbering attacks\n * @return true if clobbered, false if safe\n */\n const _isClobbered = function _isClobbered(element) {\n return element instanceof HTMLFormElement && (typeof element.nodeName !== 'string' || typeof element.textContent !== 'string' || typeof element.removeChild !== 'function' || !(element.attributes instanceof NamedNodeMap) || typeof element.removeAttribute !== 'function' || typeof element.setAttribute !== 'function' || typeof element.namespaceURI !== 'string' || typeof element.insertBefore !== 'function' || typeof element.hasChildNodes !== 'function');\n };\n /**\n * Checks whether the given object is a DOM node.\n *\n * @param value object to check whether it's a DOM node\n * @return true is object is a DOM node\n */\n const _isNode = function _isNode(value) {\n return typeof Node === 'function' && value instanceof Node;\n };\n function _executeHooks(hooks, currentNode, data) {\n arrayForEach(hooks, hook => {\n hook.call(DOMPurify, currentNode, data, CONFIG);\n });\n }\n /**\n * _sanitizeElements\n *\n * @protect nodeName\n * @protect textContent\n * @protect removeChild\n * @param currentNode to check for permission to exist\n * @return true if node was killed, false if left alive\n */\n const _sanitizeElements = function _sanitizeElements(currentNode) {\n let content = null;\n /* Execute a hook if present */\n _executeHooks(hooks.beforeSanitizeElements, currentNode, null);\n /* Check if element is clobbered or can clobber */\n if (_isClobbered(currentNode)) {\n _forceRemove(currentNode);\n return true;\n }\n /* Now let's check the element's type and name */\n const tagName = transformCaseFunc(currentNode.nodeName);\n /* Execute a hook if present */\n _executeHooks(hooks.uponSanitizeElement, currentNode, {\n tagName,\n allowedTags: ALLOWED_TAGS\n });\n /* Detect mXSS attempts abusing namespace confusion */\n if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\\w!]/g, currentNode.innerHTML) && regExpTest(/<[/\\w!]/g, currentNode.textContent)) {\n _forceRemove(currentNode);\n return true;\n }\n /* Remove any occurrence of processing instructions */\n if (currentNode.nodeType === NODE_TYPE.progressingInstruction) {\n _forceRemove(currentNode);\n return true;\n }\n /* Remove any kind of possibly harmful comments */\n if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(/<[/\\w]/g, currentNode.data)) {\n _forceRemove(currentNode);\n return true;\n }\n /* Remove element if anything forbids its presence */\n if (!(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName])) {\n /* Check if we have a custom element to handle */\n if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {\n if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {\n return false;\n }\n if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {\n return false;\n }\n }\n /* Keep content except for bad-listed elements */\n if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {\n const parentNode = getParentNode(currentNode) || currentNode.parentNode;\n const childNodes = getChildNodes(currentNode) || currentNode.childNodes;\n if (childNodes && parentNode) {\n const childCount = childNodes.length;\n for (let i = childCount - 1; i >= 0; --i) {\n const childClone = cloneNode(childNodes[i], true);\n childClone.__removalCount = (currentNode.__removalCount || 0) + 1;\n parentNode.insertBefore(childClone, getNextSibling(currentNode));\n }\n }\n }\n _forceRemove(currentNode);\n return true;\n }\n /* Check whether element has a valid namespace */\n if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) {\n _forceRemove(currentNode);\n return true;\n }\n /* Make sure that older browsers don't get fallback-tag mXSS */\n if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(/<\\/no(script|embed|frames)/i, currentNode.innerHTML)) {\n _forceRemove(currentNode);\n return true;\n }\n /* Sanitize element content to be template-safe */\n if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {\n /* Get the element's text content */\n content = currentNode.textContent;\n arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {\n content = stringReplace(content, expr, ' ');\n });\n if (currentNode.textContent !== content) {\n arrayPush(DOMPurify.removed, {\n element: currentNode.cloneNode()\n });\n currentNode.textContent = content;\n }\n }\n /* Execute a hook if present */\n _executeHooks(hooks.afterSanitizeElements, currentNode, null);\n return false;\n };\n /**\n * _isValidAttribute\n *\n * @param lcTag Lowercase tag name of containing element.\n * @param lcName Lowercase attribute name.\n * @param value Attribute value.\n * @return Returns true if `value` is valid, otherwise false.\n */\n // eslint-disable-next-line complexity\n const _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) {\n /* Make sure attribute cannot clobber */\n if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {\n return false;\n }\n /* Allow valid data-* attributes: At least one character after \"-\"\n (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)\n XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)\n We don't need to check the value; it's always URI safe. */\n if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) ; else if (EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag)) ; else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) {\n if (\n // First condition does a very basic check if a) it's basically a valid custom element tagname AND\n // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck\n // and c) if the attribute name passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.attributeNameCheck\n _isBasicCustomElement(lcTag) && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(lcTag)) && (CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName) || CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.attributeNameCheck(lcName, lcTag)) ||\n // Alternative, second condition checks if it's an `is`-attribute, AND\n // the value passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck\n lcName === 'is' && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(value))) ; else {\n return false;\n }\n /* Check value is safe. First, is attr inert? If so, is safe */\n } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) ; else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if (value) {\n return false;\n } else ;\n return true;\n };\n /**\n * _isBasicCustomElement\n * checks if at least one dash is included in tagName, and it's not the first char\n * for more sophisticated checking see https://github.com/sindresorhus/validate-element-name\n *\n * @param tagName name of the tag of the node to sanitize\n * @returns Returns true if the tag name meets the basic criteria for a custom element, otherwise false.\n */\n const _isBasicCustomElement = function _isBasicCustomElement(tagName) {\n return tagName !== 'annotation-xml' && stringMatch(tagName, CUSTOM_ELEMENT);\n };\n /**\n * _sanitizeAttributes\n *\n * @protect attributes\n * @protect nodeName\n * @protect removeAttribute\n * @protect setAttribute\n *\n * @param currentNode to sanitize\n */\n const _sanitizeAttributes = function _sanitizeAttributes(currentNode) {\n /* Execute a hook if present */\n _executeHooks(hooks.beforeSanitizeAttributes, currentNode, null);\n const {\n attributes\n } = currentNode;\n /* Check if we have attributes; if not we might have a text node */\n if (!attributes || _isClobbered(currentNode)) {\n return;\n }\n const hookEvent = {\n attrName: '',\n attrValue: '',\n keepAttr: true,\n allowedAttributes: ALLOWED_ATTR,\n forceKeepAttr: undefined\n };\n let l = attributes.length;\n /* Go backwards over all attributes; safely remove bad ones */\n while (l--) {\n const attr = attributes[l];\n const {\n name,\n namespaceURI,\n value: attrValue\n } = attr;\n const lcName = transformCaseFunc(name);\n const initValue = attrValue;\n let value = name === 'value' ? initValue : stringTrim(initValue);\n /* Execute a hook if present */\n hookEvent.attrName = lcName;\n hookEvent.attrValue = value;\n hookEvent.keepAttr = true;\n hookEvent.forceKeepAttr = undefined; // Allows developers to see this is a property they can set\n _executeHooks(hooks.uponSanitizeAttribute, currentNode, hookEvent);\n value = hookEvent.attrValue;\n /* Full DOM Clobbering protection via namespace isolation,\n * Prefix id and name attributes with `user-content-`\n */\n if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) {\n // Remove the attribute with this value\n _removeAttribute(name, currentNode);\n // Prefix the value and later re-create the attribute with the sanitized value\n value = SANITIZE_NAMED_PROPS_PREFIX + value;\n }\n /* Work around a security issue with comments inside attributes */\n if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\\/(style|title|textarea)/i, value)) {\n _removeAttribute(name, currentNode);\n continue;\n }\n /* Make sure we cannot easily use animated hrefs, even if animations are allowed */\n if (lcName === 'attributename' && stringMatch(value, 'href')) {\n _removeAttribute(name, currentNode);\n continue;\n }\n /* Did the hooks approve of the attribute? */\n if (hookEvent.forceKeepAttr) {\n continue;\n }\n /* Did the hooks approve of the attribute? */\n if (!hookEvent.keepAttr) {\n _removeAttribute(name, currentNode);\n continue;\n }\n /* Work around a security issue in jQuery 3.0 */\n if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\\/>/i, value)) {\n _removeAttribute(name, currentNode);\n continue;\n }\n /* Sanitize attribute content to be template-safe */\n if (SAFE_FOR_TEMPLATES) {\n arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {\n value = stringReplace(value, expr, ' ');\n });\n }\n /* Is `value` valid for this attribute? */\n const lcTag = transformCaseFunc(currentNode.nodeName);\n if (!_isValidAttribute(lcTag, lcName, value)) {\n _removeAttribute(name, currentNode);\n continue;\n }\n /* Handle attributes that require Trusted Types */\n if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function') {\n if (namespaceURI) ; else {\n switch (trustedTypes.getAttributeType(lcTag, lcName)) {\n case 'TrustedHTML':\n {\n value = trustedTypesPolicy.createHTML(value);\n break;\n }\n case 'TrustedScriptURL':\n {\n value = trustedTypesPolicy.createScriptURL(value);\n break;\n }\n }\n }\n }\n /* Handle invalid data-* attribute set by try-catching it */\n if (value !== initValue) {\n try {\n if (namespaceURI) {\n currentNode.setAttributeNS(namespaceURI, name, value);\n } else {\n /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. \"x-schema\". */\n currentNode.setAttribute(name, value);\n }\n if (_isClobbered(currentNode)) {\n _forceRemove(currentNode);\n } else {\n arrayPop(DOMPurify.removed);\n }\n } catch (_) {\n _removeAttribute(name, currentNode);\n }\n }\n }\n /* Execute a hook if present */\n _executeHooks(hooks.afterSanitizeAttributes, currentNode, null);\n };\n /**\n * _sanitizeShadowDOM\n *\n * @param fragment to iterate over recursively\n */\n const _sanitizeShadowDOM = function _sanitizeShadowDOM(fragment) {\n let shadowNode = null;\n const shadowIterator = _createNodeIterator(fragment);\n /* Execute a hook if present */\n _executeHooks(hooks.beforeSanitizeShadowDOM, fragment, null);\n while (shadowNode = shadowIterator.nextNode()) {\n /* Execute a hook if present */\n _executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null);\n /* Sanitize tags and elements */\n _sanitizeElements(shadowNode);\n /* Check attributes next */\n _sanitizeAttributes(shadowNode);\n /* Deep shadow DOM detected */\n if (shadowNode.content instanceof DocumentFragment) {\n _sanitizeShadowDOM(shadowNode.content);\n }\n }\n /* Execute a hook if present */\n _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);\n };\n // eslint-disable-next-line complexity\n DOMPurify.sanitize = function (dirty) {\n let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};\n let body = null;\n let importedNode = null;\n let currentNode = null;\n let returnNode = null;\n /* Make sure we have a string to sanitize.\n DO NOT return early, as this will return the wrong type if\n the user has requested a DOM object rather than a string */\n IS_EMPTY_INPUT = !dirty;\n if (IS_EMPTY_INPUT) {\n dirty = '';\n }\n /* Stringify, in case dirty is an object */\n if (typeof dirty !== 'string' && !_isNode(dirty)) {\n if (typeof dirty.toString === 'function') {\n dirty = dirty.toString();\n if (typeof dirty !== 'string') {\n throw typeErrorCreate('dirty is not a string, aborting');\n }\n } else {\n throw typeErrorCreate('toString is not a function');\n }\n }\n /* Return dirty HTML if DOMPurify cannot run */\n if (!DOMPurify.isSupported) {\n return dirty;\n }\n /* Assign config vars */\n if (!SET_CONFIG) {\n _parseConfig(cfg);\n }\n /* Clean up removed elements */\n DOMPurify.removed = [];\n /* Check if dirty is correctly typed for IN_PLACE */\n if (typeof dirty === 'string') {\n IN_PLACE = false;\n }\n if (IN_PLACE) {\n /* Do some early pre-sanitization to avoid unsafe root nodes */\n if (dirty.nodeName) {\n const tagName = transformCaseFunc(dirty.nodeName);\n if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {\n throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');\n }\n }\n } else if (dirty instanceof Node) {\n /* If dirty is a DOM element, append to an empty document to avoid\n elements being stripped by the parser */\n body = _initDocument('');\n importedNode = body.ownerDocument.importNode(dirty, true);\n if (importedNode.nodeType === NODE_TYPE.element && importedNode.nodeName === 'BODY') {\n /* Node is already a body, use as is */\n body = importedNode;\n } else if (importedNode.nodeName === 'HTML') {\n body = importedNode;\n } else {\n // eslint-disable-next-line unicorn/prefer-dom-node-append\n body.appendChild(importedNode);\n }\n } else {\n /* Exit directly if we have nothing to do */\n if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT &&\n // eslint-disable-next-line unicorn/prefer-includes\n dirty.indexOf('<') === -1) {\n return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty;\n }\n /* Initialize the document to work on */\n body = _initDocument(dirty);\n /* Check we have a DOM node from the data */\n if (!body) {\n return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : '';\n }\n }\n /* Remove first element node (ours) if FORCE_BODY is set */\n if (body && FORCE_BODY) {\n _forceRemove(body.firstChild);\n }\n /* Get node iterator */\n const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body);\n /* Now start iterating over the created document */\n while (currentNode = nodeIterator.nextNode()) {\n /* Sanitize tags and elements */\n _sanitizeElements(currentNode);\n /* Check attributes next */\n _sanitizeAttributes(currentNode);\n /* Shadow DOM detected, sanitize it */\n if (currentNode.content instanceof DocumentFragment) {\n _sanitizeShadowDOM(currentNode.content);\n }\n }\n /* If we sanitized `dirty` in-place, return it. */\n if (IN_PLACE) {\n return dirty;\n }\n /* Return sanitized string or DOM */\n if (RETURN_DOM) {\n if (RETURN_DOM_FRAGMENT) {\n returnNode = createDocumentFragment.call(body.ownerDocument);\n while (body.firstChild) {\n // eslint-disable-next-line unicorn/prefer-dom-node-append\n returnNode.appendChild(body.firstChild);\n }\n } else {\n returnNode = body;\n }\n if (ALLOWED_ATTR.shadowroot || ALLOWED_ATTR.shadowrootmode) {\n /*\n AdoptNode() is not used because internal state is not reset\n (e.g. the past names map of a HTMLFormElement), this is safe\n in theory but we would rather not risk another attack vector.\n The state that is cloned by importNode() is explicitly defined\n by the specs.\n */\n returnNode = importNode.call(originalDocument, returnNode, true);\n }\n return returnNode;\n }\n let serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML;\n /* Serialize doctype if allowed */\n if (WHOLE_DOCUMENT && ALLOWED_TAGS['!doctype'] && body.ownerDocument && body.ownerDocument.doctype && body.ownerDocument.doctype.name && regExpTest(DOCTYPE_NAME, body.ownerDocument.doctype.name)) {\n serializedHTML = '\\n' + serializedHTML;\n }\n /* Sanitize final string template-safe */\n if (SAFE_FOR_TEMPLATES) {\n arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {\n serializedHTML = stringReplace(serializedHTML, expr, ' ');\n });\n }\n return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML;\n };\n DOMPurify.setConfig = function () {\n let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n _parseConfig(cfg);\n SET_CONFIG = true;\n };\n DOMPurify.clearConfig = function () {\n CONFIG = null;\n SET_CONFIG = false;\n };\n DOMPurify.isValidAttribute = function (tag, attr, value) {\n /* Initialize shared config vars if necessary. */\n if (!CONFIG) {\n _parseConfig({});\n }\n const lcTag = transformCaseFunc(tag);\n const lcName = transformCaseFunc(attr);\n return _isValidAttribute(lcTag, lcName, value);\n };\n DOMPurify.addHook = function (entryPoint, hookFunction) {\n if (typeof hookFunction !== 'function') {\n return;\n }\n arrayPush(hooks[entryPoint], hookFunction);\n };\n DOMPurify.removeHook = function (entryPoint, hookFunction) {\n if (hookFunction !== undefined) {\n const index = arrayLastIndexOf(hooks[entryPoint], hookFunction);\n return index === -1 ? undefined : arraySplice(hooks[entryPoint], index, 1)[0];\n }\n return arrayPop(hooks[entryPoint]);\n };\n DOMPurify.removeHooks = function (entryPoint) {\n hooks[entryPoint] = [];\n };\n DOMPurify.removeAllHooks = function () {\n hooks = _createHooksMap();\n };\n return DOMPurify;\n}\nvar purify = createDOMPurify();\n\nexport { purify as default };\n//# sourceMappingURL=purify.es.mjs.map\n","/**\n * marked v17.0.1 - a markdown parser\n * Copyright (c) 2018-2025, MarkedJS. (MIT License)\n * Copyright (c) 2011-2018, Christopher Jeffrey. (MIT License)\n * https://github.com/markedjs/marked\n */\n\n/**\n * DO NOT EDIT THIS FILE\n * The code in this file is generated from files in ./src/\n */\n\nfunction L(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}var T=L();function Z(u){T=u}var C={exec:()=>null};function k(u,e=\"\"){let t=typeof u==\"string\"?u:u.source,n={replace:(r,i)=>{let s=typeof i==\"string\"?i:i.source;return s=s.replace(m.caret,\"$1\"),t=t.replace(r,s),n},getRegex:()=>new RegExp(t,e)};return n}var me=(()=>{try{return!!new RegExp(\"(?<=1)(?/,blockquoteSetextReplace:/\\n {0,3}((?:=+|-+) *)(?=\\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \\t]?/gm,listReplaceTabs:/^\\t+/,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\\[[ xX]\\] +\\S/,listReplaceTask:/^\\[[ xX]\\] +/,listTaskCheckbox:/\\[[ xX]\\]/,anyLine:/\\n.*\\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\\||\\| *$/g,tableRowBlankLine:/\\n[ \\t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^/i,startPreScriptTag:/^<(pre|code|kbd|script)(\\s|>)/i,endPreScriptTag:/^<\\/(pre|code|kbd|script)(\\s|>)/i,startAngleBracket:/^$/,pedanticHrefTitle:/^([^'\"]*[^\\s])\\s+(['\"])(.*)\\2/,unicodeAlphaNumeric:/[\\p{L}\\p{N}]/u,escapeTest:/[&<>\"']/,escapeReplace:/[&<>\"']/g,escapeTestNoEncode:/[<>\"']|&(?!(#\\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\\w+);)/,escapeReplaceNoEncode:/[<>\"']|&(?!(#\\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\\w+);)/g,unescapeTest:/&(#(?:\\d+)|(?:#x[0-9A-Fa-f]+)|(?:\\w+));?/ig,caret:/(^|[^\\[])\\^/g,percentDecode:/%25/g,findPipe:/\\|/g,splitPipe:/ \\|/,slashPipe:/\\\\\\|/g,carriageReturn:/\\r\\n|\\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\\S*/,endingNewline:/\\n$/,listItemRegex:u=>new RegExp(`^( {0,3}${u})((?:[\t ][^\\\\n]*)?(?:\\\\n|$))`),nextBulletRegex:u=>new RegExp(`^ {0,${Math.min(3,u-1)}}(?:[*+-]|\\\\d{1,9}[.)])((?:[ \t][^\\\\n]*)?(?:\\\\n|$))`),hrRegex:u=>new RegExp(`^ {0,${Math.min(3,u-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\\\* *){3,})(?:\\\\n+|$)`),fencesBeginRegex:u=>new RegExp(`^ {0,${Math.min(3,u-1)}}(?:\\`\\`\\`|~~~)`),headingBeginRegex:u=>new RegExp(`^ {0,${Math.min(3,u-1)}}#`),htmlBeginRegex:u=>new RegExp(`^ {0,${Math.min(3,u-1)}}<(?:[a-z].*>|!--)`,\"i\")},xe=/^(?:[ \\t]*(?:\\n|$))+/,be=/^((?: {4}| {0,3}\\t)[^\\n]+(?:\\n(?:[ \\t]*(?:\\n|$))*)?)+/,Re=/^ {0,3}(`{3,}(?=[^`\\n]*(?:\\n|$))|~{3,})([^\\n]*)(?:\\n|$)(?:|([\\s\\S]*?)(?:\\n|$))(?: {0,3}\\1[~`]* *(?=\\n|$)|$)/,I=/^ {0,3}((?:-[\\t ]*){3,}|(?:_[ \\t]*){3,}|(?:\\*[ \\t]*){3,})(?:\\n+|$)/,Te=/^ {0,3}(#{1,6})(?=\\s|$)(.*)(?:\\n+|$)/,N=/(?:[*+-]|\\d{1,9}[.)])/,re=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\\n(?!\\s*?\\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\\n {0,3}(=+|-+) *(?:\\n+|$)/,se=k(re).replace(/bull/g,N).replace(/blockCode/g,/(?: {4}| {0,3}\\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\\n>]+>\\n/).replace(/\\|table/g,\"\").getRegex(),Oe=k(re).replace(/bull/g,N).replace(/blockCode/g,/(?: {4}| {0,3}\\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\\n>]+>\\n/).replace(/table/g,/ {0,3}\\|?(?:[:\\- ]*\\|)+[\\:\\- ]*\\n/).getRegex(),Q=/^([^\\n]+(?:\\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\\n)[^\\n]+)*)/,we=/^[^\\n]+/,F=/(?!\\s*\\])(?:\\\\[\\s\\S]|[^\\[\\]\\\\])+/,ye=k(/^ {0,3}\\[(label)\\]: *(?:\\n[ \\t]*)?([^<\\s][^\\s]*|<.*?>)(?:(?: +(?:\\n[ \\t]*)?| *\\n[ \\t]*)(title))? *(?:\\n+|$)/).replace(\"label\",F).replace(\"title\",/(?:\"(?:\\\\\"?|[^\"\\\\])*\"|'[^'\\n]*(?:\\n[^'\\n]+)*\\n?'|\\([^()]*\\))/).getRegex(),Pe=k(/^( {0,3}bull)([ \\t][^\\n]+?)?(?:\\n|$)/).replace(/bull/g,N).getRegex(),v=\"address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul\",j=/|$))/,Se=k(\"^ {0,3}(?:<(script|pre|style|textarea)[\\\\s>][\\\\s\\\\S]*?(?:[^\\\\n]*\\\\n+|$)|comment[^\\\\n]*(\\\\n+|$)|<\\\\?[\\\\s\\\\S]*?(?:\\\\?>\\\\n*|$)|\\\\n*|$)|\\\\n*|$)|)[\\\\s\\\\S]*?(?:(?:\\\\n[ \t]*)+\\\\n|$)|<(?!script|pre|style|textarea)([a-z][\\\\w-]*)(?:attribute)*? */?>(?=[ \\\\t]*(?:\\\\n|$))[\\\\s\\\\S]*?(?:(?:\\\\n[ \t]*)+\\\\n|$)|(?=[ \\\\t]*(?:\\\\n|$))[\\\\s\\\\S]*?(?:(?:\\\\n[ \t]*)+\\\\n|$))\",\"i\").replace(\"comment\",j).replace(\"tag\",v).replace(\"attribute\",/ +[a-zA-Z:_][\\w.:-]*(?: *= *\"[^\"\\n]*\"| *= *'[^'\\n]*'| *= *[^\\s\"'=<>`]+)?/).getRegex(),ie=k(Q).replace(\"hr\",I).replace(\"heading\",\" {0,3}#{1,6}(?:\\\\s|$)\").replace(\"|lheading\",\"\").replace(\"|table\",\"\").replace(\"blockquote\",\" {0,3}>\").replace(\"fences\",\" {0,3}(?:`{3,}(?=[^`\\\\n]*\\\\n)|~{3,})[^\\\\n]*\\\\n\").replace(\"list\",\" {0,3}(?:[*+-]|1[.)]) \").replace(\"html\",\")|<(?:script|pre|style|textarea|!--)\").replace(\"tag\",v).getRegex(),$e=k(/^( {0,3}> ?(paragraph|[^\\n]*)(?:\\n|$))+/).replace(\"paragraph\",ie).getRegex(),U={blockquote:$e,code:be,def:ye,fences:Re,heading:Te,hr:I,html:Se,lheading:se,list:Pe,newline:xe,paragraph:ie,table:C,text:we},te=k(\"^ *([^\\\\n ].*)\\\\n {0,3}((?:\\\\| *)?:?-+:? *(?:\\\\| *:?-+:? *)*(?:\\\\| *)?)(?:\\\\n((?:(?! *\\\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\\\n|$))*)\\\\n*|$)\").replace(\"hr\",I).replace(\"heading\",\" {0,3}#{1,6}(?:\\\\s|$)\").replace(\"blockquote\",\" {0,3}>\").replace(\"code\",\"(?: {4}| {0,3}\t)[^\\\\n]\").replace(\"fences\",\" {0,3}(?:`{3,}(?=[^`\\\\n]*\\\\n)|~{3,})[^\\\\n]*\\\\n\").replace(\"list\",\" {0,3}(?:[*+-]|1[.)]) \").replace(\"html\",\")|<(?:script|pre|style|textarea|!--)\").replace(\"tag\",v).getRegex(),_e={...U,lheading:Oe,table:te,paragraph:k(Q).replace(\"hr\",I).replace(\"heading\",\" {0,3}#{1,6}(?:\\\\s|$)\").replace(\"|lheading\",\"\").replace(\"table\",te).replace(\"blockquote\",\" {0,3}>\").replace(\"fences\",\" {0,3}(?:`{3,}(?=[^`\\\\n]*\\\\n)|~{3,})[^\\\\n]*\\\\n\").replace(\"list\",\" {0,3}(?:[*+-]|1[.)]) \").replace(\"html\",\")|<(?:script|pre|style|textarea|!--)\").replace(\"tag\",v).getRegex()},Le={...U,html:k(`^ *(?:comment *(?:\\\\n|\\\\s*$)|<(tag)[\\\\s\\\\S]+? *(?:\\\\n{2,}|\\\\s*$)|\\\\s]*)*?/?> *(?:\\\\n{2,}|\\\\s*$))`).replace(\"comment\",j).replace(/tag/g,\"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\\\b)\\\\w+(?!:|[^\\\\w\\\\s@]*@)\\\\b\").getRegex(),def:/^ *\\[([^\\]]+)\\]: *]+)>?(?: +([\"(][^\\n]+[\")]))? *(?:\\n+|$)/,heading:/^(#{1,6})(.*)(?:\\n+|$)/,fences:C,lheading:/^(.+?)\\n {0,3}(=+|-+) *(?:\\n+|$)/,paragraph:k(Q).replace(\"hr\",I).replace(\"heading\",` *#{1,6} *[^\n]`).replace(\"lheading\",se).replace(\"|table\",\"\").replace(\"blockquote\",\" {0,3}>\").replace(\"|fences\",\"\").replace(\"|list\",\"\").replace(\"|html\",\"\").replace(\"|tag\",\"\").getRegex()},Me=/^\\\\([!\"#$%&'()*+,\\-./:;<=>?@\\[\\]\\\\^_`{|}~])/,ze=/^(`+)([^`]|[^`][\\s\\S]*?[^`])\\1(?!`)/,oe=/^( {2,}|\\\\)\\n(?!\\s*$)/,Ae=/^(`+|[^`])(?:(?= {2,}\\n)|[\\s\\S]*?(?:(?=[\\\\`+)[^`]+\\k(?!`))*?\\]\\((?:\\\\[\\s\\S]|[^\\\\\\(\\)]|\\((?:\\\\[\\s\\S]|[^\\\\\\(\\)])*\\))*\\)/).replace(\"precode-\",me?\"(?`+)[^`]+\\k(?!`)/).replace(\"html\",/<(?! )[^<>]*?>/).getRegex(),ue=/^(?:\\*+(?:((?!\\*)punct)|[^\\s*]))|^_+(?:((?!_)punct)|([^\\s_]))/,qe=k(ue,\"u\").replace(/punct/g,D).getRegex(),ve=k(ue,\"u\").replace(/punct/g,le).getRegex(),pe=\"^[^_*]*?__[^_*]*?\\\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\\\*)punct(\\\\*+)(?=[\\\\s]|$)|notPunctSpace(\\\\*+)(?!\\\\*)(?=punctSpace|$)|(?!\\\\*)punctSpace(\\\\*+)(?=notPunctSpace)|[\\\\s](\\\\*+)(?!\\\\*)(?=punct)|(?!\\\\*)punct(\\\\*+)(?!\\\\*)(?=punct)|notPunctSpace(\\\\*+)(?=notPunctSpace)\",De=k(pe,\"gu\").replace(/notPunctSpace/g,ae).replace(/punctSpace/g,K).replace(/punct/g,D).getRegex(),He=k(pe,\"gu\").replace(/notPunctSpace/g,Ee).replace(/punctSpace/g,Ie).replace(/punct/g,le).getRegex(),Ze=k(\"^[^_*]*?\\\\*\\\\*[^_*]*?_[^_*]*?(?=\\\\*\\\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)\",\"gu\").replace(/notPunctSpace/g,ae).replace(/punctSpace/g,K).replace(/punct/g,D).getRegex(),Ge=k(/\\\\(punct)/,\"gu\").replace(/punct/g,D).getRegex(),Ne=k(/^<(scheme:[^\\s\\x00-\\x1f<>]*|email)>/).replace(\"scheme\",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace(\"email\",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),Qe=k(j).replace(\"(?:-->|$)\",\"-->\").getRegex(),Fe=k(\"^comment|^|^<[a-zA-Z][\\\\w-]*(?:attribute)*?\\\\s*/?>|^<\\\\?[\\\\s\\\\S]*?\\\\?>|^|^\").replace(\"comment\",Qe).replace(\"attribute\",/\\s+[a-zA-Z:_][\\w.:-]*(?:\\s*=\\s*\"[^\"]*\"|\\s*=\\s*'[^']*'|\\s*=\\s*[^\\s\"'=<>`]+)?/).getRegex(),q=/(?:\\[(?:\\\\[\\s\\S]|[^\\[\\]\\\\])*\\]|\\\\[\\s\\S]|`+[^`]*?`+(?!`)|[^\\[\\]\\\\`])*?/,je=k(/^!?\\[(label)\\]\\(\\s*(href)(?:(?:[ \\t]*(?:\\n[ \\t]*)?)(title))?\\s*\\)/).replace(\"label\",q).replace(\"href\",/<(?:\\\\.|[^\\n<>\\\\])+>|[^ \\t\\n\\x00-\\x1f]*/).replace(\"title\",/\"(?:\\\\\"?|[^\"\\\\])*\"|'(?:\\\\'?|[^'\\\\])*'|\\((?:\\\\\\)?|[^)\\\\])*\\)/).getRegex(),ce=k(/^!?\\[(label)\\]\\[(ref)\\]/).replace(\"label\",q).replace(\"ref\",F).getRegex(),he=k(/^!?\\[(ref)\\](?:\\[\\])?/).replace(\"ref\",F).getRegex(),Ue=k(\"reflink|nolink(?!\\\\()\",\"g\").replace(\"reflink\",ce).replace(\"nolink\",he).getRegex(),ne=/[hH][tT][tT][pP][sS]?|[fF][tT][pP]/,W={_backpedal:C,anyPunctuation:Ge,autolink:Ne,blockSkip:Be,br:oe,code:ze,del:C,emStrongLDelim:qe,emStrongRDelimAst:De,emStrongRDelimUnd:Ze,escape:Me,link:je,nolink:he,punctuation:Ce,reflink:ce,reflinkSearch:Ue,tag:Fe,text:Ae,url:C},Ke={...W,link:k(/^!?\\[(label)\\]\\((.*?)\\)/).replace(\"label\",q).getRegex(),reflink:k(/^!?\\[(label)\\]\\s*\\[([^\\]]*)\\]/).replace(\"label\",q).getRegex()},G={...W,emStrongRDelimAst:He,emStrongLDelim:ve,url:k(/^((?:protocol):\\/\\/|www\\.)(?:[a-zA-Z0-9\\-]+\\.?)+[^\\s<]*|^email/).replace(\"protocol\",ne).replace(\"email\",/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(),_backpedal:/(?:[^?!.,:;*_'\"~()&]+|\\([^)]*\\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'\"~)]+(?!$))+/,del:/^(~~?)(?=[^\\s~])((?:\\\\[\\s\\S]|[^\\\\])*?(?:\\\\[\\s\\S]|[^\\s~\\\\]))\\1(?=[^~]|$)/,text:k(/^([`~]+|[^`~])(?:(?= {2,}\\n)|(?=[a-zA-Z0-9.!#$%&'*+\\/=?_`{\\|}~-]+@)|[\\s\\S]*?(?:(?=[\\\\\":\">\",'\"':\""\",\"'\":\"'\"},ke=u=>Xe[u];function w(u,e){if(e){if(m.escapeTest.test(u))return u.replace(m.escapeReplace,ke)}else if(m.escapeTestNoEncode.test(u))return u.replace(m.escapeReplaceNoEncode,ke);return u}function X(u){try{u=encodeURI(u).replace(m.percentDecode,\"%\")}catch{return null}return u}function J(u,e){let t=u.replace(m.findPipe,(i,s,a)=>{let o=!1,l=s;for(;--l>=0&&a[l]===\"\\\\\";)o=!o;return o?\"|\":\" |\"}),n=t.split(m.splitPipe),r=0;if(n[0].trim()||n.shift(),n.length>0&&!n.at(-1)?.trim()&&n.pop(),e)if(n.length>e)n.splice(e);else for(;n.length0?-2:-1}function ge(u,e,t,n,r){let i=e.href,s=e.title||null,a=u[1].replace(r.other.outputLinkReplace,\"$1\");n.state.inLink=!0;let o={type:u[0].charAt(0)===\"!\"?\"image\":\"link\",raw:t,href:i,title:s,text:a,tokens:n.inlineTokens(a)};return n.state.inLink=!1,o}function Je(u,e,t){let n=u.match(t.other.indentCodeCompensation);if(n===null)return e;let r=n[1];return e.split(`\n`).map(i=>{let s=i.match(t.other.beginningSpace);if(s===null)return i;let[a]=s;return a.length>=r.length?i.slice(r.length):i}).join(`\n`)}var y=class{options;rules;lexer;constructor(e){this.options=e||T}space(e){let t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:\"space\",raw:t[0]}}code(e){let t=this.rules.block.code.exec(e);if(t){let n=t[0].replace(this.rules.other.codeRemoveIndent,\"\");return{type:\"code\",raw:t[0],codeBlockStyle:\"indented\",text:this.options.pedantic?n:z(n,`\n`)}}}fences(e){let t=this.rules.block.fences.exec(e);if(t){let n=t[0],r=Je(n,t[3]||\"\",this.rules);return{type:\"code\",raw:n,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,\"$1\"):t[2],text:r}}}heading(e){let t=this.rules.block.heading.exec(e);if(t){let n=t[2].trim();if(this.rules.other.endingHash.test(n)){let r=z(n,\"#\");(this.options.pedantic||!r||this.rules.other.endingSpaceChar.test(r))&&(n=r.trim())}return{type:\"heading\",raw:t[0],depth:t[1].length,text:n,tokens:this.lexer.inline(n)}}}hr(e){let t=this.rules.block.hr.exec(e);if(t)return{type:\"hr\",raw:z(t[0],`\n`)}}blockquote(e){let t=this.rules.block.blockquote.exec(e);if(t){let n=z(t[0],`\n`).split(`\n`),r=\"\",i=\"\",s=[];for(;n.length>0;){let a=!1,o=[],l;for(l=0;l1,i={type:\"list\",raw:\"\",ordered:r,start:r?+n.slice(0,-1):\"\",loose:!1,items:[]};n=r?`\\\\d{1,9}\\\\${n.slice(-1)}`:`\\\\${n}`,this.options.pedantic&&(n=r?n:\"[*+-]\");let s=this.rules.other.listItemRegex(n),a=!1;for(;e;){let l=!1,p=\"\",c=\"\";if(!(t=s.exec(e))||this.rules.block.hr.test(e))break;p=t[0],e=e.substring(p.length);let g=t[2].split(`\n`,1)[0].replace(this.rules.other.listReplaceTabs,O=>\" \".repeat(3*O.length)),h=e.split(`\n`,1)[0],R=!g.trim(),f=0;if(this.options.pedantic?(f=2,c=g.trimStart()):R?f=t[1].length+1:(f=t[2].search(this.rules.other.nonSpaceChar),f=f>4?1:f,c=g.slice(f),f+=t[1].length),R&&this.rules.other.blankLine.test(h)&&(p+=h+`\n`,e=e.substring(h.length+1),l=!0),!l){let O=this.rules.other.nextBulletRegex(f),V=this.rules.other.hrRegex(f),Y=this.rules.other.fencesBeginRegex(f),ee=this.rules.other.headingBeginRegex(f),fe=this.rules.other.htmlBeginRegex(f);for(;e;){let H=e.split(`\n`,1)[0],A;if(h=H,this.options.pedantic?(h=h.replace(this.rules.other.listReplaceNesting,\" \"),A=h):A=h.replace(this.rules.other.tabCharGlobal,\" \"),Y.test(h)||ee.test(h)||fe.test(h)||O.test(h)||V.test(h))break;if(A.search(this.rules.other.nonSpaceChar)>=f||!h.trim())c+=`\n`+A.slice(f);else{if(R||g.replace(this.rules.other.tabCharGlobal,\" \").search(this.rules.other.nonSpaceChar)>=4||Y.test(g)||ee.test(g)||V.test(g))break;c+=`\n`+h}!R&&!h.trim()&&(R=!0),p+=H+`\n`,e=e.substring(H.length+1),g=A.slice(f)}}i.loose||(a?i.loose=!0:this.rules.other.doubleBlankLine.test(p)&&(a=!0)),i.items.push({type:\"list_item\",raw:p,task:!!this.options.gfm&&this.rules.other.listIsTask.test(c),loose:!1,text:c,tokens:[]}),i.raw+=p}let o=i.items.at(-1);if(o)o.raw=o.raw.trimEnd(),o.text=o.text.trimEnd();else return;i.raw=i.raw.trimEnd();for(let l of i.items){if(this.lexer.state.top=!1,l.tokens=this.lexer.blockTokens(l.text,[]),l.task){if(l.text=l.text.replace(this.rules.other.listReplaceTask,\"\"),l.tokens[0]?.type===\"text\"||l.tokens[0]?.type===\"paragraph\"){l.tokens[0].raw=l.tokens[0].raw.replace(this.rules.other.listReplaceTask,\"\"),l.tokens[0].text=l.tokens[0].text.replace(this.rules.other.listReplaceTask,\"\");for(let c=this.lexer.inlineQueue.length-1;c>=0;c--)if(this.rules.other.listIsTask.test(this.lexer.inlineQueue[c].src)){this.lexer.inlineQueue[c].src=this.lexer.inlineQueue[c].src.replace(this.rules.other.listReplaceTask,\"\");break}}let p=this.rules.other.listTaskCheckbox.exec(l.raw);if(p){let c={type:\"checkbox\",raw:p[0]+\" \",checked:p[0]!==\"[ ]\"};l.checked=c.checked,i.loose?l.tokens[0]&&[\"paragraph\",\"text\"].includes(l.tokens[0].type)&&\"tokens\"in l.tokens[0]&&l.tokens[0].tokens?(l.tokens[0].raw=c.raw+l.tokens[0].raw,l.tokens[0].text=c.raw+l.tokens[0].text,l.tokens[0].tokens.unshift(c)):l.tokens.unshift({type:\"paragraph\",raw:c.raw,text:c.raw,tokens:[c]}):l.tokens.unshift(c)}}if(!i.loose){let p=l.tokens.filter(g=>g.type===\"space\"),c=p.length>0&&p.some(g=>this.rules.other.anyLine.test(g.raw));i.loose=c}}if(i.loose)for(let l of i.items){l.loose=!0;for(let p of l.tokens)p.type===\"text\"&&(p.type=\"paragraph\")}return i}}html(e){let t=this.rules.block.html.exec(e);if(t)return{type:\"html\",block:!0,raw:t[0],pre:t[1]===\"pre\"||t[1]===\"script\"||t[1]===\"style\",text:t[0]}}def(e){let t=this.rules.block.def.exec(e);if(t){let n=t[1].toLowerCase().replace(this.rules.other.multipleSpaceGlobal,\" \"),r=t[2]?t[2].replace(this.rules.other.hrefBrackets,\"$1\").replace(this.rules.inline.anyPunctuation,\"$1\"):\"\",i=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,\"$1\"):t[3];return{type:\"def\",tag:n,raw:t[0],href:r,title:i}}}table(e){let t=this.rules.block.table.exec(e);if(!t||!this.rules.other.tableDelimiter.test(t[2]))return;let n=J(t[1]),r=t[2].replace(this.rules.other.tableAlignChars,\"\").split(\"|\"),i=t[3]?.trim()?t[3].replace(this.rules.other.tableRowBlankLine,\"\").split(`\n`):[],s={type:\"table\",raw:t[0],header:[],align:[],rows:[]};if(n.length===r.length){for(let a of r)this.rules.other.tableAlignRight.test(a)?s.align.push(\"right\"):this.rules.other.tableAlignCenter.test(a)?s.align.push(\"center\"):this.rules.other.tableAlignLeft.test(a)?s.align.push(\"left\"):s.align.push(null);for(let a=0;a({text:o,tokens:this.lexer.inline(o),header:!1,align:s.align[l]})));return s}}lheading(e){let t=this.rules.block.lheading.exec(e);if(t)return{type:\"heading\",raw:t[0],depth:t[2].charAt(0)===\"=\"?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){let t=this.rules.block.paragraph.exec(e);if(t){let n=t[1].charAt(t[1].length-1)===`\n`?t[1].slice(0,-1):t[1];return{type:\"paragraph\",raw:t[0],text:n,tokens:this.lexer.inline(n)}}}text(e){let t=this.rules.block.text.exec(e);if(t)return{type:\"text\",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){let t=this.rules.inline.escape.exec(e);if(t)return{type:\"escape\",raw:t[0],text:t[1]}}tag(e){let t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:\"html\",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){let t=this.rules.inline.link.exec(e);if(t){let n=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(n)){if(!this.rules.other.endAngleBracket.test(n))return;let s=z(n.slice(0,-1),\"\\\\\");if((n.length-s.length)%2===0)return}else{let s=de(t[2],\"()\");if(s===-2)return;if(s>-1){let o=(t[0].indexOf(\"!\")===0?5:4)+t[1].length+s;t[2]=t[2].substring(0,s),t[0]=t[0].substring(0,o).trim(),t[3]=\"\"}}let r=t[2],i=\"\";if(this.options.pedantic){let s=this.rules.other.pedanticHrefTitle.exec(r);s&&(r=s[1],i=s[3])}else i=t[3]?t[3].slice(1,-1):\"\";return r=r.trim(),this.rules.other.startAngleBracket.test(r)&&(this.options.pedantic&&!this.rules.other.endAngleBracket.test(n)?r=r.slice(1):r=r.slice(1,-1)),ge(t,{href:r&&r.replace(this.rules.inline.anyPunctuation,\"$1\"),title:i&&i.replace(this.rules.inline.anyPunctuation,\"$1\")},t[0],this.lexer,this.rules)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){let r=(n[2]||n[1]).replace(this.rules.other.multipleSpaceGlobal,\" \"),i=t[r.toLowerCase()];if(!i){let s=n[0].charAt(0);return{type:\"text\",raw:s,text:s}}return ge(n,i,n[0],this.lexer,this.rules)}}emStrong(e,t,n=\"\"){let r=this.rules.inline.emStrongLDelim.exec(e);if(!r||r[3]&&n.match(this.rules.other.unicodeAlphaNumeric))return;if(!(r[1]||r[2]||\"\")||!n||this.rules.inline.punctuation.exec(n)){let s=[...r[0]].length-1,a,o,l=s,p=0,c=r[0][0]===\"*\"?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(c.lastIndex=0,t=t.slice(-1*e.length+s);(r=c.exec(t))!=null;){if(a=r[1]||r[2]||r[3]||r[4]||r[5]||r[6],!a)continue;if(o=[...a].length,r[3]||r[4]){l+=o;continue}else if((r[5]||r[6])&&s%3&&!((s+o)%3)){p+=o;continue}if(l-=o,l>0)continue;o=Math.min(o,o+l+p);let g=[...r[0]][0].length,h=e.slice(0,s+r.index+g+o);if(Math.min(s,o)%2){let f=h.slice(1,-1);return{type:\"em\",raw:h,text:f,tokens:this.lexer.inlineTokens(f)}}let R=h.slice(2,-2);return{type:\"strong\",raw:h,text:R,tokens:this.lexer.inlineTokens(R)}}}}codespan(e){let t=this.rules.inline.code.exec(e);if(t){let n=t[2].replace(this.rules.other.newLineCharGlobal,\" \"),r=this.rules.other.nonSpaceChar.test(n),i=this.rules.other.startingSpaceChar.test(n)&&this.rules.other.endingSpaceChar.test(n);return r&&i&&(n=n.substring(1,n.length-1)),{type:\"codespan\",raw:t[0],text:n}}}br(e){let t=this.rules.inline.br.exec(e);if(t)return{type:\"br\",raw:t[0]}}del(e){let t=this.rules.inline.del.exec(e);if(t)return{type:\"del\",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){let t=this.rules.inline.autolink.exec(e);if(t){let n,r;return t[2]===\"@\"?(n=t[1],r=\"mailto:\"+n):(n=t[1],r=n),{type:\"link\",raw:t[0],text:n,href:r,tokens:[{type:\"text\",raw:n,text:n}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let n,r;if(t[2]===\"@\")n=t[0],r=\"mailto:\"+n;else{let i;do i=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??\"\";while(i!==t[0]);n=t[0],t[1]===\"www.\"?r=\"http://\"+t[0]:r=t[0]}return{type:\"link\",raw:t[0],text:n,href:r,tokens:[{type:\"text\",raw:n,text:n}]}}}inlineText(e){let t=this.rules.inline.text.exec(e);if(t){let n=this.lexer.state.inRawBlock;return{type:\"text\",raw:t[0],text:t[0],escaped:n}}}};var x=class u{tokens;options;state;inlineQueue;tokenizer;constructor(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||T,this.options.tokenizer=this.options.tokenizer||new y,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};let t={other:m,block:E.normal,inline:M.normal};this.options.pedantic?(t.block=E.pedantic,t.inline=M.pedantic):this.options.gfm&&(t.block=E.gfm,this.options.breaks?t.inline=M.breaks:t.inline=M.gfm),this.tokenizer.rules=t}static get rules(){return{block:E,inline:M}}static lex(e,t){return new u(t).lex(e)}static lexInline(e,t){return new u(t).inlineTokens(e)}lex(e){e=e.replace(m.carriageReturn,`\n`),this.blockTokens(e,this.tokens);for(let t=0;t(r=s.call({lexer:this},e,t))?(e=e.substring(r.raw.length),t.push(r),!0):!1))continue;if(r=this.tokenizer.space(e)){e=e.substring(r.raw.length);let s=t.at(-1);r.raw.length===1&&s!==void 0?s.raw+=`\n`:t.push(r);continue}if(r=this.tokenizer.code(e)){e=e.substring(r.raw.length);let s=t.at(-1);s?.type===\"paragraph\"||s?.type===\"text\"?(s.raw+=(s.raw.endsWith(`\n`)?\"\":`\n`)+r.raw,s.text+=`\n`+r.text,this.inlineQueue.at(-1).src=s.text):t.push(r);continue}if(r=this.tokenizer.fences(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.heading(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.hr(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.blockquote(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.list(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.html(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.def(e)){e=e.substring(r.raw.length);let s=t.at(-1);s?.type===\"paragraph\"||s?.type===\"text\"?(s.raw+=(s.raw.endsWith(`\n`)?\"\":`\n`)+r.raw,s.text+=`\n`+r.raw,this.inlineQueue.at(-1).src=s.text):this.tokens.links[r.tag]||(this.tokens.links[r.tag]={href:r.href,title:r.title},t.push(r));continue}if(r=this.tokenizer.table(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.lheading(e)){e=e.substring(r.raw.length),t.push(r);continue}let i=e;if(this.options.extensions?.startBlock){let s=1/0,a=e.slice(1),o;this.options.extensions.startBlock.forEach(l=>{o=l.call({lexer:this},a),typeof o==\"number\"&&o>=0&&(s=Math.min(s,o))}),s<1/0&&s>=0&&(i=e.substring(0,s+1))}if(this.state.top&&(r=this.tokenizer.paragraph(i))){let s=t.at(-1);n&&s?.type===\"paragraph\"?(s.raw+=(s.raw.endsWith(`\n`)?\"\":`\n`)+r.raw,s.text+=`\n`+r.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=s.text):t.push(r),n=i.length!==e.length,e=e.substring(r.raw.length);continue}if(r=this.tokenizer.text(e)){e=e.substring(r.raw.length);let s=t.at(-1);s?.type===\"text\"?(s.raw+=(s.raw.endsWith(`\n`)?\"\":`\n`)+r.raw,s.text+=`\n`+r.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=s.text):t.push(r);continue}if(e){let s=\"Infinite loop on byte: \"+e.charCodeAt(0);if(this.options.silent){console.error(s);break}else throw new Error(s)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n=e,r=null;if(this.tokens.links){let o=Object.keys(this.tokens.links);if(o.length>0)for(;(r=this.tokenizer.rules.inline.reflinkSearch.exec(n))!=null;)o.includes(r[0].slice(r[0].lastIndexOf(\"[\")+1,-1))&&(n=n.slice(0,r.index)+\"[\"+\"a\".repeat(r[0].length-2)+\"]\"+n.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;(r=this.tokenizer.rules.inline.anyPunctuation.exec(n))!=null;)n=n.slice(0,r.index)+\"++\"+n.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);let i;for(;(r=this.tokenizer.rules.inline.blockSkip.exec(n))!=null;)i=r[2]?r[2].length:0,n=n.slice(0,r.index+i)+\"[\"+\"a\".repeat(r[0].length-i-2)+\"]\"+n.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);n=this.options.hooks?.emStrongMask?.call({lexer:this},n)??n;let s=!1,a=\"\";for(;e;){s||(a=\"\"),s=!1;let o;if(this.options.extensions?.inline?.some(p=>(o=p.call({lexer:this},e,t))?(e=e.substring(o.raw.length),t.push(o),!0):!1))continue;if(o=this.tokenizer.escape(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.tag(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.link(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.reflink(e,this.tokens.links)){e=e.substring(o.raw.length);let p=t.at(-1);o.type===\"text\"&&p?.type===\"text\"?(p.raw+=o.raw,p.text+=o.text):t.push(o);continue}if(o=this.tokenizer.emStrong(e,n,a)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.codespan(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.br(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.del(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.autolink(e)){e=e.substring(o.raw.length),t.push(o);continue}if(!this.state.inLink&&(o=this.tokenizer.url(e))){e=e.substring(o.raw.length),t.push(o);continue}let l=e;if(this.options.extensions?.startInline){let p=1/0,c=e.slice(1),g;this.options.extensions.startInline.forEach(h=>{g=h.call({lexer:this},c),typeof g==\"number\"&&g>=0&&(p=Math.min(p,g))}),p<1/0&&p>=0&&(l=e.substring(0,p+1))}if(o=this.tokenizer.inlineText(l)){e=e.substring(o.raw.length),o.raw.slice(-1)!==\"_\"&&(a=o.raw.slice(-1)),s=!0;let p=t.at(-1);p?.type===\"text\"?(p.raw+=o.raw,p.text+=o.text):t.push(o);continue}if(e){let p=\"Infinite loop on byte: \"+e.charCodeAt(0);if(this.options.silent){console.error(p);break}else throw new Error(p)}}return t}};var P=class{options;parser;constructor(e){this.options=e||T}space(e){return\"\"}code({text:e,lang:t,escaped:n}){let r=(t||\"\").match(m.notSpaceStart)?.[0],i=e.replace(m.endingNewline,\"\")+`\n`;return r?'
    '+(n?i:w(i,!0))+`
    \n`:\"
    \"+(n?i:w(i,!0))+`
    \n`}blockquote({tokens:e}){return`
    \n${this.parser.parse(e)}
    \n`}html({text:e}){return e}def(e){return\"\"}heading({tokens:e,depth:t}){return`${this.parser.parseInline(e)}\n`}hr(e){return`
    \n`}list(e){let t=e.ordered,n=e.start,r=\"\";for(let a=0;a\n`+r+\"\n`}listitem(e){return`
  • ${this.parser.parse(e.tokens)}
  • \n`}checkbox({checked:e}){return\" '}paragraph({tokens:e}){return`

    ${this.parser.parseInline(e)}

    \n`}table(e){let t=\"\",n=\"\";for(let i=0;i${r}`),`\n\n`+t+`\n`+r+`
    \n`}tablerow({text:e}){return`\n${e}\n`}tablecell(e){let t=this.parser.parseInline(e.tokens),n=e.header?\"th\":\"td\";return(e.align?`<${n} align=\"${e.align}\">`:`<${n}>`)+t+`\n`}strong({tokens:e}){return`${this.parser.parseInline(e)}`}em({tokens:e}){return`${this.parser.parseInline(e)}`}codespan({text:e}){return`${w(e,!0)}`}br(e){return\"
    \"}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:t,tokens:n}){let r=this.parser.parseInline(n),i=X(e);if(i===null)return r;e=i;let s='
    \"+r+\"\",s}image({href:e,title:t,text:n,tokens:r}){r&&(n=this.parser.parseInline(r,this.parser.textRenderer));let i=X(e);if(i===null)return w(n);e=i;let s=`\"${n}\"`;return\",s}text(e){return\"tokens\"in e&&e.tokens?this.parser.parseInline(e.tokens):\"escaped\"in e&&e.escaped?e.text:w(e.text)}};var $=class{strong({text:e}){return e}em({text:e}){return e}codespan({text:e}){return e}del({text:e}){return e}html({text:e}){return e}text({text:e}){return e}link({text:e}){return\"\"+e}image({text:e}){return\"\"+e}br(){return\"\"}checkbox({raw:e}){return e}};var b=class u{options;renderer;textRenderer;constructor(e){this.options=e||T,this.options.renderer=this.options.renderer||new P,this.renderer=this.options.renderer,this.renderer.options=this.options,this.renderer.parser=this,this.textRenderer=new $}static parse(e,t){return new u(t).parse(e)}static parseInline(e,t){return new u(t).parseInline(e)}parse(e){let t=\"\";for(let n=0;n{let a=i[s].flat(1/0);n=n.concat(this.walkTokens(a,t))}):i.tokens&&(n=n.concat(this.walkTokens(i.tokens,t)))}}return n}use(...e){let t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach(n=>{let r={...n};if(r.async=this.defaults.async||r.async||!1,n.extensions&&(n.extensions.forEach(i=>{if(!i.name)throw new Error(\"extension name required\");if(\"renderer\"in i){let s=t.renderers[i.name];s?t.renderers[i.name]=function(...a){let o=i.renderer.apply(this,a);return o===!1&&(o=s.apply(this,a)),o}:t.renderers[i.name]=i.renderer}if(\"tokenizer\"in i){if(!i.level||i.level!==\"block\"&&i.level!==\"inline\")throw new Error(\"extension level must be 'block' or 'inline'\");let s=t[i.level];s?s.unshift(i.tokenizer):t[i.level]=[i.tokenizer],i.start&&(i.level===\"block\"?t.startBlock?t.startBlock.push(i.start):t.startBlock=[i.start]:i.level===\"inline\"&&(t.startInline?t.startInline.push(i.start):t.startInline=[i.start]))}\"childTokens\"in i&&i.childTokens&&(t.childTokens[i.name]=i.childTokens)}),r.extensions=t),n.renderer){let i=this.defaults.renderer||new P(this.defaults);for(let s in n.renderer){if(!(s in i))throw new Error(`renderer '${s}' does not exist`);if([\"options\",\"parser\"].includes(s))continue;let a=s,o=n.renderer[a],l=i[a];i[a]=(...p)=>{let c=o.apply(i,p);return c===!1&&(c=l.apply(i,p)),c||\"\"}}r.renderer=i}if(n.tokenizer){let i=this.defaults.tokenizer||new y(this.defaults);for(let s in n.tokenizer){if(!(s in i))throw new Error(`tokenizer '${s}' does not exist`);if([\"options\",\"rules\",\"lexer\"].includes(s))continue;let a=s,o=n.tokenizer[a],l=i[a];i[a]=(...p)=>{let c=o.apply(i,p);return c===!1&&(c=l.apply(i,p)),c}}r.tokenizer=i}if(n.hooks){let i=this.defaults.hooks||new S;for(let s in n.hooks){if(!(s in i))throw new Error(`hook '${s}' does not exist`);if([\"options\",\"block\"].includes(s))continue;let a=s,o=n.hooks[a],l=i[a];S.passThroughHooks.has(s)?i[a]=p=>{if(this.defaults.async&&S.passThroughHooksRespectAsync.has(s))return(async()=>{let g=await o.call(i,p);return l.call(i,g)})();let c=o.call(i,p);return l.call(i,c)}:i[a]=(...p)=>{if(this.defaults.async)return(async()=>{let g=await o.apply(i,p);return g===!1&&(g=await l.apply(i,p)),g})();let c=o.apply(i,p);return c===!1&&(c=l.apply(i,p)),c}}r.hooks=i}if(n.walkTokens){let i=this.defaults.walkTokens,s=n.walkTokens;r.walkTokens=function(a){let o=[];return o.push(s.call(this,a)),i&&(o=o.concat(i.call(this,a))),o}}this.defaults={...this.defaults,...r}}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return x.lex(e,t??this.defaults)}parser(e,t){return b.parse(e,t??this.defaults)}parseMarkdown(e){return(n,r)=>{let i={...r},s={...this.defaults,...i},a=this.onError(!!s.silent,!!s.async);if(this.defaults.async===!0&&i.async===!1)return a(new Error(\"marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise.\"));if(typeof n>\"u\"||n===null)return a(new Error(\"marked(): input parameter is undefined or null\"));if(typeof n!=\"string\")return a(new Error(\"marked(): input parameter is of type \"+Object.prototype.toString.call(n)+\", string expected\"));if(s.hooks&&(s.hooks.options=s,s.hooks.block=e),s.async)return(async()=>{let o=s.hooks?await s.hooks.preprocess(n):n,p=await(s.hooks?await s.hooks.provideLexer():e?x.lex:x.lexInline)(o,s),c=s.hooks?await s.hooks.processAllTokens(p):p;s.walkTokens&&await Promise.all(this.walkTokens(c,s.walkTokens));let h=await(s.hooks?await s.hooks.provideParser():e?b.parse:b.parseInline)(c,s);return s.hooks?await s.hooks.postprocess(h):h})().catch(a);try{s.hooks&&(n=s.hooks.preprocess(n));let l=(s.hooks?s.hooks.provideLexer():e?x.lex:x.lexInline)(n,s);s.hooks&&(l=s.hooks.processAllTokens(l)),s.walkTokens&&this.walkTokens(l,s.walkTokens);let c=(s.hooks?s.hooks.provideParser():e?b.parse:b.parseInline)(l,s);return s.hooks&&(c=s.hooks.postprocess(c)),c}catch(o){return a(o)}}}onError(e,t){return n=>{if(n.message+=`\nPlease report this to https://github.com/markedjs/marked.`,e){let r=\"

    An error occurred:

    \"+w(n.message+\"\",!0)+\"
    \";return t?Promise.resolve(r):r}if(t)return Promise.reject(n);throw n}}};var _=new B;function d(u,e){return _.parse(u,e)}d.options=d.setOptions=function(u){return _.setOptions(u),d.defaults=_.defaults,Z(d.defaults),d};d.getDefaults=L;d.defaults=T;d.use=function(...u){return _.use(...u),d.defaults=_.defaults,Z(d.defaults),d};d.walkTokens=function(u,e){return _.walkTokens(u,e)};d.parseInline=_.parseInline;d.Parser=b;d.parser=b.parse;d.Renderer=P;d.TextRenderer=$;d.Lexer=x;d.lexer=x.lex;d.Tokenizer=y;d.Hooks=S;d.parse=d;var Dt=d.options,Ht=d.setOptions,Zt=d.use,Gt=d.walkTokens,Nt=d.parseInline,Qt=d,Ft=b.parse,jt=x.lex;export{S as Hooks,x as Lexer,B as Marked,b as Parser,P as Renderer,$ as TextRenderer,y as Tokenizer,T as defaults,L as getDefaults,jt as lexer,d as marked,Dt as options,Qt as parse,Nt as parseInline,Ft as parser,Ht as setOptions,Zt as use,Gt as walkTokens};\n//# sourceMappingURL=marked.esm.js.map\n","import DOMPurify from \"dompurify\";\nimport { marked } from \"marked\";\nimport { truncateText } from \"./format\";\n\nmarked.setOptions({\n gfm: true,\n breaks: true,\n mangle: false,\n});\n\nconst allowedTags = [\n \"a\",\n \"b\",\n \"blockquote\",\n \"br\",\n \"code\",\n \"del\",\n \"em\",\n \"h1\",\n \"h2\",\n \"h3\",\n \"h4\",\n \"hr\",\n \"i\",\n \"li\",\n \"ol\",\n \"p\",\n \"pre\",\n \"strong\",\n \"table\",\n \"tbody\",\n \"td\",\n \"th\",\n \"thead\",\n \"tr\",\n \"ul\",\n];\n\nconst allowedAttrs = [\"class\", \"href\", \"rel\", \"target\", \"title\", \"start\"];\n\nlet hooksInstalled = false;\nconst MARKDOWN_CHAR_LIMIT = 140_000;\nconst MARKDOWN_PARSE_LIMIT = 40_000;\nconst MARKDOWN_CACHE_LIMIT = 200;\nconst MARKDOWN_CACHE_MAX_CHARS = 50_000;\nconst markdownCache = new Map();\n\nfunction getCachedMarkdown(key: string): string | null {\n const cached = markdownCache.get(key);\n if (cached === undefined) return null;\n markdownCache.delete(key);\n markdownCache.set(key, cached);\n return cached;\n}\n\nfunction setCachedMarkdown(key: string, value: string) {\n markdownCache.set(key, value);\n if (markdownCache.size <= MARKDOWN_CACHE_LIMIT) return;\n const oldest = markdownCache.keys().next().value;\n if (oldest) markdownCache.delete(oldest);\n}\n\nfunction installHooks() {\n if (hooksInstalled) return;\n hooksInstalled = true;\n\n DOMPurify.addHook(\"afterSanitizeAttributes\", (node) => {\n if (!(node instanceof HTMLAnchorElement)) return;\n const href = node.getAttribute(\"href\");\n if (!href) return;\n node.setAttribute(\"rel\", \"noreferrer noopener\");\n node.setAttribute(\"target\", \"_blank\");\n });\n}\n\nexport function toSanitizedMarkdownHtml(markdown: string): string {\n const input = markdown.trim();\n if (!input) return \"\";\n installHooks();\n if (input.length <= MARKDOWN_CACHE_MAX_CHARS) {\n const cached = getCachedMarkdown(input);\n if (cached !== null) return cached;\n }\n const truncated = truncateText(input, MARKDOWN_CHAR_LIMIT);\n const suffix = truncated.truncated\n ? `\\n\\n… truncated (${truncated.total} chars, showing first ${truncated.text.length}).`\n : \"\";\n if (truncated.text.length > MARKDOWN_PARSE_LIMIT) {\n const escaped = escapeHtml(`${truncated.text}${suffix}`);\n const html = `
    ${escaped}
    `;\n const sanitized = DOMPurify.sanitize(html, {\n ALLOWED_TAGS: allowedTags,\n ALLOWED_ATTR: allowedAttrs,\n });\n if (input.length <= MARKDOWN_CACHE_MAX_CHARS) {\n setCachedMarkdown(input, sanitized);\n }\n return sanitized;\n }\n const rendered = marked.parse(`${truncated.text}${suffix}`) as string;\n const sanitized = DOMPurify.sanitize(rendered, {\n ALLOWED_TAGS: allowedTags,\n ALLOWED_ATTR: allowedAttrs,\n });\n if (input.length <= MARKDOWN_CACHE_MAX_CHARS) {\n setCachedMarkdown(input, sanitized);\n }\n return sanitized;\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, \"&\")\n .replace(//g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n","import { html, type TemplateResult } from \"lit\";\nimport { icons } from \"../icons\";\n\nconst COPIED_FOR_MS = 1500;\nconst ERROR_FOR_MS = 2000;\nconst COPY_LABEL = \"Copy as markdown\";\nconst COPIED_LABEL = \"Copied\";\nconst ERROR_LABEL = \"Copy failed\";\n\ntype CopyButtonOptions = {\n text: () => string;\n label?: string;\n};\n\nasync function copyTextToClipboard(text: string): Promise {\n if (!text) return false;\n\n try {\n await navigator.clipboard.writeText(text);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction setButtonLabel(button: HTMLButtonElement, label: string) {\n button.title = label;\n button.setAttribute(\"aria-label\", label);\n}\n\nfunction createCopyButton(options: CopyButtonOptions): TemplateResult {\n const idleLabel = options.label ?? COPY_LABEL;\n return html`\n {\n const btn = e.currentTarget as HTMLButtonElement | null;\n const iconContainer = btn?.querySelector(\n \".chat-copy-btn__icon\",\n ) as HTMLElement | null;\n\n if (!btn || btn.dataset.copying === \"1\") return;\n\n btn.dataset.copying = \"1\";\n btn.setAttribute(\"aria-busy\", \"true\");\n btn.disabled = true;\n\n const copied = await copyTextToClipboard(options.text());\n if (!btn.isConnected) return;\n\n delete btn.dataset.copying;\n btn.removeAttribute(\"aria-busy\");\n btn.disabled = false;\n\n if (!copied) {\n btn.dataset.error = \"1\";\n setButtonLabel(btn, ERROR_LABEL);\n\n window.setTimeout(() => {\n if (!btn.isConnected) return;\n delete btn.dataset.error;\n setButtonLabel(btn, idleLabel);\n }, ERROR_FOR_MS);\n return;\n }\n\n btn.dataset.copied = \"1\";\n setButtonLabel(btn, COPIED_LABEL);\n\n window.setTimeout(() => {\n if (!btn.isConnected) return;\n delete btn.dataset.copied;\n setButtonLabel(btn, idleLabel);\n }, COPIED_FOR_MS);\n }}\n >\n \n ${icons.copy}\n ${icons.check}\n \n \n `;\n}\n\nexport function renderCopyAsMarkdownButton(markdown: string): TemplateResult {\n return createCopyButton({ text: () => markdown, label: COPY_LABEL });\n}\n","import rawConfig from \"./tool-display.json\";\nimport type { IconName } from \"./icons\";\n\ntype ToolDisplayActionSpec = {\n label?: string;\n detailKeys?: string[];\n};\n\ntype ToolDisplaySpec = {\n icon?: string;\n title?: string;\n label?: string;\n detailKeys?: string[];\n actions?: Record;\n};\n\ntype ToolDisplayConfig = {\n version?: number;\n fallback?: ToolDisplaySpec;\n tools?: Record;\n};\n\nexport type ToolDisplay = {\n name: string;\n icon: IconName;\n title: string;\n label: string;\n verb?: string;\n detail?: string;\n};\n\nconst TOOL_DISPLAY_CONFIG = rawConfig as ToolDisplayConfig;\nconst FALLBACK = TOOL_DISPLAY_CONFIG.fallback ?? { icon: \"puzzle\" };\nconst TOOL_MAP = TOOL_DISPLAY_CONFIG.tools ?? {};\n\nfunction normalizeToolName(name?: string): string {\n return (name ?? \"tool\").trim();\n}\n\nfunction defaultTitle(name: string): string {\n const cleaned = name.replace(/_/g, \" \").trim();\n if (!cleaned) return \"Tool\";\n return cleaned\n .split(/\\s+/)\n .map((part) =>\n part.length <= 2 && part.toUpperCase() === part\n ? part\n : `${part.at(0)?.toUpperCase() ?? \"\"}${part.slice(1)}`,\n )\n .join(\" \");\n}\n\nfunction normalizeVerb(value?: string): string | undefined {\n const trimmed = value?.trim();\n if (!trimmed) return undefined;\n return trimmed.replace(/_/g, \" \");\n}\n\nfunction coerceDisplayValue(value: unknown): string | undefined {\n if (value === null || value === undefined) return undefined;\n if (typeof value === \"string\") {\n const trimmed = value.trim();\n if (!trimmed) return undefined;\n const firstLine = trimmed.split(/\\r?\\n/)[0]?.trim() ?? \"\";\n if (!firstLine) return undefined;\n return firstLine.length > 160 ? `${firstLine.slice(0, 157)}…` : firstLine;\n }\n if (typeof value === \"number\" || typeof value === \"boolean\") {\n return String(value);\n }\n if (Array.isArray(value)) {\n const values = value\n .map((item) => coerceDisplayValue(item))\n .filter((item): item is string => Boolean(item));\n if (values.length === 0) return undefined;\n const preview = values.slice(0, 3).join(\", \");\n return values.length > 3 ? `${preview}…` : preview;\n }\n return undefined;\n}\n\nfunction lookupValueByPath(args: unknown, path: string): unknown {\n if (!args || typeof args !== \"object\") return undefined;\n let current: unknown = args;\n for (const segment of path.split(\".\")) {\n if (!segment) return undefined;\n if (!current || typeof current !== \"object\") return undefined;\n const record = current as Record;\n current = record[segment];\n }\n return current;\n}\n\nfunction resolveDetailFromKeys(args: unknown, keys: string[]): string | undefined {\n for (const key of keys) {\n const value = lookupValueByPath(args, key);\n const display = coerceDisplayValue(value);\n if (display) return display;\n }\n return undefined;\n}\n\nfunction resolveReadDetail(args: unknown): string | undefined {\n if (!args || typeof args !== \"object\") return undefined;\n const record = args as Record;\n const path = typeof record.path === \"string\" ? record.path : undefined;\n if (!path) return undefined;\n const offset = typeof record.offset === \"number\" ? record.offset : undefined;\n const limit = typeof record.limit === \"number\" ? record.limit : undefined;\n if (offset !== undefined && limit !== undefined) {\n return `${path}:${offset}-${offset + limit}`;\n }\n return path;\n}\n\nfunction resolveWriteDetail(args: unknown): string | undefined {\n if (!args || typeof args !== \"object\") return undefined;\n const record = args as Record;\n const path = typeof record.path === \"string\" ? record.path : undefined;\n return path;\n}\n\nfunction resolveActionSpec(\n spec: ToolDisplaySpec | undefined,\n action: string | undefined,\n): ToolDisplayActionSpec | undefined {\n if (!spec || !action) return undefined;\n return spec.actions?.[action] ?? undefined;\n}\n\nexport function resolveToolDisplay(params: {\n name?: string;\n args?: unknown;\n meta?: string;\n}): ToolDisplay {\n const name = normalizeToolName(params.name);\n const key = name.toLowerCase();\n const spec = TOOL_MAP[key];\n const icon = (spec?.icon ?? FALLBACK.icon ?? \"puzzle\") as IconName;\n const title = spec?.title ?? defaultTitle(name);\n const label = spec?.label ?? name;\n const actionRaw =\n params.args && typeof params.args === \"object\"\n ? ((params.args as Record).action as string | undefined)\n : undefined;\n const action = typeof actionRaw === \"string\" ? actionRaw.trim() : undefined;\n const actionSpec = resolveActionSpec(spec, action);\n const verb = normalizeVerb(actionSpec?.label ?? action);\n\n let detail: string | undefined;\n if (key === \"read\") detail = resolveReadDetail(params.args);\n if (!detail && (key === \"write\" || key === \"edit\" || key === \"attach\")) {\n detail = resolveWriteDetail(params.args);\n }\n\n const detailKeys =\n actionSpec?.detailKeys ?? spec?.detailKeys ?? FALLBACK.detailKeys ?? [];\n if (!detail && detailKeys.length > 0) {\n detail = resolveDetailFromKeys(params.args, detailKeys);\n }\n\n if (!detail && params.meta) {\n detail = params.meta;\n }\n\n if (detail) {\n detail = shortenHomeInString(detail);\n }\n\n return {\n name,\n icon,\n title,\n label,\n verb,\n detail,\n };\n}\n\nexport function formatToolDetail(display: ToolDisplay): string | undefined {\n const parts: string[] = [];\n if (display.verb) parts.push(display.verb);\n if (display.detail) parts.push(display.detail);\n if (parts.length === 0) return undefined;\n return parts.join(\" · \");\n}\n\nexport function formatToolSummary(display: ToolDisplay): string {\n const detail = formatToolDetail(display);\n return detail ? `${display.label}: ${detail}` : display.label;\n}\n\nfunction shortenHomeInString(input: string): string {\n if (!input) return input;\n return input\n .replace(/\\/Users\\/[^/]+/g, \"~\")\n .replace(/\\/home\\/[^/]+/g, \"~\");\n}\n","/**\n * Chat-related constants for the UI layer.\n */\n\n/** Character threshold for showing tool output inline vs collapsed */\nexport const TOOL_INLINE_THRESHOLD = 80;\n\n/** Maximum lines to show in collapsed preview */\nexport const PREVIEW_MAX_LINES = 2;\n\n/** Maximum characters to show in collapsed preview */\nexport const PREVIEW_MAX_CHARS = 100;\n","/**\n * Helper functions for tool card rendering.\n */\n\nimport { PREVIEW_MAX_CHARS, PREVIEW_MAX_LINES } from \"./constants\";\n\n/**\n * Format tool output content for display in the sidebar.\n * Detects JSON and wraps it in a code block with formatting.\n */\nexport function formatToolOutputForSidebar(text: string): string {\n const trimmed = text.trim();\n // Try to detect and format JSON\n if (trimmed.startsWith(\"{\") || trimmed.startsWith(\"[\")) {\n try {\n const parsed = JSON.parse(trimmed);\n return \"```json\\n\" + JSON.stringify(parsed, null, 2) + \"\\n```\";\n } catch {\n // Not valid JSON, return as-is\n }\n }\n return text;\n}\n\n/**\n * Get a truncated preview of tool output text.\n * Truncates to first N lines or first N characters, whichever is shorter.\n */\nexport function getTruncatedPreview(text: string): string {\n const allLines = text.split(\"\\n\");\n const lines = allLines.slice(0, PREVIEW_MAX_LINES);\n const preview = lines.join(\"\\n\");\n if (preview.length > PREVIEW_MAX_CHARS) {\n return preview.slice(0, PREVIEW_MAX_CHARS) + \"…\";\n }\n return lines.length < allLines.length ? preview + \"…\" : preview;\n}\n","import { html, nothing } from \"lit\";\n\nimport { formatToolDetail, resolveToolDisplay } from \"../tool-display\";\nimport { icons } from \"../icons\";\nimport type { ToolCard } from \"../types/chat-types\";\nimport { TOOL_INLINE_THRESHOLD } from \"./constants\";\nimport {\n formatToolOutputForSidebar,\n getTruncatedPreview,\n} from \"./tool-helpers\";\nimport { isToolResultMessage } from \"./message-normalizer\";\nimport { extractTextCached } from \"./message-extract\";\n\nexport function extractToolCards(message: unknown): ToolCard[] {\n const m = message as Record;\n const content = normalizeContent(m.content);\n const cards: ToolCard[] = [];\n\n for (const item of content) {\n const kind = String(item.type ?? \"\").toLowerCase();\n const isToolCall =\n [\"toolcall\", \"tool_call\", \"tooluse\", \"tool_use\"].includes(kind) ||\n (typeof item.name === \"string\" && item.arguments != null);\n if (isToolCall) {\n cards.push({\n kind: \"call\",\n name: (item.name as string) ?? \"tool\",\n args: coerceArgs(item.arguments ?? item.args),\n });\n }\n }\n\n for (const item of content) {\n const kind = String(item.type ?? \"\").toLowerCase();\n if (kind !== \"toolresult\" && kind !== \"tool_result\") continue;\n const text = extractToolText(item);\n const name = typeof item.name === \"string\" ? item.name : \"tool\";\n cards.push({ kind: \"result\", name, text });\n }\n\n if (\n isToolResultMessage(message) &&\n !cards.some((card) => card.kind === \"result\")\n ) {\n const name =\n (typeof m.toolName === \"string\" && m.toolName) ||\n (typeof m.tool_name === \"string\" && m.tool_name) ||\n \"tool\";\n const text = extractTextCached(message) ?? undefined;\n cards.push({ kind: \"result\", name, text });\n }\n\n return cards;\n}\n\nexport function renderToolCardSidebar(\n card: ToolCard,\n onOpenSidebar?: (content: string) => void,\n) {\n const display = resolveToolDisplay({ name: card.name, args: card.args });\n const detail = formatToolDetail(display);\n const hasText = Boolean(card.text?.trim());\n\n const canClick = Boolean(onOpenSidebar);\n const handleClick = canClick\n ? () => {\n if (hasText) {\n onOpenSidebar!(formatToolOutputForSidebar(card.text!));\n return;\n }\n const info = `## ${display.label}\\n\\n${\n detail ? `**Command:** \\`${detail}\\`\\n\\n` : \"\"\n }*No output — tool completed successfully.*`;\n onOpenSidebar!(info);\n }\n : undefined;\n\n const isShort = hasText && (card.text?.length ?? 0) <= TOOL_INLINE_THRESHOLD;\n const showCollapsed = hasText && !isShort;\n const showInline = hasText && isShort;\n const isEmpty = !hasText;\n\n return html`\n {\n if (e.key !== \"Enter\" && e.key !== \" \") return;\n e.preventDefault();\n handleClick?.();\n }\n : nothing}\n >\n
    \n
    \n ${icons[display.icon]}\n ${display.label}\n
    \n ${canClick\n ? html`${hasText ? \"View\" : \"\"} ${icons.check}`\n : nothing}\n ${isEmpty && !canClick ? html`${icons.check}` : nothing}\n
    \n ${detail\n ? html`
    ${detail}
    `\n : nothing}\n ${isEmpty\n ? html`
    Completed
    `\n : nothing}\n ${showCollapsed\n ? html`
    ${getTruncatedPreview(card.text!)}
    `\n : nothing}\n ${showInline\n ? html`
    ${card.text}
    `\n : nothing}\n \n `;\n}\n\nfunction normalizeContent(content: unknown): Array> {\n if (!Array.isArray(content)) return [];\n return content.filter(Boolean) as Array>;\n}\n\nfunction coerceArgs(value: unknown): unknown {\n if (typeof value !== \"string\") return value;\n const trimmed = value.trim();\n if (!trimmed) return value;\n if (!trimmed.startsWith(\"{\") && !trimmed.startsWith(\"[\")) return value;\n try {\n return JSON.parse(trimmed);\n } catch {\n return value;\n }\n}\n\nfunction extractToolText(item: Record): string | undefined {\n if (typeof item.text === \"string\") return item.text;\n if (typeof item.content === \"string\") return item.content;\n return undefined;\n}\n","import { html, nothing } from \"lit\";\nimport { unsafeHTML } from \"lit/directives/unsafe-html.js\";\n\nimport type { AssistantIdentity } from \"../assistant-identity\";\nimport { toSanitizedMarkdownHtml } from \"../markdown\";\nimport type { MessageGroup } from \"../types/chat-types\";\nimport { renderCopyAsMarkdownButton } from \"./copy-as-markdown\";\nimport { isToolResultMessage, normalizeRoleForGrouping } from \"./message-normalizer\";\nimport {\n extractTextCached,\n extractThinkingCached,\n formatReasoningMarkdown,\n} from \"./message-extract\";\nimport { extractToolCards, renderToolCardSidebar } from \"./tool-cards\";\n\nexport function renderReadingIndicatorGroup(assistant?: AssistantIdentity) {\n return html`\n
    \n ${renderAvatar(\"assistant\", assistant)}\n
    \n
    \n \n \n \n
    \n
    \n
    \n `;\n}\n\nexport function renderStreamingGroup(\n text: string,\n startedAt: number,\n onOpenSidebar?: (content: string) => void,\n assistant?: AssistantIdentity,\n) {\n const timestamp = new Date(startedAt).toLocaleTimeString([], {\n hour: \"numeric\",\n minute: \"2-digit\",\n });\n const name = assistant?.name ?? \"Assistant\";\n\n return html`\n
    \n ${renderAvatar(\"assistant\", assistant)}\n
    \n ${renderGroupedMessage(\n {\n role: \"assistant\",\n content: [{ type: \"text\", text }],\n timestamp: startedAt,\n },\n { isStreaming: true, showReasoning: false },\n onOpenSidebar,\n )}\n
    \n ${name}\n ${timestamp}\n
    \n
    \n
    \n `;\n}\n\nexport function renderMessageGroup(\n group: MessageGroup,\n opts: {\n onOpenSidebar?: (content: string) => void;\n showReasoning: boolean;\n assistantName?: string;\n assistantAvatar?: string | null;\n },\n) {\n const normalizedRole = normalizeRoleForGrouping(group.role);\n const assistantName = opts.assistantName ?? \"Assistant\";\n const who =\n normalizedRole === \"user\"\n ? \"You\"\n : normalizedRole === \"assistant\"\n ? assistantName\n : normalizedRole;\n const roleClass =\n normalizedRole === \"user\"\n ? \"user\"\n : normalizedRole === \"assistant\"\n ? \"assistant\"\n : \"other\";\n const timestamp = new Date(group.timestamp).toLocaleTimeString([], {\n hour: \"numeric\",\n minute: \"2-digit\",\n });\n\n return html`\n
    \n ${renderAvatar(group.role, {\n name: assistantName,\n avatar: opts.assistantAvatar ?? null,\n })}\n
    \n ${group.messages.map((item, index) =>\n renderGroupedMessage(\n item.message,\n {\n isStreaming:\n group.isStreaming && index === group.messages.length - 1,\n showReasoning: opts.showReasoning,\n },\n opts.onOpenSidebar,\n ),\n )}\n
    \n ${who}\n ${timestamp}\n
    \n
    \n
    \n `;\n}\n\nfunction renderAvatar(\n role: string,\n assistant?: Pick,\n) {\n const normalized = normalizeRoleForGrouping(role);\n const assistantName = assistant?.name?.trim() || \"Assistant\";\n const assistantAvatar = assistant?.avatar?.trim() || \"\";\n const initial =\n normalized === \"user\"\n ? \"U\"\n : normalized === \"assistant\"\n ? assistantName.charAt(0).toUpperCase() || \"A\"\n : normalized === \"tool\"\n ? \"⚙\"\n : \"?\";\n const className =\n normalized === \"user\"\n ? \"user\"\n : normalized === \"assistant\"\n ? \"assistant\"\n : normalized === \"tool\"\n ? \"tool\"\n : \"other\";\n\n if (assistantAvatar && normalized === \"assistant\") {\n if (isAvatarUrl(assistantAvatar)) {\n return html``;\n }\n return html`
    ${assistantAvatar}
    `;\n }\n\n return html`
    ${initial}
    `;\n}\n\nfunction isAvatarUrl(value: string): boolean {\n return (\n /^https?:\\/\\//i.test(value) ||\n /^data:image\\//i.test(value) ||\n /^\\//.test(value) // Relative paths from avatar endpoint\n );\n}\n\nfunction renderGroupedMessage(\n message: unknown,\n opts: { isStreaming: boolean; showReasoning: boolean },\n onOpenSidebar?: (content: string) => void,\n) {\n const m = message as Record;\n const role = typeof m.role === \"string\" ? m.role : \"unknown\";\n const isToolResult =\n isToolResultMessage(message) ||\n role.toLowerCase() === \"toolresult\" ||\n role.toLowerCase() === \"tool_result\" ||\n typeof m.toolCallId === \"string\" ||\n typeof m.tool_call_id === \"string\";\n\n const toolCards = extractToolCards(message);\n const hasToolCards = toolCards.length > 0;\n\n const extractedText = extractTextCached(message);\n const extractedThinking =\n opts.showReasoning && role === \"assistant\"\n ? extractThinkingCached(message)\n : null;\n const markdownBase = extractedText?.trim() ? extractedText : null;\n const reasoningMarkdown = extractedThinking\n ? formatReasoningMarkdown(extractedThinking)\n : null;\n const markdown = markdownBase;\n const canCopyMarkdown = role === \"assistant\" && Boolean(markdown?.trim());\n\n const bubbleClasses = [\n \"chat-bubble\",\n canCopyMarkdown ? \"has-copy\" : \"\",\n opts.isStreaming ? \"streaming\" : \"\",\n \"fade-in\",\n ]\n .filter(Boolean)\n .join(\" \");\n\n if (!markdown && hasToolCards && isToolResult) {\n return html`${toolCards.map((card) =>\n renderToolCardSidebar(card, onOpenSidebar),\n )}`;\n }\n\n if (!markdown && !hasToolCards) return nothing;\n\n return html`\n
    \n ${canCopyMarkdown ? renderCopyAsMarkdownButton(markdown!) : nothing}\n ${reasoningMarkdown\n ? html`
    ${unsafeHTML(\n toSanitizedMarkdownHtml(reasoningMarkdown),\n )}
    `\n : nothing}\n ${markdown\n ? html`
    ${unsafeHTML(toSanitizedMarkdownHtml(markdown))}
    `\n : nothing}\n ${toolCards.map((card) => renderToolCardSidebar(card, onOpenSidebar))}\n
    \n `;\n}\n","import { html, nothing } from \"lit\";\nimport { unsafeHTML } from \"lit/directives/unsafe-html.js\";\n\nimport { icons } from \"../icons\";\nimport { toSanitizedMarkdownHtml } from \"../markdown\";\n\nexport type MarkdownSidebarProps = {\n content: string | null;\n error: string | null;\n onClose: () => void;\n onViewRawText: () => void;\n};\n\nexport function renderMarkdownSidebar(props: MarkdownSidebarProps) {\n return html`\n
    \n
    \n
    Tool Output
    \n \n
    \n
    \n ${props.error\n ? html`\n
    ${props.error}
    \n \n `\n : props.content\n ? html`
    ${unsafeHTML(toSanitizedMarkdownHtml(props.content))}
    `\n : html`
    No content available
    `}\n
    \n
    \n `;\n}\n","import { LitElement, html, css } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\n\n/**\n * A draggable divider for resizable split views.\n * Dispatches 'resize' events with { splitRatio: number } detail.\n */\n@customElement(\"resizable-divider\")\nexport class ResizableDivider extends LitElement {\n @property({ type: Number }) splitRatio = 0.6;\n @property({ type: Number }) minRatio = 0.4;\n @property({ type: Number }) maxRatio = 0.7;\n\n private isDragging = false;\n private startX = 0;\n private startRatio = 0;\n\n static styles = css`\n :host {\n width: 4px;\n cursor: col-resize;\n background: var(--border, #333);\n transition: background 150ms ease-out;\n flex-shrink: 0;\n position: relative;\n }\n\n :host::before {\n content: \"\";\n position: absolute;\n top: 0;\n left: -4px;\n right: -4px;\n bottom: 0;\n }\n\n :host(:hover) {\n background: var(--accent, #007bff);\n }\n\n :host(.dragging) {\n background: var(--accent, #007bff);\n }\n `;\n\n render() {\n return html``;\n }\n\n connectedCallback() {\n super.connectedCallback();\n this.addEventListener(\"mousedown\", this.handleMouseDown);\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n this.removeEventListener(\"mousedown\", this.handleMouseDown);\n document.removeEventListener(\"mousemove\", this.handleMouseMove);\n document.removeEventListener(\"mouseup\", this.handleMouseUp);\n }\n\n private handleMouseDown = (e: MouseEvent) => {\n this.isDragging = true;\n this.startX = e.clientX;\n this.startRatio = this.splitRatio;\n this.classList.add(\"dragging\");\n\n document.addEventListener(\"mousemove\", this.handleMouseMove);\n document.addEventListener(\"mouseup\", this.handleMouseUp);\n\n e.preventDefault();\n };\n\n private handleMouseMove = (e: MouseEvent) => {\n if (!this.isDragging) return;\n\n const container = this.parentElement;\n if (!container) return;\n\n const containerWidth = container.getBoundingClientRect().width;\n const deltaX = e.clientX - this.startX;\n const deltaRatio = deltaX / containerWidth;\n\n let newRatio = this.startRatio + deltaRatio;\n newRatio = Math.max(this.minRatio, Math.min(this.maxRatio, newRatio));\n\n this.dispatchEvent(\n new CustomEvent(\"resize\", {\n detail: { splitRatio: newRatio },\n bubbles: true,\n composed: true,\n })\n );\n };\n\n private handleMouseUp = () => {\n this.isDragging = false;\n this.classList.remove(\"dragging\");\n\n document.removeEventListener(\"mousemove\", this.handleMouseMove);\n document.removeEventListener(\"mouseup\", this.handleMouseUp);\n };\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"resizable-divider\": ResizableDivider;\n }\n}\n","import { html, nothing } from \"lit\";\nimport { repeat } from \"lit/directives/repeat.js\";\nimport type { SessionsListResult } from \"../types\";\nimport type { ChatQueueItem } from \"../ui-types\";\nimport type { ChatItem, MessageGroup } from \"../types/chat-types\";\nimport { icons } from \"../icons\";\nimport {\n normalizeMessage,\n normalizeRoleForGrouping,\n} from \"../chat/message-normalizer\";\nimport {\n renderMessageGroup,\n renderReadingIndicatorGroup,\n renderStreamingGroup,\n} from \"../chat/grouped-render\";\nimport { renderMarkdownSidebar } from \"./markdown-sidebar\";\nimport \"../components/resizable-divider\";\n\nexport type CompactionIndicatorStatus = {\n active: boolean;\n startedAt: number | null;\n completedAt: number | null;\n};\n\nexport type ChatProps = {\n sessionKey: string;\n onSessionKeyChange: (next: string) => void;\n thinkingLevel: string | null;\n showThinking: boolean;\n loading: boolean;\n sending: boolean;\n canAbort?: boolean;\n compactionStatus?: CompactionIndicatorStatus | null;\n messages: unknown[];\n toolMessages: unknown[];\n stream: string | null;\n streamStartedAt: number | null;\n assistantAvatarUrl?: string | null;\n draft: string;\n queue: ChatQueueItem[];\n connected: boolean;\n canSend: boolean;\n disabledReason: string | null;\n error: string | null;\n sessions: SessionsListResult | null;\n // Focus mode\n focusMode: boolean;\n // Sidebar state\n sidebarOpen?: boolean;\n sidebarContent?: string | null;\n sidebarError?: string | null;\n splitRatio?: number;\n assistantName: string;\n assistantAvatar: string | null;\n // Event handlers\n onRefresh: () => void;\n onToggleFocusMode: () => void;\n onDraftChange: (next: string) => void;\n onSend: () => void;\n onAbort?: () => void;\n onQueueRemove: (id: string) => void;\n onNewSession: () => void;\n onOpenSidebar?: (content: string) => void;\n onCloseSidebar?: () => void;\n onSplitRatioChange?: (ratio: number) => void;\n onChatScroll?: (event: Event) => void;\n};\n\nconst COMPACTION_TOAST_DURATION_MS = 5000;\n\nfunction renderCompactionIndicator(status: CompactionIndicatorStatus | null | undefined) {\n if (!status) return nothing;\n \n // Show \"compacting...\" while active\n if (status.active) {\n return html`\n
    \n ${icons.loader} Compacting context...\n
    \n `;\n }\n\n // Show \"compaction complete\" briefly after completion\n if (status.completedAt) {\n const elapsed = Date.now() - status.completedAt;\n if (elapsed < COMPACTION_TOAST_DURATION_MS) {\n return html`\n
    \n ${icons.check} Context compacted\n
    \n `;\n }\n }\n \n return nothing;\n}\n\nexport function renderChat(props: ChatProps) {\n const canCompose = props.connected;\n const isBusy = props.sending || props.stream !== null;\n const canAbort = Boolean(props.canAbort && props.onAbort);\n const activeSession = props.sessions?.sessions?.find(\n (row) => row.key === props.sessionKey,\n );\n const reasoningLevel = activeSession?.reasoningLevel ?? \"off\";\n const showReasoning = props.showThinking && reasoningLevel !== \"off\";\n const assistantIdentity = {\n name: props.assistantName,\n avatar: props.assistantAvatar ?? props.assistantAvatarUrl ?? null,\n };\n\n const composePlaceholder = props.connected\n ? \"Message (↩ to send, Shift+↩ for line breaks)\"\n : \"Connect to the gateway to start chatting…\";\n\n const splitRatio = props.splitRatio ?? 0.6;\n const sidebarOpen = Boolean(props.sidebarOpen && props.onCloseSidebar);\n const thread = html`\n \n ${props.loading ? html`
    Loading chat…
    ` : nothing}\n ${repeat(buildChatItems(props), (item) => item.key, (item) => {\n if (item.kind === \"reading-indicator\") {\n return renderReadingIndicatorGroup(assistantIdentity);\n }\n\n if (item.kind === \"stream\") {\n return renderStreamingGroup(\n item.text,\n item.startedAt,\n props.onOpenSidebar,\n assistantIdentity,\n );\n }\n\n if (item.kind === \"group\") {\n return renderMessageGroup(item, {\n onOpenSidebar: props.onOpenSidebar,\n showReasoning,\n assistantName: props.assistantName,\n assistantAvatar: assistantIdentity.avatar,\n });\n }\n\n return nothing;\n })}\n \n `;\n\n return html`\n
    \n ${props.disabledReason\n ? html`
    ${props.disabledReason}
    `\n : nothing}\n\n ${props.error\n ? html`
    ${props.error}
    `\n : nothing}\n\n ${renderCompactionIndicator(props.compactionStatus)}\n\n ${props.focusMode\n ? html`\n \n ${icons.x}\n \n `\n : nothing}\n\n \n \n ${thread}\n \n\n ${sidebarOpen\n ? html`\n \n props.onSplitRatioChange?.(e.detail.splitRatio)}\n >\n
    \n ${renderMarkdownSidebar({\n content: props.sidebarContent ?? null,\n error: props.sidebarError ?? null,\n onClose: props.onCloseSidebar!,\n onViewRawText: () => {\n if (!props.sidebarContent || !props.onOpenSidebar) return;\n props.onOpenSidebar(`\\`\\`\\`\\n${props.sidebarContent}\\n\\`\\`\\``);\n },\n })}\n
    \n `\n : nothing}\n \n\n ${props.queue.length\n ? html`\n
    \n
    Queued (${props.queue.length})
    \n
    \n ${props.queue.map(\n (item) => html`\n
    \n
    ${item.text}
    \n props.onQueueRemove(item.id)}\n >\n ${icons.x}\n \n
    \n `,\n )}\n
    \n
    \n `\n : nothing}\n\n
    \n \n
    \n \n ${canAbort ? \"Stop\" : \"New session\"}\n \n \n ${isBusy ? \"Queue\" : \"Send\"}\n \n
    \n
    \n
    \n `;\n}\n\nconst CHAT_HISTORY_RENDER_LIMIT = 200;\n\nfunction groupMessages(items: ChatItem[]): Array {\n const result: Array = [];\n let currentGroup: MessageGroup | null = null;\n\n for (const item of items) {\n if (item.kind !== \"message\") {\n if (currentGroup) {\n result.push(currentGroup);\n currentGroup = null;\n }\n result.push(item);\n continue;\n }\n\n const normalized = normalizeMessage(item.message);\n const role = normalizeRoleForGrouping(normalized.role);\n const timestamp = normalized.timestamp || Date.now();\n\n if (!currentGroup || currentGroup.role !== role) {\n if (currentGroup) result.push(currentGroup);\n currentGroup = {\n kind: \"group\",\n key: `group:${role}:${item.key}`,\n role,\n messages: [{ message: item.message, key: item.key }],\n timestamp,\n isStreaming: false,\n };\n } else {\n currentGroup.messages.push({ message: item.message, key: item.key });\n }\n }\n\n if (currentGroup) result.push(currentGroup);\n return result;\n}\n\nfunction buildChatItems(props: ChatProps): Array {\n const items: ChatItem[] = [];\n const history = Array.isArray(props.messages) ? props.messages : [];\n const tools = Array.isArray(props.toolMessages) ? props.toolMessages : [];\n const historyStart = Math.max(0, history.length - CHAT_HISTORY_RENDER_LIMIT);\n if (historyStart > 0) {\n items.push({\n kind: \"message\",\n key: \"chat:history:notice\",\n message: {\n role: \"system\",\n content: `Showing last ${CHAT_HISTORY_RENDER_LIMIT} messages (${historyStart} hidden).`,\n timestamp: Date.now(),\n },\n });\n }\n for (let i = historyStart; i < history.length; i++) {\n const msg = history[i];\n const normalized = normalizeMessage(msg);\n\n if (!props.showThinking && normalized.role.toLowerCase() === \"toolresult\") {\n continue;\n }\n\n items.push({\n kind: \"message\",\n key: messageKey(msg, i),\n message: msg,\n });\n }\n if (props.showThinking) {\n for (let i = 0; i < tools.length; i++) {\n items.push({\n kind: \"message\",\n key: messageKey(tools[i], i + history.length),\n message: tools[i],\n });\n }\n }\n\n if (props.stream !== null) {\n const key = `stream:${props.sessionKey}:${props.streamStartedAt ?? \"live\"}`;\n if (props.stream.trim().length > 0) {\n items.push({\n kind: \"stream\",\n key,\n text: props.stream,\n startedAt: props.streamStartedAt ?? Date.now(),\n });\n } else {\n items.push({ kind: \"reading-indicator\", key });\n }\n }\n\n return groupMessages(items);\n}\n\nfunction messageKey(message: unknown, index: number): string {\n const m = message as Record;\n const toolCallId = typeof m.toolCallId === \"string\" ? m.toolCallId : \"\";\n if (toolCallId) return `tool:${toolCallId}`;\n const id = typeof m.id === \"string\" ? m.id : \"\";\n if (id) return `msg:${id}`;\n const messageId = typeof m.messageId === \"string\" ? m.messageId : \"\";\n if (messageId) return `msg:${messageId}`;\n const timestamp = typeof m.timestamp === \"number\" ? m.timestamp : null;\n const role = typeof m.role === \"string\" ? m.role : \"unknown\";\n if (timestamp != null) return `msg:${role}:${timestamp}:${index}`;\n return `msg:${role}:${index}`;\n}\n","import type { ConfigUiHints } from \"../types\";\n\nexport type JsonSchema = {\n type?: string | string[];\n title?: string;\n description?: string;\n properties?: Record;\n items?: JsonSchema | JsonSchema[];\n additionalProperties?: JsonSchema | boolean;\n enum?: unknown[];\n const?: unknown;\n default?: unknown;\n anyOf?: JsonSchema[];\n oneOf?: JsonSchema[];\n allOf?: JsonSchema[];\n nullable?: boolean;\n};\n\nexport function schemaType(schema: JsonSchema): string | undefined {\n if (!schema) return undefined;\n if (Array.isArray(schema.type)) {\n const filtered = schema.type.filter((t) => t !== \"null\");\n return filtered[0] ?? schema.type[0];\n }\n return schema.type;\n}\n\nexport function defaultValue(schema?: JsonSchema): unknown {\n if (!schema) return \"\";\n if (schema.default !== undefined) return schema.default;\n const type = schemaType(schema);\n switch (type) {\n case \"object\":\n return {};\n case \"array\":\n return [];\n case \"boolean\":\n return false;\n case \"number\":\n case \"integer\":\n return 0;\n case \"string\":\n return \"\";\n default:\n return \"\";\n }\n}\n\nexport function pathKey(path: Array): string {\n return path.filter((segment) => typeof segment === \"string\").join(\".\");\n}\n\nexport function hintForPath(path: Array, hints: ConfigUiHints) {\n const key = pathKey(path);\n const direct = hints[key];\n if (direct) return direct;\n const segments = key.split(\".\");\n for (const [hintKey, hint] of Object.entries(hints)) {\n if (!hintKey.includes(\"*\")) continue;\n const hintSegments = hintKey.split(\".\");\n if (hintSegments.length !== segments.length) continue;\n let match = true;\n for (let i = 0; i < segments.length; i += 1) {\n if (hintSegments[i] !== \"*\" && hintSegments[i] !== segments[i]) {\n match = false;\n break;\n }\n }\n if (match) return hint;\n }\n return undefined;\n}\n\nexport function humanize(raw: string) {\n return raw\n .replace(/_/g, \" \")\n .replace(/([a-z0-9])([A-Z])/g, \"$1 $2\")\n .replace(/\\s+/g, \" \")\n .replace(/^./, (m) => m.toUpperCase());\n}\n\nexport function isSensitivePath(path: Array): boolean {\n const key = pathKey(path).toLowerCase();\n return (\n key.includes(\"token\") ||\n key.includes(\"password\") ||\n key.includes(\"secret\") ||\n key.includes(\"apikey\") ||\n key.endsWith(\"key\")\n );\n}\n\n","import { html, nothing, type TemplateResult } from \"lit\";\nimport type { ConfigUiHints } from \"../types\";\nimport {\n defaultValue,\n hintForPath,\n humanize,\n isSensitivePath,\n pathKey,\n schemaType,\n type JsonSchema,\n} from \"./config-form.shared\";\n\nconst META_KEYS = new Set([\"title\", \"description\", \"default\", \"nullable\"]);\n\nfunction isAnySchema(schema: JsonSchema): boolean {\n const keys = Object.keys(schema ?? {}).filter((key) => !META_KEYS.has(key));\n return keys.length === 0;\n}\n\nfunction jsonValue(value: unknown): string {\n if (value === undefined) return \"\";\n try {\n return JSON.stringify(value, null, 2) ?? \"\";\n } catch {\n return \"\";\n }\n}\n\n// SVG Icons as template literals\nconst icons = {\n chevronDown: html``,\n plus: html``,\n minus: html``,\n trash: html``,\n edit: html``,\n};\n\nexport function renderNode(params: {\n schema: JsonSchema;\n value: unknown;\n path: Array;\n hints: ConfigUiHints;\n unsupported: Set;\n disabled: boolean;\n showLabel?: boolean;\n onPatch: (path: Array, value: unknown) => void;\n}): TemplateResult | typeof nothing {\n const { schema, value, path, hints, unsupported, disabled, onPatch } = params;\n const showLabel = params.showLabel ?? true;\n const type = schemaType(schema);\n const hint = hintForPath(path, hints);\n const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));\n const help = hint?.help ?? schema.description;\n const key = pathKey(path);\n\n if (unsupported.has(key)) {\n return html`
    \n
    ${label}
    \n
    Unsupported schema node. Use Raw mode.
    \n
    `;\n }\n\n // Handle anyOf/oneOf unions\n if (schema.anyOf || schema.oneOf) {\n const variants = schema.anyOf ?? schema.oneOf ?? [];\n const nonNull = variants.filter(\n (v) => !(v.type === \"null\" || (Array.isArray(v.type) && v.type.includes(\"null\")))\n );\n\n if (nonNull.length === 1) {\n return renderNode({ ...params, schema: nonNull[0] });\n }\n\n // Check if it's a set of literal values (enum-like)\n const extractLiteral = (v: JsonSchema): unknown | undefined => {\n if (v.const !== undefined) return v.const;\n if (v.enum && v.enum.length === 1) return v.enum[0];\n return undefined;\n };\n const literals = nonNull.map(extractLiteral);\n const allLiterals = literals.every((v) => v !== undefined);\n\n if (allLiterals && literals.length > 0 && literals.length <= 5) {\n // Use segmented control for small sets\n const resolvedValue = value ?? schema.default;\n return html`\n
    \n ${showLabel ? html`` : nothing}\n ${help ? html`
    ${help}
    ` : nothing}\n
    \n ${literals.map((lit, idx) => html`\n onPatch(path, lit)}\n >\n ${String(lit)}\n \n `)}\n
    \n
    \n `;\n }\n\n if (allLiterals && literals.length > 5) {\n // Use dropdown for larger sets\n return renderSelect({ ...params, options: literals, value: value ?? schema.default });\n }\n\n // Handle mixed primitive types\n const primitiveTypes = new Set(\n nonNull.map((variant) => schemaType(variant)).filter(Boolean)\n );\n const normalizedTypes = new Set(\n [...primitiveTypes].map((v) => (v === \"integer\" ? \"number\" : v))\n );\n\n if ([...normalizedTypes].every((v) => [\"string\", \"number\", \"boolean\"].includes(v as string))) {\n const hasString = normalizedTypes.has(\"string\");\n const hasNumber = normalizedTypes.has(\"number\");\n const hasBoolean = normalizedTypes.has(\"boolean\");\n \n if (hasBoolean && normalizedTypes.size === 1) {\n return renderNode({\n ...params,\n schema: { ...schema, type: \"boolean\", anyOf: undefined, oneOf: undefined },\n });\n }\n\n if (hasString || hasNumber) {\n return renderTextInput({\n ...params,\n inputType: hasNumber && !hasString ? \"number\" : \"text\",\n });\n }\n }\n }\n\n // Enum - use segmented for small, dropdown for large\n if (schema.enum) {\n const options = schema.enum;\n if (options.length <= 5) {\n const resolvedValue = value ?? schema.default;\n return html`\n
    \n ${showLabel ? html`` : nothing}\n ${help ? html`
    ${help}
    ` : nothing}\n
    \n ${options.map((opt) => html`\n onPatch(path, opt)}\n >\n ${String(opt)}\n \n `)}\n
    \n
    \n `;\n }\n return renderSelect({ ...params, options, value: value ?? schema.default });\n }\n\n // Object type - collapsible section\n if (type === \"object\") {\n return renderObject(params);\n }\n\n // Array type\n if (type === \"array\") {\n return renderArray(params);\n }\n\n // Boolean - toggle row\n if (type === \"boolean\") {\n const displayValue = typeof value === \"boolean\" ? value : typeof schema.default === \"boolean\" ? schema.default : false;\n return html`\n \n `;\n }\n\n // Number/Integer\n if (type === \"number\" || type === \"integer\") {\n return renderNumberInput(params);\n }\n\n // String\n if (type === \"string\") {\n return renderTextInput({ ...params, inputType: \"text\" });\n }\n\n // Fallback\n return html`\n
    \n
    ${label}
    \n
    Unsupported type: ${type}. Use Raw mode.
    \n
    \n `;\n}\n\nfunction renderTextInput(params: {\n schema: JsonSchema;\n value: unknown;\n path: Array;\n hints: ConfigUiHints;\n disabled: boolean;\n showLabel?: boolean;\n inputType: \"text\" | \"number\";\n onPatch: (path: Array, value: unknown) => void;\n}): TemplateResult {\n const { schema, value, path, hints, disabled, onPatch, inputType } = params;\n const showLabel = params.showLabel ?? true;\n const hint = hintForPath(path, hints);\n const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));\n const help = hint?.help ?? schema.description;\n const isSensitive = hint?.sensitive ?? isSensitivePath(path);\n const placeholder =\n hint?.placeholder ??\n (isSensitive ? \"••••\" : schema.default !== undefined ? `Default: ${schema.default}` : \"\");\n const displayValue = value ?? \"\";\n\n return html`\n
    \n ${showLabel ? html`` : nothing}\n ${help ? html`
    ${help}
    ` : nothing}\n
    \n {\n const raw = (e.target as HTMLInputElement).value;\n if (inputType === \"number\") {\n if (raw.trim() === \"\") {\n onPatch(path, undefined);\n return;\n }\n const parsed = Number(raw);\n onPatch(path, Number.isNaN(parsed) ? raw : parsed);\n return;\n }\n onPatch(path, raw);\n }}\n />\n ${schema.default !== undefined ? html`\n onPatch(path, schema.default)}\n >↺\n ` : nothing}\n
    \n
    \n `;\n}\n\nfunction renderNumberInput(params: {\n schema: JsonSchema;\n value: unknown;\n path: Array;\n hints: ConfigUiHints;\n disabled: boolean;\n showLabel?: boolean;\n onPatch: (path: Array, value: unknown) => void;\n}): TemplateResult {\n const { schema, value, path, hints, disabled, onPatch } = params;\n const showLabel = params.showLabel ?? true;\n const hint = hintForPath(path, hints);\n const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));\n const help = hint?.help ?? schema.description;\n const displayValue = value ?? schema.default ?? \"\";\n const numValue = typeof displayValue === \"number\" ? displayValue : 0;\n\n return html`\n
    \n ${showLabel ? html`` : nothing}\n ${help ? html`
    ${help}
    ` : nothing}\n
    \n onPatch(path, numValue - 1)}\n >−\n {\n const raw = (e.target as HTMLInputElement).value;\n const parsed = raw === \"\" ? undefined : Number(raw);\n onPatch(path, parsed);\n }}\n />\n onPatch(path, numValue + 1)}\n >+\n
    \n
    \n `;\n}\n\nfunction renderSelect(params: {\n schema: JsonSchema;\n value: unknown;\n path: Array;\n hints: ConfigUiHints;\n disabled: boolean;\n showLabel?: boolean;\n options: unknown[];\n onPatch: (path: Array, value: unknown) => void;\n}): TemplateResult {\n const { schema, value, path, hints, disabled, options, onPatch } = params;\n const showLabel = params.showLabel ?? true;\n const hint = hintForPath(path, hints);\n const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));\n const help = hint?.help ?? schema.description;\n const resolvedValue = value ?? schema.default;\n const currentIndex = options.findIndex(\n (opt) => opt === resolvedValue || String(opt) === String(resolvedValue),\n );\n const unset = \"__unset__\";\n\n return html`\n
    \n ${showLabel ? html`` : nothing}\n ${help ? html`
    ${help}
    ` : nothing}\n = 0 ? String(currentIndex) : unset}\n @change=${(e: Event) => {\n const val = (e.target as HTMLSelectElement).value;\n onPatch(path, val === unset ? undefined : options[Number(val)]);\n }}\n >\n \n ${options.map((opt, idx) => html`\n \n `)}\n \n
    \n `;\n}\n\nfunction renderObject(params: {\n schema: JsonSchema;\n value: unknown;\n path: Array;\n hints: ConfigUiHints;\n unsupported: Set;\n disabled: boolean;\n showLabel?: boolean;\n onPatch: (path: Array, value: unknown) => void;\n}): TemplateResult {\n const { schema, value, path, hints, unsupported, disabled, onPatch } = params;\n const showLabel = params.showLabel ?? true;\n const hint = hintForPath(path, hints);\n const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));\n const help = hint?.help ?? schema.description;\n \n const fallback = value ?? schema.default;\n const obj = fallback && typeof fallback === \"object\" && !Array.isArray(fallback)\n ? (fallback as Record)\n : {};\n const props = schema.properties ?? {};\n const entries = Object.entries(props);\n \n // Sort by hint order\n const sorted = entries.sort((a, b) => {\n const orderA = hintForPath([...path, a[0]], hints)?.order ?? 0;\n const orderB = hintForPath([...path, b[0]], hints)?.order ?? 0;\n if (orderA !== orderB) return orderA - orderB;\n return a[0].localeCompare(b[0]);\n });\n\n const reserved = new Set(Object.keys(props));\n const additional = schema.additionalProperties;\n const allowExtra = Boolean(additional) && typeof additional === \"object\";\n\n // For top-level, don't wrap in collapsible\n if (path.length === 1) {\n return html`\n
    \n ${sorted.map(([propKey, node]) =>\n renderNode({\n schema: node,\n value: obj[propKey],\n path: [...path, propKey],\n hints,\n unsupported,\n disabled,\n onPatch,\n })\n )}\n ${allowExtra ? renderMapField({\n schema: additional as JsonSchema,\n value: obj,\n path,\n hints,\n unsupported,\n disabled,\n reservedKeys: reserved,\n onPatch,\n }) : nothing}\n
    \n `;\n }\n\n // Nested objects get collapsible treatment\n return html`\n
    \n \n ${label}\n ${icons.chevronDown}\n \n ${help ? html`
    ${help}
    ` : nothing}\n
    \n ${sorted.map(([propKey, node]) =>\n renderNode({\n schema: node,\n value: obj[propKey],\n path: [...path, propKey],\n hints,\n unsupported,\n disabled,\n onPatch,\n })\n )}\n ${allowExtra ? renderMapField({\n schema: additional as JsonSchema,\n value: obj,\n path,\n hints,\n unsupported,\n disabled,\n reservedKeys: reserved,\n onPatch,\n }) : nothing}\n
    \n
    \n `;\n}\n\nfunction renderArray(params: {\n schema: JsonSchema;\n value: unknown;\n path: Array;\n hints: ConfigUiHints;\n unsupported: Set;\n disabled: boolean;\n showLabel?: boolean;\n onPatch: (path: Array, value: unknown) => void;\n}): TemplateResult {\n const { schema, value, path, hints, unsupported, disabled, onPatch } = params;\n const showLabel = params.showLabel ?? true;\n const hint = hintForPath(path, hints);\n const label = hint?.label ?? schema.title ?? humanize(String(path.at(-1)));\n const help = hint?.help ?? schema.description;\n\n const itemsSchema = Array.isArray(schema.items) ? schema.items[0] : schema.items;\n if (!itemsSchema) {\n return html`\n
    \n
    ${label}
    \n
    Unsupported array schema. Use Raw mode.
    \n
    \n `;\n }\n\n const arr = Array.isArray(value) ? value : Array.isArray(schema.default) ? schema.default : [];\n\n return html`\n
    \n
    \n ${showLabel ? html`${label}` : nothing}\n ${arr.length} item${arr.length !== 1 ? 's' : ''}\n {\n const next = [...arr, defaultValue(itemsSchema)];\n onPatch(path, next);\n }}\n >\n ${icons.plus}\n Add\n \n
    \n ${help ? html`
    ${help}
    ` : nothing}\n \n ${arr.length === 0 ? html`\n
    \n No items yet. Click \"Add\" to create one.\n
    \n ` : html`\n
    \n ${arr.map((item, idx) => html`\n
    \n
    \n #${idx + 1}\n {\n const next = [...arr];\n next.splice(idx, 1);\n onPatch(path, next);\n }}\n >\n ${icons.trash}\n \n
    \n
    \n ${renderNode({\n schema: itemsSchema,\n value: item,\n path: [...path, idx],\n hints,\n unsupported,\n disabled,\n showLabel: false,\n onPatch,\n })}\n
    \n
    \n `)}\n
    \n `}\n
    \n `;\n}\n\nfunction renderMapField(params: {\n schema: JsonSchema;\n value: Record;\n path: Array;\n hints: ConfigUiHints;\n unsupported: Set;\n disabled: boolean;\n reservedKeys: Set;\n onPatch: (path: Array, value: unknown) => void;\n}): TemplateResult {\n const { schema, value, path, hints, unsupported, disabled, reservedKeys, onPatch } = params;\n const anySchema = isAnySchema(schema);\n const entries = Object.entries(value ?? {}).filter(([key]) => !reservedKeys.has(key));\n\n return html`\n
    \n
    \n Custom entries\n {\n const next = { ...(value ?? {}) };\n let index = 1;\n let key = `custom-${index}`;\n while (key in next) {\n index += 1;\n key = `custom-${index}`;\n }\n next[key] = anySchema ? {} : defaultValue(schema);\n onPatch(path, next);\n }}\n >\n ${icons.plus}\n Add Entry\n \n
    \n \n ${entries.length === 0 ? html`\n
    No custom entries.
    \n ` : html`\n
    \n ${entries.map(([key, entryValue]) => {\n const valuePath = [...path, key];\n const fallback = jsonValue(entryValue);\n return html`\n
    \n
    \n {\n const nextKey = (e.target as HTMLInputElement).value.trim();\n if (!nextKey || nextKey === key) return;\n const next = { ...(value ?? {}) };\n if (nextKey in next) return;\n next[nextKey] = next[key];\n delete next[key];\n onPatch(path, next);\n }}\n />\n
    \n
    \n ${anySchema\n ? html`\n {\n const target = e.target as HTMLTextAreaElement;\n const raw = target.value.trim();\n if (!raw) {\n onPatch(valuePath, undefined);\n return;\n }\n try {\n onPatch(valuePath, JSON.parse(raw));\n } catch {\n target.value = fallback;\n }\n }}\n >\n `\n : renderNode({\n schema,\n value: entryValue,\n path: valuePath,\n hints,\n unsupported,\n disabled,\n showLabel: false,\n onPatch,\n })}\n
    \n {\n const next = { ...(value ?? {}) };\n delete next[key];\n onPatch(path, next);\n }}\n >\n ${icons.trash}\n \n
    \n `;\n })}\n
    \n `}\n
    \n `;\n}\n","import { html, nothing } from \"lit\";\nimport type { ConfigUiHints } from \"../types\";\nimport { icons } from \"../icons\";\nimport {\n hintForPath,\n humanize,\n schemaType,\n type JsonSchema,\n} from \"./config-form.shared\";\nimport { renderNode } from \"./config-form.node\";\n\nexport type ConfigFormProps = {\n schema: JsonSchema | null;\n uiHints: ConfigUiHints;\n value: Record | null;\n disabled?: boolean;\n unsupportedPaths?: string[];\n searchQuery?: string;\n activeSection?: string | null;\n activeSubsection?: string | null;\n onPatch: (path: Array, value: unknown) => void;\n};\n\n// SVG Icons for section cards (Lucide-style)\nconst sectionIcons = {\n env: html``,\n update: html``,\n agents: html``,\n auth: html``,\n channels: html``,\n messages: html``,\n commands: html``,\n hooks: html``,\n skills: html``,\n tools: html``,\n gateway: html``,\n wizard: html``,\n // Additional sections\n meta: html``,\n logging: html``,\n browser: html``,\n ui: html``,\n models: html``,\n bindings: html``,\n broadcast: html``,\n audio: html``,\n session: html``,\n cron: html``,\n web: html``,\n discovery: html``,\n canvasHost: html``,\n talk: html``,\n plugins: html``,\n default: html``,\n};\n\n// Section metadata\nexport const SECTION_META: Record = {\n env: { label: \"Environment Variables\", description: \"Environment variables passed to the gateway process\" },\n update: { label: \"Updates\", description: \"Auto-update settings and release channel\" },\n agents: { label: \"Agents\", description: \"Agent configurations, models, and identities\" },\n auth: { label: \"Authentication\", description: \"API keys and authentication profiles\" },\n channels: { label: \"Channels\", description: \"Messaging channels (Telegram, Discord, Slack, etc.)\" },\n messages: { label: \"Messages\", description: \"Message handling and routing settings\" },\n commands: { label: \"Commands\", description: \"Custom slash commands\" },\n hooks: { label: \"Hooks\", description: \"Webhooks and event hooks\" },\n skills: { label: \"Skills\", description: \"Skill packs and capabilities\" },\n tools: { label: \"Tools\", description: \"Tool configurations (browser, search, etc.)\" },\n gateway: { label: \"Gateway\", description: \"Gateway server settings (port, auth, binding)\" },\n wizard: { label: \"Setup Wizard\", description: \"Setup wizard state and history\" },\n // Additional sections\n meta: { label: \"Metadata\", description: \"Gateway metadata and version information\" },\n logging: { label: \"Logging\", description: \"Log levels and output configuration\" },\n browser: { label: \"Browser\", description: \"Browser automation settings\" },\n ui: { label: \"UI\", description: \"User interface preferences\" },\n models: { label: \"Models\", description: \"AI model configurations and providers\" },\n bindings: { label: \"Bindings\", description: \"Key bindings and shortcuts\" },\n broadcast: { label: \"Broadcast\", description: \"Broadcast and notification settings\" },\n audio: { label: \"Audio\", description: \"Audio input/output settings\" },\n session: { label: \"Session\", description: \"Session management and persistence\" },\n cron: { label: \"Cron\", description: \"Scheduled tasks and automation\" },\n web: { label: \"Web\", description: \"Web server and API settings\" },\n discovery: { label: \"Discovery\", description: \"Service discovery and networking\" },\n canvasHost: { label: \"Canvas Host\", description: \"Canvas rendering and display\" },\n talk: { label: \"Talk\", description: \"Voice and speech settings\" },\n plugins: { label: \"Plugins\", description: \"Plugin management and extensions\" },\n};\n\nfunction getSectionIcon(key: string) {\n return sectionIcons[key as keyof typeof sectionIcons] ?? sectionIcons.default;\n}\n\nfunction matchesSearch(key: string, schema: JsonSchema, query: string): boolean {\n if (!query) return true;\n const q = query.toLowerCase();\n const meta = SECTION_META[key];\n \n // Check key name\n if (key.toLowerCase().includes(q)) return true;\n \n // Check label and description\n if (meta) {\n if (meta.label.toLowerCase().includes(q)) return true;\n if (meta.description.toLowerCase().includes(q)) return true;\n }\n \n return schemaMatches(schema, q);\n}\n\nfunction schemaMatches(schema: JsonSchema, query: string): boolean {\n if (schema.title?.toLowerCase().includes(query)) return true;\n if (schema.description?.toLowerCase().includes(query)) return true;\n if (schema.enum?.some((value) => String(value).toLowerCase().includes(query))) return true;\n\n if (schema.properties) {\n for (const [propKey, propSchema] of Object.entries(schema.properties)) {\n if (propKey.toLowerCase().includes(query)) return true;\n if (schemaMatches(propSchema, query)) return true;\n }\n }\n\n if (schema.items) {\n const items = Array.isArray(schema.items) ? schema.items : [schema.items];\n for (const item of items) {\n if (item && schemaMatches(item, query)) return true;\n }\n }\n\n if (schema.additionalProperties && typeof schema.additionalProperties === \"object\") {\n if (schemaMatches(schema.additionalProperties, query)) return true;\n }\n\n const unions = schema.anyOf ?? schema.oneOf ?? schema.allOf;\n if (unions) {\n for (const entry of unions) {\n if (entry && schemaMatches(entry, query)) return true;\n }\n }\n\n return false;\n}\n\nexport function renderConfigForm(props: ConfigFormProps) {\n if (!props.schema) {\n return html`
    Schema unavailable.
    `;\n }\n const schema = props.schema;\n const value = props.value ?? {};\n if (schemaType(schema) !== \"object\" || !schema.properties) {\n return html`
    Unsupported schema. Use Raw.
    `;\n }\n const unsupported = new Set(props.unsupportedPaths ?? []);\n const properties = schema.properties;\n const searchQuery = props.searchQuery ?? \"\";\n const activeSection = props.activeSection;\n const activeSubsection = props.activeSubsection ?? null;\n\n const entries = Object.entries(properties).sort((a, b) => {\n const orderA = hintForPath([a[0]], props.uiHints)?.order ?? 50;\n const orderB = hintForPath([b[0]], props.uiHints)?.order ?? 50;\n if (orderA !== orderB) return orderA - orderB;\n return a[0].localeCompare(b[0]);\n });\n\n const filteredEntries = entries.filter(([key, node]) => {\n if (activeSection && key !== activeSection) return false;\n if (searchQuery && !matchesSearch(key, node, searchQuery)) return false;\n return true;\n });\n\n let subsectionContext:\n | { sectionKey: string; subsectionKey: string; schema: JsonSchema }\n | null = null;\n if (activeSection && activeSubsection && filteredEntries.length === 1) {\n const sectionSchema = filteredEntries[0]?.[1];\n if (\n sectionSchema &&\n schemaType(sectionSchema) === \"object\" &&\n sectionSchema.properties &&\n sectionSchema.properties[activeSubsection]\n ) {\n subsectionContext = {\n sectionKey: activeSection,\n subsectionKey: activeSubsection,\n schema: sectionSchema.properties[activeSubsection],\n };\n }\n }\n\n if (filteredEntries.length === 0) {\n return html`\n
    \n
    ${icons.search}
    \n
    \n ${searchQuery \n ? `No settings match \"${searchQuery}\"` \n : \"No settings in this section\"}\n
    \n
    \n `;\n }\n\n return html`\n
    \n ${subsectionContext\n ? (() => {\n const { sectionKey, subsectionKey, schema: node } = subsectionContext;\n const hint = hintForPath([sectionKey, subsectionKey], props.uiHints);\n const label = hint?.label ?? node.title ?? humanize(subsectionKey);\n const description = hint?.help ?? node.description ?? \"\";\n const sectionValue = (value as Record)[sectionKey];\n const scopedValue =\n sectionValue && typeof sectionValue === \"object\"\n ? (sectionValue as Record)[subsectionKey]\n : undefined;\n const id = `config-section-${sectionKey}-${subsectionKey}`;\n return html`\n
    \n
    \n ${getSectionIcon(sectionKey)}\n
    \n

    ${label}

    \n ${description\n ? html`

    ${description}

    `\n : nothing}\n
    \n
    \n
    \n ${renderNode({\n schema: node,\n value: scopedValue,\n path: [sectionKey, subsectionKey],\n hints: props.uiHints,\n unsupported,\n disabled: props.disabled ?? false,\n showLabel: false,\n onPatch: props.onPatch,\n })}\n
    \n
    \n `;\n })()\n : filteredEntries.map(([key, node]) => {\n const meta = SECTION_META[key] ?? {\n label: key.charAt(0).toUpperCase() + key.slice(1),\n description: node.description ?? \"\",\n };\n\n return html`\n
    \n
    \n ${getSectionIcon(key)}\n
    \n

    ${meta.label}

    \n ${meta.description\n ? html`

    ${meta.description}

    `\n : nothing}\n
    \n
    \n
    \n ${renderNode({\n schema: node,\n value: (value as Record)[key],\n path: [key],\n hints: props.uiHints,\n unsupported,\n disabled: props.disabled ?? false,\n showLabel: false,\n onPatch: props.onPatch,\n })}\n
    \n
    \n `;\n })}\n
    \n `;\n}\n","import { pathKey, schemaType, type JsonSchema } from \"./config-form.shared\";\n\nexport type ConfigSchemaAnalysis = {\n schema: JsonSchema | null;\n unsupportedPaths: string[];\n};\n\nconst META_KEYS = new Set([\"title\", \"description\", \"default\", \"nullable\"]);\n\nfunction isAnySchema(schema: JsonSchema): boolean {\n const keys = Object.keys(schema ?? {}).filter((key) => !META_KEYS.has(key));\n return keys.length === 0;\n}\n\nfunction normalizeEnum(values: unknown[]): { enumValues: unknown[]; nullable: boolean } {\n const filtered = values.filter((value) => value != null);\n const nullable = filtered.length !== values.length;\n const enumValues: unknown[] = [];\n for (const value of filtered) {\n if (!enumValues.some((existing) => Object.is(existing, value))) {\n enumValues.push(value);\n }\n }\n return { enumValues, nullable };\n}\n\nexport function analyzeConfigSchema(raw: unknown): ConfigSchemaAnalysis {\n if (!raw || typeof raw !== \"object\") {\n return { schema: null, unsupportedPaths: [\"\"] };\n }\n return normalizeSchemaNode(raw as JsonSchema, []);\n}\n\nfunction normalizeSchemaNode(\n schema: JsonSchema,\n path: Array,\n): ConfigSchemaAnalysis {\n const unsupported = new Set();\n const normalized: JsonSchema = { ...schema };\n const pathLabel = pathKey(path) || \"\";\n\n if (schema.anyOf || schema.oneOf || schema.allOf) {\n const union = normalizeUnion(schema, path);\n if (union) return union;\n return { schema, unsupportedPaths: [pathLabel] };\n }\n\n const nullable = Array.isArray(schema.type) && schema.type.includes(\"null\");\n const type =\n schemaType(schema) ??\n (schema.properties || schema.additionalProperties ? \"object\" : undefined);\n normalized.type = type ?? schema.type;\n normalized.nullable = nullable || schema.nullable;\n\n if (normalized.enum) {\n const { enumValues, nullable: enumNullable } = normalizeEnum(normalized.enum);\n normalized.enum = enumValues;\n if (enumNullable) normalized.nullable = true;\n if (enumValues.length === 0) unsupported.add(pathLabel);\n }\n\n if (type === \"object\") {\n const properties = schema.properties ?? {};\n const normalizedProps: Record = {};\n for (const [key, value] of Object.entries(properties)) {\n const res = normalizeSchemaNode(value, [...path, key]);\n if (res.schema) normalizedProps[key] = res.schema;\n for (const entry of res.unsupportedPaths) unsupported.add(entry);\n }\n normalized.properties = normalizedProps;\n\n if (schema.additionalProperties === true) {\n unsupported.add(pathLabel);\n } else if (schema.additionalProperties === false) {\n normalized.additionalProperties = false;\n } else if (\n schema.additionalProperties &&\n typeof schema.additionalProperties === \"object\"\n ) {\n if (!isAnySchema(schema.additionalProperties as JsonSchema)) {\n const res = normalizeSchemaNode(\n schema.additionalProperties as JsonSchema,\n [...path, \"*\"],\n );\n normalized.additionalProperties =\n res.schema ?? (schema.additionalProperties as JsonSchema);\n if (res.unsupportedPaths.length > 0) unsupported.add(pathLabel);\n }\n }\n } else if (type === \"array\") {\n const itemsSchema = Array.isArray(schema.items)\n ? schema.items[0]\n : schema.items;\n if (!itemsSchema) {\n unsupported.add(pathLabel);\n } else {\n const res = normalizeSchemaNode(itemsSchema, [...path, \"*\"]);\n normalized.items = res.schema ?? itemsSchema;\n if (res.unsupportedPaths.length > 0) unsupported.add(pathLabel);\n }\n } else if (\n type !== \"string\" &&\n type !== \"number\" &&\n type !== \"integer\" &&\n type !== \"boolean\" &&\n !normalized.enum\n ) {\n unsupported.add(pathLabel);\n }\n\n return {\n schema: normalized,\n unsupportedPaths: Array.from(unsupported),\n };\n}\n\nfunction normalizeUnion(\n schema: JsonSchema,\n path: Array,\n): ConfigSchemaAnalysis | null {\n if (schema.allOf) return null;\n const union = schema.anyOf ?? schema.oneOf;\n if (!union) return null;\n\n const literals: unknown[] = [];\n const remaining: JsonSchema[] = [];\n let nullable = false;\n\n for (const entry of union) {\n if (!entry || typeof entry !== \"object\") return null;\n if (Array.isArray(entry.enum)) {\n const { enumValues, nullable: enumNullable } = normalizeEnum(entry.enum);\n literals.push(...enumValues);\n if (enumNullable) nullable = true;\n continue;\n }\n if (\"const\" in entry) {\n if (entry.const == null) {\n nullable = true;\n continue;\n }\n literals.push(entry.const);\n continue;\n }\n if (schemaType(entry) === \"null\") {\n nullable = true;\n continue;\n }\n remaining.push(entry);\n }\n\n if (literals.length > 0 && remaining.length === 0) {\n const unique: unknown[] = [];\n for (const value of literals) {\n if (!unique.some((existing) => Object.is(existing, value))) {\n unique.push(value);\n }\n }\n return {\n schema: {\n ...schema,\n enum: unique,\n nullable,\n anyOf: undefined,\n oneOf: undefined,\n allOf: undefined,\n },\n unsupportedPaths: [],\n };\n }\n\n if (remaining.length === 1) {\n const res = normalizeSchemaNode(remaining[0], path);\n if (res.schema) {\n res.schema.nullable = nullable || res.schema.nullable;\n }\n return res;\n }\n\n const primitiveTypes = [\"string\", \"number\", \"integer\", \"boolean\"];\n if (\n remaining.length > 0 &&\n literals.length === 0 &&\n remaining.every((entry) => entry.type && primitiveTypes.includes(String(entry.type)))\n ) {\n return {\n schema: {\n ...schema,\n nullable,\n },\n unsupportedPaths: [],\n };\n }\n\n return null;\n}\n","import { html, nothing } from \"lit\";\nimport type { ConfigUiHints } from \"../types\";\nimport { analyzeConfigSchema, renderConfigForm, SECTION_META } from \"./config-form\";\nimport {\n hintForPath,\n humanize,\n schemaType,\n type JsonSchema,\n} from \"./config-form.shared\";\n\nexport type ConfigProps = {\n raw: string;\n originalRaw: string;\n valid: boolean | null;\n issues: unknown[];\n loading: boolean;\n saving: boolean;\n applying: boolean;\n updating: boolean;\n connected: boolean;\n schema: unknown | null;\n schemaLoading: boolean;\n uiHints: ConfigUiHints;\n formMode: \"form\" | \"raw\";\n formValue: Record | null;\n originalValue: Record | null;\n searchQuery: string;\n activeSection: string | null;\n activeSubsection: string | null;\n onRawChange: (next: string) => void;\n onFormModeChange: (mode: \"form\" | \"raw\") => void;\n onFormPatch: (path: Array, value: unknown) => void;\n onSearchChange: (query: string) => void;\n onSectionChange: (section: string | null) => void;\n onSubsectionChange: (section: string | null) => void;\n onReload: () => void;\n onSave: () => void;\n onApply: () => void;\n onUpdate: () => void;\n};\n\n// SVG Icons for sidebar (Lucide-style)\nconst sidebarIcons = {\n all: html``,\n env: html``,\n update: html``,\n agents: html``,\n auth: html``,\n channels: html``,\n messages: html``,\n commands: html``,\n hooks: html``,\n skills: html``,\n tools: html``,\n gateway: html``,\n wizard: html``,\n // Additional sections\n meta: html``,\n logging: html``,\n browser: html``,\n ui: html``,\n models: html``,\n bindings: html``,\n broadcast: html``,\n audio: html``,\n session: html``,\n cron: html``,\n web: html``,\n discovery: html``,\n canvasHost: html``,\n talk: html``,\n plugins: html``,\n default: html``,\n};\n\n// Section definitions\nconst SECTIONS: Array<{ key: string; label: string }> = [\n { key: \"env\", label: \"Environment\" },\n { key: \"update\", label: \"Updates\" },\n { key: \"agents\", label: \"Agents\" },\n { key: \"auth\", label: \"Authentication\" },\n { key: \"channels\", label: \"Channels\" },\n { key: \"messages\", label: \"Messages\" },\n { key: \"commands\", label: \"Commands\" },\n { key: \"hooks\", label: \"Hooks\" },\n { key: \"skills\", label: \"Skills\" },\n { key: \"tools\", label: \"Tools\" },\n { key: \"gateway\", label: \"Gateway\" },\n { key: \"wizard\", label: \"Setup Wizard\" },\n];\n\ntype SubsectionEntry = {\n key: string;\n label: string;\n description?: string;\n order: number;\n};\n\nconst ALL_SUBSECTION = \"__all__\";\n\nfunction getSectionIcon(key: string) {\n return sidebarIcons[key as keyof typeof sidebarIcons] ?? sidebarIcons.default;\n}\n\nfunction resolveSectionMeta(key: string, schema?: JsonSchema): {\n label: string;\n description?: string;\n} {\n const meta = SECTION_META[key];\n if (meta) return meta;\n return {\n label: schema?.title ?? humanize(key),\n description: schema?.description ?? \"\",\n };\n}\n\nfunction resolveSubsections(params: {\n key: string;\n schema: JsonSchema | undefined;\n uiHints: ConfigUiHints;\n}): SubsectionEntry[] {\n const { key, schema, uiHints } = params;\n if (!schema || schemaType(schema) !== \"object\" || !schema.properties) return [];\n const entries = Object.entries(schema.properties).map(([subKey, node]) => {\n const hint = hintForPath([key, subKey], uiHints);\n const label = hint?.label ?? node.title ?? humanize(subKey);\n const description = hint?.help ?? node.description ?? \"\";\n const order = hint?.order ?? 50;\n return { key: subKey, label, description, order };\n });\n entries.sort((a, b) => (a.order !== b.order ? a.order - b.order : a.key.localeCompare(b.key)));\n return entries;\n}\n\nfunction computeDiff(\n original: Record | null,\n current: Record | null\n): Array<{ path: string; from: unknown; to: unknown }> {\n if (!original || !current) return [];\n const changes: Array<{ path: string; from: unknown; to: unknown }> = [];\n \n function compare(orig: unknown, curr: unknown, path: string) {\n if (orig === curr) return;\n if (typeof orig !== typeof curr) {\n changes.push({ path, from: orig, to: curr });\n return;\n }\n if (typeof orig !== \"object\" || orig === null || curr === null) {\n if (orig !== curr) {\n changes.push({ path, from: orig, to: curr });\n }\n return;\n }\n if (Array.isArray(orig) && Array.isArray(curr)) {\n if (JSON.stringify(orig) !== JSON.stringify(curr)) {\n changes.push({ path, from: orig, to: curr });\n }\n return;\n }\n const origObj = orig as Record;\n const currObj = curr as Record;\n const allKeys = new Set([...Object.keys(origObj), ...Object.keys(currObj)]);\n for (const key of allKeys) {\n compare(origObj[key], currObj[key], path ? `${path}.${key}` : key);\n }\n }\n \n compare(original, current, \"\");\n return changes;\n}\n\nfunction truncateValue(value: unknown, maxLen = 40): string {\n let str: string;\n try {\n const json = JSON.stringify(value);\n str = json ?? String(value);\n } catch {\n str = String(value);\n }\n if (str.length <= maxLen) return str;\n return str.slice(0, maxLen - 3) + \"...\";\n}\n\nexport function renderConfig(props: ConfigProps) {\n const validity =\n props.valid == null ? \"unknown\" : props.valid ? \"valid\" : \"invalid\";\n const analysis = analyzeConfigSchema(props.schema);\n const formUnsafe = analysis.schema\n ? analysis.unsupportedPaths.length > 0\n : false;\n\n // Get available sections from schema\n const schemaProps = analysis.schema?.properties ?? {};\n const availableSections = SECTIONS.filter(s => s.key in schemaProps);\n\n // Add any sections in schema but not in our list\n const knownKeys = new Set(SECTIONS.map(s => s.key));\n const extraSections = Object.keys(schemaProps)\n .filter(k => !knownKeys.has(k))\n .map(k => ({ key: k, label: k.charAt(0).toUpperCase() + k.slice(1) }));\n\n const allSections = [...availableSections, ...extraSections];\n\n const activeSectionSchema =\n props.activeSection && analysis.schema && schemaType(analysis.schema) === \"object\"\n ? (analysis.schema.properties?.[props.activeSection] as JsonSchema | undefined)\n : undefined;\n const activeSectionMeta = props.activeSection\n ? resolveSectionMeta(props.activeSection, activeSectionSchema)\n : null;\n const subsections = props.activeSection\n ? resolveSubsections({\n key: props.activeSection,\n schema: activeSectionSchema,\n uiHints: props.uiHints,\n })\n : [];\n const allowSubnav =\n props.formMode === \"form\" &&\n Boolean(props.activeSection) &&\n subsections.length > 0;\n const isAllSubsection = props.activeSubsection === ALL_SUBSECTION;\n const effectiveSubsection = props.searchQuery\n ? null\n : isAllSubsection\n ? null\n : props.activeSubsection ?? (subsections[0]?.key ?? null);\n\n // Compute diff for showing changes (works for both form and raw modes)\n const diff = props.formMode === \"form\"\n ? computeDiff(props.originalValue, props.formValue)\n : [];\n const hasRawChanges = props.formMode === \"raw\" && props.raw !== props.originalRaw;\n const hasChanges = props.formMode === \"form\" ? diff.length > 0 : hasRawChanges;\n\n // Save/apply buttons require actual changes to be enabled.\n // Note: formUnsafe warns about unsupported schema paths but shouldn't block saving.\n const canSaveForm =\n Boolean(props.formValue) && !props.loading && Boolean(analysis.schema);\n const canSave =\n props.connected &&\n !props.saving &&\n hasChanges &&\n (props.formMode === \"raw\" ? true : canSaveForm);\n const canApply =\n props.connected &&\n !props.applying &&\n !props.updating &&\n hasChanges &&\n (props.formMode === \"raw\" ? true : canSaveForm);\n const canUpdate = props.connected && !props.applying && !props.updating;\n\n return html`\n
    \n \n \n \n \n
    \n \n
    \n
    \n ${hasChanges ? html`\n ${props.formMode === \"raw\" ? \"Unsaved changes\" : `${diff.length} unsaved change${diff.length !== 1 ? \"s\" : \"\"}`}\n ` : html`\n No changes\n `}\n
    \n
    \n \n \n ${props.saving ? \"Saving…\" : \"Save\"}\n \n \n ${props.applying ? \"Applying…\" : \"Apply\"}\n \n \n ${props.updating ? \"Updating…\" : \"Update\"}\n \n
    \n
    \n \n \n ${hasChanges && props.formMode === \"form\" ? html`\n
    \n \n View ${diff.length} pending change${diff.length !== 1 ? \"s\" : \"\"}\n \n \n \n \n
    \n ${diff.map(change => html`\n
    \n
    ${change.path}
    \n
    \n ${truncateValue(change.from)}\n \n ${truncateValue(change.to)}\n
    \n
    \n `)}\n
    \n
    \n ` : nothing}\n\n ${activeSectionMeta && props.formMode === \"form\"\n ? html`\n
    \n
    ${getSectionIcon(props.activeSection ?? \"\")}
    \n
    \n
    ${activeSectionMeta.label}
    \n ${activeSectionMeta.description\n ? html`
    ${activeSectionMeta.description}
    `\n : nothing}\n
    \n
    \n `\n : nothing}\n\n ${allowSubnav\n ? html`\n
    \n props.onSubsectionChange(ALL_SUBSECTION)}\n >\n All\n \n ${subsections.map(\n (entry) => html`\n props.onSubsectionChange(entry.key)}\n >\n ${entry.label}\n \n `,\n )}\n
    \n `\n : nothing}\n\n \n
    \n ${props.formMode === \"form\"\n ? html`\n ${props.schemaLoading\n ? html`
    \n
    \n Loading schema…\n
    `\n : renderConfigForm({\n schema: analysis.schema,\n uiHints: props.uiHints,\n value: props.formValue,\n disabled: props.loading || !props.formValue,\n unsupportedPaths: analysis.unsupportedPaths,\n onPatch: props.onFormPatch,\n searchQuery: props.searchQuery,\n activeSection: props.activeSection,\n activeSubsection: effectiveSubsection,\n })}\n ${formUnsafe\n ? html`
    \n Form view can't safely edit some fields.\n Use Raw to avoid losing config entries.\n
    `\n : nothing}\n `\n : html`\n \n `}\n
    \n\n ${props.issues.length > 0\n ? html`
    \n
    ${JSON.stringify(props.issues, null, 2)}
    \n
    `\n : nothing}\n
    \n
    \n `;\n}\n","import { html, nothing } from \"lit\";\n\nimport type { ChannelAccountSnapshot } from \"../types\";\nimport type { ChannelKey, ChannelsProps } from \"./channels.types\";\n\nexport function formatDuration(ms?: number | null) {\n if (!ms && ms !== 0) return \"n/a\";\n const sec = Math.round(ms / 1000);\n if (sec < 60) return `${sec}s`;\n const min = Math.round(sec / 60);\n if (min < 60) return `${min}m`;\n const hr = Math.round(min / 60);\n return `${hr}h`;\n}\n\nexport function channelEnabled(key: ChannelKey, props: ChannelsProps) {\n const snapshot = props.snapshot;\n const channels = snapshot?.channels as Record | null;\n if (!snapshot || !channels) return false;\n const channelStatus = channels[key] as Record | undefined;\n const configured = typeof channelStatus?.configured === \"boolean\" && channelStatus.configured;\n const running = typeof channelStatus?.running === \"boolean\" && channelStatus.running;\n const connected = typeof channelStatus?.connected === \"boolean\" && channelStatus.connected;\n const accounts = snapshot.channelAccounts?.[key] ?? [];\n const accountActive = accounts.some(\n (account) => account.configured || account.running || account.connected,\n );\n return configured || running || connected || accountActive;\n}\n\nexport function getChannelAccountCount(\n key: ChannelKey,\n channelAccounts?: Record | null,\n): number {\n return channelAccounts?.[key]?.length ?? 0;\n}\n\nexport function renderChannelAccountCount(\n key: ChannelKey,\n channelAccounts?: Record | null,\n) {\n const count = getChannelAccountCount(key, channelAccounts);\n if (count < 2) return nothing;\n return html`
    Accounts (${count})
    `;\n}\n\n","import { html } from \"lit\";\n\nimport type { ConfigUiHints } from \"../types\";\nimport type { ChannelsProps } from \"./channels.types\";\nimport {\n analyzeConfigSchema,\n renderNode,\n schemaType,\n type JsonSchema,\n} from \"./config-form\";\n\ntype ChannelConfigFormProps = {\n channelId: string;\n configValue: Record | null;\n schema: unknown | null;\n uiHints: ConfigUiHints;\n disabled: boolean;\n onPatch: (path: Array, value: unknown) => void;\n};\n\nfunction resolveSchemaNode(\n schema: JsonSchema | null,\n path: Array,\n): JsonSchema | null {\n let current = schema;\n for (const key of path) {\n if (!current) return null;\n const type = schemaType(current);\n if (type === \"object\") {\n const properties = current.properties ?? {};\n if (typeof key === \"string\" && properties[key]) {\n current = properties[key];\n continue;\n }\n const additional = current.additionalProperties;\n if (typeof key === \"string\" && additional && typeof additional === \"object\") {\n current = additional as JsonSchema;\n continue;\n }\n return null;\n }\n if (type === \"array\") {\n if (typeof key !== \"number\") return null;\n const items = Array.isArray(current.items) ? current.items[0] : current.items;\n current = items ?? null;\n continue;\n }\n return null;\n }\n return current;\n}\n\nfunction resolveChannelValue(\n config: Record,\n channelId: string,\n): Record {\n const channels = (config.channels ?? {}) as Record;\n const fromChannels = channels[channelId];\n const fallback = config[channelId];\n const resolved =\n (fromChannels && typeof fromChannels === \"object\"\n ? (fromChannels as Record)\n : null) ??\n (fallback && typeof fallback === \"object\"\n ? (fallback as Record)\n : null);\n return resolved ?? {};\n}\n\nexport function renderChannelConfigForm(props: ChannelConfigFormProps) {\n const analysis = analyzeConfigSchema(props.schema);\n const normalized = analysis.schema;\n if (!normalized) {\n return html`
    Schema unavailable. Use Raw.
    `;\n }\n const node = resolveSchemaNode(normalized, [\"channels\", props.channelId]);\n if (!node) {\n return html`
    Channel config schema unavailable.
    `;\n }\n const configValue = props.configValue ?? {};\n const value = resolveChannelValue(configValue, props.channelId);\n return html`\n
    \n ${renderNode({\n schema: node,\n value,\n path: [\"channels\", props.channelId],\n hints: props.uiHints,\n unsupported: new Set(analysis.unsupportedPaths),\n disabled: props.disabled,\n showLabel: false,\n onPatch: props.onPatch,\n })}\n
    \n `;\n}\n\nexport function renderChannelConfigSection(params: {\n channelId: string;\n props: ChannelsProps;\n}) {\n const { channelId, props } = params;\n const disabled = props.configSaving || props.configSchemaLoading;\n return html`\n
    \n ${props.configSchemaLoading\n ? html`
    Loading config schema…
    `\n : renderChannelConfigForm({\n channelId,\n configValue: props.configForm,\n schema: props.configSchema,\n uiHints: props.configUiHints,\n disabled,\n onPatch: props.onConfigPatch,\n })}\n
    \n props.onConfigSave()}\n >\n ${props.configSaving ? \"Saving…\" : \"Save\"}\n \n props.onConfigReload()}\n >\n Reload\n \n
    \n
    \n `;\n}\n","import { html, nothing } from \"lit\";\n\nimport { formatAgo } from \"../format\";\nimport type { DiscordStatus } from \"../types\";\nimport type { ChannelsProps } from \"./channels.types\";\nimport { renderChannelConfigSection } from \"./channels.config\";\n\nexport function renderDiscordCard(params: {\n props: ChannelsProps;\n discord?: DiscordStatus | null;\n accountCountLabel: unknown;\n}) {\n const { props, discord, accountCountLabel } = params;\n\n return html`\n
    \n
    Discord
    \n
    Bot status and channel configuration.
    \n ${accountCountLabel}\n\n
    \n
    \n Configured\n ${discord?.configured ? \"Yes\" : \"No\"}\n
    \n
    \n Running\n ${discord?.running ? \"Yes\" : \"No\"}\n
    \n
    \n Last start\n ${discord?.lastStartAt ? formatAgo(discord.lastStartAt) : \"n/a\"}\n
    \n
    \n Last probe\n ${discord?.lastProbeAt ? formatAgo(discord.lastProbeAt) : \"n/a\"}\n
    \n
    \n\n ${discord?.lastError\n ? html`
    \n ${discord.lastError}\n
    `\n : nothing}\n\n ${discord?.probe\n ? html`
    \n Probe ${discord.probe.ok ? \"ok\" : \"failed\"} ·\n ${discord.probe.status ?? \"\"} ${discord.probe.error ?? \"\"}\n
    `\n : nothing}\n\n ${renderChannelConfigSection({ channelId: \"discord\", props })}\n\n
    \n \n
    \n
    \n `;\n}\n","import { html, nothing } from \"lit\";\n\nimport { formatAgo } from \"../format\";\nimport type { GoogleChatStatus } from \"../types\";\nimport { renderChannelConfigSection } from \"./channels.config\";\nimport type { ChannelsProps } from \"./channels.types\";\n\nexport function renderGoogleChatCard(params: {\n props: ChannelsProps;\n googleChat?: GoogleChatStatus | null;\n accountCountLabel: unknown;\n}) {\n const { props, googleChat, accountCountLabel } = params;\n\n return html`\n
    \n
    Google Chat
    \n
    Chat API webhook status and channel configuration.
    \n ${accountCountLabel}\n\n
    \n
    \n Configured\n ${googleChat ? (googleChat.configured ? \"Yes\" : \"No\") : \"n/a\"}\n
    \n
    \n Running\n ${googleChat ? (googleChat.running ? \"Yes\" : \"No\") : \"n/a\"}\n
    \n
    \n Credential\n ${googleChat?.credentialSource ?? \"n/a\"}\n
    \n
    \n Audience\n \n ${googleChat?.audienceType\n ? `${googleChat.audienceType}${googleChat.audience ? ` · ${googleChat.audience}` : \"\"}`\n : \"n/a\"}\n \n
    \n
    \n Last start\n ${googleChat?.lastStartAt ? formatAgo(googleChat.lastStartAt) : \"n/a\"}\n
    \n
    \n Last probe\n ${googleChat?.lastProbeAt ? formatAgo(googleChat.lastProbeAt) : \"n/a\"}\n
    \n
    \n\n ${googleChat?.lastError\n ? html`
    \n ${googleChat.lastError}\n
    `\n : nothing}\n\n ${googleChat?.probe\n ? html`
    \n Probe ${googleChat.probe.ok ? \"ok\" : \"failed\"} ·\n ${googleChat.probe.status ?? \"\"} ${googleChat.probe.error ?? \"\"}\n
    `\n : nothing}\n\n ${renderChannelConfigSection({ channelId: \"googlechat\", props })}\n\n
    \n \n
    \n
    \n `;\n}\n","import { html, nothing } from \"lit\";\n\nimport { formatAgo } from \"../format\";\nimport type { IMessageStatus } from \"../types\";\nimport type { ChannelsProps } from \"./channels.types\";\nimport { renderChannelConfigSection } from \"./channels.config\";\n\nexport function renderIMessageCard(params: {\n props: ChannelsProps;\n imessage?: IMessageStatus | null;\n accountCountLabel: unknown;\n}) {\n const { props, imessage, accountCountLabel } = params;\n\n return html`\n
    \n
    iMessage
    \n
    macOS bridge status and channel configuration.
    \n ${accountCountLabel}\n\n
    \n
    \n Configured\n ${imessage?.configured ? \"Yes\" : \"No\"}\n
    \n
    \n Running\n ${imessage?.running ? \"Yes\" : \"No\"}\n
    \n
    \n Last start\n ${imessage?.lastStartAt ? formatAgo(imessage.lastStartAt) : \"n/a\"}\n
    \n
    \n Last probe\n ${imessage?.lastProbeAt ? formatAgo(imessage.lastProbeAt) : \"n/a\"}\n
    \n
    \n\n ${imessage?.lastError\n ? html`
    \n ${imessage.lastError}\n
    `\n : nothing}\n\n ${imessage?.probe\n ? html`
    \n Probe ${imessage.probe.ok ? \"ok\" : \"failed\"} ·\n ${imessage.probe.error ?? \"\"}\n
    `\n : nothing}\n\n ${renderChannelConfigSection({ channelId: \"imessage\", props })}\n\n
    \n \n
    \n
    \n `;\n}\n","/**\n * Nostr Profile Edit Form\n *\n * Provides UI for editing and publishing Nostr profile (kind:0).\n */\n\nimport { html, nothing, type TemplateResult } from \"lit\";\n\nimport type { NostrProfile as NostrProfileType } from \"../types\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface NostrProfileFormState {\n /** Current form values */\n values: NostrProfileType;\n /** Original values for dirty detection */\n original: NostrProfileType;\n /** Whether the form is currently submitting */\n saving: boolean;\n /** Whether import is in progress */\n importing: boolean;\n /** Last error message */\n error: string | null;\n /** Last success message */\n success: string | null;\n /** Validation errors per field */\n fieldErrors: Record;\n /** Whether to show advanced fields */\n showAdvanced: boolean;\n}\n\nexport interface NostrProfileFormCallbacks {\n /** Called when a field value changes */\n onFieldChange: (field: keyof NostrProfileType, value: string) => void;\n /** Called when save is clicked */\n onSave: () => void;\n /** Called when import is clicked */\n onImport: () => void;\n /** Called when cancel is clicked */\n onCancel: () => void;\n /** Called when toggle advanced is clicked */\n onToggleAdvanced: () => void;\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction isFormDirty(state: NostrProfileFormState): boolean {\n const { values, original } = state;\n return (\n values.name !== original.name ||\n values.displayName !== original.displayName ||\n values.about !== original.about ||\n values.picture !== original.picture ||\n values.banner !== original.banner ||\n values.website !== original.website ||\n values.nip05 !== original.nip05 ||\n values.lud16 !== original.lud16\n );\n}\n\n// ============================================================================\n// Form Rendering\n// ============================================================================\n\nexport function renderNostrProfileForm(params: {\n state: NostrProfileFormState;\n callbacks: NostrProfileFormCallbacks;\n accountId: string;\n}): TemplateResult {\n const { state, callbacks, accountId } = params;\n const isDirty = isFormDirty(state);\n\n const renderField = (\n field: keyof NostrProfileType,\n label: string,\n opts: {\n type?: \"text\" | \"url\" | \"textarea\";\n placeholder?: string;\n maxLength?: number;\n help?: string;\n } = {}\n ) => {\n const { type = \"text\", placeholder, maxLength, help } = opts;\n const value = state.values[field] ?? \"\";\n const error = state.fieldErrors[field];\n\n const inputId = `nostr-profile-${field}`;\n\n if (type === \"textarea\") {\n return html`\n
    \n \n {\n const target = e.target as HTMLTextAreaElement;\n callbacks.onFieldChange(field, target.value);\n }}\n ?disabled=${state.saving}\n >\n ${help ? html`
    ${help}
    ` : nothing}\n ${error ? html`
    ${error}
    ` : nothing}\n
    \n `;\n }\n\n return html`\n
    \n \n {\n const target = e.target as HTMLInputElement;\n callbacks.onFieldChange(field, target.value);\n }}\n ?disabled=${state.saving}\n />\n ${help ? html`
    ${help}
    ` : nothing}\n ${error ? html`
    ${error}
    ` : nothing}\n
    \n `;\n };\n\n const renderPicturePreview = () => {\n const picture = state.values.picture;\n if (!picture) return nothing;\n\n return html`\n
    \n {\n const img = e.target as HTMLImageElement;\n img.style.display = \"none\";\n }}\n @load=${(e: Event) => {\n const img = e.target as HTMLImageElement;\n img.style.display = \"block\";\n }}\n />\n
    \n `;\n };\n\n return html`\n
    \n
    \n
    Edit Profile
    \n
    Account: ${accountId}
    \n
    \n\n ${state.error\n ? html`
    ${state.error}
    `\n : nothing}\n\n ${state.success\n ? html`
    ${state.success}
    `\n : nothing}\n\n ${renderPicturePreview()}\n\n ${renderField(\"name\", \"Username\", {\n placeholder: \"satoshi\",\n maxLength: 256,\n help: \"Short username (e.g., satoshi)\",\n })}\n\n ${renderField(\"displayName\", \"Display Name\", {\n placeholder: \"Satoshi Nakamoto\",\n maxLength: 256,\n help: \"Your full display name\",\n })}\n\n ${renderField(\"about\", \"Bio\", {\n type: \"textarea\",\n placeholder: \"Tell people about yourself...\",\n maxLength: 2000,\n help: \"A brief bio or description\",\n })}\n\n ${renderField(\"picture\", \"Avatar URL\", {\n type: \"url\",\n placeholder: \"https://example.com/avatar.jpg\",\n help: \"HTTPS URL to your profile picture\",\n })}\n\n ${state.showAdvanced\n ? html`\n
    \n
    Advanced
    \n\n ${renderField(\"banner\", \"Banner URL\", {\n type: \"url\",\n placeholder: \"https://example.com/banner.jpg\",\n help: \"HTTPS URL to a banner image\",\n })}\n\n ${renderField(\"website\", \"Website\", {\n type: \"url\",\n placeholder: \"https://example.com\",\n help: \"Your personal website\",\n })}\n\n ${renderField(\"nip05\", \"NIP-05 Identifier\", {\n placeholder: \"you@example.com\",\n help: \"Verifiable identifier (e.g., you@domain.com)\",\n })}\n\n ${renderField(\"lud16\", \"Lightning Address\", {\n placeholder: \"you@getalby.com\",\n help: \"Lightning address for tips (LUD-16)\",\n })}\n
    \n `\n : nothing}\n\n
    \n \n ${state.saving ? \"Saving...\" : \"Save & Publish\"}\n \n\n \n ${state.importing ? \"Importing...\" : \"Import from Relays\"}\n \n\n \n ${state.showAdvanced ? \"Hide Advanced\" : \"Show Advanced\"}\n \n\n \n Cancel\n \n
    \n\n ${isDirty\n ? html`
    \n You have unsaved changes\n
    `\n : nothing}\n
    \n `;\n}\n\n// ============================================================================\n// Factory\n// ============================================================================\n\n/**\n * Create initial form state from existing profile\n */\nexport function createNostrProfileFormState(\n profile: NostrProfileType | undefined\n): NostrProfileFormState {\n const values: NostrProfileType = {\n name: profile?.name ?? \"\",\n displayName: profile?.displayName ?? \"\",\n about: profile?.about ?? \"\",\n picture: profile?.picture ?? \"\",\n banner: profile?.banner ?? \"\",\n website: profile?.website ?? \"\",\n nip05: profile?.nip05 ?? \"\",\n lud16: profile?.lud16 ?? \"\",\n };\n\n return {\n values,\n original: { ...values },\n saving: false,\n importing: false,\n error: null,\n success: null,\n fieldErrors: {},\n showAdvanced: Boolean(\n profile?.banner || profile?.website || profile?.nip05 || profile?.lud16\n ),\n };\n}\n","import { html, nothing } from \"lit\";\n\nimport { formatAgo } from \"../format\";\nimport type { ChannelAccountSnapshot, NostrStatus } from \"../types\";\nimport type { ChannelsProps } from \"./channels.types\";\nimport { renderChannelConfigSection } from \"./channels.config\";\nimport {\n renderNostrProfileForm,\n type NostrProfileFormState,\n type NostrProfileFormCallbacks,\n} from \"./channels.nostr-profile-form\";\n\n/**\n * Truncate a pubkey for display (shows first and last 8 chars)\n */\nfunction truncatePubkey(pubkey: string | null | undefined): string {\n if (!pubkey) return \"n/a\";\n if (pubkey.length <= 20) return pubkey;\n return `${pubkey.slice(0, 8)}...${pubkey.slice(-8)}`;\n}\n\nexport function renderNostrCard(params: {\n props: ChannelsProps;\n nostr?: NostrStatus | null;\n nostrAccounts: ChannelAccountSnapshot[];\n accountCountLabel: unknown;\n /** Profile form state (optional - if provided, shows form) */\n profileFormState?: NostrProfileFormState | null;\n /** Profile form callbacks */\n profileFormCallbacks?: NostrProfileFormCallbacks | null;\n /** Called when Edit Profile is clicked */\n onEditProfile?: () => void;\n}) {\n const {\n props,\n nostr,\n nostrAccounts,\n accountCountLabel,\n profileFormState,\n profileFormCallbacks,\n onEditProfile,\n } = params;\n const primaryAccount = nostrAccounts[0];\n const summaryConfigured = nostr?.configured ?? primaryAccount?.configured ?? false;\n const summaryRunning = nostr?.running ?? primaryAccount?.running ?? false;\n const summaryPublicKey =\n nostr?.publicKey ??\n (primaryAccount as { publicKey?: string } | undefined)?.publicKey;\n const summaryLastStartAt = nostr?.lastStartAt ?? primaryAccount?.lastStartAt ?? null;\n const summaryLastError = nostr?.lastError ?? primaryAccount?.lastError ?? null;\n const hasMultipleAccounts = nostrAccounts.length > 1;\n const showingForm = profileFormState !== null && profileFormState !== undefined;\n\n const renderAccountCard = (account: ChannelAccountSnapshot) => {\n const publicKey = (account as { publicKey?: string }).publicKey;\n const profile = (account as { profile?: { name?: string; displayName?: string } }).profile;\n const displayName = profile?.displayName ?? profile?.name ?? account.name ?? account.accountId;\n\n return html`\n
    \n
    \n
    ${displayName}
    \n
    ${account.accountId}
    \n
    \n
    \n
    \n Running\n ${account.running ? \"Yes\" : \"No\"}\n
    \n
    \n Configured\n ${account.configured ? \"Yes\" : \"No\"}\n
    \n
    \n Public Key\n ${truncatePubkey(publicKey)}\n
    \n
    \n Last inbound\n ${account.lastInboundAt ? formatAgo(account.lastInboundAt) : \"n/a\"}\n
    \n ${account.lastError\n ? html`\n
    ${account.lastError}
    \n `\n : nothing}\n
    \n
    \n `;\n };\n\n const renderProfileSection = () => {\n // If showing form, render the form instead of the read-only view\n if (showingForm && profileFormCallbacks) {\n return renderNostrProfileForm({\n state: profileFormState,\n callbacks: profileFormCallbacks,\n accountId: nostrAccounts[0]?.accountId ?? \"default\",\n });\n }\n\n const profile =\n (primaryAccount as\n | {\n profile?: {\n name?: string;\n displayName?: string;\n about?: string;\n picture?: string;\n nip05?: string;\n };\n }\n | undefined)?.profile ?? nostr?.profile;\n const { name, displayName, about, picture, nip05 } = profile ?? {};\n const hasAnyProfileData = name || displayName || about || picture || nip05;\n\n return html`\n
    \n
    \n
    Profile
    \n ${summaryConfigured\n ? html`\n \n Edit Profile\n \n `\n : nothing}\n
    \n ${hasAnyProfileData\n ? html`\n
    \n ${picture\n ? html`\n
    \n {\n (e.target as HTMLImageElement).style.display = \"none\";\n }}\n />\n
    \n `\n : nothing}\n ${name ? html`
    Name${name}
    ` : nothing}\n ${displayName\n ? html`
    Display Name${displayName}
    `\n : nothing}\n ${about\n ? html`
    About${about}
    `\n : nothing}\n ${nip05 ? html`
    NIP-05${nip05}
    ` : nothing}\n
    \n `\n : html`\n
    \n No profile set. Click \"Edit Profile\" to add your name, bio, and avatar.\n
    \n `}\n
    \n `;\n };\n\n return html`\n
    \n
    Nostr
    \n
    Decentralized DMs via Nostr relays (NIP-04).
    \n ${accountCountLabel}\n\n ${hasMultipleAccounts\n ? html`\n
    \n ${nostrAccounts.map((account) => renderAccountCard(account))}\n
    \n `\n : html`\n
    \n
    \n Configured\n ${summaryConfigured ? \"Yes\" : \"No\"}\n
    \n
    \n Running\n ${summaryRunning ? \"Yes\" : \"No\"}\n
    \n
    \n Public Key\n ${truncatePubkey(summaryPublicKey)}\n
    \n
    \n Last start\n ${summaryLastStartAt ? formatAgo(summaryLastStartAt) : \"n/a\"}\n
    \n
    \n `}\n\n ${summaryLastError\n ? html`
    ${summaryLastError}
    `\n : nothing}\n\n ${renderProfileSection()}\n\n ${renderChannelConfigSection({ channelId: \"nostr\", props })}\n\n
    \n \n
    \n
    \n `;\n}\n","import { html, nothing } from \"lit\";\n\nimport { formatAgo } from \"../format\";\nimport type { SignalStatus } from \"../types\";\nimport type { ChannelsProps } from \"./channels.types\";\nimport { renderChannelConfigSection } from \"./channels.config\";\n\nexport function renderSignalCard(params: {\n props: ChannelsProps;\n signal?: SignalStatus | null;\n accountCountLabel: unknown;\n}) {\n const { props, signal, accountCountLabel } = params;\n\n return html`\n
    \n
    Signal
    \n
    signal-cli status and channel configuration.
    \n ${accountCountLabel}\n\n
    \n
    \n Configured\n ${signal?.configured ? \"Yes\" : \"No\"}\n
    \n
    \n Running\n ${signal?.running ? \"Yes\" : \"No\"}\n
    \n
    \n Base URL\n ${signal?.baseUrl ?? \"n/a\"}\n
    \n
    \n Last start\n ${signal?.lastStartAt ? formatAgo(signal.lastStartAt) : \"n/a\"}\n
    \n
    \n Last probe\n ${signal?.lastProbeAt ? formatAgo(signal.lastProbeAt) : \"n/a\"}\n
    \n
    \n\n ${signal?.lastError\n ? html`
    \n ${signal.lastError}\n
    `\n : nothing}\n\n ${signal?.probe\n ? html`
    \n Probe ${signal.probe.ok ? \"ok\" : \"failed\"} ·\n ${signal.probe.status ?? \"\"} ${signal.probe.error ?? \"\"}\n
    `\n : nothing}\n\n ${renderChannelConfigSection({ channelId: \"signal\", props })}\n\n
    \n \n
    \n
    \n `;\n}\n","import { html, nothing } from \"lit\";\n\nimport { formatAgo } from \"../format\";\nimport type { SlackStatus } from \"../types\";\nimport type { ChannelsProps } from \"./channels.types\";\nimport { renderChannelConfigSection } from \"./channels.config\";\n\nexport function renderSlackCard(params: {\n props: ChannelsProps;\n slack?: SlackStatus | null;\n accountCountLabel: unknown;\n}) {\n const { props, slack, accountCountLabel } = params;\n\n return html`\n
    \n
    Slack
    \n
    Socket mode status and channel configuration.
    \n ${accountCountLabel}\n\n
    \n
    \n Configured\n ${slack?.configured ? \"Yes\" : \"No\"}\n
    \n
    \n Running\n ${slack?.running ? \"Yes\" : \"No\"}\n
    \n
    \n Last start\n ${slack?.lastStartAt ? formatAgo(slack.lastStartAt) : \"n/a\"}\n
    \n
    \n Last probe\n ${slack?.lastProbeAt ? formatAgo(slack.lastProbeAt) : \"n/a\"}\n
    \n
    \n\n ${slack?.lastError\n ? html`
    \n ${slack.lastError}\n
    `\n : nothing}\n\n ${slack?.probe\n ? html`
    \n Probe ${slack.probe.ok ? \"ok\" : \"failed\"} ·\n ${slack.probe.status ?? \"\"} ${slack.probe.error ?? \"\"}\n
    `\n : nothing}\n\n ${renderChannelConfigSection({ channelId: \"slack\", props })}\n\n
    \n \n
    \n
    \n `;\n}\n","import { html, nothing } from \"lit\";\n\nimport { formatAgo } from \"../format\";\nimport type { ChannelAccountSnapshot, TelegramStatus } from \"../types\";\nimport type { ChannelsProps } from \"./channels.types\";\nimport { renderChannelConfigSection } from \"./channels.config\";\n\nexport function renderTelegramCard(params: {\n props: ChannelsProps;\n telegram?: TelegramStatus;\n telegramAccounts: ChannelAccountSnapshot[];\n accountCountLabel: unknown;\n}) {\n const { props, telegram, telegramAccounts, accountCountLabel } = params;\n const hasMultipleAccounts = telegramAccounts.length > 1;\n\n const renderAccountCard = (account: ChannelAccountSnapshot) => {\n const probe = account.probe as { bot?: { username?: string } } | undefined;\n const botUsername = probe?.bot?.username;\n const label = account.name || account.accountId;\n return html`\n
    \n
    \n
    \n ${botUsername ? `@${botUsername}` : label}\n
    \n
    ${account.accountId}
    \n
    \n
    \n
    \n Running\n ${account.running ? \"Yes\" : \"No\"}\n
    \n
    \n Configured\n ${account.configured ? \"Yes\" : \"No\"}\n
    \n
    \n Last inbound\n ${account.lastInboundAt ? formatAgo(account.lastInboundAt) : \"n/a\"}\n
    \n ${account.lastError\n ? html`\n
    \n ${account.lastError}\n
    \n `\n : nothing}\n
    \n
    \n `;\n };\n\n return html`\n
    \n
    Telegram
    \n
    Bot status and channel configuration.
    \n ${accountCountLabel}\n\n ${hasMultipleAccounts\n ? html`\n
    \n ${telegramAccounts.map((account) => renderAccountCard(account))}\n
    \n `\n : html`\n
    \n
    \n Configured\n ${telegram?.configured ? \"Yes\" : \"No\"}\n
    \n
    \n Running\n ${telegram?.running ? \"Yes\" : \"No\"}\n
    \n
    \n Mode\n ${telegram?.mode ?? \"n/a\"}\n
    \n
    \n Last start\n ${telegram?.lastStartAt ? formatAgo(telegram.lastStartAt) : \"n/a\"}\n
    \n
    \n Last probe\n ${telegram?.lastProbeAt ? formatAgo(telegram.lastProbeAt) : \"n/a\"}\n
    \n
    \n `}\n\n ${telegram?.lastError\n ? html`
    \n ${telegram.lastError}\n
    `\n : nothing}\n\n ${telegram?.probe\n ? html`
    \n Probe ${telegram.probe.ok ? \"ok\" : \"failed\"} ·\n ${telegram.probe.status ?? \"\"} ${telegram.probe.error ?? \"\"}\n
    `\n : nothing}\n\n ${renderChannelConfigSection({ channelId: \"telegram\", props })}\n\n
    \n \n
    \n
    \n `;\n}\n","import { html, nothing } from \"lit\";\n\nimport { formatAgo } from \"../format\";\nimport type { WhatsAppStatus } from \"../types\";\nimport type { ChannelsProps } from \"./channels.types\";\nimport { renderChannelConfigSection } from \"./channels.config\";\nimport { formatDuration } from \"./channels.shared\";\n\nexport function renderWhatsAppCard(params: {\n props: ChannelsProps;\n whatsapp?: WhatsAppStatus;\n accountCountLabel: unknown;\n}) {\n const { props, whatsapp, accountCountLabel } = params;\n\n return html`\n
    \n
    WhatsApp
    \n
    Link WhatsApp Web and monitor connection health.
    \n ${accountCountLabel}\n\n
    \n
    \n Configured\n ${whatsapp?.configured ? \"Yes\" : \"No\"}\n
    \n
    \n Linked\n ${whatsapp?.linked ? \"Yes\" : \"No\"}\n
    \n
    \n Running\n ${whatsapp?.running ? \"Yes\" : \"No\"}\n
    \n
    \n Connected\n ${whatsapp?.connected ? \"Yes\" : \"No\"}\n
    \n
    \n Last connect\n \n ${whatsapp?.lastConnectedAt\n ? formatAgo(whatsapp.lastConnectedAt)\n : \"n/a\"}\n \n
    \n
    \n Last message\n \n ${whatsapp?.lastMessageAt ? formatAgo(whatsapp.lastMessageAt) : \"n/a\"}\n \n
    \n
    \n Auth age\n \n ${whatsapp?.authAgeMs != null\n ? formatDuration(whatsapp.authAgeMs)\n : \"n/a\"}\n \n
    \n
    \n\n ${whatsapp?.lastError\n ? html`
    \n ${whatsapp.lastError}\n
    `\n : nothing}\n\n ${props.whatsappMessage\n ? html`
    \n ${props.whatsappMessage}\n
    `\n : nothing}\n\n ${props.whatsappQrDataUrl\n ? html`
    \n \"WhatsApp\n
    `\n : nothing}\n\n
    \n props.onWhatsAppStart(false)}\n >\n ${props.whatsappBusy ? \"Working…\" : \"Show QR\"}\n \n props.onWhatsAppStart(true)}\n >\n Relink\n \n props.onWhatsAppWait()}\n >\n Wait for scan\n \n props.onWhatsAppLogout()}\n >\n Logout\n \n \n
    \n\n ${renderChannelConfigSection({ channelId: \"whatsapp\", props })}\n
    \n `;\n}\n\n","import { html, nothing } from \"lit\";\n\nimport { formatAgo } from \"../format\";\nimport type {\n ChannelAccountSnapshot,\n ChannelUiMetaEntry,\n ChannelsStatusSnapshot,\n DiscordStatus,\n GoogleChatStatus,\n IMessageStatus,\n NostrProfile,\n NostrStatus,\n SignalStatus,\n SlackStatus,\n TelegramStatus,\n WhatsAppStatus,\n} from \"../types\";\nimport type {\n ChannelKey,\n ChannelsChannelData,\n ChannelsProps,\n} from \"./channels.types\";\nimport { channelEnabled, renderChannelAccountCount } from \"./channels.shared\";\nimport { renderChannelConfigSection } from \"./channels.config\";\nimport { renderDiscordCard } from \"./channels.discord\";\nimport { renderGoogleChatCard } from \"./channels.googlechat\";\nimport { renderIMessageCard } from \"./channels.imessage\";\nimport { renderNostrCard } from \"./channels.nostr\";\nimport { renderSignalCard } from \"./channels.signal\";\nimport { renderSlackCard } from \"./channels.slack\";\nimport { renderTelegramCard } from \"./channels.telegram\";\nimport { renderWhatsAppCard } from \"./channels.whatsapp\";\n\nexport function renderChannels(props: ChannelsProps) {\n const channels = props.snapshot?.channels as Record | null;\n const whatsapp = (channels?.whatsapp ?? undefined) as\n | WhatsAppStatus\n | undefined;\n const telegram = (channels?.telegram ?? undefined) as\n | TelegramStatus\n | undefined;\n const discord = (channels?.discord ?? null) as DiscordStatus | null;\n const googlechat = (channels?.googlechat ?? null) as GoogleChatStatus | null;\n const slack = (channels?.slack ?? null) as SlackStatus | null;\n const signal = (channels?.signal ?? null) as SignalStatus | null;\n const imessage = (channels?.imessage ?? null) as IMessageStatus | null;\n const nostr = (channels?.nostr ?? null) as NostrStatus | null;\n const channelOrder = resolveChannelOrder(props.snapshot);\n const orderedChannels = channelOrder\n .map((key, index) => ({\n key,\n enabled: channelEnabled(key, props),\n order: index,\n }))\n .sort((a, b) => {\n if (a.enabled !== b.enabled) return a.enabled ? -1 : 1;\n return a.order - b.order;\n });\n\n return html`\n
    \n ${orderedChannels.map((channel) =>\n renderChannel(channel.key, props, {\n whatsapp,\n telegram,\n discord,\n googlechat,\n slack,\n signal,\n imessage,\n nostr,\n channelAccounts: props.snapshot?.channelAccounts ?? null,\n }),\n )}\n
    \n\n
    \n
    \n
    \n
    Channel health
    \n
    Channel status snapshots from the gateway.
    \n
    \n
    ${props.lastSuccessAt ? formatAgo(props.lastSuccessAt) : \"n/a\"}
    \n
    \n ${props.lastError\n ? html`
    \n ${props.lastError}\n
    `\n : nothing}\n
    \n${props.snapshot ? JSON.stringify(props.snapshot, null, 2) : \"No snapshot yet.\"}\n      
    \n
    \n `;\n}\n\nfunction resolveChannelOrder(snapshot: ChannelsStatusSnapshot | null): ChannelKey[] {\n if (snapshot?.channelMeta?.length) {\n return snapshot.channelMeta.map((entry) => entry.id) as ChannelKey[];\n }\n if (snapshot?.channelOrder?.length) {\n return snapshot.channelOrder;\n }\n return [\n \"whatsapp\",\n \"telegram\",\n \"discord\",\n \"googlechat\",\n \"slack\",\n \"signal\",\n \"imessage\",\n \"nostr\",\n ];\n}\n\nfunction renderChannel(\n key: ChannelKey,\n props: ChannelsProps,\n data: ChannelsChannelData,\n) {\n const accountCountLabel = renderChannelAccountCount(\n key,\n data.channelAccounts,\n );\n switch (key) {\n case \"whatsapp\":\n return renderWhatsAppCard({\n props,\n whatsapp: data.whatsapp,\n accountCountLabel,\n });\n case \"telegram\":\n return renderTelegramCard({\n props,\n telegram: data.telegram,\n telegramAccounts: data.channelAccounts?.telegram ?? [],\n accountCountLabel,\n });\n case \"discord\":\n return renderDiscordCard({\n props,\n discord: data.discord,\n accountCountLabel,\n });\n case \"googlechat\":\n return renderGoogleChatCard({\n props,\n googlechat: data.googlechat,\n accountCountLabel,\n });\n case \"slack\":\n return renderSlackCard({\n props,\n slack: data.slack,\n accountCountLabel,\n });\n case \"signal\":\n return renderSignalCard({\n props,\n signal: data.signal,\n accountCountLabel,\n });\n case \"imessage\":\n return renderIMessageCard({\n props,\n imessage: data.imessage,\n accountCountLabel,\n });\n case \"nostr\": {\n const nostrAccounts = data.channelAccounts?.nostr ?? [];\n const primaryAccount = nostrAccounts[0];\n const accountId = primaryAccount?.accountId ?? \"default\";\n const profile =\n (primaryAccount as { profile?: NostrProfile | null } | undefined)?.profile ?? null;\n const showForm =\n props.nostrProfileAccountId === accountId ? props.nostrProfileFormState : null;\n const profileFormCallbacks = showForm\n ? {\n onFieldChange: props.onNostrProfileFieldChange,\n onSave: props.onNostrProfileSave,\n onImport: props.onNostrProfileImport,\n onCancel: props.onNostrProfileCancel,\n onToggleAdvanced: props.onNostrProfileToggleAdvanced,\n }\n : null;\n return renderNostrCard({\n props,\n nostr: data.nostr,\n nostrAccounts,\n accountCountLabel,\n profileFormState: showForm,\n profileFormCallbacks,\n onEditProfile: () => props.onNostrProfileEdit(accountId, profile),\n });\n }\n default:\n return renderGenericChannelCard(key, props, data.channelAccounts ?? {});\n }\n}\n\nfunction renderGenericChannelCard(\n key: ChannelKey,\n props: ChannelsProps,\n channelAccounts: Record,\n) {\n const label = resolveChannelLabel(props.snapshot, key);\n const status = props.snapshot?.channels?.[key] as Record | undefined;\n const configured = typeof status?.configured === \"boolean\" ? status.configured : undefined;\n const running = typeof status?.running === \"boolean\" ? status.running : undefined;\n const connected = typeof status?.connected === \"boolean\" ? status.connected : undefined;\n const lastError = typeof status?.lastError === \"string\" ? status.lastError : undefined;\n const accounts = channelAccounts[key] ?? [];\n const accountCountLabel = renderChannelAccountCount(key, channelAccounts);\n\n return html`\n
    \n
    ${label}
    \n
    Channel status and configuration.
    \n ${accountCountLabel}\n\n ${accounts.length > 0\n ? html`\n
    \n ${accounts.map((account) => renderGenericAccount(account))}\n
    \n `\n : html`\n
    \n
    \n Configured\n ${configured == null ? \"n/a\" : configured ? \"Yes\" : \"No\"}\n
    \n
    \n Running\n ${running == null ? \"n/a\" : running ? \"Yes\" : \"No\"}\n
    \n
    \n Connected\n ${connected == null ? \"n/a\" : connected ? \"Yes\" : \"No\"}\n
    \n
    \n `}\n\n ${lastError\n ? html`
    \n ${lastError}\n
    `\n : nothing}\n\n ${renderChannelConfigSection({ channelId: key, props })}\n
    \n `;\n}\n\nfunction resolveChannelMetaMap(\n snapshot: ChannelsStatusSnapshot | null,\n): Record {\n if (!snapshot?.channelMeta?.length) return {};\n return Object.fromEntries(snapshot.channelMeta.map((entry) => [entry.id, entry]));\n}\n\nfunction resolveChannelLabel(\n snapshot: ChannelsStatusSnapshot | null,\n key: string,\n): string {\n const meta = resolveChannelMetaMap(snapshot)[key];\n return meta?.label ?? snapshot?.channelLabels?.[key] ?? key;\n}\n\nconst RECENT_ACTIVITY_THRESHOLD_MS = 10 * 60 * 1000; // 10 minutes\n\nfunction hasRecentActivity(account: ChannelAccountSnapshot): boolean {\n if (!account.lastInboundAt) return false;\n return Date.now() - account.lastInboundAt < RECENT_ACTIVITY_THRESHOLD_MS;\n}\n\nfunction deriveRunningStatus(account: ChannelAccountSnapshot): \"Yes\" | \"No\" | \"Active\" {\n if (account.running) return \"Yes\";\n // If we have recent inbound activity, the channel is effectively running\n if (hasRecentActivity(account)) return \"Active\";\n return \"No\";\n}\n\nfunction deriveConnectedStatus(account: ChannelAccountSnapshot): \"Yes\" | \"No\" | \"Active\" | \"n/a\" {\n if (account.connected === true) return \"Yes\";\n if (account.connected === false) return \"No\";\n // If connected is null/undefined but we have recent activity, show as active\n if (hasRecentActivity(account)) return \"Active\";\n return \"n/a\";\n}\n\nfunction renderGenericAccount(account: ChannelAccountSnapshot) {\n const runningStatus = deriveRunningStatus(account);\n const connectedStatus = deriveConnectedStatus(account);\n\n return html`\n
    \n
    \n
    ${account.name || account.accountId}
    \n
    ${account.accountId}
    \n
    \n
    \n
    \n Running\n ${runningStatus}\n
    \n
    \n Configured\n ${account.configured ? \"Yes\" : \"No\"}\n
    \n
    \n Connected\n ${connectedStatus}\n
    \n
    \n Last inbound\n ${account.lastInboundAt ? formatAgo(account.lastInboundAt) : \"n/a\"}\n
    \n ${account.lastError\n ? html`\n
    \n ${account.lastError}\n
    \n `\n : nothing}\n
    \n
    \n `;\n}\n","import { formatAgo, formatDurationMs, formatMs } from \"./format\";\nimport type { CronJob, GatewaySessionRow, PresenceEntry } from \"./types\";\n\nexport function formatPresenceSummary(entry: PresenceEntry): string {\n const host = entry.host ?? \"unknown\";\n const ip = entry.ip ? `(${entry.ip})` : \"\";\n const mode = entry.mode ?? \"\";\n const version = entry.version ?? \"\";\n return `${host} ${ip} ${mode} ${version}`.trim();\n}\n\nexport function formatPresenceAge(entry: PresenceEntry): string {\n const ts = entry.ts ?? null;\n return ts ? formatAgo(ts) : \"n/a\";\n}\n\nexport function formatNextRun(ms?: number | null) {\n if (!ms) return \"n/a\";\n return `${formatMs(ms)} (${formatAgo(ms)})`;\n}\n\nexport function formatSessionTokens(row: GatewaySessionRow) {\n if (row.totalTokens == null) return \"n/a\";\n const total = row.totalTokens ?? 0;\n const ctx = row.contextTokens ?? 0;\n return ctx ? `${total} / ${ctx}` : String(total);\n}\n\nexport function formatEventPayload(payload: unknown): string {\n if (payload == null) return \"\";\n try {\n return JSON.stringify(payload, null, 2);\n } catch {\n return String(payload);\n }\n}\n\nexport function formatCronState(job: CronJob) {\n const state = job.state ?? {};\n const next = state.nextRunAtMs ? formatMs(state.nextRunAtMs) : \"n/a\";\n const last = state.lastRunAtMs ? formatMs(state.lastRunAtMs) : \"n/a\";\n const status = state.lastStatus ?? \"n/a\";\n return `${status} · next ${next} · last ${last}`;\n}\n\nexport function formatCronSchedule(job: CronJob) {\n const s = job.schedule;\n if (s.kind === \"at\") return `At ${formatMs(s.atMs)}`;\n if (s.kind === \"every\") return `Every ${formatDurationMs(s.everyMs)}`;\n return `Cron ${s.expr}${s.tz ? ` (${s.tz})` : \"\"}`;\n}\n\nexport function formatCronPayload(job: CronJob) {\n const p = job.payload;\n if (p.kind === \"systemEvent\") return `System: ${p.text}`;\n return `Agent: ${p.message}`;\n}\n\n","import { html, nothing } from \"lit\";\n\nimport { formatMs } from \"../format\";\nimport {\n formatCronPayload,\n formatCronSchedule,\n formatCronState,\n formatNextRun,\n} from \"../presenter\";\nimport type { ChannelUiMetaEntry, CronJob, CronRunLogEntry, CronStatus } from \"../types\";\nimport type { CronFormState } from \"../ui-types\";\n\nexport type CronProps = {\n loading: boolean;\n status: CronStatus | null;\n jobs: CronJob[];\n error: string | null;\n busy: boolean;\n form: CronFormState;\n channels: string[];\n channelLabels?: Record;\n channelMeta?: ChannelUiMetaEntry[];\n runsJobId: string | null;\n runs: CronRunLogEntry[];\n onFormChange: (patch: Partial) => void;\n onRefresh: () => void;\n onAdd: () => void;\n onToggle: (job: CronJob, enabled: boolean) => void;\n onRun: (job: CronJob) => void;\n onRemove: (job: CronJob) => void;\n onLoadRuns: (jobId: string) => void;\n};\n\nfunction buildChannelOptions(props: CronProps): string[] {\n const options = [\"last\", ...props.channels.filter(Boolean)];\n const current = props.form.channel?.trim();\n if (current && !options.includes(current)) {\n options.push(current);\n }\n const seen = new Set();\n return options.filter((value) => {\n if (seen.has(value)) return false;\n seen.add(value);\n return true;\n });\n}\n\nfunction resolveChannelLabel(props: CronProps, channel: string): string {\n if (channel === \"last\") return \"last\";\n const meta = props.channelMeta?.find((entry) => entry.id === channel);\n if (meta?.label) return meta.label;\n return props.channelLabels?.[channel] ?? channel;\n}\n\nexport function renderCron(props: CronProps) {\n const channelOptions = buildChannelOptions(props);\n return html`\n
    \n
    \n
    Scheduler
    \n
    Gateway-owned cron scheduler status.
    \n
    \n
    \n
    Enabled
    \n
    \n ${props.status\n ? props.status.enabled\n ? \"Yes\"\n : \"No\"\n : \"n/a\"}\n
    \n
    \n
    \n
    Jobs
    \n
    ${props.status?.jobs ?? \"n/a\"}
    \n
    \n
    \n
    Next wake
    \n
    ${formatNextRun(props.status?.nextWakeAtMs ?? null)}
    \n
    \n
    \n
    \n \n ${props.error ? html`${props.error}` : nothing}\n
    \n
    \n\n
    \n
    New Job
    \n
    Create a scheduled wakeup or agent run.
    \n
    \n \n \n \n \n \n
    \n ${renderScheduleFields(props)}\n
    \n \n \n \n
    \n \n\t ${props.form.payloadKind === \"agentTurn\"\n\t ? html`\n\t
    \n \n\t \n \n \n ${props.form.sessionTarget === \"isolated\"\n ? html`\n \n `\n : nothing}\n
    \n `\n : nothing}\n
    \n \n
    \n
    \n
    \n\n
    \n
    Jobs
    \n
    All scheduled jobs stored in the gateway.
    \n ${props.jobs.length === 0\n ? html`
    No jobs yet.
    `\n : html`\n
    \n ${props.jobs.map((job) => renderJob(job, props))}\n
    \n `}\n
    \n\n
    \n
    Run history
    \n
    Latest runs for ${props.runsJobId ?? \"(select a job)\"}.
    \n ${props.runsJobId == null\n ? html`\n
    \n Select a job to inspect run history.\n
    \n `\n : props.runs.length === 0\n ? html`
    No runs yet.
    `\n : html`\n
    \n ${props.runs.map((entry) => renderRun(entry))}\n
    \n `}\n
    \n `;\n}\n\nfunction renderScheduleFields(props: CronProps) {\n const form = props.form;\n if (form.scheduleKind === \"at\") {\n return html`\n \n `;\n }\n if (form.scheduleKind === \"every\") {\n return html`\n
    \n \n \n
    \n `;\n }\n return html`\n
    \n \n \n
    \n `;\n}\n\nfunction renderJob(job: CronJob, props: CronProps) {\n const isSelected = props.runsJobId === job.id;\n const itemClass = `list-item list-item-clickable${isSelected ? \" list-item-selected\" : \"\"}`;\n return html`\n
    props.onLoadRuns(job.id)}>\n
    \n
    ${job.name}
    \n
    ${formatCronSchedule(job)}
    \n
    ${formatCronPayload(job)}
    \n ${job.agentId ? html`
    Agent: ${job.agentId}
    ` : nothing}\n
    \n ${job.enabled ? \"enabled\" : \"disabled\"}\n ${job.sessionTarget}\n ${job.wakeMode}\n
    \n
    \n
    \n
    ${formatCronState(job)}
    \n
    \n {\n event.stopPropagation();\n props.onToggle(job, !job.enabled);\n }}\n >\n ${job.enabled ? \"Disable\" : \"Enable\"}\n \n {\n event.stopPropagation();\n props.onRun(job);\n }}\n >\n Run\n \n {\n event.stopPropagation();\n props.onLoadRuns(job.id);\n }}\n >\n Runs\n \n {\n event.stopPropagation();\n props.onRemove(job);\n }}\n >\n Remove\n \n
    \n
    \n
    \n `;\n}\n\nfunction renderRun(entry: CronRunLogEntry) {\n return html`\n
    \n
    \n
    ${entry.status}
    \n
    ${entry.summary ?? \"\"}
    \n
    \n
    \n
    ${formatMs(entry.ts)}
    \n
    ${entry.durationMs ?? 0}ms
    \n ${entry.error ? html`
    ${entry.error}
    ` : nothing}\n
    \n
    \n `;\n}\n","import { html, nothing } from \"lit\";\n\nimport { formatEventPayload } from \"../presenter\";\nimport type { EventLogEntry } from \"../app-events\";\n\nexport type DebugProps = {\n loading: boolean;\n status: Record | null;\n health: Record | null;\n models: unknown[];\n heartbeat: unknown;\n eventLog: EventLogEntry[];\n callMethod: string;\n callParams: string;\n callResult: string | null;\n callError: string | null;\n onCallMethodChange: (next: string) => void;\n onCallParamsChange: (next: string) => void;\n onRefresh: () => void;\n onCall: () => void;\n};\n\nexport function renderDebug(props: DebugProps) {\n return html`\n
    \n
    \n
    \n
    \n
    Snapshots
    \n
    Status, health, and heartbeat data.
    \n
    \n \n
    \n
    \n
    \n
    Status
    \n
    ${JSON.stringify(props.status ?? {}, null, 2)}
    \n
    \n
    \n
    Health
    \n
    ${JSON.stringify(props.health ?? {}, null, 2)}
    \n
    \n
    \n
    Last heartbeat
    \n
    ${JSON.stringify(props.heartbeat ?? {}, null, 2)}
    \n
    \n
    \n
    \n\n
    \n
    Manual RPC
    \n
    Send a raw gateway method with JSON params.
    \n
    \n \n \n
    \n
    \n \n
    \n ${props.callError\n ? html`
    \n ${props.callError}\n
    `\n : nothing}\n ${props.callResult\n ? html`
    ${props.callResult}
    `\n : nothing}\n
    \n
    \n\n
    \n
    Models
    \n
    Catalog from models.list.
    \n
    ${JSON.stringify(\n        props.models ?? [],\n        null,\n        2,\n      )}
    \n
    \n\n
    \n
    Event Log
    \n
    Latest gateway events.
    \n ${props.eventLog.length === 0\n ? html`
    No events yet.
    `\n : html`\n
    \n ${props.eventLog.map(\n (evt) => html`\n
    \n
    \n
    ${evt.event}
    \n
    ${new Date(evt.ts).toLocaleTimeString()}
    \n
    \n
    \n
    ${formatEventPayload(evt.payload)}
    \n
    \n
    \n `,\n )}\n
    \n `}\n
    \n `;\n}\n","import { html, nothing } from \"lit\";\n\nimport { formatPresenceAge, formatPresenceSummary } from \"../presenter\";\nimport type { PresenceEntry } from \"../types\";\n\nexport type InstancesProps = {\n loading: boolean;\n entries: PresenceEntry[];\n lastError: string | null;\n statusMessage: string | null;\n onRefresh: () => void;\n};\n\nexport function renderInstances(props: InstancesProps) {\n return html`\n
    \n
    \n
    \n
    Connected Instances
    \n
    Presence beacons from the gateway and clients.
    \n
    \n \n
    \n ${props.lastError\n ? html`
    \n ${props.lastError}\n
    `\n : nothing}\n ${props.statusMessage\n ? html`
    \n ${props.statusMessage}\n
    `\n : nothing}\n
    \n ${props.entries.length === 0\n ? html`
    No instances reported yet.
    `\n : props.entries.map((entry) => renderEntry(entry))}\n
    \n
    \n `;\n}\n\nfunction renderEntry(entry: PresenceEntry) {\n const lastInput =\n entry.lastInputSeconds != null\n ? `${entry.lastInputSeconds}s ago`\n : \"n/a\";\n const mode = entry.mode ?? \"unknown\";\n const roles = Array.isArray(entry.roles) ? entry.roles.filter(Boolean) : [];\n const scopes = Array.isArray(entry.scopes) ? entry.scopes.filter(Boolean) : [];\n const scopesLabel =\n scopes.length > 0\n ? scopes.length > 3\n ? `${scopes.length} scopes`\n : `scopes: ${scopes.join(\", \")}`\n : null;\n return html`\n
    \n
    \n
    ${entry.host ?? \"unknown host\"}
    \n
    ${formatPresenceSummary(entry)}
    \n
    \n ${mode}\n ${roles.map((role) => html`${role}`)}\n ${scopesLabel ? html`${scopesLabel}` : nothing}\n ${entry.platform ? html`${entry.platform}` : nothing}\n ${entry.deviceFamily\n ? html`${entry.deviceFamily}`\n : nothing}\n ${entry.modelIdentifier\n ? html`${entry.modelIdentifier}`\n : nothing}\n ${entry.version ? html`${entry.version}` : nothing}\n
    \n
    \n
    \n
    ${formatPresenceAge(entry)}
    \n
    Last input ${lastInput}
    \n
    Reason ${entry.reason ?? \"\"}
    \n
    \n
    \n `;\n}\n","import { html, nothing } from \"lit\";\n\nimport type { LogEntry, LogLevel } from \"../types\";\n\nconst LEVELS: LogLevel[] = [\"trace\", \"debug\", \"info\", \"warn\", \"error\", \"fatal\"];\n\nexport type LogsProps = {\n loading: boolean;\n error: string | null;\n file: string | null;\n entries: LogEntry[];\n filterText: string;\n levelFilters: Record;\n autoFollow: boolean;\n truncated: boolean;\n onFilterTextChange: (next: string) => void;\n onLevelToggle: (level: LogLevel, enabled: boolean) => void;\n onToggleAutoFollow: (next: boolean) => void;\n onRefresh: () => void;\n onExport: (lines: string[], label: string) => void;\n onScroll: (event: Event) => void;\n};\n\nfunction formatTime(value?: string | null) {\n if (!value) return \"\";\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return value;\n return date.toLocaleTimeString();\n}\n\nfunction matchesFilter(entry: LogEntry, needle: string) {\n if (!needle) return true;\n const haystack = [entry.message, entry.subsystem, entry.raw]\n .filter(Boolean)\n .join(\" \")\n .toLowerCase();\n return haystack.includes(needle);\n}\n\nexport function renderLogs(props: LogsProps) {\n const needle = props.filterText.trim().toLowerCase();\n const levelFiltered = LEVELS.some((level) => !props.levelFilters[level]);\n const filtered = props.entries.filter((entry) => {\n if (entry.level && !props.levelFilters[entry.level]) return false;\n return matchesFilter(entry, needle);\n });\n const exportLabel = needle || levelFiltered ? \"filtered\" : \"visible\";\n\n return html`\n
    \n
    \n
    \n
    Logs
    \n
    Gateway file logs (JSONL).
    \n
    \n
    \n \n props.onExport(filtered.map((entry) => entry.raw), exportLabel)}\n >\n Export ${exportLabel}\n \n
    \n
    \n\n
    \n \n \n
    \n\n
    \n ${LEVELS.map(\n (level) => html`\n \n `,\n )}\n
    \n\n ${props.file\n ? html`
    File: ${props.file}
    `\n : nothing}\n ${props.truncated\n ? html`
    \n Log output truncated; showing latest chunk.\n
    `\n : nothing}\n ${props.error\n ? html`
    ${props.error}
    `\n : nothing}\n\n
    \n ${filtered.length === 0\n ? html`
    No log entries.
    `\n : filtered.map(\n (entry) => html`\n
    \n
    ${formatTime(entry.time)}
    \n
    ${entry.level ?? \"\"}
    \n
    ${entry.subsystem ?? \"\"}
    \n
    ${entry.message ?? entry.raw}
    \n
    \n `,\n )}\n
    \n
    \n `;\n}\n","import { html, nothing } from \"lit\";\n\nimport { clampText, formatAgo, formatList } from \"../format\";\nimport type {\n ExecApprovalsAllowlistEntry,\n ExecApprovalsFile,\n ExecApprovalsSnapshot,\n} from \"../controllers/exec-approvals\";\nimport type {\n DevicePairingList,\n DeviceTokenSummary,\n PairedDevice,\n PendingDevice,\n} from \"../controllers/devices\";\n\nexport type NodesProps = {\n loading: boolean;\n nodes: Array>;\n devicesLoading: boolean;\n devicesError: string | null;\n devicesList: DevicePairingList | null;\n configForm: Record | null;\n configLoading: boolean;\n configSaving: boolean;\n configDirty: boolean;\n configFormMode: \"form\" | \"raw\";\n execApprovalsLoading: boolean;\n execApprovalsSaving: boolean;\n execApprovalsDirty: boolean;\n execApprovalsSnapshot: ExecApprovalsSnapshot | null;\n execApprovalsForm: ExecApprovalsFile | null;\n execApprovalsSelectedAgent: string | null;\n execApprovalsTarget: \"gateway\" | \"node\";\n execApprovalsTargetNodeId: string | null;\n onRefresh: () => void;\n onDevicesRefresh: () => void;\n onDeviceApprove: (requestId: string) => void;\n onDeviceReject: (requestId: string) => void;\n onDeviceRotate: (deviceId: string, role: string, scopes?: string[]) => void;\n onDeviceRevoke: (deviceId: string, role: string) => void;\n onLoadConfig: () => void;\n onLoadExecApprovals: () => void;\n onBindDefault: (nodeId: string | null) => void;\n onBindAgent: (agentIndex: number, nodeId: string | null) => void;\n onSaveBindings: () => void;\n onExecApprovalsTargetChange: (kind: \"gateway\" | \"node\", nodeId: string | null) => void;\n onExecApprovalsSelectAgent: (agentId: string) => void;\n onExecApprovalsPatch: (path: Array, value: unknown) => void;\n onExecApprovalsRemove: (path: Array) => void;\n onSaveExecApprovals: () => void;\n};\n\nexport function renderNodes(props: NodesProps) {\n const bindingState = resolveBindingsState(props);\n const approvalsState = resolveExecApprovalsState(props);\n return html`\n ${renderExecApprovals(approvalsState)}\n ${renderBindings(bindingState)}\n ${renderDevices(props)}\n
    \n
    \n
    \n
    Nodes
    \n
    Paired devices and live links.
    \n
    \n \n
    \n
    \n ${props.nodes.length === 0\n ? html`
    No nodes found.
    `\n : props.nodes.map((n) => renderNode(n))}\n
    \n
    \n `;\n}\n\nfunction renderDevices(props: NodesProps) {\n const list = props.devicesList ?? { pending: [], paired: [] };\n const pending = Array.isArray(list.pending) ? list.pending : [];\n const paired = Array.isArray(list.paired) ? list.paired : [];\n return html`\n
    \n
    \n
    \n
    Devices
    \n
    Pairing requests + role tokens.
    \n
    \n \n
    \n ${props.devicesError\n ? html`
    ${props.devicesError}
    `\n : nothing}\n
    \n ${pending.length > 0\n ? html`\n
    Pending
    \n ${pending.map((req) => renderPendingDevice(req, props))}\n `\n : nothing}\n ${paired.length > 0\n ? html`\n
    Paired
    \n ${paired.map((device) => renderPairedDevice(device, props))}\n `\n : nothing}\n ${pending.length === 0 && paired.length === 0\n ? html`
    No paired devices.
    `\n : nothing}\n
    \n
    \n `;\n}\n\nfunction renderPendingDevice(req: PendingDevice, props: NodesProps) {\n const name = req.displayName?.trim() || req.deviceId;\n const age = typeof req.ts === \"number\" ? formatAgo(req.ts) : \"n/a\";\n const role = req.role?.trim() ? `role: ${req.role}` : \"role: -\";\n const repair = req.isRepair ? \" · repair\" : \"\";\n const ip = req.remoteIp ? ` · ${req.remoteIp}` : \"\";\n return html`\n
    \n
    \n
    ${name}
    \n
    ${req.deviceId}${ip}
    \n
    \n ${role} · requested ${age}${repair}\n
    \n
    \n
    \n
    \n \n \n
    \n
    \n
    \n `;\n}\n\nfunction renderPairedDevice(device: PairedDevice, props: NodesProps) {\n const name = device.displayName?.trim() || device.deviceId;\n const ip = device.remoteIp ? ` · ${device.remoteIp}` : \"\";\n const roles = `roles: ${formatList(device.roles)}`;\n const scopes = `scopes: ${formatList(device.scopes)}`;\n const tokens = Array.isArray(device.tokens) ? device.tokens : [];\n return html`\n
    \n
    \n
    ${name}
    \n
    ${device.deviceId}${ip}
    \n
    ${roles} · ${scopes}
    \n ${tokens.length === 0\n ? html`
    Tokens: none
    `\n : html`\n
    Tokens
    \n
    \n ${tokens.map((token) => renderTokenRow(device.deviceId, token, props))}\n
    \n `}\n
    \n
    \n `;\n}\n\nfunction renderTokenRow(deviceId: string, token: DeviceTokenSummary, props: NodesProps) {\n const status = token.revokedAtMs ? \"revoked\" : \"active\";\n const scopes = `scopes: ${formatList(token.scopes)}`;\n const when = formatAgo(token.rotatedAtMs ?? token.createdAtMs ?? token.lastUsedAtMs ?? null);\n return html`\n
    \n
    ${token.role} · ${status} · ${scopes} · ${when}
    \n
    \n props.onDeviceRotate(deviceId, token.role, token.scopes)}\n >\n Rotate\n \n ${token.revokedAtMs\n ? nothing\n : html`\n props.onDeviceRevoke(deviceId, token.role)}\n >\n Revoke\n \n `}\n
    \n
    \n `;\n}\n\ntype BindingAgent = {\n id: string;\n name?: string;\n index: number;\n isDefault: boolean;\n binding?: string | null;\n};\n\ntype BindingNode = {\n id: string;\n label: string;\n};\n\ntype BindingState = {\n ready: boolean;\n disabled: boolean;\n configDirty: boolean;\n configLoading: boolean;\n configSaving: boolean;\n defaultBinding?: string | null;\n agents: BindingAgent[];\n nodes: BindingNode[];\n onBindDefault: (nodeId: string | null) => void;\n onBindAgent: (agentIndex: number, nodeId: string | null) => void;\n onSave: () => void;\n onLoadConfig: () => void;\n formMode: \"form\" | \"raw\";\n};\n\ntype ExecSecurity = \"deny\" | \"allowlist\" | \"full\";\ntype ExecAsk = \"off\" | \"on-miss\" | \"always\";\n\ntype ExecApprovalsResolvedDefaults = {\n security: ExecSecurity;\n ask: ExecAsk;\n askFallback: ExecSecurity;\n autoAllowSkills: boolean;\n};\n\ntype ExecApprovalsAgentOption = {\n id: string;\n name?: string;\n isDefault?: boolean;\n};\n\ntype ExecApprovalsTargetNode = {\n id: string;\n label: string;\n};\n\ntype ExecApprovalsState = {\n ready: boolean;\n disabled: boolean;\n dirty: boolean;\n loading: boolean;\n saving: boolean;\n form: ExecApprovalsFile | null;\n defaults: ExecApprovalsResolvedDefaults;\n selectedScope: string;\n selectedAgent: Record | null;\n agents: ExecApprovalsAgentOption[];\n allowlist: ExecApprovalsAllowlistEntry[];\n target: \"gateway\" | \"node\";\n targetNodeId: string | null;\n targetNodes: ExecApprovalsTargetNode[];\n onSelectScope: (agentId: string) => void;\n onSelectTarget: (kind: \"gateway\" | \"node\", nodeId: string | null) => void;\n onPatch: (path: Array, value: unknown) => void;\n onRemove: (path: Array) => void;\n onLoad: () => void;\n onSave: () => void;\n};\n\nconst EXEC_APPROVALS_DEFAULT_SCOPE = \"__defaults__\";\n\nconst SECURITY_OPTIONS: Array<{ value: ExecSecurity; label: string }> = [\n { value: \"deny\", label: \"Deny\" },\n { value: \"allowlist\", label: \"Allowlist\" },\n { value: \"full\", label: \"Full\" },\n];\n\nconst ASK_OPTIONS: Array<{ value: ExecAsk; label: string }> = [\n { value: \"off\", label: \"Off\" },\n { value: \"on-miss\", label: \"On miss\" },\n { value: \"always\", label: \"Always\" },\n];\n\nfunction resolveBindingsState(props: NodesProps): BindingState {\n const config = props.configForm;\n const nodes = resolveExecNodes(props.nodes);\n const { defaultBinding, agents } = resolveAgentBindings(config);\n const ready = Boolean(config);\n const disabled = props.configSaving || props.configFormMode === \"raw\";\n return {\n ready,\n disabled,\n configDirty: props.configDirty,\n configLoading: props.configLoading,\n configSaving: props.configSaving,\n defaultBinding,\n agents,\n nodes,\n onBindDefault: props.onBindDefault,\n onBindAgent: props.onBindAgent,\n onSave: props.onSaveBindings,\n onLoadConfig: props.onLoadConfig,\n formMode: props.configFormMode,\n };\n}\n\nfunction normalizeSecurity(value?: string): ExecSecurity {\n if (value === \"allowlist\" || value === \"full\" || value === \"deny\") return value;\n return \"deny\";\n}\n\nfunction normalizeAsk(value?: string): ExecAsk {\n if (value === \"always\" || value === \"off\" || value === \"on-miss\") return value;\n return \"on-miss\";\n}\n\nfunction resolveExecApprovalsDefaults(\n form: ExecApprovalsFile | null,\n): ExecApprovalsResolvedDefaults {\n const defaults = form?.defaults ?? {};\n return {\n security: normalizeSecurity(defaults.security),\n ask: normalizeAsk(defaults.ask),\n askFallback: normalizeSecurity(defaults.askFallback ?? \"deny\"),\n autoAllowSkills: Boolean(defaults.autoAllowSkills ?? false),\n };\n}\n\nfunction resolveConfigAgents(config: Record | null): ExecApprovalsAgentOption[] {\n const agentsNode = (config?.agents ?? {}) as Record;\n const list = Array.isArray(agentsNode.list) ? agentsNode.list : [];\n const agents: ExecApprovalsAgentOption[] = [];\n list.forEach((entry) => {\n if (!entry || typeof entry !== \"object\") return;\n const record = entry as Record;\n const id = typeof record.id === \"string\" ? record.id.trim() : \"\";\n if (!id) return;\n const name = typeof record.name === \"string\" ? record.name.trim() : undefined;\n const isDefault = record.default === true;\n agents.push({ id, name: name || undefined, isDefault });\n });\n return agents;\n}\n\nfunction resolveExecApprovalsAgents(\n config: Record | null,\n form: ExecApprovalsFile | null,\n): ExecApprovalsAgentOption[] {\n const configAgents = resolveConfigAgents(config);\n const approvalsAgents = Object.keys(form?.agents ?? {});\n const merged = new Map();\n configAgents.forEach((agent) => merged.set(agent.id, agent));\n approvalsAgents.forEach((id) => {\n if (merged.has(id)) return;\n merged.set(id, { id });\n });\n const agents = Array.from(merged.values());\n if (agents.length === 0) {\n agents.push({ id: \"main\", isDefault: true });\n }\n agents.sort((a, b) => {\n if (a.isDefault && !b.isDefault) return -1;\n if (!a.isDefault && b.isDefault) return 1;\n const aLabel = a.name?.trim() ? a.name : a.id;\n const bLabel = b.name?.trim() ? b.name : b.id;\n return aLabel.localeCompare(bLabel);\n });\n return agents;\n}\n\nfunction resolveExecApprovalsScope(\n selected: string | null,\n agents: ExecApprovalsAgentOption[],\n): string {\n if (selected === EXEC_APPROVALS_DEFAULT_SCOPE) return EXEC_APPROVALS_DEFAULT_SCOPE;\n if (selected && agents.some((agent) => agent.id === selected)) return selected;\n return EXEC_APPROVALS_DEFAULT_SCOPE;\n}\n\nfunction resolveExecApprovalsState(props: NodesProps): ExecApprovalsState {\n const form = props.execApprovalsForm ?? props.execApprovalsSnapshot?.file ?? null;\n const ready = Boolean(form);\n const defaults = resolveExecApprovalsDefaults(form);\n const agents = resolveExecApprovalsAgents(props.configForm, form);\n const targetNodes = resolveExecApprovalsNodes(props.nodes);\n const target = props.execApprovalsTarget;\n let targetNodeId =\n target === \"node\" && props.execApprovalsTargetNodeId\n ? props.execApprovalsTargetNodeId\n : null;\n if (target === \"node\" && targetNodeId && !targetNodes.some((node) => node.id === targetNodeId)) {\n targetNodeId = null;\n }\n const selectedScope = resolveExecApprovalsScope(props.execApprovalsSelectedAgent, agents);\n const selectedAgent =\n selectedScope !== EXEC_APPROVALS_DEFAULT_SCOPE\n ? ((form?.agents ?? {})[selectedScope] as Record | undefined) ??\n null\n : null;\n const allowlist = Array.isArray((selectedAgent as { allowlist?: unknown })?.allowlist)\n ? ((selectedAgent as { allowlist?: ExecApprovalsAllowlistEntry[] }).allowlist ??\n [])\n : [];\n return {\n ready,\n disabled: props.execApprovalsSaving || props.execApprovalsLoading,\n dirty: props.execApprovalsDirty,\n loading: props.execApprovalsLoading,\n saving: props.execApprovalsSaving,\n form,\n defaults,\n selectedScope,\n selectedAgent,\n agents,\n allowlist,\n target,\n targetNodeId,\n targetNodes,\n onSelectScope: props.onExecApprovalsSelectAgent,\n onSelectTarget: props.onExecApprovalsTargetChange,\n onPatch: props.onExecApprovalsPatch,\n onRemove: props.onExecApprovalsRemove,\n onLoad: props.onLoadExecApprovals,\n onSave: props.onSaveExecApprovals,\n };\n}\n\nfunction renderBindings(state: BindingState) {\n const supportsBinding = state.nodes.length > 0;\n const defaultValue = state.defaultBinding ?? \"\";\n return html`\n
    \n
    \n
    \n
    Exec node binding
    \n
    \n Pin agents to a specific node when using exec host=node.\n
    \n
    \n \n ${state.configSaving ? \"Saving…\" : \"Save\"}\n \n
    \n\n ${state.formMode === \"raw\"\n ? html`
    \n Switch the Config tab to Form mode to edit bindings here.\n
    `\n : nothing}\n\n ${!state.ready\n ? html`
    \n
    Load config to edit bindings.
    \n \n
    `\n : html`\n
    \n
    \n
    \n
    Default binding
    \n
    Used when agents do not override a node binding.
    \n
    \n
    \n \n ${!supportsBinding\n ? html`
    No nodes with system.run available.
    `\n : nothing}\n
    \n
    \n\n ${state.agents.length === 0\n ? html`
    No agents found.
    `\n : state.agents.map((agent) =>\n renderAgentBinding(agent, state),\n )}\n
    \n `}\n
    \n `;\n}\n\nfunction renderExecApprovals(state: ExecApprovalsState) {\n const ready = state.ready;\n const targetReady = state.target !== \"node\" || Boolean(state.targetNodeId);\n return html`\n
    \n
    \n
    \n
    Exec approvals
    \n
    \n Allowlist and approval policy for exec host=gateway/node.\n
    \n
    \n \n ${state.saving ? \"Saving…\" : \"Save\"}\n \n
    \n\n ${renderExecApprovalsTarget(state)}\n\n ${!ready\n ? html`
    \n
    Load exec approvals to edit allowlists.
    \n \n
    `\n : html`\n ${renderExecApprovalsTabs(state)}\n ${renderExecApprovalsPolicy(state)}\n ${state.selectedScope === EXEC_APPROVALS_DEFAULT_SCOPE\n ? nothing\n : renderExecApprovalsAllowlist(state)}\n `}\n
    \n `;\n}\n\nfunction renderExecApprovalsTarget(state: ExecApprovalsState) {\n const hasNodes = state.targetNodes.length > 0;\n const nodeValue = state.targetNodeId ?? \"\";\n return html`\n
    \n
    \n
    \n
    Target
    \n
    \n Gateway edits local approvals; node edits the selected node.\n
    \n
    \n
    \n \n ${state.target === \"node\"\n ? html`\n \n `\n : nothing}\n
    \n
    \n ${state.target === \"node\" && !hasNodes\n ? html`
    No nodes advertise exec approvals yet.
    `\n : nothing}\n
    \n `;\n}\n\nfunction renderExecApprovalsTabs(state: ExecApprovalsState) {\n return html`\n
    \n Scope\n
    \n state.onSelectScope(EXEC_APPROVALS_DEFAULT_SCOPE)}\n >\n Defaults\n \n ${state.agents.map((agent) => {\n const label = agent.name?.trim() ? `${agent.name} (${agent.id})` : agent.id;\n return html`\n state.onSelectScope(agent.id)}\n >\n ${label}\n \n `;\n })}\n
    \n
    \n `;\n}\n\nfunction renderExecApprovalsPolicy(state: ExecApprovalsState) {\n const isDefaults = state.selectedScope === EXEC_APPROVALS_DEFAULT_SCOPE;\n const defaults = state.defaults;\n const agent = state.selectedAgent ?? {};\n const basePath = isDefaults ? [\"defaults\"] : [\"agents\", state.selectedScope];\n const agentSecurity = typeof agent.security === \"string\" ? agent.security : undefined;\n const agentAsk = typeof agent.ask === \"string\" ? agent.ask : undefined;\n const agentAskFallback =\n typeof agent.askFallback === \"string\" ? agent.askFallback : undefined;\n const securityValue = isDefaults ? defaults.security : agentSecurity ?? \"__default__\";\n const askValue = isDefaults ? defaults.ask : agentAsk ?? \"__default__\";\n const askFallbackValue = isDefaults\n ? defaults.askFallback\n : agentAskFallback ?? \"__default__\";\n const autoOverride =\n typeof agent.autoAllowSkills === \"boolean\" ? agent.autoAllowSkills : undefined;\n const autoEffective = autoOverride ?? defaults.autoAllowSkills;\n const autoIsDefault = autoOverride == null;\n\n return html`\n
    \n
    \n
    \n
    Security
    \n
    \n ${isDefaults\n ? \"Default security mode.\"\n : `Default: ${defaults.security}.`}\n
    \n
    \n
    \n \n
    \n
    \n\n
    \n
    \n
    Ask
    \n
    \n ${isDefaults ? \"Default prompt policy.\" : `Default: ${defaults.ask}.`}\n
    \n
    \n
    \n \n
    \n
    \n\n
    \n
    \n
    Ask fallback
    \n
    \n ${isDefaults\n ? \"Applied when the UI prompt is unavailable.\"\n : `Default: ${defaults.askFallback}.`}\n
    \n
    \n
    \n \n
    \n
    \n\n
    \n
    \n
    Auto-allow skill CLIs
    \n
    \n ${isDefaults\n ? \"Allow skill executables listed by the Gateway.\"\n : autoIsDefault\n ? `Using default (${defaults.autoAllowSkills ? \"on\" : \"off\"}).`\n : `Override (${autoEffective ? \"on\" : \"off\"}).`}\n
    \n
    \n
    \n \n ${!isDefaults && !autoIsDefault\n ? html` state.onRemove([...basePath, \"autoAllowSkills\"])}\n >\n Use default\n `\n : nothing}\n
    \n
    \n
    \n `;\n}\n\nfunction renderExecApprovalsAllowlist(state: ExecApprovalsState) {\n const allowlistPath = [\"agents\", state.selectedScope, \"allowlist\"];\n const entries = state.allowlist;\n return html`\n
    \n
    \n
    Allowlist
    \n
    Case-insensitive glob patterns.
    \n
    \n {\n const next = [...entries, { pattern: \"\" }];\n state.onPatch(allowlistPath, next);\n }}\n >\n Add pattern\n \n
    \n
    \n ${entries.length === 0\n ? html`
    No allowlist entries yet.
    `\n : entries.map((entry, index) =>\n renderAllowlistEntry(state, entry, index),\n )}\n
    \n `;\n}\n\nfunction renderAllowlistEntry(\n state: ExecApprovalsState,\n entry: ExecApprovalsAllowlistEntry,\n index: number,\n) {\n const lastUsed = entry.lastUsedAt ? formatAgo(entry.lastUsedAt) : \"never\";\n const lastCommand = entry.lastUsedCommand\n ? clampText(entry.lastUsedCommand, 120)\n : null;\n const lastPath = entry.lastResolvedPath\n ? clampText(entry.lastResolvedPath, 120)\n : null;\n return html`\n
    \n
    \n
    ${entry.pattern?.trim() ? entry.pattern : \"New pattern\"}
    \n
    Last used: ${lastUsed}
    \n ${lastCommand ? html`
    ${lastCommand}
    ` : nothing}\n ${lastPath ? html`
    ${lastPath}
    ` : nothing}\n
    \n
    \n \n {\n if (state.allowlist.length <= 1) {\n state.onRemove([\"agents\", state.selectedScope, \"allowlist\"]);\n return;\n }\n state.onRemove([\"agents\", state.selectedScope, \"allowlist\", index]);\n }}\n >\n Remove\n \n
    \n
    \n `;\n}\n\nfunction renderAgentBinding(agent: BindingAgent, state: BindingState) {\n const bindingValue = agent.binding ?? \"__default__\";\n const label = agent.name?.trim() ? `${agent.name} (${agent.id})` : agent.id;\n const supportsBinding = state.nodes.length > 0;\n return html`\n
    \n
    \n
    ${label}
    \n
    \n ${agent.isDefault ? \"default agent\" : \"agent\"} ·\n ${bindingValue === \"__default__\"\n ? `uses default (${state.defaultBinding ?? \"any\"})`\n : `override: ${agent.binding}`}\n
    \n
    \n
    \n \n
    \n
    \n `;\n}\n\nfunction resolveExecNodes(nodes: Array>): BindingNode[] {\n const list: BindingNode[] = [];\n for (const node of nodes) {\n const commands = Array.isArray(node.commands) ? node.commands : [];\n const supports = commands.some((cmd) => String(cmd) === \"system.run\");\n if (!supports) continue;\n const nodeId = typeof node.nodeId === \"string\" ? node.nodeId.trim() : \"\";\n if (!nodeId) continue;\n const displayName =\n typeof node.displayName === \"string\" && node.displayName.trim()\n ? node.displayName.trim()\n : nodeId;\n list.push({ id: nodeId, label: displayName === nodeId ? nodeId : `${displayName} · ${nodeId}` });\n }\n list.sort((a, b) => a.label.localeCompare(b.label));\n return list;\n}\n\nfunction resolveExecApprovalsNodes(nodes: Array>): ExecApprovalsTargetNode[] {\n const list: ExecApprovalsTargetNode[] = [];\n for (const node of nodes) {\n const commands = Array.isArray(node.commands) ? node.commands : [];\n const supports = commands.some(\n (cmd) => String(cmd) === \"system.execApprovals.get\" || String(cmd) === \"system.execApprovals.set\",\n );\n if (!supports) continue;\n const nodeId = typeof node.nodeId === \"string\" ? node.nodeId.trim() : \"\";\n if (!nodeId) continue;\n const displayName =\n typeof node.displayName === \"string\" && node.displayName.trim()\n ? node.displayName.trim()\n : nodeId;\n list.push({ id: nodeId, label: displayName === nodeId ? nodeId : `${displayName} · ${nodeId}` });\n }\n list.sort((a, b) => a.label.localeCompare(b.label));\n return list;\n}\n\nfunction resolveAgentBindings(config: Record | null): {\n defaultBinding?: string | null;\n agents: BindingAgent[];\n} {\n const fallbackAgent: BindingAgent = {\n id: \"main\",\n name: undefined,\n index: 0,\n isDefault: true,\n binding: null,\n };\n if (!config || typeof config !== \"object\") {\n return { defaultBinding: null, agents: [fallbackAgent] };\n }\n const tools = (config.tools ?? {}) as Record;\n const exec = (tools.exec ?? {}) as Record;\n const defaultBinding =\n typeof exec.node === \"string\" && exec.node.trim() ? exec.node.trim() : null;\n\n const agentsNode = (config.agents ?? {}) as Record;\n const list = Array.isArray(agentsNode.list) ? agentsNode.list : [];\n if (list.length === 0) {\n return { defaultBinding, agents: [fallbackAgent] };\n }\n\n const agents: BindingAgent[] = [];\n list.forEach((entry, index) => {\n if (!entry || typeof entry !== \"object\") return;\n const record = entry as Record;\n const id = typeof record.id === \"string\" ? record.id.trim() : \"\";\n if (!id) return;\n const name = typeof record.name === \"string\" ? record.name.trim() : undefined;\n const isDefault = record.default === true;\n const toolsEntry = (record.tools ?? {}) as Record;\n const execEntry = (toolsEntry.exec ?? {}) as Record;\n const binding =\n typeof execEntry.node === \"string\" && execEntry.node.trim()\n ? execEntry.node.trim()\n : null;\n agents.push({\n id,\n name: name || undefined,\n index,\n isDefault,\n binding,\n });\n });\n\n if (agents.length === 0) {\n agents.push(fallbackAgent);\n }\n\n return { defaultBinding, agents };\n}\n\nfunction renderNode(node: Record) {\n const connected = Boolean(node.connected);\n const paired = Boolean(node.paired);\n const title =\n (typeof node.displayName === \"string\" && node.displayName.trim()) ||\n (typeof node.nodeId === \"string\" ? node.nodeId : \"unknown\");\n const caps = Array.isArray(node.caps) ? (node.caps as unknown[]) : [];\n const commands = Array.isArray(node.commands) ? (node.commands as unknown[]) : [];\n return html`\n
    \n
    \n
    ${title}
    \n
    \n ${typeof node.nodeId === \"string\" ? node.nodeId : \"\"}\n ${typeof node.remoteIp === \"string\" ? ` · ${node.remoteIp}` : \"\"}\n ${typeof node.version === \"string\" ? ` · ${node.version}` : \"\"}\n
    \n
    \n ${paired ? \"paired\" : \"unpaired\"}\n \n ${connected ? \"connected\" : \"offline\"}\n \n ${caps.slice(0, 12).map((c) => html`${String(c)}`)}\n ${commands\n .slice(0, 8)\n .map((c) => html`${String(c)}`)}\n
    \n
    \n
    \n `;\n}\n","import { html } from \"lit\";\n\nimport type { GatewayHelloOk } from \"../gateway\";\nimport { formatAgo, formatDurationMs } from \"../format\";\nimport { formatNextRun } from \"../presenter\";\nimport type { UiSettings } from \"../storage\";\n\nexport type OverviewProps = {\n connected: boolean;\n hello: GatewayHelloOk | null;\n settings: UiSettings;\n password: string;\n lastError: string | null;\n presenceCount: number;\n sessionsCount: number | null;\n cronEnabled: boolean | null;\n cronNext: number | null;\n lastChannelsRefresh: number | null;\n onSettingsChange: (next: UiSettings) => void;\n onPasswordChange: (next: string) => void;\n onSessionKeyChange: (next: string) => void;\n onConnect: () => void;\n onRefresh: () => void;\n};\n\nexport function renderOverview(props: OverviewProps) {\n const snapshot = props.hello?.snapshot as\n | { uptimeMs?: number; policy?: { tickIntervalMs?: number } }\n | undefined;\n const uptime = snapshot?.uptimeMs ? formatDurationMs(snapshot.uptimeMs) : \"n/a\";\n const tick = snapshot?.policy?.tickIntervalMs\n ? `${snapshot.policy.tickIntervalMs}ms`\n : \"n/a\";\n const authHint = (() => {\n if (props.connected || !props.lastError) return null;\n const lower = props.lastError.toLowerCase();\n const authFailed = lower.includes(\"unauthorized\") || lower.includes(\"connect failed\");\n if (!authFailed) return null;\n const hasToken = Boolean(props.settings.token.trim());\n const hasPassword = Boolean(props.password.trim());\n if (!hasToken && !hasPassword) {\n return html`\n
    \n This gateway requires auth. Add a token or password, then click Connect.\n
    \n clawdbot dashboard --no-open → tokenized URL
    \n clawdbot doctor --generate-gateway-token → set token\n
    \n
    \n Docs: Control UI auth\n
    \n
    \n `;\n }\n return html`\n
    \n Auth failed. Re-copy a tokenized URL with\n clawdbot dashboard --no-open, or update the token,\n then click Connect.\n
    \n Docs: Control UI auth\n
    \n
    \n `;\n })();\n const insecureContextHint = (() => {\n if (props.connected || !props.lastError) return null;\n const isSecureContext = typeof window !== \"undefined\" ? window.isSecureContext : true;\n if (isSecureContext !== false) return null;\n const lower = props.lastError.toLowerCase();\n if (!lower.includes(\"secure context\") && !lower.includes(\"device identity required\")) {\n return null;\n }\n return html`\n
    \n This page is HTTP, so the browser blocks device identity. Use HTTPS (Tailscale Serve) or\n open http://127.0.0.1:18789 on the gateway host.\n
    \n If you must stay on HTTP, set\n gateway.controlUi.allowInsecureAuth: true (token-only).\n
    \n
    \n Docs: Tailscale Serve\n · \n Docs: Insecure HTTP\n
    \n
    \n `;\n })();\n\n return html`\n
    \n
    \n
    Gateway Access
    \n
    Where the dashboard connects and how it authenticates.
    \n
    \n \n \n \n \n
    \n
    \n \n \n Click Connect to apply connection changes.\n
    \n
    \n\n
    \n
    Snapshot
    \n
    Latest gateway handshake information.
    \n
    \n
    \n
    Status
    \n
    \n ${props.connected ? \"Connected\" : \"Disconnected\"}\n
    \n
    \n
    \n
    Uptime
    \n
    ${uptime}
    \n
    \n
    \n
    Tick Interval
    \n
    ${tick}
    \n
    \n
    \n
    Last Channels Refresh
    \n
    \n ${props.lastChannelsRefresh\n ? formatAgo(props.lastChannelsRefresh)\n : \"n/a\"}\n
    \n
    \n
    \n ${props.lastError\n ? html`
    \n
    ${props.lastError}
    \n ${authHint ?? \"\"}\n ${insecureContextHint ?? \"\"}\n
    `\n : html`
    \n Use Channels to link WhatsApp, Telegram, Discord, Signal, or iMessage.\n
    `}\n
    \n
    \n\n
    \n
    \n
    Instances
    \n
    ${props.presenceCount}
    \n
    Presence beacons in the last 5 minutes.
    \n
    \n
    \n
    Sessions
    \n
    ${props.sessionsCount ?? \"n/a\"}
    \n
    Recent session keys tracked by the gateway.
    \n
    \n
    \n
    Cron
    \n
    \n ${props.cronEnabled == null\n ? \"n/a\"\n : props.cronEnabled\n ? \"Enabled\"\n : \"Disabled\"}\n
    \n
    Next wake ${formatNextRun(props.cronNext)}
    \n
    \n
    \n\n
    \n
    Notes
    \n
    Quick reminders for remote control setups.
    \n
    \n
    \n
    Tailscale serve
    \n
    \n Prefer serve mode to keep the gateway on loopback with tailnet auth.\n
    \n
    \n
    \n
    Session hygiene
    \n
    Use /new or sessions.patch to reset context.
    \n
    \n
    \n
    Cron reminders
    \n
    Use isolated sessions for recurring runs.
    \n
    \n
    \n
    \n `;\n}\n","import { html, nothing } from \"lit\";\n\nimport { formatAgo } from \"../format\";\nimport { formatSessionTokens } from \"../presenter\";\nimport { pathForTab } from \"../navigation\";\nimport type { GatewaySessionRow, SessionsListResult } from \"../types\";\n\nexport type SessionsProps = {\n loading: boolean;\n result: SessionsListResult | null;\n error: string | null;\n activeMinutes: string;\n limit: string;\n includeGlobal: boolean;\n includeUnknown: boolean;\n basePath: string;\n onFiltersChange: (next: {\n activeMinutes: string;\n limit: string;\n includeGlobal: boolean;\n includeUnknown: boolean;\n }) => void;\n onRefresh: () => void;\n onPatch: (\n key: string,\n patch: {\n label?: string | null;\n thinkingLevel?: string | null;\n verboseLevel?: string | null;\n reasoningLevel?: string | null;\n },\n ) => void;\n onDelete: (key: string) => void;\n};\n\nconst THINK_LEVELS = [\"\", \"off\", \"minimal\", \"low\", \"medium\", \"high\"] as const;\nconst BINARY_THINK_LEVELS = [\"\", \"off\", \"on\"] as const;\nconst VERBOSE_LEVELS = [\n { value: \"\", label: \"inherit\" },\n { value: \"off\", label: \"off (explicit)\" },\n { value: \"on\", label: \"on\" },\n] as const;\nconst REASONING_LEVELS = [\"\", \"off\", \"on\", \"stream\"] as const;\n\nfunction normalizeProviderId(provider?: string | null): string {\n if (!provider) return \"\";\n const normalized = provider.trim().toLowerCase();\n if (normalized === \"z.ai\" || normalized === \"z-ai\") return \"zai\";\n return normalized;\n}\n\nfunction isBinaryThinkingProvider(provider?: string | null): boolean {\n return normalizeProviderId(provider) === \"zai\";\n}\n\nfunction resolveThinkLevelOptions(provider?: string | null): readonly string[] {\n return isBinaryThinkingProvider(provider) ? BINARY_THINK_LEVELS : THINK_LEVELS;\n}\n\nfunction resolveThinkLevelDisplay(value: string, isBinary: boolean): string {\n if (!isBinary) return value;\n if (!value || value === \"off\") return value;\n return \"on\";\n}\n\nfunction resolveThinkLevelPatchValue(value: string, isBinary: boolean): string | null {\n if (!value) return null;\n if (!isBinary) return value;\n if (value === \"on\") return \"low\";\n return value;\n}\n\nexport function renderSessions(props: SessionsProps) {\n const rows = props.result?.sessions ?? [];\n return html`\n
    \n
    \n
    \n
    Sessions
    \n
    Active session keys and per-session overrides.
    \n
    \n \n
    \n\n
    \n \n \n \n \n
    \n\n ${props.error\n ? html`
    ${props.error}
    `\n : nothing}\n\n
    \n ${props.result ? `Store: ${props.result.path}` : \"\"}\n
    \n\n
    \n
    \n
    Key
    \n
    Label
    \n
    Kind
    \n
    Updated
    \n
    Tokens
    \n
    Thinking
    \n
    Verbose
    \n
    Reasoning
    \n
    Actions
    \n
    \n ${rows.length === 0\n ? html`
    No sessions found.
    `\n : rows.map((row) =>\n renderRow(row, props.basePath, props.onPatch, props.onDelete, props.loading),\n )}\n
    \n
    \n `;\n}\n\nfunction renderRow(\n row: GatewaySessionRow,\n basePath: string,\n onPatch: SessionsProps[\"onPatch\"],\n onDelete: SessionsProps[\"onDelete\"],\n disabled: boolean,\n) {\n const updated = row.updatedAt ? formatAgo(row.updatedAt) : \"n/a\";\n const rawThinking = row.thinkingLevel ?? \"\";\n const isBinaryThinking = isBinaryThinkingProvider(row.modelProvider);\n const thinking = resolveThinkLevelDisplay(rawThinking, isBinaryThinking);\n const thinkLevels = resolveThinkLevelOptions(row.modelProvider);\n const verbose = row.verboseLevel ?? \"\";\n const reasoning = row.reasoningLevel ?? \"\";\n const displayName = row.displayName ?? row.key;\n const canLink = row.kind !== \"global\";\n const chatUrl = canLink\n ? `${pathForTab(\"chat\", basePath)}?session=${encodeURIComponent(row.key)}`\n : null;\n\n return html`\n
    \n
    ${canLink\n ? html`${displayName}`\n : displayName}
    \n
    \n {\n const value = (e.target as HTMLInputElement).value.trim();\n onPatch(row.key, { label: value || null });\n }}\n />\n
    \n
    ${row.kind}
    \n
    ${updated}
    \n
    ${formatSessionTokens(row)}
    \n
    \n {\n const value = (e.target as HTMLSelectElement).value;\n onPatch(row.key, {\n thinkingLevel: resolveThinkLevelPatchValue(value, isBinaryThinking),\n });\n }}\n >\n ${thinkLevels.map((level) =>\n html``,\n )}\n \n
    \n
    \n {\n const value = (e.target as HTMLSelectElement).value;\n onPatch(row.key, { verboseLevel: value || null });\n }}\n >\n ${VERBOSE_LEVELS.map(\n (level) => html``,\n )}\n \n
    \n
    \n {\n const value = (e.target as HTMLSelectElement).value;\n onPatch(row.key, { reasoningLevel: value || null });\n }}\n >\n ${REASONING_LEVELS.map((level) =>\n html``,\n )}\n \n
    \n
    \n \n
    \n
    \n `;\n}\n","import { html, nothing } from \"lit\";\n\nimport type { AppViewState } from \"../app-view-state\";\n\nfunction formatRemaining(ms: number): string {\n const remaining = Math.max(0, ms);\n const totalSeconds = Math.floor(remaining / 1000);\n if (totalSeconds < 60) return `${totalSeconds}s`;\n const minutes = Math.floor(totalSeconds / 60);\n if (minutes < 60) return `${minutes}m`;\n const hours = Math.floor(minutes / 60);\n return `${hours}h`;\n}\n\nfunction renderMetaRow(label: string, value?: string | null) {\n if (!value) return nothing;\n return html`
    ${label}${value}
    `;\n}\n\nexport function renderExecApprovalPrompt(state: AppViewState) {\n const active = state.execApprovalQueue[0];\n if (!active) return nothing;\n const request = active.request;\n const remainingMs = active.expiresAtMs - Date.now();\n const remaining = remainingMs > 0 ? `expires in ${formatRemaining(remainingMs)}` : \"expired\";\n const queueCount = state.execApprovalQueue.length;\n return html`\n
    \n
    \n
    \n
    \n
    Exec approval needed
    \n
    ${remaining}
    \n
    \n ${queueCount > 1\n ? html`
    ${queueCount} pending
    `\n : nothing}\n
    \n
    ${request.command}
    \n
    \n ${renderMetaRow(\"Host\", request.host)}\n ${renderMetaRow(\"Agent\", request.agentId)}\n ${renderMetaRow(\"Session\", request.sessionKey)}\n ${renderMetaRow(\"CWD\", request.cwd)}\n ${renderMetaRow(\"Resolved\", request.resolvedPath)}\n ${renderMetaRow(\"Security\", request.security)}\n ${renderMetaRow(\"Ask\", request.ask)}\n
    \n ${state.execApprovalError\n ? html`
    ${state.execApprovalError}
    `\n : nothing}\n
    \n state.handleExecApprovalDecision(\"allow-once\")}\n >\n Allow once\n \n state.handleExecApprovalDecision(\"allow-always\")}\n >\n Always allow\n \n state.handleExecApprovalDecision(\"deny\")}\n >\n Deny\n \n
    \n
    \n
    \n `;\n}\n","import { html, nothing } from \"lit\";\n\nimport { clampText } from \"../format\";\nimport type { SkillStatusEntry, SkillStatusReport } from \"../types\";\nimport type { SkillMessageMap } from \"../controllers/skills\";\n\nexport type SkillsProps = {\n loading: boolean;\n report: SkillStatusReport | null;\n error: string | null;\n filter: string;\n edits: Record;\n busyKey: string | null;\n messages: SkillMessageMap;\n onFilterChange: (next: string) => void;\n onRefresh: () => void;\n onToggle: (skillKey: string, enabled: boolean) => void;\n onEdit: (skillKey: string, value: string) => void;\n onSaveKey: (skillKey: string) => void;\n onInstall: (skillKey: string, name: string, installId: string) => void;\n};\n\nexport function renderSkills(props: SkillsProps) {\n const skills = props.report?.skills ?? [];\n const filter = props.filter.trim().toLowerCase();\n const filtered = filter\n ? skills.filter((skill) =>\n [skill.name, skill.description, skill.source]\n .join(\" \")\n .toLowerCase()\n .includes(filter),\n )\n : skills;\n\n return html`\n
    \n
    \n
    \n
    Skills
    \n
    Bundled, managed, and workspace skills.
    \n
    \n \n
    \n\n
    \n \n
    ${filtered.length} shown
    \n
    \n\n ${props.error\n ? html`
    ${props.error}
    `\n : nothing}\n\n ${filtered.length === 0\n ? html`
    No skills found.
    `\n : html`\n
    \n ${filtered.map((skill) => renderSkill(skill, props))}\n
    \n `}\n
    \n `;\n}\n\nfunction renderSkill(skill: SkillStatusEntry, props: SkillsProps) {\n const busy = props.busyKey === skill.skillKey;\n const apiKey = props.edits[skill.skillKey] ?? \"\";\n const message = props.messages[skill.skillKey] ?? null;\n const canInstall =\n skill.install.length > 0 && skill.missing.bins.length > 0;\n const missing = [\n ...skill.missing.bins.map((b) => `bin:${b}`),\n ...skill.missing.env.map((e) => `env:${e}`),\n ...skill.missing.config.map((c) => `config:${c}`),\n ...skill.missing.os.map((o) => `os:${o}`),\n ];\n const reasons: string[] = [];\n if (skill.disabled) reasons.push(\"disabled\");\n if (skill.blockedByAllowlist) reasons.push(\"blocked by allowlist\");\n return html`\n
    \n
    \n
    \n ${skill.emoji ? `${skill.emoji} ` : \"\"}${skill.name}\n
    \n
    ${clampText(skill.description, 140)}
    \n
    \n ${skill.source}\n \n ${skill.eligible ? \"eligible\" : \"blocked\"}\n \n ${skill.disabled ? html`disabled` : nothing}\n
    \n ${missing.length > 0\n ? html`\n
    \n Missing: ${missing.join(\", \")}\n
    \n `\n : nothing}\n ${reasons.length > 0\n ? html`\n
    \n Reason: ${reasons.join(\", \")}\n
    \n `\n : nothing}\n
    \n
    \n
    \n props.onToggle(skill.skillKey, skill.disabled)}\n >\n ${skill.disabled ? \"Enable\" : \"Disable\"}\n \n ${canInstall\n ? html`\n props.onInstall(skill.skillKey, skill.name, skill.install[0].id)}\n >\n ${busy ? \"Installing…\" : skill.install[0].label}\n `\n : nothing}\n
    \n ${message\n ? html`\n ${message.message}\n
    `\n : nothing}\n ${skill.primaryEnv\n ? html`\n
    \n API key\n \n props.onEdit(skill.skillKey, (e.target as HTMLInputElement).value)}\n />\n
    \n props.onSaveKey(skill.skillKey)}\n >\n Save key\n \n `\n : nothing}\n
    \n \n `;\n}\n","import { html } from \"lit\";\nimport { repeat } from \"lit/directives/repeat.js\";\n\nimport type { AppViewState } from \"./app-view-state\";\nimport { iconForTab, pathForTab, titleForTab, type Tab } from \"./navigation\";\nimport { icons } from \"./icons\";\nimport { loadChatHistory } from \"./controllers/chat\";\nimport { syncUrlWithSessionKey } from \"./app-settings\";\nimport type { SessionsListResult } from \"./types\";\nimport type { ThemeMode } from \"./theme\";\nimport type { ThemeTransitionContext } from \"./theme-transition\";\n\nexport function renderTab(state: AppViewState, tab: Tab) {\n const href = pathForTab(tab, state.basePath);\n return html`\n {\n if (\n event.defaultPrevented ||\n event.button !== 0 ||\n event.metaKey ||\n event.ctrlKey ||\n event.shiftKey ||\n event.altKey\n ) {\n return;\n }\n event.preventDefault();\n state.setTab(tab);\n }}\n title=${titleForTab(tab)}\n >\n ${icons[iconForTab(tab)]}\n ${titleForTab(tab)}\n \n `;\n}\n\nexport function renderChatControls(state: AppViewState) {\n const sessionOptions = resolveSessionOptions(state.sessionKey, state.sessionsResult);\n const disableThinkingToggle = state.onboarding;\n const disableFocusToggle = state.onboarding;\n const showThinking = state.onboarding ? false : state.settings.chatShowThinking;\n const focusActive = state.onboarding ? true : state.settings.chatFocusMode;\n // Refresh icon\n const refreshIcon = html``;\n const focusIcon = html``;\n return html`\n
    \n \n {\n state.resetToolStream();\n void loadChatHistory(state);\n }}\n title=\"Refresh chat history\"\n >\n ${refreshIcon}\n \n |\n {\n if (disableThinkingToggle) return;\n state.applySettings({\n ...state.settings,\n chatShowThinking: !state.settings.chatShowThinking,\n });\n }}\n aria-pressed=${showThinking}\n title=${disableThinkingToggle\n ? \"Disabled during onboarding\"\n : \"Toggle assistant thinking/working output\"}\n >\n ${icons.brain}\n \n {\n if (disableFocusToggle) return;\n state.applySettings({\n ...state.settings,\n chatFocusMode: !state.settings.chatFocusMode,\n });\n }}\n aria-pressed=${focusActive}\n title=${disableFocusToggle\n ? \"Disabled during onboarding\"\n : \"Toggle focus mode (hide sidebar + page header)\"}\n >\n ${focusIcon}\n \n
    \n `;\n}\n\nfunction resolveSessionOptions(sessionKey: string, sessions: SessionsListResult | null) {\n const seen = new Set();\n const options: Array<{ key: string; displayName?: string }> = [];\n\n const resolvedCurrent = sessions?.sessions?.find((s) => s.key === sessionKey);\n\n // Add current session key first\n seen.add(sessionKey);\n options.push({ key: sessionKey, displayName: resolvedCurrent?.displayName });\n\n // Add sessions from the result\n if (sessions?.sessions) {\n for (const s of sessions.sessions) {\n if (!seen.has(s.key)) {\n seen.add(s.key);\n options.push({ key: s.key, displayName: s.displayName });\n }\n }\n }\n\n return options;\n}\n\nconst THEME_ORDER: ThemeMode[] = [\"system\", \"light\", \"dark\"];\n\nexport function renderThemeToggle(state: AppViewState) {\n const index = Math.max(0, THEME_ORDER.indexOf(state.theme));\n const applyTheme = (next: ThemeMode) => (event: MouseEvent) => {\n const element = event.currentTarget as HTMLElement;\n const context: ThemeTransitionContext = { element };\n if (event.clientX || event.clientY) {\n context.pointerClientX = event.clientX;\n context.pointerClientY = event.clientY;\n }\n state.setTheme(next, context);\n };\n\n return html`\n
    \n
    \n \n \n ${renderMonitorIcon()}\n \n \n ${renderSunIcon()}\n \n \n ${renderMoonIcon()}\n \n
    \n
    \n `;\n}\n\nfunction renderSunIcon() {\n return html`\n \n \n \n \n \n \n \n \n \n \n \n `;\n}\n\nfunction renderMoonIcon() {\n return html`\n \n \n \n `;\n}\n\nfunction renderMonitorIcon() {\n return html`\n \n \n \n \n \n `;\n}\n","import { html, nothing } from \"lit\";\n\nimport type { GatewayBrowserClient, GatewayHelloOk } from \"./gateway\";\nimport type { AppViewState } from \"./app-view-state\";\nimport { parseAgentSessionKey } from \"../../../src/routing/session-key.js\";\nimport {\n TAB_GROUPS,\n iconForTab,\n pathForTab,\n subtitleForTab,\n titleForTab,\n type Tab,\n} from \"./navigation\";\nimport { icons } from \"./icons\";\nimport type { UiSettings } from \"./storage\";\nimport type { ThemeMode } from \"./theme\";\nimport type { ThemeTransitionContext } from \"./theme-transition\";\nimport type {\n ConfigSnapshot,\n CronJob,\n CronRunLogEntry,\n CronStatus,\n HealthSnapshot,\n LogEntry,\n LogLevel,\n PresenceEntry,\n ChannelsStatusSnapshot,\n SessionsListResult,\n SkillStatusReport,\n StatusSummary,\n} from \"./types\";\nimport type { ChatQueueItem, CronFormState } from \"./ui-types\";\nimport { refreshChatAvatar } from \"./app-chat\";\nimport { renderChat } from \"./views/chat\";\nimport { renderConfig } from \"./views/config\";\nimport { renderChannels } from \"./views/channels\";\nimport { renderCron } from \"./views/cron\";\nimport { renderDebug } from \"./views/debug\";\nimport { renderInstances } from \"./views/instances\";\nimport { renderLogs } from \"./views/logs\";\nimport { renderNodes } from \"./views/nodes\";\nimport { renderOverview } from \"./views/overview\";\nimport { renderSessions } from \"./views/sessions\";\nimport { renderExecApprovalPrompt } from \"./views/exec-approval\";\nimport {\n approveDevicePairing,\n loadDevices,\n rejectDevicePairing,\n revokeDeviceToken,\n rotateDeviceToken,\n} from \"./controllers/devices\";\nimport { renderSkills } from \"./views/skills\";\nimport { renderChatControls, renderTab, renderThemeToggle } from \"./app-render.helpers\";\nimport { loadChannels } from \"./controllers/channels\";\nimport { loadPresence } from \"./controllers/presence\";\nimport { deleteSession, loadSessions, patchSession } from \"./controllers/sessions\";\nimport {\n installSkill,\n loadSkills,\n saveSkillApiKey,\n updateSkillEdit,\n updateSkillEnabled,\n type SkillMessage,\n} from \"./controllers/skills\";\nimport { loadNodes } from \"./controllers/nodes\";\nimport { loadChatHistory } from \"./controllers/chat\";\nimport {\n applyConfig,\n loadConfig,\n runUpdate,\n saveConfig,\n updateConfigFormValue,\n removeConfigFormValue,\n} from \"./controllers/config\";\nimport {\n loadExecApprovals,\n removeExecApprovalsFormValue,\n saveExecApprovals,\n updateExecApprovalsFormValue,\n} from \"./controllers/exec-approvals\";\nimport { loadCronRuns, toggleCronJob, runCronJob, removeCronJob, addCronJob } from \"./controllers/cron\";\nimport { loadDebug, callDebugMethod } from \"./controllers/debug\";\nimport { loadLogs } from \"./controllers/logs\";\n\nconst AVATAR_DATA_RE = /^data:/i;\nconst AVATAR_HTTP_RE = /^https?:\\/\\//i;\n\nfunction resolveAssistantAvatarUrl(state: AppViewState): string | undefined {\n const list = state.agentsList?.agents ?? [];\n const parsed = parseAgentSessionKey(state.sessionKey);\n const agentId =\n parsed?.agentId ??\n state.agentsList?.defaultId ??\n \"main\";\n const agent = list.find((entry) => entry.id === agentId);\n const identity = agent?.identity;\n const candidate = identity?.avatarUrl ?? identity?.avatar;\n if (!candidate) return undefined;\n if (AVATAR_DATA_RE.test(candidate) || AVATAR_HTTP_RE.test(candidate)) return candidate;\n return identity?.avatarUrl;\n}\n\nexport function renderApp(state: AppViewState) {\n const presenceCount = state.presenceEntries.length;\n const sessionsCount = state.sessionsResult?.count ?? null;\n const cronNext = state.cronStatus?.nextWakeAtMs ?? null;\n const chatDisabledReason = state.connected ? null : \"Disconnected from gateway.\";\n const isChat = state.tab === \"chat\";\n const chatFocus = isChat && (state.settings.chatFocusMode || state.onboarding);\n const showThinking = state.onboarding ? false : state.settings.chatShowThinking;\n const assistantAvatarUrl = resolveAssistantAvatarUrl(state);\n const chatAvatarUrl = state.chatAvatarUrl ?? assistantAvatarUrl ?? null;\n\n return html`\n
    \n
    \n
    \n \n state.applySettings({\n ...state.settings,\n navCollapsed: !state.settings.navCollapsed,\n })}\n title=\"${state.settings.navCollapsed ? \"Expand sidebar\" : \"Collapse sidebar\"}\"\n aria-label=\"${state.settings.navCollapsed ? \"Expand sidebar\" : \"Collapse sidebar\"}\"\n >\n ${icons.menu}\n \n
    \n
    \n \"Clawdbot\"\n
    \n
    \n
    CLAWDBOT
    \n
    Gateway Dashboard
    \n
    \n
    \n
    \n
    \n
    \n \n Health\n ${state.connected ? \"OK\" : \"Offline\"}\n
    \n ${renderThemeToggle(state)}\n
    \n
    \n \n
    \n
    \n
    \n
    ${titleForTab(state.tab)}
    \n
    ${subtitleForTab(state.tab)}
    \n
    \n
    \n ${state.lastError\n ? html`
    ${state.lastError}
    `\n : nothing}\n ${isChat ? renderChatControls(state) : nothing}\n
    \n
    \n\n ${state.tab === \"overview\"\n ? renderOverview({\n connected: state.connected,\n hello: state.hello,\n settings: state.settings,\n password: state.password,\n lastError: state.lastError,\n presenceCount,\n sessionsCount,\n cronEnabled: state.cronStatus?.enabled ?? null,\n cronNext,\n lastChannelsRefresh: state.channelsLastSuccess,\n onSettingsChange: (next) => state.applySettings(next),\n onPasswordChange: (next) => (state.password = next),\n onSessionKeyChange: (next) => {\n state.sessionKey = next;\n state.chatMessage = \"\";\n state.resetToolStream();\n state.applySettings({\n ...state.settings,\n sessionKey: next,\n lastActiveSessionKey: next,\n });\n void state.loadAssistantIdentity();\n },\n onConnect: () => state.connect(),\n onRefresh: () => state.loadOverview(),\n })\n : nothing}\n\n ${state.tab === \"channels\"\n ? renderChannels({\n connected: state.connected,\n loading: state.channelsLoading,\n snapshot: state.channelsSnapshot,\n lastError: state.channelsError,\n lastSuccessAt: state.channelsLastSuccess,\n whatsappMessage: state.whatsappLoginMessage,\n whatsappQrDataUrl: state.whatsappLoginQrDataUrl,\n whatsappConnected: state.whatsappLoginConnected,\n whatsappBusy: state.whatsappBusy,\n configSchema: state.configSchema,\n configSchemaLoading: state.configSchemaLoading,\n configForm: state.configForm,\n configUiHints: state.configUiHints,\n configSaving: state.configSaving,\n configFormDirty: state.configFormDirty,\n nostrProfileFormState: state.nostrProfileFormState,\n nostrProfileAccountId: state.nostrProfileAccountId,\n onRefresh: (probe) => loadChannels(state, probe),\n onWhatsAppStart: (force) => state.handleWhatsAppStart(force),\n onWhatsAppWait: () => state.handleWhatsAppWait(),\n onWhatsAppLogout: () => state.handleWhatsAppLogout(),\n onConfigPatch: (path, value) => updateConfigFormValue(state, path, value),\n onConfigSave: () => state.handleChannelConfigSave(),\n onConfigReload: () => state.handleChannelConfigReload(),\n onNostrProfileEdit: (accountId, profile) =>\n state.handleNostrProfileEdit(accountId, profile),\n onNostrProfileCancel: () => state.handleNostrProfileCancel(),\n onNostrProfileFieldChange: (field, value) =>\n state.handleNostrProfileFieldChange(field, value),\n onNostrProfileSave: () => state.handleNostrProfileSave(),\n onNostrProfileImport: () => state.handleNostrProfileImport(),\n onNostrProfileToggleAdvanced: () => state.handleNostrProfileToggleAdvanced(),\n })\n : nothing}\n\n ${state.tab === \"instances\"\n ? renderInstances({\n loading: state.presenceLoading,\n entries: state.presenceEntries,\n lastError: state.presenceError,\n statusMessage: state.presenceStatus,\n onRefresh: () => loadPresence(state),\n })\n : nothing}\n\n ${state.tab === \"sessions\"\n ? renderSessions({\n loading: state.sessionsLoading,\n result: state.sessionsResult,\n error: state.sessionsError,\n activeMinutes: state.sessionsFilterActive,\n limit: state.sessionsFilterLimit,\n includeGlobal: state.sessionsIncludeGlobal,\n includeUnknown: state.sessionsIncludeUnknown,\n basePath: state.basePath,\n onFiltersChange: (next) => {\n state.sessionsFilterActive = next.activeMinutes;\n state.sessionsFilterLimit = next.limit;\n state.sessionsIncludeGlobal = next.includeGlobal;\n state.sessionsIncludeUnknown = next.includeUnknown;\n\t },\n\t onRefresh: () => loadSessions(state),\n\t onPatch: (key, patch) => patchSession(state, key, patch),\n\t onDelete: (key) => deleteSession(state, key),\n\t })\n\t : nothing}\n\n ${state.tab === \"cron\"\n ? renderCron({\n loading: state.cronLoading,\n status: state.cronStatus,\n jobs: state.cronJobs,\n error: state.cronError,\n busy: state.cronBusy,\n form: state.cronForm,\n channels: state.channelsSnapshot?.channelMeta?.length\n ? state.channelsSnapshot.channelMeta.map((entry) => entry.id)\n : state.channelsSnapshot?.channelOrder ?? [],\n channelLabels: state.channelsSnapshot?.channelLabels ?? {},\n channelMeta: state.channelsSnapshot?.channelMeta ?? [],\n runsJobId: state.cronRunsJobId,\n runs: state.cronRuns,\n onFormChange: (patch) => (state.cronForm = { ...state.cronForm, ...patch }),\n onRefresh: () => state.loadCron(),\n onAdd: () => addCronJob(state),\n onToggle: (job, enabled) => toggleCronJob(state, job, enabled),\n onRun: (job) => runCronJob(state, job),\n onRemove: (job) => removeCronJob(state, job),\n onLoadRuns: (jobId) => loadCronRuns(state, jobId),\n })\n : nothing}\n\n ${state.tab === \"skills\"\n ? renderSkills({\n loading: state.skillsLoading,\n report: state.skillsReport,\n error: state.skillsError,\n filter: state.skillsFilter,\n edits: state.skillEdits,\n messages: state.skillMessages,\n busyKey: state.skillsBusyKey,\n onFilterChange: (next) => (state.skillsFilter = next),\n onRefresh: () => loadSkills(state, { clearMessages: true }),\n onToggle: (key, enabled) => updateSkillEnabled(state, key, enabled),\n onEdit: (key, value) => updateSkillEdit(state, key, value),\n onSaveKey: (key) => saveSkillApiKey(state, key),\n onInstall: (skillKey, name, installId) =>\n installSkill(state, skillKey, name, installId),\n })\n : nothing}\n\n ${state.tab === \"nodes\"\n ? renderNodes({\n loading: state.nodesLoading,\n nodes: state.nodes,\n devicesLoading: state.devicesLoading,\n devicesError: state.devicesError,\n devicesList: state.devicesList,\n configForm: state.configForm ?? (state.configSnapshot?.config as Record | null),\n configLoading: state.configLoading,\n configSaving: state.configSaving,\n configDirty: state.configFormDirty,\n configFormMode: state.configFormMode,\n execApprovalsLoading: state.execApprovalsLoading,\n execApprovalsSaving: state.execApprovalsSaving,\n execApprovalsDirty: state.execApprovalsDirty,\n execApprovalsSnapshot: state.execApprovalsSnapshot,\n execApprovalsForm: state.execApprovalsForm,\n execApprovalsSelectedAgent: state.execApprovalsSelectedAgent,\n execApprovalsTarget: state.execApprovalsTarget,\n execApprovalsTargetNodeId: state.execApprovalsTargetNodeId,\n onRefresh: () => loadNodes(state),\n onDevicesRefresh: () => loadDevices(state),\n onDeviceApprove: (requestId) => approveDevicePairing(state, requestId),\n onDeviceReject: (requestId) => rejectDevicePairing(state, requestId),\n onDeviceRotate: (deviceId, role, scopes) =>\n rotateDeviceToken(state, { deviceId, role, scopes }),\n onDeviceRevoke: (deviceId, role) =>\n revokeDeviceToken(state, { deviceId, role }),\n onLoadConfig: () => loadConfig(state),\n onLoadExecApprovals: () => {\n const target =\n state.execApprovalsTarget === \"node\" && state.execApprovalsTargetNodeId\n ? { kind: \"node\" as const, nodeId: state.execApprovalsTargetNodeId }\n : { kind: \"gateway\" as const };\n return loadExecApprovals(state, target);\n },\n onBindDefault: (nodeId) => {\n if (nodeId) {\n updateConfigFormValue(state, [\"tools\", \"exec\", \"node\"], nodeId);\n } else {\n removeConfigFormValue(state, [\"tools\", \"exec\", \"node\"]);\n }\n },\n onBindAgent: (agentIndex, nodeId) => {\n const basePath = [\"agents\", \"list\", agentIndex, \"tools\", \"exec\", \"node\"];\n if (nodeId) {\n updateConfigFormValue(state, basePath, nodeId);\n } else {\n removeConfigFormValue(state, basePath);\n }\n },\n onSaveBindings: () => saveConfig(state),\n onExecApprovalsTargetChange: (kind, nodeId) => {\n state.execApprovalsTarget = kind;\n state.execApprovalsTargetNodeId = nodeId;\n state.execApprovalsSnapshot = null;\n state.execApprovalsForm = null;\n state.execApprovalsDirty = false;\n state.execApprovalsSelectedAgent = null;\n },\n onExecApprovalsSelectAgent: (agentId) => {\n state.execApprovalsSelectedAgent = agentId;\n },\n onExecApprovalsPatch: (path, value) =>\n updateExecApprovalsFormValue(state, path, value),\n onExecApprovalsRemove: (path) =>\n removeExecApprovalsFormValue(state, path),\n onSaveExecApprovals: () => {\n const target =\n state.execApprovalsTarget === \"node\" && state.execApprovalsTargetNodeId\n ? { kind: \"node\" as const, nodeId: state.execApprovalsTargetNodeId }\n : { kind: \"gateway\" as const };\n return saveExecApprovals(state, target);\n },\n })\n : nothing}\n\n ${state.tab === \"chat\"\n ? renderChat({\n sessionKey: state.sessionKey,\n onSessionKeyChange: (next) => {\n state.sessionKey = next;\n state.chatMessage = \"\";\n state.chatStream = null;\n state.chatStreamStartedAt = null;\n state.chatRunId = null;\n state.chatQueue = [];\n state.resetToolStream();\n state.resetChatScroll();\n state.applySettings({\n ...state.settings,\n sessionKey: next,\n lastActiveSessionKey: next,\n });\n void state.loadAssistantIdentity();\n void loadChatHistory(state);\n void refreshChatAvatar(state);\n },\n thinkingLevel: state.chatThinkingLevel,\n showThinking,\n loading: state.chatLoading,\n sending: state.chatSending,\n compactionStatus: state.compactionStatus,\n assistantAvatarUrl: chatAvatarUrl,\n messages: state.chatMessages,\n toolMessages: state.chatToolMessages,\n stream: state.chatStream,\n streamStartedAt: state.chatStreamStartedAt,\n draft: state.chatMessage,\n queue: state.chatQueue,\n connected: state.connected,\n canSend: state.connected,\n disabledReason: chatDisabledReason,\n error: state.lastError,\n sessions: state.sessionsResult,\n focusMode: chatFocus,\n onRefresh: () => {\n state.resetToolStream();\n return Promise.all([loadChatHistory(state), refreshChatAvatar(state)]);\n },\n onToggleFocusMode: () => {\n if (state.onboarding) return;\n state.applySettings({\n ...state.settings,\n chatFocusMode: !state.settings.chatFocusMode,\n });\n },\n onChatScroll: (event) => state.handleChatScroll(event),\n onDraftChange: (next) => (state.chatMessage = next),\n onSend: () => state.handleSendChat(),\n canAbort: Boolean(state.chatRunId),\n onAbort: () => void state.handleAbortChat(),\n onQueueRemove: (id) => state.removeQueuedMessage(id),\n onNewSession: () =>\n state.handleSendChat(\"/new\", { restoreDraft: true }),\n // Sidebar props for tool output viewing\n sidebarOpen: state.sidebarOpen,\n sidebarContent: state.sidebarContent,\n sidebarError: state.sidebarError,\n splitRatio: state.splitRatio,\n onOpenSidebar: (content: string) => state.handleOpenSidebar(content),\n onCloseSidebar: () => state.handleCloseSidebar(),\n onSplitRatioChange: (ratio: number) => state.handleSplitRatioChange(ratio),\n assistantName: state.assistantName,\n assistantAvatar: state.assistantAvatar,\n })\n : nothing}\n\n ${state.tab === \"config\"\n ? renderConfig({\n raw: state.configRaw,\n originalRaw: state.configRawOriginal,\n valid: state.configValid,\n issues: state.configIssues,\n loading: state.configLoading,\n saving: state.configSaving,\n applying: state.configApplying,\n updating: state.updateRunning,\n connected: state.connected,\n schema: state.configSchema,\n schemaLoading: state.configSchemaLoading,\n uiHints: state.configUiHints,\n formMode: state.configFormMode,\n formValue: state.configForm,\n originalValue: state.configFormOriginal,\n searchQuery: state.configSearchQuery,\n activeSection: state.configActiveSection,\n activeSubsection: state.configActiveSubsection,\n onRawChange: (next) => {\n state.configRaw = next;\n },\n onFormModeChange: (mode) => (state.configFormMode = mode),\n onFormPatch: (path, value) => updateConfigFormValue(state, path, value),\n onSearchChange: (query) => (state.configSearchQuery = query),\n onSectionChange: (section) => {\n state.configActiveSection = section;\n state.configActiveSubsection = null;\n },\n onSubsectionChange: (section) => (state.configActiveSubsection = section),\n onReload: () => loadConfig(state),\n onSave: () => saveConfig(state),\n onApply: () => applyConfig(state),\n onUpdate: () => runUpdate(state),\n })\n : nothing}\n\n ${state.tab === \"debug\"\n ? renderDebug({\n loading: state.debugLoading,\n status: state.debugStatus,\n health: state.debugHealth,\n models: state.debugModels,\n heartbeat: state.debugHeartbeat,\n eventLog: state.eventLog,\n callMethod: state.debugCallMethod,\n callParams: state.debugCallParams,\n callResult: state.debugCallResult,\n callError: state.debugCallError,\n onCallMethodChange: (next) => (state.debugCallMethod = next),\n onCallParamsChange: (next) => (state.debugCallParams = next),\n onRefresh: () => loadDebug(state),\n onCall: () => callDebugMethod(state),\n })\n : nothing}\n\n ${state.tab === \"logs\"\n ? renderLogs({\n loading: state.logsLoading,\n error: state.logsError,\n file: state.logsFile,\n entries: state.logsEntries,\n filterText: state.logsFilterText,\n levelFilters: state.logsLevelFilters,\n autoFollow: state.logsAutoFollow,\n truncated: state.logsTruncated,\n onFilterTextChange: (next) => (state.logsFilterText = next),\n onLevelToggle: (level, enabled) => {\n state.logsLevelFilters = { ...state.logsLevelFilters, [level]: enabled };\n },\n onToggleAutoFollow: (next) => (state.logsAutoFollow = next),\n onRefresh: () => loadLogs(state, { reset: true }),\n onExport: (lines, label) => state.exportLogs(lines, label),\n onScroll: (event) => state.handleLogsScroll(event),\n })\n : nothing}\n
    \n ${renderExecApprovalPrompt(state)}\n
    \n `;\n}\n","import type { LogLevel } from \"./types\";\nimport type { CronFormState } from \"./ui-types\";\n\nexport const DEFAULT_LOG_LEVEL_FILTERS: Record = {\n trace: true,\n debug: true,\n info: true,\n warn: true,\n error: true,\n fatal: true,\n};\n\nexport const DEFAULT_CRON_FORM: CronFormState = {\n name: \"\",\n description: \"\",\n agentId: \"\",\n enabled: true,\n scheduleKind: \"every\",\n scheduleAt: \"\",\n everyAmount: \"30\",\n everyUnit: \"minutes\",\n cronExpr: \"0 7 * * *\",\n cronTz: \"\",\n sessionTarget: \"main\",\n wakeMode: \"next-heartbeat\",\n payloadKind: \"systemEvent\",\n payloadText: \"\",\n deliver: false,\n channel: \"last\",\n to: \"\",\n timeoutSeconds: \"\",\n postToMainPrefix: \"\",\n};\n","import type { GatewayBrowserClient } from \"../gateway\";\nimport type { AgentsListResult } from \"../types\";\n\nexport type AgentsState = {\n client: GatewayBrowserClient | null;\n connected: boolean;\n agentsLoading: boolean;\n agentsError: string | null;\n agentsList: AgentsListResult | null;\n};\n\nexport async function loadAgents(state: AgentsState) {\n if (!state.client || !state.connected) return;\n if (state.agentsLoading) return;\n state.agentsLoading = true;\n state.agentsError = null;\n try {\n const res = (await state.client.request(\"agents.list\", {})) as AgentsListResult | undefined;\n if (res) state.agentsList = res;\n } catch (err) {\n state.agentsError = String(err);\n } finally {\n state.agentsLoading = false;\n }\n}\n","export const GATEWAY_CLIENT_IDS = {\n WEBCHAT_UI: \"webchat-ui\",\n CONTROL_UI: \"clawdbot-control-ui\",\n WEBCHAT: \"webchat\",\n CLI: \"cli\",\n GATEWAY_CLIENT: \"gateway-client\",\n MACOS_APP: \"clawdbot-macos\",\n IOS_APP: \"clawdbot-ios\",\n ANDROID_APP: \"clawdbot-android\",\n NODE_HOST: \"node-host\",\n TEST: \"test\",\n FINGERPRINT: \"fingerprint\",\n PROBE: \"clawdbot-probe\",\n} as const;\n\nexport type GatewayClientId = (typeof GATEWAY_CLIENT_IDS)[keyof typeof GATEWAY_CLIENT_IDS];\n\n// Back-compat naming (internal): these values are IDs, not display names.\nexport const GATEWAY_CLIENT_NAMES = GATEWAY_CLIENT_IDS;\nexport type GatewayClientName = GatewayClientId;\n\nexport const GATEWAY_CLIENT_MODES = {\n WEBCHAT: \"webchat\",\n CLI: \"cli\",\n UI: \"ui\",\n BACKEND: \"backend\",\n NODE: \"node\",\n PROBE: \"probe\",\n TEST: \"test\",\n} as const;\n\nexport type GatewayClientMode = (typeof GATEWAY_CLIENT_MODES)[keyof typeof GATEWAY_CLIENT_MODES];\n\nexport type GatewayClientInfo = {\n id: GatewayClientId;\n displayName?: string;\n version: string;\n platform: string;\n deviceFamily?: string;\n modelIdentifier?: string;\n mode: GatewayClientMode;\n instanceId?: string;\n};\n\nconst GATEWAY_CLIENT_ID_SET = new Set(Object.values(GATEWAY_CLIENT_IDS));\nconst GATEWAY_CLIENT_MODE_SET = new Set(Object.values(GATEWAY_CLIENT_MODES));\n\nexport function normalizeGatewayClientId(raw?: string | null): GatewayClientId | undefined {\n const normalized = raw?.trim().toLowerCase();\n if (!normalized) return undefined;\n return GATEWAY_CLIENT_ID_SET.has(normalized as GatewayClientId)\n ? (normalized as GatewayClientId)\n : undefined;\n}\n\nexport function normalizeGatewayClientName(raw?: string | null): GatewayClientName | undefined {\n return normalizeGatewayClientId(raw);\n}\n\nexport function normalizeGatewayClientMode(raw?: string | null): GatewayClientMode | undefined {\n const normalized = raw?.trim().toLowerCase();\n if (!normalized) return undefined;\n return GATEWAY_CLIENT_MODE_SET.has(normalized as GatewayClientMode)\n ? (normalized as GatewayClientMode)\n : undefined;\n}\n","export type DeviceAuthPayloadParams = {\n deviceId: string;\n clientId: string;\n clientMode: string;\n role: string;\n scopes: string[];\n signedAtMs: number;\n token?: string | null;\n nonce?: string | null;\n version?: \"v1\" | \"v2\";\n};\n\nexport function buildDeviceAuthPayload(params: DeviceAuthPayloadParams): string {\n const version = params.version ?? (params.nonce ? \"v2\" : \"v1\");\n const scopes = params.scopes.join(\",\");\n const token = params.token ?? \"\";\n const base = [\n version,\n params.deviceId,\n params.clientId,\n params.clientMode,\n params.role,\n scopes,\n String(params.signedAtMs),\n token,\n ];\n if (version === \"v2\") {\n base.push(params.nonce ?? \"\");\n }\n return base.join(\"|\");\n}\n","import { generateUUID } from \"./uuid\";\nimport {\n GATEWAY_CLIENT_MODES,\n GATEWAY_CLIENT_NAMES,\n type GatewayClientMode,\n type GatewayClientName,\n} from \"../../../src/gateway/protocol/client-info.js\";\nimport { buildDeviceAuthPayload } from \"../../../src/gateway/device-auth.js\";\nimport { loadOrCreateDeviceIdentity, signDevicePayload } from \"./device-identity\";\nimport { clearDeviceAuthToken, loadDeviceAuthToken, storeDeviceAuthToken } from \"./device-auth\";\n\nexport type GatewayEventFrame = {\n type: \"event\";\n event: string;\n payload?: unknown;\n seq?: number;\n stateVersion?: { presence: number; health: number };\n};\n\nexport type GatewayResponseFrame = {\n type: \"res\";\n id: string;\n ok: boolean;\n payload?: unknown;\n error?: { code: string; message: string; details?: unknown };\n};\n\nexport type GatewayHelloOk = {\n type: \"hello-ok\";\n protocol: number;\n features?: { methods?: string[]; events?: string[] };\n snapshot?: unknown;\n auth?: {\n deviceToken?: string;\n role?: string;\n scopes?: string[];\n issuedAtMs?: number;\n };\n policy?: { tickIntervalMs?: number };\n};\n\ntype Pending = {\n resolve: (value: unknown) => void;\n reject: (err: unknown) => void;\n};\n\nexport type GatewayBrowserClientOptions = {\n url: string;\n token?: string;\n password?: string;\n clientName?: GatewayClientName;\n clientVersion?: string;\n platform?: string;\n mode?: GatewayClientMode;\n instanceId?: string;\n onHello?: (hello: GatewayHelloOk) => void;\n onEvent?: (evt: GatewayEventFrame) => void;\n onClose?: (info: { code: number; reason: string }) => void;\n onGap?: (info: { expected: number; received: number }) => void;\n};\n\n// 4008 = application-defined code (browser rejects 1008 \"Policy Violation\")\nconst CONNECT_FAILED_CLOSE_CODE = 4008;\n\nexport class GatewayBrowserClient {\n private ws: WebSocket | null = null;\n private pending = new Map();\n private closed = false;\n private lastSeq: number | null = null;\n private connectNonce: string | null = null;\n private connectSent = false;\n private connectTimer: number | null = null;\n private backoffMs = 800;\n\n constructor(private opts: GatewayBrowserClientOptions) {}\n\n start() {\n this.closed = false;\n this.connect();\n }\n\n stop() {\n this.closed = true;\n this.ws?.close();\n this.ws = null;\n this.flushPending(new Error(\"gateway client stopped\"));\n }\n\n get connected() {\n return this.ws?.readyState === WebSocket.OPEN;\n }\n\n private connect() {\n if (this.closed) return;\n this.ws = new WebSocket(this.opts.url);\n this.ws.onopen = () => this.queueConnect();\n this.ws.onmessage = (ev) => this.handleMessage(String(ev.data ?? \"\"));\n this.ws.onclose = (ev) => {\n const reason = String(ev.reason ?? \"\");\n this.ws = null;\n this.flushPending(new Error(`gateway closed (${ev.code}): ${reason}`));\n this.opts.onClose?.({ code: ev.code, reason });\n this.scheduleReconnect();\n };\n this.ws.onerror = () => {\n // ignored; close handler will fire\n };\n }\n\n private scheduleReconnect() {\n if (this.closed) return;\n const delay = this.backoffMs;\n this.backoffMs = Math.min(this.backoffMs * 1.7, 15_000);\n window.setTimeout(() => this.connect(), delay);\n }\n\n private flushPending(err: Error) {\n for (const [, p] of this.pending) p.reject(err);\n this.pending.clear();\n }\n\n private async sendConnect() {\n if (this.connectSent) return;\n this.connectSent = true;\n if (this.connectTimer !== null) {\n window.clearTimeout(this.connectTimer);\n this.connectTimer = null;\n }\n\n // crypto.subtle is only available in secure contexts (HTTPS, localhost).\n // Over plain HTTP, we skip device identity and fall back to token-only auth.\n // Gateways may reject this unless gateway.controlUi.allowInsecureAuth is enabled.\n const isSecureContext = typeof crypto !== \"undefined\" && !!crypto.subtle;\n\n const scopes = [\"operator.admin\", \"operator.approvals\", \"operator.pairing\"];\n const role = \"operator\";\n let deviceIdentity: Awaited> | null = null;\n let canFallbackToShared = false;\n let authToken = this.opts.token;\n\n if (isSecureContext) {\n deviceIdentity = await loadOrCreateDeviceIdentity();\n const storedToken = loadDeviceAuthToken({\n deviceId: deviceIdentity.deviceId,\n role,\n })?.token;\n authToken = storedToken ?? this.opts.token;\n canFallbackToShared = Boolean(storedToken && this.opts.token);\n }\n const auth =\n authToken || this.opts.password\n ? {\n token: authToken,\n password: this.opts.password,\n }\n : undefined;\n\n let device:\n | {\n id: string;\n publicKey: string;\n signature: string;\n signedAt: number;\n nonce: string | undefined;\n }\n | undefined;\n\n if (isSecureContext && deviceIdentity) {\n const signedAtMs = Date.now();\n const nonce = this.connectNonce ?? undefined;\n const payload = buildDeviceAuthPayload({\n deviceId: deviceIdentity.deviceId,\n clientId: this.opts.clientName ?? GATEWAY_CLIENT_NAMES.CONTROL_UI,\n clientMode: this.opts.mode ?? GATEWAY_CLIENT_MODES.WEBCHAT,\n role,\n scopes,\n signedAtMs,\n token: authToken ?? null,\n nonce,\n });\n const signature = await signDevicePayload(deviceIdentity.privateKey, payload);\n device = {\n id: deviceIdentity.deviceId,\n publicKey: deviceIdentity.publicKey,\n signature,\n signedAt: signedAtMs,\n nonce,\n };\n }\n const params = {\n minProtocol: 3,\n maxProtocol: 3,\n client: {\n id: this.opts.clientName ?? GATEWAY_CLIENT_NAMES.CONTROL_UI,\n version: this.opts.clientVersion ?? \"dev\",\n platform: this.opts.platform ?? navigator.platform ?? \"web\",\n mode: this.opts.mode ?? GATEWAY_CLIENT_MODES.WEBCHAT,\n instanceId: this.opts.instanceId,\n },\n role,\n scopes,\n device,\n caps: [],\n auth,\n userAgent: navigator.userAgent,\n locale: navigator.language,\n };\n\n void this.request(\"connect\", params)\n .then((hello) => {\n if (hello?.auth?.deviceToken && deviceIdentity) {\n storeDeviceAuthToken({\n deviceId: deviceIdentity.deviceId,\n role: hello.auth.role ?? role,\n token: hello.auth.deviceToken,\n scopes: hello.auth.scopes ?? [],\n });\n }\n this.backoffMs = 800;\n this.opts.onHello?.(hello);\n })\n .catch(() => {\n if (canFallbackToShared && deviceIdentity) {\n clearDeviceAuthToken({ deviceId: deviceIdentity.deviceId, role });\n }\n this.ws?.close(CONNECT_FAILED_CLOSE_CODE, \"connect failed\");\n });\n }\n\n private handleMessage(raw: string) {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return;\n }\n\n const frame = parsed as { type?: unknown };\n if (frame.type === \"event\") {\n const evt = parsed as GatewayEventFrame;\n if (evt.event === \"connect.challenge\") {\n const payload = evt.payload as { nonce?: unknown } | undefined;\n const nonce = payload && typeof payload.nonce === \"string\" ? payload.nonce : null;\n if (nonce) {\n this.connectNonce = nonce;\n void this.sendConnect();\n }\n return;\n }\n const seq = typeof evt.seq === \"number\" ? evt.seq : null;\n if (seq !== null) {\n if (this.lastSeq !== null && seq > this.lastSeq + 1) {\n this.opts.onGap?.({ expected: this.lastSeq + 1, received: seq });\n }\n this.lastSeq = seq;\n }\n try {\n this.opts.onEvent?.(evt);\n } catch (err) {\n console.error(\"[gateway] event handler error:\", err);\n }\n return;\n }\n\n if (frame.type === \"res\") {\n const res = parsed as GatewayResponseFrame;\n const pending = this.pending.get(res.id);\n if (!pending) return;\n this.pending.delete(res.id);\n if (res.ok) pending.resolve(res.payload);\n else pending.reject(new Error(res.error?.message ?? \"request failed\"));\n return;\n }\n }\n\n request(method: string, params?: unknown): Promise {\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {\n return Promise.reject(new Error(\"gateway not connected\"));\n }\n const id = generateUUID();\n const frame = { type: \"req\", id, method, params };\n const p = new Promise((resolve, reject) => {\n this.pending.set(id, { resolve: (v) => resolve(v as T), reject });\n });\n this.ws.send(JSON.stringify(frame));\n return p;\n }\n\n private queueConnect() {\n this.connectNonce = null;\n this.connectSent = false;\n if (this.connectTimer !== null) window.clearTimeout(this.connectTimer);\n this.connectTimer = window.setTimeout(() => {\n void this.sendConnect();\n }, 750);\n }\n}\n","export type ExecApprovalRequestPayload = {\n command: string;\n cwd?: string | null;\n host?: string | null;\n security?: string | null;\n ask?: string | null;\n agentId?: string | null;\n resolvedPath?: string | null;\n sessionKey?: string | null;\n};\n\nexport type ExecApprovalRequest = {\n id: string;\n request: ExecApprovalRequestPayload;\n createdAtMs: number;\n expiresAtMs: number;\n};\n\nexport type ExecApprovalResolved = {\n id: string;\n decision?: string | null;\n resolvedBy?: string | null;\n ts?: number | null;\n};\n\nfunction isRecord(value: unknown): value is Record {\n return typeof value === \"object\" && value !== null;\n}\n\nexport function parseExecApprovalRequested(payload: unknown): ExecApprovalRequest | null {\n if (!isRecord(payload)) return null;\n const id = typeof payload.id === \"string\" ? payload.id.trim() : \"\";\n const request = payload.request;\n if (!id || !isRecord(request)) return null;\n const command = typeof request.command === \"string\" ? request.command.trim() : \"\";\n if (!command) return null;\n const createdAtMs = typeof payload.createdAtMs === \"number\" ? payload.createdAtMs : 0;\n const expiresAtMs = typeof payload.expiresAtMs === \"number\" ? payload.expiresAtMs : 0;\n if (!createdAtMs || !expiresAtMs) return null;\n return {\n id,\n request: {\n command,\n cwd: typeof request.cwd === \"string\" ? request.cwd : null,\n host: typeof request.host === \"string\" ? request.host : null,\n security: typeof request.security === \"string\" ? request.security : null,\n ask: typeof request.ask === \"string\" ? request.ask : null,\n agentId: typeof request.agentId === \"string\" ? request.agentId : null,\n resolvedPath: typeof request.resolvedPath === \"string\" ? request.resolvedPath : null,\n sessionKey: typeof request.sessionKey === \"string\" ? request.sessionKey : null,\n },\n createdAtMs,\n expiresAtMs,\n };\n}\n\nexport function parseExecApprovalResolved(payload: unknown): ExecApprovalResolved | null {\n if (!isRecord(payload)) return null;\n const id = typeof payload.id === \"string\" ? payload.id.trim() : \"\";\n if (!id) return null;\n return {\n id,\n decision: typeof payload.decision === \"string\" ? payload.decision : null,\n resolvedBy: typeof payload.resolvedBy === \"string\" ? payload.resolvedBy : null,\n ts: typeof payload.ts === \"number\" ? payload.ts : null,\n };\n}\n\nexport function pruneExecApprovalQueue(queue: ExecApprovalRequest[]): ExecApprovalRequest[] {\n const now = Date.now();\n return queue.filter((entry) => entry.expiresAtMs > now);\n}\n\nexport function addExecApproval(\n queue: ExecApprovalRequest[],\n entry: ExecApprovalRequest,\n): ExecApprovalRequest[] {\n const next = pruneExecApprovalQueue(queue).filter((item) => item.id !== entry.id);\n next.push(entry);\n return next;\n}\n\nexport function removeExecApproval(queue: ExecApprovalRequest[], id: string): ExecApprovalRequest[] {\n return pruneExecApprovalQueue(queue).filter((entry) => entry.id !== id);\n}\n","import type { GatewayBrowserClient } from \"../gateway\";\nimport {\n normalizeAssistantIdentity,\n type AssistantIdentity,\n} from \"../assistant-identity\";\n\nexport type AssistantIdentityState = {\n client: GatewayBrowserClient | null;\n connected: boolean;\n sessionKey: string;\n assistantName: string;\n assistantAvatar: string | null;\n assistantAgentId: string | null;\n};\n\nexport async function loadAssistantIdentity(\n state: AssistantIdentityState,\n opts?: { sessionKey?: string },\n) {\n if (!state.client || !state.connected) return;\n const sessionKey = opts?.sessionKey?.trim() || state.sessionKey.trim();\n const params = sessionKey ? { sessionKey } : {};\n try {\n const res = (await state.client.request(\"agent.identity.get\", params)) as\n | Partial\n | undefined;\n if (!res) return;\n const normalized = normalizeAssistantIdentity(res);\n state.assistantName = normalized.name;\n state.assistantAvatar = normalized.avatar;\n state.assistantAgentId = normalized.agentId ?? null;\n } catch {\n // Ignore errors; keep last known identity.\n }\n}\n","import { loadChatHistory } from \"./controllers/chat\";\nimport { loadDevices } from \"./controllers/devices\";\nimport { loadNodes } from \"./controllers/nodes\";\nimport { loadAgents } from \"./controllers/agents\";\nimport type { GatewayEventFrame, GatewayHelloOk } from \"./gateway\";\nimport { GatewayBrowserClient } from \"./gateway\";\nimport type { EventLogEntry } from \"./app-events\";\nimport type { AgentsListResult, PresenceEntry, HealthSnapshot, StatusSummary } from \"./types\";\nimport type { Tab } from \"./navigation\";\nimport type { UiSettings } from \"./storage\";\nimport { handleAgentEvent, resetToolStream, type AgentEventPayload } from \"./app-tool-stream\";\nimport { flushChatQueueForEvent } from \"./app-chat\";\nimport {\n applySettings,\n loadCron,\n refreshActiveTab,\n setLastActiveSessionKey,\n} from \"./app-settings\";\nimport { handleChatEvent, type ChatEventPayload } from \"./controllers/chat\";\nimport {\n addExecApproval,\n parseExecApprovalRequested,\n parseExecApprovalResolved,\n removeExecApproval,\n} from \"./controllers/exec-approval\";\nimport type { ClawdbotApp } from \"./app\";\nimport type { ExecApprovalRequest } from \"./controllers/exec-approval\";\nimport { loadAssistantIdentity } from \"./controllers/assistant-identity\";\n\ntype GatewayHost = {\n settings: UiSettings;\n password: string;\n client: GatewayBrowserClient | null;\n connected: boolean;\n hello: GatewayHelloOk | null;\n lastError: string | null;\n onboarding?: boolean;\n eventLogBuffer: EventLogEntry[];\n eventLog: EventLogEntry[];\n tab: Tab;\n presenceEntries: PresenceEntry[];\n presenceError: string | null;\n presenceStatus: StatusSummary | null;\n agentsLoading: boolean;\n agentsList: AgentsListResult | null;\n agentsError: string | null;\n debugHealth: HealthSnapshot | null;\n assistantName: string;\n assistantAvatar: string | null;\n assistantAgentId: string | null;\n sessionKey: string;\n chatRunId: string | null;\n execApprovalQueue: ExecApprovalRequest[];\n execApprovalError: string | null;\n};\n\ntype SessionDefaultsSnapshot = {\n defaultAgentId?: string;\n mainKey?: string;\n mainSessionKey?: string;\n scope?: string;\n};\n\nfunction normalizeSessionKeyForDefaults(\n value: string | undefined,\n defaults: SessionDefaultsSnapshot,\n): string {\n const raw = (value ?? \"\").trim();\n const mainSessionKey = defaults.mainSessionKey?.trim();\n if (!mainSessionKey) return raw;\n if (!raw) return mainSessionKey;\n const mainKey = defaults.mainKey?.trim() || \"main\";\n const defaultAgentId = defaults.defaultAgentId?.trim();\n const isAlias =\n raw === \"main\" ||\n raw === mainKey ||\n (defaultAgentId &&\n (raw === `agent:${defaultAgentId}:main` ||\n raw === `agent:${defaultAgentId}:${mainKey}`));\n return isAlias ? mainSessionKey : raw;\n}\n\nfunction applySessionDefaults(host: GatewayHost, defaults?: SessionDefaultsSnapshot) {\n if (!defaults?.mainSessionKey) return;\n const resolvedSessionKey = normalizeSessionKeyForDefaults(host.sessionKey, defaults);\n const resolvedSettingsSessionKey = normalizeSessionKeyForDefaults(\n host.settings.sessionKey,\n defaults,\n );\n const resolvedLastActiveSessionKey = normalizeSessionKeyForDefaults(\n host.settings.lastActiveSessionKey,\n defaults,\n );\n const nextSessionKey = resolvedSessionKey || resolvedSettingsSessionKey || host.sessionKey;\n const nextSettings = {\n ...host.settings,\n sessionKey: resolvedSettingsSessionKey || nextSessionKey,\n lastActiveSessionKey: resolvedLastActiveSessionKey || nextSessionKey,\n };\n const shouldUpdateSettings =\n nextSettings.sessionKey !== host.settings.sessionKey ||\n nextSettings.lastActiveSessionKey !== host.settings.lastActiveSessionKey;\n if (nextSessionKey !== host.sessionKey) {\n host.sessionKey = nextSessionKey;\n }\n if (shouldUpdateSettings) {\n applySettings(host as unknown as Parameters[0], nextSettings);\n }\n}\n\nexport function connectGateway(host: GatewayHost) {\n host.lastError = null;\n host.hello = null;\n host.connected = false;\n host.execApprovalQueue = [];\n host.execApprovalError = null;\n\n host.client?.stop();\n host.client = new GatewayBrowserClient({\n url: host.settings.gatewayUrl,\n token: host.settings.token.trim() ? host.settings.token : undefined,\n password: host.password.trim() ? host.password : undefined,\n clientName: \"clawdbot-control-ui\",\n mode: \"webchat\",\n onHello: (hello) => {\n host.connected = true;\n host.lastError = null;\n host.hello = hello;\n applySnapshot(host, hello);\n void loadAssistantIdentity(host as unknown as ClawdbotApp);\n void loadAgents(host as unknown as ClawdbotApp);\n void loadNodes(host as unknown as ClawdbotApp, { quiet: true });\n void loadDevices(host as unknown as ClawdbotApp, { quiet: true });\n void refreshActiveTab(host as unknown as Parameters[0]);\n },\n onClose: ({ code, reason }) => {\n host.connected = false;\n // Code 1012 = Service Restart (expected during config saves, don't show as error)\n if (code !== 1012) {\n host.lastError = `disconnected (${code}): ${reason || \"no reason\"}`;\n }\n },\n onEvent: (evt) => handleGatewayEvent(host, evt),\n onGap: ({ expected, received }) => {\n host.lastError = `event gap detected (expected seq ${expected}, got ${received}); refresh recommended`;\n },\n });\n host.client.start();\n}\n\nexport function handleGatewayEvent(host: GatewayHost, evt: GatewayEventFrame) {\n try {\n handleGatewayEventUnsafe(host, evt);\n } catch (err) {\n console.error(\"[gateway] handleGatewayEvent error:\", evt.event, err);\n }\n}\n\nfunction handleGatewayEventUnsafe(host: GatewayHost, evt: GatewayEventFrame) {\n host.eventLogBuffer = [\n { ts: Date.now(), event: evt.event, payload: evt.payload },\n ...host.eventLogBuffer,\n ].slice(0, 250);\n if (host.tab === \"debug\") {\n host.eventLog = host.eventLogBuffer;\n }\n\n if (evt.event === \"agent\") {\n if (host.onboarding) return;\n handleAgentEvent(\n host as unknown as Parameters[0],\n evt.payload as AgentEventPayload | undefined,\n );\n return;\n }\n\n if (evt.event === \"chat\") {\n const payload = evt.payload as ChatEventPayload | undefined;\n if (payload?.sessionKey) {\n setLastActiveSessionKey(\n host as unknown as Parameters[0],\n payload.sessionKey,\n );\n }\n const state = handleChatEvent(host as unknown as ClawdbotApp, payload);\n if (state === \"final\" || state === \"error\" || state === \"aborted\") {\n resetToolStream(host as unknown as Parameters[0]);\n void flushChatQueueForEvent(\n host as unknown as Parameters[0],\n );\n }\n if (state === \"final\") void loadChatHistory(host as unknown as ClawdbotApp);\n return;\n }\n\n if (evt.event === \"presence\") {\n const payload = evt.payload as { presence?: PresenceEntry[] } | undefined;\n if (payload?.presence && Array.isArray(payload.presence)) {\n host.presenceEntries = payload.presence;\n host.presenceError = null;\n host.presenceStatus = null;\n }\n return;\n }\n\n if (evt.event === \"cron\" && host.tab === \"cron\") {\n void loadCron(host as unknown as Parameters[0]);\n }\n\n if (evt.event === \"device.pair.requested\" || evt.event === \"device.pair.resolved\") {\n void loadDevices(host as unknown as ClawdbotApp, { quiet: true });\n }\n\n if (evt.event === \"exec.approval.requested\") {\n const entry = parseExecApprovalRequested(evt.payload);\n if (entry) {\n host.execApprovalQueue = addExecApproval(host.execApprovalQueue, entry);\n host.execApprovalError = null;\n const delay = Math.max(0, entry.expiresAtMs - Date.now() + 500);\n window.setTimeout(() => {\n host.execApprovalQueue = removeExecApproval(host.execApprovalQueue, entry.id);\n }, delay);\n }\n return;\n }\n\n if (evt.event === \"exec.approval.resolved\") {\n const resolved = parseExecApprovalResolved(evt.payload);\n if (resolved) {\n host.execApprovalQueue = removeExecApproval(host.execApprovalQueue, resolved.id);\n }\n }\n}\n\nexport function applySnapshot(host: GatewayHost, hello: GatewayHelloOk) {\n const snapshot = hello.snapshot as\n | {\n presence?: PresenceEntry[];\n health?: HealthSnapshot;\n sessionDefaults?: SessionDefaultsSnapshot;\n }\n | undefined;\n if (snapshot?.presence && Array.isArray(snapshot.presence)) {\n host.presenceEntries = snapshot.presence;\n }\n if (snapshot?.health) {\n host.debugHealth = snapshot.health;\n }\n if (snapshot?.sessionDefaults) {\n applySessionDefaults(host, snapshot.sessionDefaults);\n }\n}\n","import type { Tab } from \"./navigation\";\nimport { connectGateway } from \"./app-gateway\";\nimport {\n applySettingsFromUrl,\n attachThemeListener,\n detachThemeListener,\n inferBasePath,\n syncTabWithLocation,\n syncThemeWithSettings,\n} from \"./app-settings\";\nimport { observeTopbar, scheduleChatScroll, scheduleLogsScroll } from \"./app-scroll\";\nimport {\n startLogsPolling,\n startNodesPolling,\n stopLogsPolling,\n stopNodesPolling,\n startDebugPolling,\n stopDebugPolling,\n} from \"./app-polling\";\n\ntype LifecycleHost = {\n basePath: string;\n tab: Tab;\n chatHasAutoScrolled: boolean;\n chatLoading: boolean;\n chatMessages: unknown[];\n chatToolMessages: unknown[];\n chatStream: string;\n logsAutoFollow: boolean;\n logsAtBottom: boolean;\n logsEntries: unknown[];\n popStateHandler: () => void;\n topbarObserver: ResizeObserver | null;\n};\n\nexport function handleConnected(host: LifecycleHost) {\n host.basePath = inferBasePath();\n syncTabWithLocation(\n host as unknown as Parameters[0],\n true,\n );\n syncThemeWithSettings(\n host as unknown as Parameters[0],\n );\n attachThemeListener(\n host as unknown as Parameters[0],\n );\n window.addEventListener(\"popstate\", host.popStateHandler);\n applySettingsFromUrl(\n host as unknown as Parameters[0],\n );\n connectGateway(host as unknown as Parameters[0]);\n startNodesPolling(host as unknown as Parameters[0]);\n if (host.tab === \"logs\") {\n startLogsPolling(host as unknown as Parameters[0]);\n }\n if (host.tab === \"debug\") {\n startDebugPolling(host as unknown as Parameters[0]);\n }\n}\n\nexport function handleFirstUpdated(host: LifecycleHost) {\n observeTopbar(host as unknown as Parameters[0]);\n}\n\nexport function handleDisconnected(host: LifecycleHost) {\n window.removeEventListener(\"popstate\", host.popStateHandler);\n stopNodesPolling(host as unknown as Parameters[0]);\n stopLogsPolling(host as unknown as Parameters[0]);\n stopDebugPolling(host as unknown as Parameters[0]);\n detachThemeListener(\n host as unknown as Parameters[0],\n );\n host.topbarObserver?.disconnect();\n host.topbarObserver = null;\n}\n\nexport function handleUpdated(\n host: LifecycleHost,\n changed: Map,\n) {\n if (\n host.tab === \"chat\" &&\n (changed.has(\"chatMessages\") ||\n changed.has(\"chatToolMessages\") ||\n changed.has(\"chatStream\") ||\n changed.has(\"chatLoading\") ||\n changed.has(\"tab\"))\n ) {\n const forcedByTab = changed.has(\"tab\");\n const forcedByLoad =\n changed.has(\"chatLoading\") &&\n changed.get(\"chatLoading\") === true &&\n host.chatLoading === false;\n scheduleChatScroll(\n host as unknown as Parameters[0],\n forcedByTab || forcedByLoad || !host.chatHasAutoScrolled,\n );\n }\n if (\n host.tab === \"logs\" &&\n (changed.has(\"logsEntries\") || changed.has(\"logsAutoFollow\") || changed.has(\"tab\"))\n ) {\n if (host.logsAutoFollow && host.logsAtBottom) {\n scheduleLogsScroll(\n host as unknown as Parameters[0],\n changed.has(\"tab\") || changed.has(\"logsAutoFollow\"),\n );\n }\n }\n}\n","import {\n loadChannels,\n logoutWhatsApp,\n startWhatsAppLogin,\n waitWhatsAppLogin,\n} from \"./controllers/channels\";\nimport { loadConfig, saveConfig } from \"./controllers/config\";\nimport type { ClawdbotApp } from \"./app\";\nimport type { NostrProfile } from \"./types\";\nimport { createNostrProfileFormState } from \"./views/channels.nostr-profile-form\";\n\nexport async function handleWhatsAppStart(host: ClawdbotApp, force: boolean) {\n await startWhatsAppLogin(host, force);\n await loadChannels(host, true);\n}\n\nexport async function handleWhatsAppWait(host: ClawdbotApp) {\n await waitWhatsAppLogin(host);\n await loadChannels(host, true);\n}\n\nexport async function handleWhatsAppLogout(host: ClawdbotApp) {\n await logoutWhatsApp(host);\n await loadChannels(host, true);\n}\n\nexport async function handleChannelConfigSave(host: ClawdbotApp) {\n await saveConfig(host);\n await loadConfig(host);\n await loadChannels(host, true);\n}\n\nexport async function handleChannelConfigReload(host: ClawdbotApp) {\n await loadConfig(host);\n await loadChannels(host, true);\n}\n\nfunction parseValidationErrors(details: unknown): Record {\n if (!Array.isArray(details)) return {};\n const errors: Record = {};\n for (const entry of details) {\n if (typeof entry !== \"string\") continue;\n const [rawField, ...rest] = entry.split(\":\");\n if (!rawField || rest.length === 0) continue;\n const field = rawField.trim();\n const message = rest.join(\":\").trim();\n if (field && message) errors[field] = message;\n }\n return errors;\n}\n\nfunction resolveNostrAccountId(host: ClawdbotApp): string {\n const accounts = host.channelsSnapshot?.channelAccounts?.nostr ?? [];\n return accounts[0]?.accountId ?? host.nostrProfileAccountId ?? \"default\";\n}\n\nfunction buildNostrProfileUrl(accountId: string, suffix = \"\"): string {\n return `/api/channels/nostr/${encodeURIComponent(accountId)}/profile${suffix}`;\n}\n\nexport function handleNostrProfileEdit(\n host: ClawdbotApp,\n accountId: string,\n profile: NostrProfile | null,\n) {\n host.nostrProfileAccountId = accountId;\n host.nostrProfileFormState = createNostrProfileFormState(profile ?? undefined);\n}\n\nexport function handleNostrProfileCancel(host: ClawdbotApp) {\n host.nostrProfileFormState = null;\n host.nostrProfileAccountId = null;\n}\n\nexport function handleNostrProfileFieldChange(\n host: ClawdbotApp,\n field: keyof NostrProfile,\n value: string,\n) {\n const state = host.nostrProfileFormState;\n if (!state) return;\n host.nostrProfileFormState = {\n ...state,\n values: {\n ...state.values,\n [field]: value,\n },\n fieldErrors: {\n ...state.fieldErrors,\n [field]: \"\",\n },\n };\n}\n\nexport function handleNostrProfileToggleAdvanced(host: ClawdbotApp) {\n const state = host.nostrProfileFormState;\n if (!state) return;\n host.nostrProfileFormState = {\n ...state,\n showAdvanced: !state.showAdvanced,\n };\n}\n\nexport async function handleNostrProfileSave(host: ClawdbotApp) {\n const state = host.nostrProfileFormState;\n if (!state || state.saving) return;\n const accountId = resolveNostrAccountId(host);\n\n host.nostrProfileFormState = {\n ...state,\n saving: true,\n error: null,\n success: null,\n fieldErrors: {},\n };\n\n try {\n const response = await fetch(buildNostrProfileUrl(accountId), {\n method: \"PUT\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(state.values),\n });\n const data = (await response.json().catch(() => null)) as\n | { ok?: boolean; error?: string; details?: unknown; persisted?: boolean }\n | null;\n\n if (!response.ok || data?.ok === false || !data) {\n const errorMessage = data?.error ?? `Profile update failed (${response.status})`;\n host.nostrProfileFormState = {\n ...state,\n saving: false,\n error: errorMessage,\n success: null,\n fieldErrors: parseValidationErrors(data?.details),\n };\n return;\n }\n\n if (!data.persisted) {\n host.nostrProfileFormState = {\n ...state,\n saving: false,\n error: \"Profile publish failed on all relays.\",\n success: null,\n };\n return;\n }\n\n host.nostrProfileFormState = {\n ...state,\n saving: false,\n error: null,\n success: \"Profile published to relays.\",\n fieldErrors: {},\n original: { ...state.values },\n };\n await loadChannels(host, true);\n } catch (err) {\n host.nostrProfileFormState = {\n ...state,\n saving: false,\n error: `Profile update failed: ${String(err)}`,\n success: null,\n };\n }\n}\n\nexport async function handleNostrProfileImport(host: ClawdbotApp) {\n const state = host.nostrProfileFormState;\n if (!state || state.importing) return;\n const accountId = resolveNostrAccountId(host);\n\n host.nostrProfileFormState = {\n ...state,\n importing: true,\n error: null,\n success: null,\n };\n\n try {\n const response = await fetch(buildNostrProfileUrl(accountId, \"/import\"), {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({ autoMerge: true }),\n });\n const data = (await response.json().catch(() => null)) as\n | { ok?: boolean; error?: string; imported?: NostrProfile; merged?: NostrProfile; saved?: boolean }\n | null;\n\n if (!response.ok || data?.ok === false || !data) {\n const errorMessage = data?.error ?? `Profile import failed (${response.status})`;\n host.nostrProfileFormState = {\n ...state,\n importing: false,\n error: errorMessage,\n success: null,\n };\n return;\n }\n\n const merged = data.merged ?? data.imported ?? null;\n const nextValues = merged ? { ...state.values, ...merged } : state.values;\n const showAdvanced = Boolean(\n nextValues.banner || nextValues.website || nextValues.nip05 || nextValues.lud16,\n );\n\n host.nostrProfileFormState = {\n ...state,\n importing: false,\n values: nextValues,\n error: null,\n success: data.saved\n ? \"Profile imported from relays. Review and publish.\"\n : \"Profile imported. Review and publish.\",\n showAdvanced,\n };\n\n if (data.saved) {\n await loadChannels(host, true);\n }\n } catch (err) {\n host.nostrProfileFormState = {\n ...state,\n importing: false,\n error: `Profile import failed: ${String(err)}`,\n success: null,\n };\n }\n}\n","import { LitElement, html, nothing } from \"lit\";\nimport { customElement, state } from \"lit/decorators.js\";\n\nimport type { GatewayBrowserClient, GatewayHelloOk } from \"./gateway\";\nimport { resolveInjectedAssistantIdentity } from \"./assistant-identity\";\nimport { loadSettings, type UiSettings } from \"./storage\";\nimport { renderApp } from \"./app-render\";\nimport type { Tab } from \"./navigation\";\nimport type { ResolvedTheme, ThemeMode } from \"./theme\";\nimport type {\n AgentsListResult,\n ConfigSnapshot,\n ConfigUiHints,\n CronJob,\n CronRunLogEntry,\n CronStatus,\n HealthSnapshot,\n LogEntry,\n LogLevel,\n PresenceEntry,\n ChannelsStatusSnapshot,\n SessionsListResult,\n SkillStatusReport,\n StatusSummary,\n NostrProfile,\n} from \"./types\";\nimport { type ChatQueueItem, type CronFormState } from \"./ui-types\";\nimport type { EventLogEntry } from \"./app-events\";\nimport { DEFAULT_CRON_FORM, DEFAULT_LOG_LEVEL_FILTERS } from \"./app-defaults\";\nimport type {\n ExecApprovalsFile,\n ExecApprovalsSnapshot,\n} from \"./controllers/exec-approvals\";\nimport type { DevicePairingList } from \"./controllers/devices\";\nimport type { ExecApprovalRequest } from \"./controllers/exec-approval\";\nimport {\n resetToolStream as resetToolStreamInternal,\n type ToolStreamEntry,\n} from \"./app-tool-stream\";\nimport {\n exportLogs as exportLogsInternal,\n handleChatScroll as handleChatScrollInternal,\n handleLogsScroll as handleLogsScrollInternal,\n resetChatScroll as resetChatScrollInternal,\n} from \"./app-scroll\";\nimport { connectGateway as connectGatewayInternal } from \"./app-gateway\";\nimport {\n handleConnected,\n handleDisconnected,\n handleFirstUpdated,\n handleUpdated,\n} from \"./app-lifecycle\";\nimport {\n applySettings as applySettingsInternal,\n loadCron as loadCronInternal,\n loadOverview as loadOverviewInternal,\n setTab as setTabInternal,\n setTheme as setThemeInternal,\n onPopState as onPopStateInternal,\n} from \"./app-settings\";\nimport {\n handleAbortChat as handleAbortChatInternal,\n handleSendChat as handleSendChatInternal,\n removeQueuedMessage as removeQueuedMessageInternal,\n} from \"./app-chat\";\nimport {\n handleChannelConfigReload as handleChannelConfigReloadInternal,\n handleChannelConfigSave as handleChannelConfigSaveInternal,\n handleNostrProfileCancel as handleNostrProfileCancelInternal,\n handleNostrProfileEdit as handleNostrProfileEditInternal,\n handleNostrProfileFieldChange as handleNostrProfileFieldChangeInternal,\n handleNostrProfileImport as handleNostrProfileImportInternal,\n handleNostrProfileSave as handleNostrProfileSaveInternal,\n handleNostrProfileToggleAdvanced as handleNostrProfileToggleAdvancedInternal,\n handleWhatsAppLogout as handleWhatsAppLogoutInternal,\n handleWhatsAppStart as handleWhatsAppStartInternal,\n handleWhatsAppWait as handleWhatsAppWaitInternal,\n} from \"./app-channels\";\nimport type { NostrProfileFormState } from \"./views/channels.nostr-profile-form\";\nimport { loadAssistantIdentity as loadAssistantIdentityInternal } from \"./controllers/assistant-identity\";\n\ndeclare global {\n interface Window {\n __CLAWDBOT_CONTROL_UI_BASE_PATH__?: string;\n }\n}\n\nconst injectedAssistantIdentity = resolveInjectedAssistantIdentity();\n\nfunction resolveOnboardingMode(): boolean {\n if (!window.location.search) return false;\n const params = new URLSearchParams(window.location.search);\n const raw = params.get(\"onboarding\");\n if (!raw) return false;\n const normalized = raw.trim().toLowerCase();\n return normalized === \"1\" || normalized === \"true\" || normalized === \"yes\" || normalized === \"on\";\n}\n\n@customElement(\"clawdbot-app\")\nexport class ClawdbotApp extends LitElement {\n @state() settings: UiSettings = loadSettings();\n @state() password = \"\";\n @state() tab: Tab = \"chat\";\n @state() onboarding = resolveOnboardingMode();\n @state() connected = false;\n @state() theme: ThemeMode = this.settings.theme ?? \"system\";\n @state() themeResolved: ResolvedTheme = \"dark\";\n @state() hello: GatewayHelloOk | null = null;\n @state() lastError: string | null = null;\n @state() eventLog: EventLogEntry[] = [];\n private eventLogBuffer: EventLogEntry[] = [];\n private toolStreamSyncTimer: number | null = null;\n private sidebarCloseTimer: number | null = null;\n\n @state() assistantName = injectedAssistantIdentity.name;\n @state() assistantAvatar = injectedAssistantIdentity.avatar;\n @state() assistantAgentId = injectedAssistantIdentity.agentId ?? null;\n\n @state() sessionKey = this.settings.sessionKey;\n @state() chatLoading = false;\n @state() chatSending = false;\n @state() chatMessage = \"\";\n @state() chatMessages: unknown[] = [];\n @state() chatToolMessages: unknown[] = [];\n @state() chatStream: string | null = null;\n @state() chatStreamStartedAt: number | null = null;\n @state() chatRunId: string | null = null;\n @state() compactionStatus: import(\"./app-tool-stream\").CompactionStatus | null = null;\n @state() chatAvatarUrl: string | null = null;\n @state() chatThinkingLevel: string | null = null;\n @state() chatQueue: ChatQueueItem[] = [];\n // Sidebar state for tool output viewing\n @state() sidebarOpen = false;\n @state() sidebarContent: string | null = null;\n @state() sidebarError: string | null = null;\n @state() splitRatio = this.settings.splitRatio;\n\n @state() nodesLoading = false;\n @state() nodes: Array> = [];\n @state() devicesLoading = false;\n @state() devicesError: string | null = null;\n @state() devicesList: DevicePairingList | null = null;\n @state() execApprovalsLoading = false;\n @state() execApprovalsSaving = false;\n @state() execApprovalsDirty = false;\n @state() execApprovalsSnapshot: ExecApprovalsSnapshot | null = null;\n @state() execApprovalsForm: ExecApprovalsFile | null = null;\n @state() execApprovalsSelectedAgent: string | null = null;\n @state() execApprovalsTarget: \"gateway\" | \"node\" = \"gateway\";\n @state() execApprovalsTargetNodeId: string | null = null;\n @state() execApprovalQueue: ExecApprovalRequest[] = [];\n @state() execApprovalBusy = false;\n @state() execApprovalError: string | null = null;\n\n @state() configLoading = false;\n @state() configRaw = \"{\\n}\\n\";\n @state() configRawOriginal = \"\";\n @state() configValid: boolean | null = null;\n @state() configIssues: unknown[] = [];\n @state() configSaving = false;\n @state() configApplying = false;\n @state() updateRunning = false;\n @state() applySessionKey = this.settings.lastActiveSessionKey;\n @state() configSnapshot: ConfigSnapshot | null = null;\n @state() configSchema: unknown | null = null;\n @state() configSchemaVersion: string | null = null;\n @state() configSchemaLoading = false;\n @state() configUiHints: ConfigUiHints = {};\n @state() configForm: Record | null = null;\n @state() configFormOriginal: Record | null = null;\n @state() configFormDirty = false;\n @state() configFormMode: \"form\" | \"raw\" = \"form\";\n @state() configSearchQuery = \"\";\n @state() configActiveSection: string | null = null;\n @state() configActiveSubsection: string | null = null;\n\n @state() channelsLoading = false;\n @state() channelsSnapshot: ChannelsStatusSnapshot | null = null;\n @state() channelsError: string | null = null;\n @state() channelsLastSuccess: number | null = null;\n @state() whatsappLoginMessage: string | null = null;\n @state() whatsappLoginQrDataUrl: string | null = null;\n @state() whatsappLoginConnected: boolean | null = null;\n @state() whatsappBusy = false;\n @state() nostrProfileFormState: NostrProfileFormState | null = null;\n @state() nostrProfileAccountId: string | null = null;\n\n @state() presenceLoading = false;\n @state() presenceEntries: PresenceEntry[] = [];\n @state() presenceError: string | null = null;\n @state() presenceStatus: string | null = null;\n\n @state() agentsLoading = false;\n @state() agentsList: AgentsListResult | null = null;\n @state() agentsError: string | null = null;\n\n @state() sessionsLoading = false;\n @state() sessionsResult: SessionsListResult | null = null;\n @state() sessionsError: string | null = null;\n @state() sessionsFilterActive = \"\";\n @state() sessionsFilterLimit = \"120\";\n @state() sessionsIncludeGlobal = true;\n @state() sessionsIncludeUnknown = false;\n\n @state() cronLoading = false;\n @state() cronJobs: CronJob[] = [];\n @state() cronStatus: CronStatus | null = null;\n @state() cronError: string | null = null;\n @state() cronForm: CronFormState = { ...DEFAULT_CRON_FORM };\n @state() cronRunsJobId: string | null = null;\n @state() cronRuns: CronRunLogEntry[] = [];\n @state() cronBusy = false;\n\n @state() skillsLoading = false;\n @state() skillsReport: SkillStatusReport | null = null;\n @state() skillsError: string | null = null;\n @state() skillsFilter = \"\";\n @state() skillEdits: Record = {};\n @state() skillsBusyKey: string | null = null;\n @state() skillMessages: Record = {};\n\n @state() debugLoading = false;\n @state() debugStatus: StatusSummary | null = null;\n @state() debugHealth: HealthSnapshot | null = null;\n @state() debugModels: unknown[] = [];\n @state() debugHeartbeat: unknown | null = null;\n @state() debugCallMethod = \"\";\n @state() debugCallParams = \"{}\";\n @state() debugCallResult: string | null = null;\n @state() debugCallError: string | null = null;\n\n @state() logsLoading = false;\n @state() logsError: string | null = null;\n @state() logsFile: string | null = null;\n @state() logsEntries: LogEntry[] = [];\n @state() logsFilterText = \"\";\n @state() logsLevelFilters: Record = {\n ...DEFAULT_LOG_LEVEL_FILTERS,\n };\n @state() logsAutoFollow = true;\n @state() logsTruncated = false;\n @state() logsCursor: number | null = null;\n @state() logsLastFetchAt: number | null = null;\n @state() logsLimit = 500;\n @state() logsMaxBytes = 250_000;\n @state() logsAtBottom = true;\n\n client: GatewayBrowserClient | null = null;\n private chatScrollFrame: number | null = null;\n private chatScrollTimeout: number | null = null;\n private chatHasAutoScrolled = false;\n private chatUserNearBottom = true;\n private nodesPollInterval: number | null = null;\n private logsPollInterval: number | null = null;\n private debugPollInterval: number | null = null;\n private logsScrollFrame: number | null = null;\n private toolStreamById = new Map();\n private toolStreamOrder: string[] = [];\n basePath = \"\";\n private popStateHandler = () =>\n onPopStateInternal(\n this as unknown as Parameters[0],\n );\n private themeMedia: MediaQueryList | null = null;\n private themeMediaHandler: ((event: MediaQueryListEvent) => void) | null = null;\n private topbarObserver: ResizeObserver | null = null;\n\n createRenderRoot() {\n return this;\n }\n\n connectedCallback() {\n super.connectedCallback();\n handleConnected(this as unknown as Parameters[0]);\n }\n\n protected firstUpdated() {\n handleFirstUpdated(this as unknown as Parameters[0]);\n }\n\n disconnectedCallback() {\n handleDisconnected(this as unknown as Parameters[0]);\n super.disconnectedCallback();\n }\n\n protected updated(changed: Map) {\n handleUpdated(\n this as unknown as Parameters[0],\n changed,\n );\n }\n\n connect() {\n connectGatewayInternal(\n this as unknown as Parameters[0],\n );\n }\n\n handleChatScroll(event: Event) {\n handleChatScrollInternal(\n this as unknown as Parameters[0],\n event,\n );\n }\n\n handleLogsScroll(event: Event) {\n handleLogsScrollInternal(\n this as unknown as Parameters[0],\n event,\n );\n }\n\n exportLogs(lines: string[], label: string) {\n exportLogsInternal(lines, label);\n }\n\n resetToolStream() {\n resetToolStreamInternal(\n this as unknown as Parameters[0],\n );\n }\n\n resetChatScroll() {\n resetChatScrollInternal(\n this as unknown as Parameters[0],\n );\n }\n\n async loadAssistantIdentity() {\n await loadAssistantIdentityInternal(this);\n }\n\n applySettings(next: UiSettings) {\n applySettingsInternal(\n this as unknown as Parameters[0],\n next,\n );\n }\n\n setTab(next: Tab) {\n setTabInternal(this as unknown as Parameters[0], next);\n }\n\n setTheme(next: ThemeMode, context?: Parameters[2]) {\n setThemeInternal(\n this as unknown as Parameters[0],\n next,\n context,\n );\n }\n\n async loadOverview() {\n await loadOverviewInternal(\n this as unknown as Parameters[0],\n );\n }\n\n async loadCron() {\n await loadCronInternal(\n this as unknown as Parameters[0],\n );\n }\n\n async handleAbortChat() {\n await handleAbortChatInternal(\n this as unknown as Parameters[0],\n );\n }\n\n removeQueuedMessage(id: string) {\n removeQueuedMessageInternal(\n this as unknown as Parameters[0],\n id,\n );\n }\n\n async handleSendChat(\n messageOverride?: string,\n opts?: Parameters[2],\n ) {\n await handleSendChatInternal(\n this as unknown as Parameters[0],\n messageOverride,\n opts,\n );\n }\n\n async handleWhatsAppStart(force: boolean) {\n await handleWhatsAppStartInternal(this, force);\n }\n\n async handleWhatsAppWait() {\n await handleWhatsAppWaitInternal(this);\n }\n\n async handleWhatsAppLogout() {\n await handleWhatsAppLogoutInternal(this);\n }\n\n async handleChannelConfigSave() {\n await handleChannelConfigSaveInternal(this);\n }\n\n async handleChannelConfigReload() {\n await handleChannelConfigReloadInternal(this);\n }\n\n handleNostrProfileEdit(accountId: string, profile: NostrProfile | null) {\n handleNostrProfileEditInternal(this, accountId, profile);\n }\n\n handleNostrProfileCancel() {\n handleNostrProfileCancelInternal(this);\n }\n\n handleNostrProfileFieldChange(field: keyof NostrProfile, value: string) {\n handleNostrProfileFieldChangeInternal(this, field, value);\n }\n\n async handleNostrProfileSave() {\n await handleNostrProfileSaveInternal(this);\n }\n\n async handleNostrProfileImport() {\n await handleNostrProfileImportInternal(this);\n }\n\n handleNostrProfileToggleAdvanced() {\n handleNostrProfileToggleAdvancedInternal(this);\n }\n\n async handleExecApprovalDecision(decision: \"allow-once\" | \"allow-always\" | \"deny\") {\n const active = this.execApprovalQueue[0];\n if (!active || !this.client || this.execApprovalBusy) return;\n this.execApprovalBusy = true;\n this.execApprovalError = null;\n try {\n await this.client.request(\"exec.approval.resolve\", {\n id: active.id,\n decision,\n });\n this.execApprovalQueue = this.execApprovalQueue.filter((entry) => entry.id !== active.id);\n } catch (err) {\n this.execApprovalError = `Exec approval failed: ${String(err)}`;\n } finally {\n this.execApprovalBusy = false;\n }\n }\n\n // Sidebar handlers for tool output viewing\n handleOpenSidebar(content: string) {\n if (this.sidebarCloseTimer != null) {\n window.clearTimeout(this.sidebarCloseTimer);\n this.sidebarCloseTimer = null;\n }\n this.sidebarContent = content;\n this.sidebarError = null;\n this.sidebarOpen = true;\n }\n\n handleCloseSidebar() {\n this.sidebarOpen = false;\n // Clear content after transition\n if (this.sidebarCloseTimer != null) {\n window.clearTimeout(this.sidebarCloseTimer);\n }\n this.sidebarCloseTimer = window.setTimeout(() => {\n if (this.sidebarOpen) return;\n this.sidebarContent = null;\n this.sidebarError = null;\n this.sidebarCloseTimer = null;\n }, 200);\n }\n\n handleSplitRatioChange(ratio: number) {\n const newRatio = Math.max(0.4, Math.min(0.7, ratio));\n this.splitRatio = newRatio;\n this.applySettings({ ...this.settings, splitRatio: newRatio });\n }\n\n render() {\n return renderApp(this);\n }\n}\n"],"names":["t","e","s","o","n$3","r","n","i","S","c","h","a","l","p","d","u","f","b","y$2","y","v","_","m","g","$","x","E","A","C","P","V","N","S$1","I","L","z","H","M","R","k","Z","I$2","Z$1","j","B","D","MAX_ASSISTANT_NAME","MAX_ASSISTANT_AVATAR","DEFAULT_ASSISTANT_NAME","coerceIdentityValue","value","maxLength","trimmed","normalizeAssistantIdentity","input","name","avatar","resolveInjectedAssistantIdentity","KEY","loadSettings","defaults","raw","parsed","saveSettings","next","parseAgentSessionKey","sessionKey","parts","agentId","rest","TAB_GROUPS","TAB_PATHS","PATH_TO_TAB","tab","path","normalizeBasePath","basePath","base","normalizePath","normalized","pathForTab","tabFromPath","pathname","inferBasePathFromPathname","segments","candidate","prefix","iconForTab","titleForTab","subtitleForTab","icons","html","QUICK_TAG_RE","FINAL_TAG_RE","THINKING_TAG_RE","applyTrim","mode","stripReasoningTagsFromText","text","options","cleaned","result","lastIndex","inThinking","match","idx","isClose","formatMs","ms","formatAgo","diff","sec","min","hr","formatDurationMs","formatList","values","clampText","max","truncateText","toNumber","fallback","stripThinkingTags","ENVELOPE_PREFIX","ENVELOPE_CHANNELS","textCache","thinkingCache","looksLikeEnvelopeHeader","header","label","stripEnvelope","extractText","message","role","content","item","joined","extractTextCached","obj","extractThinking","rawText","extractRawText","extracted","extractThinkingCached","formatReasoningMarkdown","lines","line","uuidFromBytes","bytes","hex","weakRandomBytes","now","generateUUID","cryptoLike","loadChatHistory","state","res","err","sendChatMessage","msg","runId","error","abortChatRun","handleChatEvent","payload","current","loadSessions","params","activeMinutes","limit","patchSession","key","patch","deleteSession","TOOL_STREAM_LIMIT","TOOL_STREAM_THROTTLE_MS","TOOL_OUTPUT_CHAR_LIMIT","extractToolOutputText","record","entry","part","formatToolOutput","contentText","truncated","buildToolStreamMessage","trimToolStream","host","overflow","removed","id","syncToolStreamMessages","flushToolStreamSync","scheduleToolStreamSync","force","resetToolStream","COMPACTION_TOAST_DURATION_MS","handleCompactionEvent","data","phase","handleAgentEvent","toolCallId","args","output","scheduleChatScroll","pickScrollTarget","container","overflowY","target","distanceFromBottom","retryDelay","latest","latestDistanceFromBottom","scheduleLogsScroll","handleChatScroll","event","handleLogsScroll","resetChatScroll","exportLogs","blob","url","anchor","stamp","observeTopbar","topbar","update","height","cloneConfigObject","serializeConfigForm","form","setPathValue","nextKey","lastKey","removePathValue","loadConfig","applyConfigSnapshot","loadConfigSchema","applyConfigSchema","snapshot","rawFromSnapshot","saveConfig","baseHash","applyConfig","runUpdate","updateConfigFormValue","removeConfigFormValue","loadCronStatus","loadCronJobs","buildCronSchedule","amount","unit","expr","buildCronPayload","timeoutSeconds","addCronJob","schedule","job","toggleCronJob","enabled","runCronJob","loadCronRuns","removeCronJob","jobId","loadChannels","probe","startWhatsAppLogin","waitWhatsAppLogin","logoutWhatsApp","loadDebug","status","health","models","heartbeat","modelPayload","callDebugMethod","LOG_BUFFER_LIMIT","LEVELS","parseMaybeJsonString","normalizeLevel","lowered","parseLogLine","meta","time","level","contextCandidate","contextObj","subsystem","loadLogs","opts","entries","shouldReset","ed25519_CURVE","Gx","Gy","_a","_d","L2","captureTrace","isBig","isStr","isBytes","abytes","length","title","len","needsLen","ofLen","got","u8n","u8fr","buf","padh","pad","bytesToHex","_ch","ch","hexToBytes","hl","al","array","ai","hi","n1","n2","cr","subtle","concatBytes","arrs","sum","randomBytes","big","assertRange","modN","invert","num","md","q","callHash","fn","hashes","apoint","Point","B256","X","Y","T","zip215","normed","lastByte","bytesToNumLE","y2","isValid","uvRatio","isXOdd","isLastByteOdd","X2","Y2","Z2","Z4","aX2","left","right","XY","ZT","other","X1","Y1","Z1","X1Z2","X2Z1","Y1Z2","Y2Z1","x1y1","G","F","X3","Y3","T3","Z3","T1","T2","safe","wNAF","scalar","iz","numTo32bLE","pow2","power","pow_2_252_3","b2","b4","b5","b10","b20","b40","b80","b160","b240","b250","RM1","v3","v7","pow","vx2","root1","root2","useRoot1","useRoot2","noRoot","modL_LE","hash","sha512a","sha512s","hash2extK","hashed","head","point","pointBytes","getExtendedPublicKeyAsync","secretKey","getExtendedPublicKey","getPublicKeyAsync","hashFinishA","_sign","rBytes","signAsync","randomSecretKey","seed","utils","W","scalarBits","pwindows","pwindowSize","precompute","points","w","Gpows","ctneg","cnd","comp","pow_2_w","maxNum","mask","shiftBy","wbits","off","offF","offP","isEven","isNeg","STORAGE_KEY","base64UrlEncode","binary","byte","base64UrlDecode","padded","out","fingerprintPublicKey","publicKey","generateIdentity","privateKey","loadOrCreateDeviceIdentity","derivedId","updated","identity","stored","signDevicePayload","privateKeyBase64Url","sig","normalizeRole","normalizeScopes","scopes","scope","readStore","writeStore","store","loadDeviceAuthToken","storeDeviceAuthToken","existing","clearDeviceAuthToken","loadDevices","approveDevicePairing","requestId","rejectDevicePairing","rotateDeviceToken","revokeDeviceToken","loadNodes","resolveExecApprovalsRpc","nodeId","resolveExecApprovalsSaveRpc","loadExecApprovals","rpc","applyExecApprovalsSnapshot","saveExecApprovals","file","updateExecApprovalsFormValue","removeExecApprovalsFormValue","loadPresence","setSkillMessage","getErrorMessage","loadSkills","updateSkillEdit","skillKey","updateSkillEnabled","saveSkillApiKey","apiKey","installSkill","installId","getSystemTheme","resolveTheme","clamp01","hasReducedMotionPreference","cleanupThemeTransition","root","startThemeTransition","nextTheme","applyTheme","context","currentTheme","documentReference","document_","prefersReducedMotion","xPercent","yPercent","rect","transition","startNodesPolling","stopNodesPolling","startLogsPolling","stopLogsPolling","startDebugPolling","stopDebugPolling","applySettings","applyResolvedTheme","setLastActiveSessionKey","applySettingsFromUrl","tokenRaw","passwordRaw","sessionRaw","gatewayUrlRaw","shouldCleanUrl","token","password","session","gatewayUrl","setTab","refreshActiveTab","syncUrlWithTab","setTheme","loadOverview","loadChannelsTab","loadCron","refreshChat","inferBasePath","configured","syncThemeWithSettings","resolved","attachThemeListener","detachThemeListener","syncTabWithLocation","replace","setTabFromRoute","onPopState","targetPath","currentPath","syncUrlWithSessionKey","isChatBusy","isChatStopCommand","handleAbortChat","enqueueChatMessage","sendChatMessageNow","ok","flushChatQueue","removeQueuedMessage","handleSendChat","messageOverride","previousDraft","refreshChatAvatar","flushChatQueueForEvent","resolveAgentIdForSession","buildAvatarMetaUrl","encoded","avatarUrl","i$1","normalizeMessage","hasToolId","contentRaw","contentItems","hasToolContent","hasToolName","timestamp","normalizeRoleForGrouping","lower","isToolResultMessage","setPrototypeOf","isFrozen","getPrototypeOf","getOwnPropertyDescriptor","freeze","seal","create","apply","construct","func","thisArg","_len","_key","Func","_len2","_key2","arrayForEach","unapply","arrayLastIndexOf","arrayPop","arrayPush","arraySplice","stringToLowerCase","stringToString","stringMatch","stringReplace","stringIndexOf","stringTrim","objectHasOwnProperty","regExpTest","typeErrorCreate","unconstruct","_len3","_key3","_len4","_key4","addToSet","set","transformCaseFunc","element","lcElement","cleanArray","index","clone","object","newObject","property","lookupGetter","prop","desc","fallbackValue","html$1","svg$1","svgFilters","svgDisallowed","mathMl$1","mathMlDisallowed","svg","mathMl","xml","MUSTACHE_EXPR","ERB_EXPR","TMPLIT_EXPR","DATA_ATTR","ARIA_ATTR","IS_ALLOWED_URI","IS_SCRIPT_OR_DATA","ATTR_WHITESPACE","DOCTYPE_NAME","CUSTOM_ELEMENT","EXPRESSIONS","NODE_TYPE","getGlobal","_createTrustedTypesPolicy","trustedTypes","purifyHostElement","suffix","ATTR_NAME","policyName","scriptUrl","_createHooksMap","createDOMPurify","window","DOMPurify","document","originalDocument","currentScript","DocumentFragment","HTMLTemplateElement","Node","Element","NodeFilter","NamedNodeMap","HTMLFormElement","DOMParser","ElementPrototype","cloneNode","remove","getNextSibling","getChildNodes","getParentNode","template","trustedTypesPolicy","emptyHTML","implementation","createNodeIterator","createDocumentFragment","getElementsByTagName","importNode","hooks","IS_ALLOWED_URI$1","ALLOWED_TAGS","DEFAULT_ALLOWED_TAGS","ALLOWED_ATTR","DEFAULT_ALLOWED_ATTR","CUSTOM_ELEMENT_HANDLING","FORBID_TAGS","FORBID_ATTR","EXTRA_ELEMENT_HANDLING","ALLOW_ARIA_ATTR","ALLOW_DATA_ATTR","ALLOW_UNKNOWN_PROTOCOLS","ALLOW_SELF_CLOSE_IN_ATTR","SAFE_FOR_TEMPLATES","SAFE_FOR_XML","WHOLE_DOCUMENT","SET_CONFIG","FORCE_BODY","RETURN_DOM","RETURN_DOM_FRAGMENT","RETURN_TRUSTED_TYPE","SANITIZE_DOM","SANITIZE_NAMED_PROPS","SANITIZE_NAMED_PROPS_PREFIX","KEEP_CONTENT","IN_PLACE","USE_PROFILES","FORBID_CONTENTS","DEFAULT_FORBID_CONTENTS","DATA_URI_TAGS","DEFAULT_DATA_URI_TAGS","URI_SAFE_ATTRIBUTES","DEFAULT_URI_SAFE_ATTRIBUTES","MATHML_NAMESPACE","SVG_NAMESPACE","HTML_NAMESPACE","NAMESPACE","IS_EMPTY_INPUT","ALLOWED_NAMESPACES","DEFAULT_ALLOWED_NAMESPACES","MATHML_TEXT_INTEGRATION_POINTS","HTML_INTEGRATION_POINTS","COMMON_SVG_AND_HTML_ELEMENTS","PARSER_MEDIA_TYPE","SUPPORTED_PARSER_MEDIA_TYPES","DEFAULT_PARSER_MEDIA_TYPE","CONFIG","formElement","isRegexOrFunction","testValue","_parseConfig","cfg","ALL_SVG_TAGS","ALL_MATHML_TAGS","_checkValidNamespace","parent","tagName","parentTagName","_forceRemove","node","_removeAttribute","_initDocument","dirty","doc","leadingWhitespace","matches","dirtyPayload","body","_createNodeIterator","_isClobbered","_isNode","_executeHooks","currentNode","hook","_sanitizeElements","_isBasicCustomElement","parentNode","childNodes","childCount","childClone","_isValidAttribute","lcTag","lcName","_sanitizeAttributes","attributes","hookEvent","attr","namespaceURI","attrValue","initValue","_sanitizeShadowDOM","fragment","shadowNode","shadowIterator","importedNode","returnNode","nodeIterator","serializedHTML","tag","entryPoint","hookFunction","purify","me","xe","be","Re","Te","re","se","Oe","Q","we","ye","Pe","Se","ie","$e","U","te","_e","Le","Me","ze","oe","Ae","K","ae","Ce","le","Ie","Ee","Be","ue","qe","ve","pe","De","He","Ze","Ge","Ne","Qe","Fe","je","ce","he","Ue","ne","Ke","We","Xe","ke","J","de","ge","Je","O","ee","fe","marked","allowedTags","allowedAttrs","hooksInstalled","MARKDOWN_CHAR_LIMIT","MARKDOWN_PARSE_LIMIT","MARKDOWN_CACHE_LIMIT","MARKDOWN_CACHE_MAX_CHARS","markdownCache","getCachedMarkdown","cached","setCachedMarkdown","oldest","installHooks","toSanitizedMarkdownHtml","markdown","escapeHtml","sanitized","rendered","COPIED_FOR_MS","ERROR_FOR_MS","COPY_LABEL","COPIED_LABEL","ERROR_LABEL","copyTextToClipboard","setButtonLabel","button","createCopyButton","idleLabel","btn","copied","renderCopyAsMarkdownButton","TOOL_DISPLAY_CONFIG","rawConfig","FALLBACK","TOOL_MAP","normalizeToolName","defaultTitle","normalizeVerb","coerceDisplayValue","firstLine","preview","lookupValueByPath","segment","resolveDetailFromKeys","keys","display","resolveReadDetail","offset","resolveWriteDetail","resolveActionSpec","spec","action","resolveToolDisplay","icon","actionRaw","actionSpec","verb","detail","detailKeys","shortenHomeInString","formatToolDetail","TOOL_INLINE_THRESHOLD","PREVIEW_MAX_LINES","PREVIEW_MAX_CHARS","formatToolOutputForSidebar","getTruncatedPreview","allLines","extractToolCards","normalizeContent","cards","kind","coerceArgs","extractToolText","card","renderToolCardSidebar","onOpenSidebar","hasText","canClick","handleClick","info","isShort","showCollapsed","showInline","isEmpty","nothing","renderReadingIndicatorGroup","assistant","renderAvatar","renderStreamingGroup","startedAt","renderGroupedMessage","renderMessageGroup","group","normalizedRole","assistantName","who","roleClass","assistantAvatar","initial","className","isAvatarUrl","isToolResult","toolCards","hasToolCards","extractedText","extractedThinking","markdownBase","reasoningMarkdown","canCopyMarkdown","bubbleClasses","unsafeHTML","renderMarkdownSidebar","props","ResizableDivider","LitElement","containerWidth","deltaRatio","newRatio","css","__decorateClass","customElement","renderCompactionIndicator","renderChat","canCompose","isBusy","canAbort","reasoningLevel","row","showReasoning","assistantIdentity","composePlaceholder","splitRatio","sidebarOpen","thread","repeat","buildChatItems","CHAT_HISTORY_RENDER_LIMIT","groupMessages","items","currentGroup","history","tools","historyStart","messageKey","messageId","schemaType","schema","defaultValue","pathKey","hintForPath","hints","direct","hintKey","hint","hintSegments","humanize","isSensitivePath","META_KEYS","isAnySchema","jsonValue","renderNode","unsupported","disabled","onPatch","showLabel","type","help","nonNull","extractLiteral","literals","allLiterals","resolvedValue","lit","renderSelect","primitiveTypes","variant","normalizedTypes","hasString","hasNumber","renderTextInput","opt","renderObject","renderArray","displayValue","renderNumberInput","inputType","isSensitive","placeholder","numValue","currentIndex","unset","val","sorted","orderA","orderB","reserved","additional","allowExtra","propKey","renderMapField","itemsSchema","arr","reservedKeys","anySchema","entryValue","valuePath","sectionIcons","SECTION_META","getSectionIcon","matchesSearch","query","schemaMatches","propSchema","unions","renderConfigForm","properties","searchQuery","activeSection","activeSubsection","filteredEntries","subsectionContext","sectionSchema","sectionKey","subsectionKey","description","sectionValue","scopedValue","normalizeEnum","filtered","nullable","enumValues","analyzeConfigSchema","normalizeSchemaNode","pathLabel","union","normalizeUnion","enumNullable","normalizedProps","remaining","unique","sidebarIcons","SECTIONS","ALL_SUBSECTION","resolveSectionMeta","resolveSubsections","uiHints","subKey","order","computeDiff","original","changes","compare","orig","curr","origObj","currObj","allKeys","truncateValue","maxLen","str","renderConfig","validity","analysis","formUnsafe","schemaProps","availableSections","knownKeys","extraSections","allSections","activeSectionSchema","activeSectionMeta","subsections","allowSubnav","isAllSubsection","effectiveSubsection","hasRawChanges","hasChanges","canSaveForm","canSave","canApply","canUpdate","section","change","formatDuration","channelEnabled","channels","channelStatus","running","connected","accountActive","account","getChannelAccountCount","channelAccounts","renderChannelAccountCount","count","resolveSchemaNode","resolveChannelValue","config","channelId","fromChannels","renderChannelConfigForm","configValue","renderChannelConfigSection","renderDiscordCard","discord","accountCountLabel","renderGoogleChatCard","googleChat","renderIMessageCard","imessage","isFormDirty","renderNostrProfileForm","callbacks","accountId","isDirty","renderField","field","inputId","renderPicturePreview","picture","img","createNostrProfileFormState","profile","truncatePubkey","pubkey","renderNostrCard","nostr","nostrAccounts","profileFormState","profileFormCallbacks","onEditProfile","primaryAccount","summaryConfigured","summaryRunning","summaryPublicKey","summaryLastStartAt","summaryLastError","hasMultipleAccounts","showingForm","renderAccountCard","displayName","renderProfileSection","about","nip05","hasAnyProfileData","renderSignalCard","signal","renderSlackCard","slack","renderTelegramCard","telegram","telegramAccounts","botUsername","renderWhatsAppCard","whatsapp","renderChannels","orderedChannels","resolveChannelOrder","channel","renderChannel","showForm","renderGenericChannelCard","resolveChannelLabel","lastError","accounts","renderGenericAccount","resolveChannelMetaMap","RECENT_ACTIVITY_THRESHOLD_MS","hasRecentActivity","deriveRunningStatus","deriveConnectedStatus","runningStatus","connectedStatus","formatPresenceSummary","ip","version","formatPresenceAge","ts","formatNextRun","formatSessionTokens","total","ctx","formatEventPayload","formatCronState","last","formatCronSchedule","formatCronPayload","buildChannelOptions","seen","renderCron","channelOptions","renderScheduleFields","renderJob","renderRun","itemClass","renderDebug","evt","renderInstances","renderEntry","lastInput","roles","scopesLabel","formatTime","date","matchesFilter","needle","renderLogs","levelFiltered","exportLabel","renderNodes","bindingState","resolveBindingsState","approvalsState","resolveExecApprovalsState","renderExecApprovals","renderBindings","renderDevices","list","pending","paired","req","renderPendingDevice","device","renderPairedDevice","age","repair","tokens","renderTokenRow","deviceId","when","EXEC_APPROVALS_DEFAULT_SCOPE","SECURITY_OPTIONS","ASK_OPTIONS","nodes","resolveExecNodes","defaultBinding","agents","resolveAgentBindings","ready","normalizeSecurity","normalizeAsk","resolveExecApprovalsDefaults","resolveConfigAgents","agentsNode","isDefault","resolveExecApprovalsAgents","configAgents","approvalsAgents","merged","agent","aLabel","bLabel","resolveExecApprovalsScope","selected","targetNodes","resolveExecApprovalsNodes","targetNodeId","selectedScope","selectedAgent","allowlist","supportsBinding","renderAgentBinding","targetReady","renderExecApprovalsTarget","renderExecApprovalsTabs","renderExecApprovalsPolicy","renderExecApprovalsAllowlist","hasNodes","nodeValue","first","isDefaults","agentSecurity","agentAsk","agentAskFallback","securityValue","askValue","askFallbackValue","autoOverride","autoEffective","autoIsDefault","option","allowlistPath","renderAllowlistEntry","lastUsed","lastCommand","lastPath","bindingValue","cmd","fallbackAgent","exec","execEntry","binding","caps","commands","renderOverview","uptime","tick","authHint","hasToken","hasPassword","insecureContextHint","THINK_LEVELS","BINARY_THINK_LEVELS","VERBOSE_LEVELS","REASONING_LEVELS","normalizeProviderId","provider","isBinaryThinkingProvider","resolveThinkLevelOptions","resolveThinkLevelDisplay","isBinary","resolveThinkLevelPatchValue","renderSessions","rows","renderRow","onDelete","rawThinking","isBinaryThinking","thinking","thinkLevels","verbose","reasoning","canLink","chatUrl","formatRemaining","totalSeconds","minutes","renderMetaRow","renderExecApprovalPrompt","active","request","remainingMs","queueCount","renderSkills","skills","filter","skill","renderSkill","busy","canInstall","missing","reasons","renderTab","href","renderChatControls","sessionOptions","resolveSessionOptions","disableThinkingToggle","disableFocusToggle","showThinking","focusActive","refreshIcon","focusIcon","sessions","resolvedCurrent","THEME_ORDER","renderThemeToggle","renderMonitorIcon","renderSunIcon","renderMoonIcon","AVATAR_DATA_RE","AVATAR_HTTP_RE","resolveAssistantAvatarUrl","renderApp","presenceCount","sessionsCount","cronNext","chatDisabledReason","isChat","chatFocus","assistantAvatarUrl","chatAvatarUrl","isGroupCollapsed","hasActiveTab","agentIndex","ratio","DEFAULT_LOG_LEVEL_FILTERS","DEFAULT_CRON_FORM","loadAgents","GATEWAY_CLIENT_IDS","GATEWAY_CLIENT_NAMES","GATEWAY_CLIENT_MODES","buildDeviceAuthPayload","CONNECT_FAILED_CLOSE_CODE","GatewayBrowserClient","ev","reason","delay","isSecureContext","deviceIdentity","canFallbackToShared","authToken","storedToken","auth","signedAtMs","nonce","signature","hello","frame","seq","method","resolve","reject","isRecord","parseExecApprovalRequested","command","createdAtMs","expiresAtMs","parseExecApprovalResolved","pruneExecApprovalQueue","queue","addExecApproval","removeExecApproval","loadAssistantIdentity","normalizeSessionKeyForDefaults","mainSessionKey","mainKey","defaultAgentId","applySessionDefaults","resolvedSessionKey","resolvedSettingsSessionKey","resolvedLastActiveSessionKey","nextSessionKey","nextSettings","shouldUpdateSettings","connectGateway","applySnapshot","code","handleGatewayEvent","expected","received","handleGatewayEventUnsafe","handleConnected","handleFirstUpdated","handleDisconnected","handleUpdated","changed","forcedByTab","forcedByLoad","handleWhatsAppStart","handleWhatsAppWait","handleWhatsAppLogout","handleChannelConfigSave","handleChannelConfigReload","parseValidationErrors","details","errors","rawField","resolveNostrAccountId","buildNostrProfileUrl","handleNostrProfileEdit","handleNostrProfileCancel","handleNostrProfileFieldChange","handleNostrProfileToggleAdvanced","handleNostrProfileSave","response","errorMessage","handleNostrProfileImport","nextValues","showAdvanced","injectedAssistantIdentity","resolveOnboardingMode","ClawdbotApp","onPopStateInternal","connectGatewayInternal","handleChatScrollInternal","handleLogsScrollInternal","exportLogsInternal","resetToolStreamInternal","resetChatScrollInternal","loadAssistantIdentityInternal","applySettingsInternal","setTabInternal","setThemeInternal","loadOverviewInternal","loadCronInternal","handleAbortChatInternal","removeQueuedMessageInternal","handleSendChatInternal","handleWhatsAppStartInternal","handleWhatsAppWaitInternal","handleWhatsAppLogoutInternal","handleChannelConfigSaveInternal","handleChannelConfigReloadInternal","handleNostrProfileEditInternal","handleNostrProfileCancelInternal","handleNostrProfileFieldChangeInternal","handleNostrProfileSaveInternal","handleNostrProfileImportInternal","handleNostrProfileToggleAdvancedInternal","decision"],"mappings":"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","x_google_ignoreList":[0,1,2,3,4,5,6,26,39,40,41,43,44,45]} \ No newline at end of file diff --git a/dist/control-ui/index.html b/dist/control-ui/index.html deleted file mode 100644 index 9407eea99..000000000 --- a/dist/control-ui/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - Clawdbot Control - - - - - - - - - diff --git a/docs/assets/terminal.css b/docs/assets/terminal.css index 23283d651..e5e51af9e 100644 --- a/docs/assets/terminal.css +++ b/docs/assets/terminal.css @@ -115,6 +115,9 @@ body::after { } .shell { + position: sticky; + top: 0; + z-index: 100; padding: 22px 16px 10px; } diff --git a/docs/automation/cron-vs-heartbeat.md b/docs/automation/cron-vs-heartbeat.md index 333a45d0b..325575602 100644 --- a/docs/automation/cron-vs-heartbeat.md +++ b/docs/automation/cron-vs-heartbeat.md @@ -201,7 +201,7 @@ For ad-hoc workflows, call Lobster directly. - Lobster runs as a **local subprocess** (`lobster` CLI) in tool mode and returns a **JSON envelope**. - If the tool returns `needs_approval`, you resume with a `resumeToken` and `approve` flag. -- The tool is an **optional plugin**; you must allowlist `lobster` in `tools.allow`. +- The tool is an **optional plugin**; enable it additively via `tools.alsoAllow: ["lobster"]` (recommended). - If you pass `lobsterPath`, it must be an **absolute path**. See [Lobster](/tools/lobster) for full usage and examples. diff --git a/docs/automation/gmail-pubsub.md b/docs/automation/gmail-pubsub.md index 94feba3d7..6c84fdb5e 100644 --- a/docs/automation/gmail-pubsub.md +++ b/docs/automation/gmail-pubsub.md @@ -83,6 +83,8 @@ Notes: - Per-hook `model`/`thinking` in the mapping still overrides these defaults. - Fallback order: `hooks.gmail.model` → `agents.defaults.model.fallbacks` → primary (auth/rate-limit/timeouts). - If `agents.defaults.models` is set, the Gmail model must be in the allowlist. +- Gmail hook content is wrapped with external-content safety boundaries by default. + To disable (dangerous), set `hooks.gmail.allowUnsafeExternalContent: true`. To customize payload handling further, add `hooks.mappings` or a JS/TS transform module under `hooks.transformsDir` (see [Webhooks](/automation/webhook)). diff --git a/docs/automation/webhook.md b/docs/automation/webhook.md index 0828483d2..12fc6b92a 100644 --- a/docs/automation/webhook.md +++ b/docs/automation/webhook.md @@ -27,10 +27,10 @@ Notes: ## Auth -Every request must include the hook token: -- `Authorization: Bearer ` -- or `x-clawdbot-token: ` -- or `?token=` +Every request must include the hook token. Prefer headers: +- `Authorization: Bearer ` (recommended) +- `x-clawdbot-token: ` +- `?token=` (deprecated; logs a warning and will be removed in a future major release) ## Endpoints @@ -96,6 +96,8 @@ Mapping options (summary): - TS transforms require a TS loader (e.g. `bun` or `tsx`) or precompiled `.js` at runtime. - Set `deliver: true` + `channel`/`to` on mappings to route replies to a chat surface (`channel` defaults to `last` and falls back to WhatsApp). +- `allowUnsafeExternalContent: true` disables the external content safety wrapper for that hook + (dangerous; only for trusted internal sources). - `clawdbot webhooks gmail setup` writes `hooks.gmail` config for `clawdbot webhooks gmail run`. See [Gmail Pub/Sub](/automation/gmail-pubsub) for the full Gmail watch flow. @@ -148,3 +150,6 @@ curl -X POST http://127.0.0.1:18789/hooks/gmail \ - Keep hook endpoints behind loopback, tailnet, or trusted reverse proxy. - Use a dedicated hook token; do not reuse gateway auth tokens. - Avoid including sensitive raw payloads in webhook logs. +- Hook payloads are treated as untrusted and wrapped with safety boundaries by default. + If you must disable this for a specific hook, set `allowUnsafeExternalContent: true` + in that hook's mapping (dangerous). diff --git a/docs/channels/bluebubbles.md b/docs/channels/bluebubbles.md index a1f4a0892..914dc3664 100644 --- a/docs/channels/bluebubbles.md +++ b/docs/channels/bluebubbles.md @@ -218,6 +218,7 @@ Prefer `chat_guid` for stable routing: ## Security - Webhook requests are authenticated by comparing `guid`/`password` query params or headers against `channels.bluebubbles.password`. Requests from `localhost` are also accepted. - Keep the API password and webhook endpoint secret (treat them like credentials). +- Localhost trust means a same-host reverse proxy can unintentionally bypass the password. If you proxy the gateway, require auth at the proxy and configure `gateway.trustedProxies`. See [Gateway security](/gateway/security#reverse-proxy-configuration). - Enable HTTPS + firewall rules on the BlueBubbles server if exposing it outside your LAN. ## Troubleshooting diff --git a/docs/channels/discord.md b/docs/channels/discord.md index 12dd28084..8aba9d336 100644 --- a/docs/channels/discord.md +++ b/docs/channels/discord.md @@ -10,13 +10,14 @@ Status: ready for DM and guild text channels via the official Discord bot gatewa ## Quick setup (beginner) 1) Create a Discord bot and copy the bot token. -2) Set the token for Clawdbot: +2) In the Discord app settings, enable **Message Content Intent** (and **Server Members Intent** if you plan to use allowlists or name lookups). +3) Set the token for Clawdbot: - Env: `DISCORD_BOT_TOKEN=...` - Or config: `channels.discord.token: "..."`. - If both are set, config takes precedence (env fallback is default-account only). -3) Invite the bot to your server with message permissions. -4) Start the gateway. -5) DM access is pairing by default; approve the pairing code on first contact. +4) Invite the bot to your server with message permissions (create a private server if you just want DMs). +5) Start the gateway. +6) DM access is pairing by default; approve the pairing code on first contact. Minimal config: ```json5 @@ -297,8 +298,12 @@ ack reaction after the bot replies. - `guilds."*"`: default per-guild settings applied when no explicit entry exists. - `guilds..slug`: optional friendly slug used for display names. - `guilds..users`: optional per-guild user allowlist (ids or names). +- `guilds..tools`: optional per-guild tool policy overrides (`allow`/`deny`/`alsoAllow`) used when the channel override is missing. +- `guilds..toolsBySender`: optional per-sender tool policy overrides at the guild level (applies when the channel override is missing; `"*"` wildcard supported). - `guilds..channels..allow`: allow/deny the channel when `groupPolicy="allowlist"`. - `guilds..channels..requireMention`: mention gating for the channel. +- `guilds..channels..tools`: optional per-channel tool policy overrides (`allow`/`deny`/`alsoAllow`). +- `guilds..channels..toolsBySender`: optional per-sender tool policy overrides within the channel (`"*"` wildcard supported). - `guilds..channels..users`: optional per-channel user allowlist. - `guilds..channels..skills`: skill filter (omit = all skills, empty = none). - `guilds..channels..systemPrompt`: extra system prompt for the channel (combined with channel topic). diff --git a/docs/channels/index.md b/docs/channels/index.md index 52e963b87..4c2f77581 100644 --- a/docs/channels/index.md +++ b/docs/channels/index.md @@ -21,10 +21,12 @@ Text is supported everywhere; media and reactions vary by channel. - [BlueBubbles](/channels/bluebubbles) — **Recommended for iMessage**; uses the BlueBubbles macOS server REST API with full feature support (edit, unsend, effects, reactions, group management — edit currently broken on macOS 26 Tahoe). - [iMessage](/channels/imessage) — macOS only; native integration via imsg (legacy, consider BlueBubbles for new setups). - [Microsoft Teams](/channels/msteams) — Bot Framework; enterprise support (plugin, installed separately). +- [LINE](/channels/line) — LINE Messaging API bot (plugin, installed separately). - [Nextcloud Talk](/channels/nextcloud-talk) — Self-hosted chat via Nextcloud Talk (plugin, installed separately). - [Matrix](/channels/matrix) — Matrix protocol (plugin, installed separately). - [Nostr](/channels/nostr) — Decentralized DMs via NIP-04 (plugin, installed separately). - [Tlon](/channels/tlon) — Urbit-based messenger (plugin, installed separately). +- [Twitch](/channels/twitch) — Twitch chat via IRC connection (plugin, installed separately). - [Zalo](/channels/zalo) — Zalo Bot API; Vietnam's popular messenger (plugin, installed separately). - [Zalo Personal](/channels/zalouser) — Zalo personal account via QR login (plugin, installed separately). - [WebChat](/web/webchat) — Gateway WebChat UI over WebSocket. diff --git a/docs/channels/line.md b/docs/channels/line.md new file mode 100644 index 000000000..40ed2f9f6 --- /dev/null +++ b/docs/channels/line.md @@ -0,0 +1,183 @@ +--- +summary: "LINE Messaging API plugin setup, config, and usage" +read_when: + - You want to connect Clawdbot to LINE + - You need LINE webhook + credential setup + - You want LINE-specific message options +--- + +# LINE (plugin) + +LINE connects to Clawdbot via the LINE Messaging API. The plugin runs as a webhook +receiver on the gateway and uses your channel access token + channel secret for +authentication. + +Status: supported via plugin. Direct messages, group chats, media, locations, Flex +messages, template messages, and quick replies are supported. Reactions and threads +are not supported. + +## Plugin required + +Install the LINE plugin: + +```bash +clawdbot plugins install @clawdbot/line +``` + +Local checkout (when running from a git repo): + +```bash +clawdbot plugins install ./extensions/line +``` + +## Setup + +1) Create a LINE Developers account and open the Console: + https://developers.line.biz/console/ +2) Create (or pick) a Provider and add a **Messaging API** channel. +3) Copy the **Channel access token** and **Channel secret** from the channel settings. +4) Enable **Use webhook** in the Messaging API settings. +5) Set the webhook URL to your gateway endpoint (HTTPS required): + +``` +https://gateway-host/line/webhook +``` + +The gateway responds to LINE’s webhook verification (GET) and inbound events (POST). +If you need a custom path, set `channels.line.webhookPath` or +`channels.line.accounts..webhookPath` and update the URL accordingly. + +## Configure + +Minimal config: + +```json5 +{ + channels: { + line: { + enabled: true, + channelAccessToken: "LINE_CHANNEL_ACCESS_TOKEN", + channelSecret: "LINE_CHANNEL_SECRET", + dmPolicy: "pairing" + } + } +} +``` + +Env vars (default account only): + +- `LINE_CHANNEL_ACCESS_TOKEN` +- `LINE_CHANNEL_SECRET` + +Token/secret files: + +```json5 +{ + channels: { + line: { + tokenFile: "/path/to/line-token.txt", + secretFile: "/path/to/line-secret.txt" + } + } +} +``` + +Multiple accounts: + +```json5 +{ + channels: { + line: { + accounts: { + marketing: { + channelAccessToken: "...", + channelSecret: "...", + webhookPath: "/line/marketing" + } + } + } + } +} +``` + +## Access control + +Direct messages default to pairing. Unknown senders get a pairing code and their +messages are ignored until approved. + +```bash +clawdbot pairing list line +clawdbot pairing approve line +``` + +Allowlists and policies: + +- `channels.line.dmPolicy`: `pairing | allowlist | open | disabled` +- `channels.line.allowFrom`: allowlisted LINE user IDs for DMs +- `channels.line.groupPolicy`: `allowlist | open | disabled` +- `channels.line.groupAllowFrom`: allowlisted LINE user IDs for groups +- Per-group overrides: `channels.line.groups..allowFrom` + +LINE IDs are case-sensitive. Valid IDs look like: + +- User: `U` + 32 hex chars +- Group: `C` + 32 hex chars +- Room: `R` + 32 hex chars + +## Message behavior + +- Text is chunked at 5000 characters. +- Markdown formatting is stripped; code blocks and tables are converted into Flex + cards when possible. +- Streaming responses are buffered; LINE receives full chunks with a loading + animation while the agent works. +- Media downloads are capped by `channels.line.mediaMaxMb` (default 10). + +## Channel data (rich messages) + +Use `channelData.line` to send quick replies, locations, Flex cards, or template +messages. + +```json5 +{ + text: "Here you go", + channelData: { + line: { + quickReplies: ["Status", "Help"], + location: { + title: "Office", + address: "123 Main St", + latitude: 35.681236, + longitude: 139.767125 + }, + flexMessage: { + altText: "Status card", + contents: { /* Flex payload */ } + }, + templateMessage: { + type: "confirm", + text: "Proceed?", + confirmLabel: "Yes", + confirmData: "yes", + cancelLabel: "No", + cancelData: "no" + } + } + } +} +``` + +The LINE plugin also ships a `/card` command for Flex message presets: + +``` +/card info "Welcome" "Thanks for joining!" +``` + +## Troubleshooting + +- **Webhook verification fails:** ensure the webhook URL is HTTPS and the + `channelSecret` matches the LINE console. +- **No inbound events:** confirm the webhook path matches `channels.line.webhookPath` + and that the gateway is reachable from LINE. +- **Media download errors:** raise `channels.line.mediaMaxMb` if media exceeds the + default limit. diff --git a/docs/channels/matrix.md b/docs/channels/matrix.md index 2d9025f51..8151bfed1 100644 --- a/docs/channels/matrix.md +++ b/docs/channels/matrix.md @@ -10,7 +10,7 @@ on any homeserver, so you need a Matrix account for the bot. Once it is logged i the bot directly or invite it to rooms (Matrix "groups"). Beeper is a valid client option too, but it requires E2EE to be enabled. -Status: supported via plugin (matrix-bot-sdk). Direct messages, rooms, threads, media, reactions, +Status: supported via plugin (@vector-im/matrix-bot-sdk). Direct messages, rooms, threads, media, reactions, polls (send + poll-start as text), location, and E2EE (with crypto support). ## Plugin required diff --git a/docs/channels/msteams.md b/docs/channels/msteams.md index 2f6ed5f83..e0a36ad48 100644 --- a/docs/channels/msteams.md +++ b/docs/channels/msteams.md @@ -421,8 +421,12 @@ Key settings (see `/gateway/configuration` for shared channel patterns): - `channels.msteams.replyStyle`: `thread | top-level` (see [Reply Style](#reply-style-threads-vs-posts)). - `channels.msteams.teams..replyStyle`: per-team override. - `channels.msteams.teams..requireMention`: per-team override. +- `channels.msteams.teams..tools`: default per-team tool policy overrides (`allow`/`deny`/`alsoAllow`) used when a channel override is missing. +- `channels.msteams.teams..toolsBySender`: default per-team per-sender tool policy overrides (`"*"` wildcard supported). - `channels.msteams.teams..channels..replyStyle`: per-channel override. - `channels.msteams.teams..channels..requireMention`: per-channel override. +- `channels.msteams.teams..channels..tools`: per-channel tool policy overrides (`allow`/`deny`/`alsoAllow`). +- `channels.msteams.teams..channels..toolsBySender`: per-channel per-sender tool policy overrides (`"*"` wildcard supported). - `channels.msteams.sharePointSiteId`: SharePoint site ID for file uploads in group chats/channels (see [Sending files in group chats](#sending-files-in-group-chats)). ## Routing & Sessions diff --git a/docs/channels/slack.md b/docs/channels/slack.md index 5f768db0e..8ab5846b7 100644 --- a/docs/channels/slack.md +++ b/docs/channels/slack.md @@ -464,6 +464,8 @@ For fine-grained control, use these tags in agent responses: Channel options (`channels.slack.channels.` or `channels.slack.channels.`): - `allow`: allow/deny the channel when `groupPolicy="allowlist"`. - `requireMention`: mention gating for the channel. +- `tools`: optional per-channel tool policy overrides (`allow`/`deny`/`alsoAllow`). +- `toolsBySender`: optional per-sender tool policy overrides within the channel (keys are sender ids/@handles/emails; `"*"` wildcard supported). - `allowBots`: allow bot-authored messages in this channel (default: false). - `users`: optional per-channel user allowlist. - `skills`: skill filter (omit = all skills, empty = none). diff --git a/docs/channels/telegram.md b/docs/channels/telegram.md index e708e2e64..56920f131 100644 --- a/docs/channels/telegram.md +++ b/docs/channels/telegram.md @@ -383,6 +383,133 @@ For message tool sends, set `asVoice: true` with a voice-compatible audio `media } ``` +## Stickers + +Clawdbot supports receiving and sending Telegram stickers with intelligent caching. + +### Receiving stickers + +When a user sends a sticker, Clawdbot handles it based on the sticker type: + +- **Static stickers (WEBP):** Downloaded and processed through vision. The sticker appears as a `` placeholder in the message content. +- **Animated stickers (TGS):** Skipped (Lottie format not supported for processing). +- **Video stickers (WEBM):** Skipped (video format not supported for processing). + +Template context field available when receiving stickers: +- `Sticker` — object with: + - `emoji` — emoji associated with the sticker + - `setName` — name of the sticker set + - `fileId` — Telegram file ID (send the same sticker back) + - `fileUniqueId` — stable ID for cache lookup + - `cachedDescription` — cached vision description when available + +### Sticker cache + +Stickers are processed through the AI's vision capabilities to generate descriptions. Since the same stickers are often sent repeatedly, Clawdbot caches these descriptions to avoid redundant API calls. + +**How it works:** + +1. **First encounter:** The sticker image is sent to the AI for vision analysis. The AI generates a description (e.g., "A cartoon cat waving enthusiastically"). +2. **Cache storage:** The description is saved along with the sticker's file ID, emoji, and set name. +3. **Subsequent encounters:** When the same sticker is seen again, the cached description is used directly. The image is not sent to the AI. + +**Cache location:** `~/.clawdbot/telegram/sticker-cache.json` + +**Cache entry format:** +```json +{ + "fileId": "CAACAgIAAxkBAAI...", + "fileUniqueId": "AgADBAADb6cxG2Y", + "emoji": "👋", + "setName": "CoolCats", + "description": "A cartoon cat waving enthusiastically", + "cachedAt": "2026-01-15T10:30:00.000Z" +} +``` + +**Benefits:** +- Reduces API costs by avoiding repeated vision calls for the same sticker +- Faster response times for cached stickers (no vision processing delay) +- Enables sticker search functionality based on cached descriptions + +The cache is populated automatically as stickers are received. There is no manual cache management required. + +### Sending stickers + +The agent can send and search stickers using the `sticker` and `sticker-search` actions. These are disabled by default and must be enabled in config: + +```json5 +{ + channels: { + telegram: { + actions: { + sticker: true + } + } + } +} +``` + +**Send a sticker:** + +```json5 +{ + "action": "sticker", + "channel": "telegram", + "to": "123456789", + "fileId": "CAACAgIAAxkBAAI..." +} +``` + +Parameters: +- `fileId` (required) — the Telegram file ID of the sticker. Obtain this from `Sticker.fileId` when receiving a sticker, or from a `sticker-search` result. +- `replyTo` (optional) — message ID to reply to. +- `threadId` (optional) — message thread ID for forum topics. + +**Search for stickers:** + +The agent can search cached stickers by description, emoji, or set name: + +```json5 +{ + "action": "sticker-search", + "channel": "telegram", + "query": "cat waving", + "limit": 5 +} +``` + +Returns matching stickers from the cache: +```json5 +{ + "ok": true, + "count": 2, + "stickers": [ + { + "fileId": "CAACAgIAAxkBAAI...", + "emoji": "👋", + "description": "A cartoon cat waving enthusiastically", + "setName": "CoolCats" + } + ] +} +``` + +The search uses fuzzy matching across description text, emoji characters, and set names. + +**Example with threading:** + +```json5 +{ + "action": "sticker", + "channel": "telegram", + "to": "-1001234567890", + "fileId": "CAACAgIAAxkBAAI...", + "replyTo": 42, + "threadId": 123 +} +``` + ## Streaming (drafts) Telegram can stream **draft bubbles** while the agent is generating a response. Clawdbot uses Bot API `sendMessageDraft` (not real messages) and then sends the @@ -420,7 +547,7 @@ Outbound Telegram API calls retry on transient network/429 errors with exponenti - Tool: `telegram` with `react` action (`chatId`, `messageId`, `emoji`). - Tool: `telegram` with `deleteMessage` action (`chatId`, `messageId`). - Reaction removal semantics: see [/tools/reactions](/tools/reactions). -- Tool gating: `channels.telegram.actions.reactions`, `channels.telegram.actions.sendMessage`, `channels.telegram.actions.deleteMessage` (default: enabled). +- Tool gating: `channels.telegram.actions.reactions`, `channels.telegram.actions.sendMessage`, `channels.telegram.actions.deleteMessage` (default: enabled), and `channels.telegram.actions.sticker` (default: disabled). ## Reaction notifications @@ -529,6 +656,7 @@ Provider options: - `channels.telegram.streamMode`: `off | partial | block` (draft streaming). - `channels.telegram.mediaMaxMb`: inbound/outbound media cap (MB). - `channels.telegram.retry`: retry policy for outbound Telegram API calls (attempts, minDelayMs, maxDelayMs, jitter). +- `channels.telegram.network.autoSelectFamily`: override Node autoSelectFamily (true=enable, false=disable). Defaults to disabled on Node 22 to avoid Happy Eyeballs timeouts. - `channels.telegram.proxy`: proxy URL for Bot API calls (SOCKS/HTTP). - `channels.telegram.webhookUrl`: enable webhook mode. - `channels.telegram.webhookSecret`: webhook secret (optional). @@ -536,6 +664,7 @@ Provider options: - `channels.telegram.actions.reactions`: gate Telegram tool reactions. - `channels.telegram.actions.sendMessage`: gate Telegram tool message sends. - `channels.telegram.actions.deleteMessage`: gate Telegram tool message deletes. +- `channels.telegram.actions.sticker`: gate Telegram sticker actions — send and search (default: false). - `channels.telegram.reactionNotifications`: `off | own | all` — control which reactions trigger system events (default: `own` when not set). - `channels.telegram.reactionLevel`: `off | ack | minimal | extensive` — control agent's reaction capability (default: `minimal` when not set). diff --git a/docs/channels/twitch.md b/docs/channels/twitch.md new file mode 100644 index 000000000..e92a6c255 --- /dev/null +++ b/docs/channels/twitch.md @@ -0,0 +1,366 @@ +--- +summary: "Twitch chat bot configuration and setup" +read_when: + - Setting up Twitch chat integration for Clawdbot +--- +# Twitch (plugin) + +Twitch chat support via IRC connection. Clawdbot connects as a Twitch user (bot account) to receive and send messages in channels. + +## Plugin required + +Twitch ships as a plugin and is not bundled with the core install. + +Install via CLI (npm registry): + +```bash +clawdbot plugins install @clawdbot/twitch +``` + +Local checkout (when running from a git repo): + +```bash +clawdbot plugins install ./extensions/twitch +``` + +Details: [Plugins](/plugin) + +## Quick setup (beginner) + +1) Create a dedicated Twitch account for the bot (or use an existing account). +2) Generate credentials: [Twitch Token Generator](https://twitchtokengenerator.com/) + - Select **Bot Token** + - Verify scopes `chat:read` and `chat:write` are selected + - Copy the **Client ID** and **Access Token** +3) Find your Twitch user ID: https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/ +4) Configure the token: + - Env: `CLAWDBOT_TWITCH_ACCESS_TOKEN=...` (default account only) + - Or config: `channels.twitch.accessToken` + - If both are set, config takes precedence (env fallback is default-account only). +5) Start the gateway. + +**⚠️ Important:** Add access control (`allowFrom` or `allowedRoles`) to prevent unauthorized users from triggering the bot. `requireMention` defaults to `true`. + +Minimal config: + +```json5 +{ + channels: { + twitch: { + enabled: true, + username: "clawdbot", // Bot's Twitch account + accessToken: "oauth:abc123...", // OAuth Access Token (or use CLAWDBOT_TWITCH_ACCESS_TOKEN env var) + clientId: "xyz789...", // Client ID from Token Generator + channel: "vevisk", // Which Twitch channel's chat to join (required) + allowFrom: ["123456789"] // (recommended) Your Twitch user ID only - get it from https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/ + } + } +} +``` + +## What it is + +- A Twitch channel owned by the Gateway. +- Deterministic routing: replies always go back to Twitch. +- Each account maps to an isolated session key `agent::twitch:`. +- `username` is the bot's account (who authenticates), `channel` is which chat room to join. + +## Setup (detailed) + +### Generate credentials + +Use [Twitch Token Generator](https://twitchtokengenerator.com/): +- Select **Bot Token** +- Verify scopes `chat:read` and `chat:write` are selected +- Copy the **Client ID** and **Access Token** + +No manual app registration needed. Tokens expire after several hours. + +### Configure the bot + +**Env var (default account only):** +```bash +CLAWDBOT_TWITCH_ACCESS_TOKEN=oauth:abc123... +``` + +**Or config:** +```json5 +{ + channels: { + twitch: { + enabled: true, + username: "clawdbot", + accessToken: "oauth:abc123...", + clientId: "xyz789...", + channel: "vevisk" + } + } +} +``` + +If both env and config are set, config takes precedence. + +### Access control (recommended) + +```json5 +{ + channels: { + twitch: { + allowFrom: ["123456789"], // (recommended) Your Twitch user ID only + allowedRoles: ["moderator"] // Or restrict to roles + } + } +} +``` + +**Available roles:** `"moderator"`, `"owner"`, `"vip"`, `"subscriber"`, `"all"`. + +**Why user IDs?** Usernames can change, allowing impersonation. User IDs are permanent. + +Find your Twitch user ID: https://www.streamweasels.com/tools/convert-twitch-username-%20to-user-id/ (Convert your Twitch username to ID) + +## Token refresh (optional) + +Tokens from [Twitch Token Generator](https://twitchtokengenerator.com/) cannot be automatically refreshed - regenerate when expired. + +For automatic token refresh, create your own Twitch application at [Twitch Developer Console](https://dev.twitch.tv/console) and add to config: + +```json5 +{ + channels: { + twitch: { + clientSecret: "your_client_secret", + refreshToken: "your_refresh_token" + } + } +} +``` + +The bot automatically refreshes tokens before expiration and logs refresh events. + +## Multi-account support + +Use `channels.twitch.accounts` with per-account tokens. See [`gateway/configuration`](/gateway/configuration) for the shared pattern. + +Example (one bot account in two channels): + +```json5 +{ + channels: { + twitch: { + accounts: { + channel1: { + username: "clawdbot", + accessToken: "oauth:abc123...", + clientId: "xyz789...", + channel: "vevisk" + }, + channel2: { + username: "clawdbot", + accessToken: "oauth:def456...", + clientId: "uvw012...", + channel: "secondchannel" + } + } + } + } +} +``` + +**Note:** Each account needs its own token (one token per channel). + +## Access control + +### Role-based restrictions + +```json5 +{ + channels: { + twitch: { + accounts: { + default: { + allowedRoles: ["moderator", "vip"] + } + } + } + } +} +``` + +### Allowlist by User ID (most secure) + +```json5 +{ + channels: { + twitch: { + accounts: { + default: { + allowFrom: ["123456789", "987654321"] + } + } + } + } +} +``` + +### Combined allowlist + roles + +Users in `allowFrom` bypass role checks: + +```json5 +{ + channels: { + twitch: { + accounts: { + default: { + allowFrom: ["123456789"], + allowedRoles: ["moderator"] + } + } + } + } +} +``` + +### Disable @mention requirement + +By default, `requireMention` is `true`. To disable and respond to all messages: + +```json5 +{ + channels: { + twitch: { + accounts: { + default: { + requireMention: false + } + } + } + } +} +``` + +## Troubleshooting + +First, run diagnostic commands: + +```bash +clawdbot doctor +clawdbot channels status --probe +``` + +### Bot doesn't respond to messages + +**Check access control:** Temporarily set `allowedRoles: ["all"]` to test. + +**Check the bot is in the channel:** The bot must join the channel specified in `channel`. + +### Token issues + +**"Failed to connect" or authentication errors:** +- Verify `accessToken` is the OAuth access token value (typically starts with `oauth:` prefix) +- Check token has `chat:read` and `chat:write` scopes +- If using token refresh, verify `clientSecret` and `refreshToken` are set + +### Token refresh not working + +**Check logs for refresh events:** +``` +Using env token source for mybot +Access token refreshed for user 123456 (expires in 14400s) +``` + +If you see "token refresh disabled (no refresh token)": +- Ensure `clientSecret` is provided +- Ensure `refreshToken` is provided + +## Config + +**Account config:** +- `username` - Bot username +- `accessToken` - OAuth access token with `chat:read` and `chat:write` +- `clientId` - Twitch Client ID (from Token Generator or your app) +- `channel` - Channel to join (required) +- `enabled` - Enable this account (default: `true`) +- `clientSecret` - Optional: For automatic token refresh +- `refreshToken` - Optional: For automatic token refresh +- `expiresIn` - Token expiry in seconds +- `obtainmentTimestamp` - Token obtained timestamp +- `allowFrom` - User ID allowlist +- `allowedRoles` - Role-based access control (`"moderator" | "owner" | "vip" | "subscriber" | "all"`) +- `requireMention` - Require @mention (default: `true`) + +**Provider options:** +- `channels.twitch.enabled` - Enable/disable channel startup +- `channels.twitch.username` - Bot username (simplified single-account config) +- `channels.twitch.accessToken` - OAuth access token (simplified single-account config) +- `channels.twitch.clientId` - Twitch Client ID (simplified single-account config) +- `channels.twitch.channel` - Channel to join (simplified single-account config) +- `channels.twitch.accounts.` - Multi-account config (all account fields above) + +Full example: + +```json5 +{ + channels: { + twitch: { + enabled: true, + username: "clawdbot", + accessToken: "oauth:abc123...", + clientId: "xyz789...", + channel: "vevisk", + clientSecret: "secret123...", + refreshToken: "refresh456...", + allowFrom: ["123456789"], + allowedRoles: ["moderator", "vip"], + accounts: { + default: { + username: "mybot", + accessToken: "oauth:abc123...", + clientId: "xyz789...", + channel: "your_channel", + enabled: true, + clientSecret: "secret123...", + refreshToken: "refresh456...", + expiresIn: 14400, + obtainmentTimestamp: 1706092800000, + allowFrom: ["123456789", "987654321"], + allowedRoles: ["moderator"] + } + } + } + } +} +``` + +## Tool actions + +The agent can call `twitch` with action: +- `send` - Send a message to a channel + +Example: + +```json5 +{ + "action": "twitch", + "params": { + "message": "Hello Twitch!", + "to": "#mychannel" + } +} +``` + +## Safety & ops + +- **Treat tokens like passwords** - Never commit tokens to git +- **Use automatic token refresh** for long-running bots +- **Use user ID allowlists** instead of usernames for access control +- **Monitor logs** for token refresh events and connection status +- **Scope tokens minimally** - Only request `chat:read` and `chat:write` +- **If stuck**: Restart the gateway after confirming no other process owns the session + +## Limits + +- **500 characters** per message (auto-chunked at word boundaries) +- Markdown is stripped before chunking +- No rate limiting (uses Twitch's built-in rate limits) diff --git a/docs/cli/browser.md b/docs/cli/browser.md index 8ba2bc237..6d54b8a10 100644 --- a/docs/cli/browser.md +++ b/docs/cli/browser.md @@ -1,8 +1,8 @@ --- -summary: "CLI reference for `clawdbot browser` (profiles, tabs, actions, extension relay, remote serve)" +summary: "CLI reference for `clawdbot browser` (profiles, tabs, actions, extension relay)" read_when: - You use `clawdbot browser` and want examples for common tasks - - You want to control a remote browser via `browser.controlUrl` + - You want to control a browser running on another machine via a node host - You want to use the Chrome extension relay (attach/detach via toolbar button) --- @@ -16,8 +16,10 @@ Related: ## Common flags -- `--url `: override `browser.controlUrl` for this command invocation. -- `--browser-profile `: choose a browser profile (default comes from config). +- `--url `: Gateway WebSocket URL (defaults to config). +- `--token `: Gateway token (if required). +- `--timeout `: request timeout (ms). +- `--browser-profile `: choose a browser profile (default from config). - `--json`: machine-readable output (where supported). ## Quick start (local) @@ -93,14 +95,10 @@ Then Chrome → `chrome://extensions` → enable “Developer mode” → “Loa Full guide: [Chrome extension](/tools/chrome-extension) -## Remote browser control (`clawdbot browser serve`) +## Remote browser control (node host proxy) -If the Gateway runs on a different machine than the browser, run a standalone browser control server on the machine that runs Chrome: +If the Gateway runs on a different machine than the browser, run a **node host** on the machine that has Chrome/Brave/Edge/Chromium. The Gateway will proxy browser actions to that node (no separate browser control server required). -```bash -clawdbot browser serve --bind 127.0.0.1 --port 18791 --token -``` +Use `gateway.nodes.browser.mode` to control auto-routing and `gateway.nodes.browser.node` to pin a specific node if multiple are connected. -Then point the Gateway at it using `browser.controlUrl` + `browser.controlToken` (or `CLAWDBOT_BROWSER_CONTROL_TOKEN`). - -Security + TLS best-practices: [Browser tool](/tools/browser), [Tailscale](/gateway/tailscale), [Security](/gateway/security) +Security + remote setup: [Browser tool](/tools/browser), [Remote access](/gateway/remote), [Tailscale](/gateway/tailscale), [Security](/gateway/security) diff --git a/docs/cli/index.md b/docs/cli/index.md index d23ee3a5e..9d809bde5 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -297,7 +297,7 @@ Options: - `--non-interactive` - `--mode ` - `--flow ` (manual is an alias for advanced) -- `--auth-choice ` +- `--auth-choice ` - `--token-provider ` (non-interactive; used with `--auth-choice token`) - `--token ` (non-interactive; used with `--auth-choice token`) - `--token-profile-id ` (non-interactive; default: `:manual`) @@ -314,7 +314,7 @@ Options: - `--opencode-zen-api-key ` - `--gateway-port ` - `--gateway-bind ` -- `--gateway-auth ` +- `--gateway-auth ` - `--gateway-token ` - `--gateway-password ` - `--remote-url ` @@ -358,7 +358,7 @@ Options: Manage chat channel accounts (WhatsApp/Telegram/Discord/Google Chat/Slack/Mattermost (plugin)/Signal/iMessage/MS Teams). Subcommands: -- `channels list`: show configured channels and auth profiles (Claude Code + Codex CLI OAuth sync included). +- `channels list`: show configured channels and auth profiles. - `channels status`: check gateway reachability and channel health (`--probe` runs extra checks; use `clawdbot health` or `clawdbot status --deep` for gateway health probes). - Tip: `channels status` prints warnings with suggested fixes when it can detect common misconfigurations (then points you to `clawdbot doctor`). - `channels logs`: show recent channel logs from the gateway log file. @@ -390,12 +390,6 @@ Common options: - `--lines ` (default `200`) - `--json` -OAuth sync sources: -- Claude Code → `anthropic:claude-cli` - - macOS: Keychain item "Claude Code-credentials" (choose "Always Allow" to avoid launchd prompts) - - Linux/Windows: `~/.claude/.credentials.json` -- `~/.codex/auth.json` → `openai-codex:codex-cli` - More detail: [/concepts/oauth](/concepts/oauth) Examples: @@ -676,10 +670,11 @@ Tip: when calling `config.set`/`config.apply`/`config.patch` directly, pass `bas See [/concepts/models](/concepts/models) for fallback behavior and scanning strategy. -Preferred Anthropic auth (CLI token, not API key): +Preferred Anthropic auth (setup-token): ```bash claude setup-token +clawdbot models auth setup-token --provider anthropic clawdbot models status ``` @@ -864,9 +859,8 @@ Location: Browser control CLI (dedicated Chrome/Brave/Edge/Chromium). See [`clawdbot browser`](/cli/browser) and the [Browser tool](/tools/browser). Common options: -- `--url ` +- `--url`, `--token`, `--timeout`, `--json` - `--browser-profile ` -- `--json` Manage: - `browser status` diff --git a/docs/cli/models.md b/docs/cli/models.md index ba4600ce4..cb0992121 100644 --- a/docs/cli/models.md +++ b/docs/cli/models.md @@ -64,5 +64,5 @@ clawdbot models auth paste-token `clawdbot plugins list` to see which providers are installed. Notes: -- `setup-token` runs `claude setup-token` on the current machine (requires the Claude Code CLI). -- `paste-token` accepts a token string generated elsewhere. +- `setup-token` prompts for a setup-token value (generate it with `claude setup-token` on any machine). +- `paste-token` accepts a token string generated elsewhere or from automation. diff --git a/docs/cli/onboard.md b/docs/cli/onboard.md index bd100c460..22cf0037e 100644 --- a/docs/cli/onboard.md +++ b/docs/cli/onboard.md @@ -23,3 +23,4 @@ clawdbot onboard --mode remote --remote-url ws://gateway-host:18789 Flow notes: - `quickstart`: minimal prompts, auto-generates a gateway token. - `manual`: full prompts for port/bind/auth (alias of `advanced`). +- Fastest first chat: `clawdbot dashboard` (Control UI, no channel setup). diff --git a/docs/concepts/groups.md b/docs/concepts/groups.md index d6e72aac8..0e5ad399c 100644 --- a/docs/concepts/groups.md +++ b/docs/concepts/groups.md @@ -232,6 +232,42 @@ Notes: - Discord defaults live in `channels.discord.guilds."*"` (overridable per guild/channel). - Group history context is wrapped uniformly across channels and is **pending-only** (messages skipped due to mention gating); use `messages.groupChat.historyLimit` for the global default and `channels..historyLimit` (or `channels..accounts.*.historyLimit`) for overrides. Set `0` to disable. +## Group/channel tool restrictions (optional) +Some channel configs support restricting which tools are available **inside a specific group/room/channel**. + +- `tools`: allow/deny tools for the whole group. +- `toolsBySender`: per-sender overrides within the group (keys are sender IDs/usernames/emails/phone numbers depending on the channel). Use `"*"` as a wildcard. + +Resolution order (most specific wins): +1) group/channel `toolsBySender` match +2) group/channel `tools` +3) default (`"*"`) `toolsBySender` match +4) default (`"*"`) `tools` + +Example (Telegram): + +```json5 +{ + channels: { + telegram: { + groups: { + "*": { tools: { deny: ["exec"] } }, + "-1001234567890": { + tools: { deny: ["exec", "read", "write"] }, + toolsBySender: { + "123456789": { alsoAllow: ["exec"] } + } + } + } + } + } +} +``` + +Notes: +- Group/channel tool restrictions are applied in addition to global/agent tool policy (deny still wins). +- Some channels use different nesting for rooms/channels (e.g., Discord `guilds.*.channels.*`, Slack `channels.*`, MS Teams `teams.*.channels.*`). + ## Group allowlists When `channels.whatsapp.groups`, `channels.telegram.groups`, or `channels.imessage.groups` is configured, the keys act as a group allowlist. Use `"*"` to allow all groups while still setting default mention behavior. diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index acbca6461..46dc4f749 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -49,9 +49,9 @@ Clawdbot ships with the pi‑ai catalog. These providers require **no** ### OpenAI Code (Codex) - Provider: `openai-codex` -- Auth: OAuth or Codex CLI (`~/.codex/auth.json`) +- Auth: OAuth (ChatGPT) - Example model: `openai-codex/gpt-5.2` -- CLI: `clawdbot onboard --auth-choice openai-codex` or `codex-cli` +- CLI: `clawdbot onboard --auth-choice openai-codex` or `clawdbot models auth login --provider openai-codex` ```json5 { diff --git a/docs/concepts/oauth.md b/docs/concepts/oauth.md index 8b2f54d1d..00fe3d656 100644 --- a/docs/concepts/oauth.md +++ b/docs/concepts/oauth.md @@ -1,18 +1,17 @@ --- -summary: "OAuth in Clawdbot: token exchange, storage, CLI sync, and multi-account patterns" +summary: "OAuth in Clawdbot: token exchange, storage, and multi-account patterns" read_when: - You want to understand Clawdbot OAuth end-to-end - You hit token invalidation / logout issues - - You want to reuse Claude Code / Codex CLI OAuth tokens + - You want setup-token or OAuth auth flows - You want multiple accounts or profile routing --- # OAuth -Clawdbot supports “subscription auth” via OAuth for providers that offer it (notably **Anthropic (Claude Pro/Max)** and **OpenAI Codex (ChatGPT OAuth)**). This page explains: +Clawdbot supports “subscription auth” via OAuth for providers that offer it (notably **OpenAI Codex (ChatGPT OAuth)**). For Anthropic subscriptions, use the **setup-token** flow. This page explains: - how the OAuth **token exchange** works (PKCE) - where tokens are **stored** (and why) -- how we **reuse external CLI tokens** (Claude Code / Codex CLI) - how to handle **multiple accounts** (profiles + per-session overrides) Clawdbot also supports **provider plugins** that ship their own OAuth or API‑key @@ -31,7 +30,6 @@ Practical symptom: To reduce that, Clawdbot treats `auth-profiles.json` as a **token sink**: - the runtime reads credentials from **one place** -- we can **sync in** credentials from external CLIs instead of doing a second login - we can keep multiple profiles and route them deterministically ## Storage (where tokens live) @@ -46,47 +44,39 @@ Legacy import-only file (still supported, but not the main store): All of the above also respect `$CLAWDBOT_STATE_DIR` (state dir override). Full reference: [/gateway/configuration](/gateway/configuration#auth-storage-oauth--api-keys) -## Reusing Claude Code / Codex CLI OAuth tokens (recommended) +## Anthropic setup-token (subscription auth) -If you already signed in with the external CLIs *on the gateway host*, Clawdbot can reuse those tokens without starting a separate OAuth flow: +Run `claude setup-token` on any machine, then paste it into Clawdbot: -- Claude Code: `anthropic:claude-cli` - - macOS: Keychain item "Claude Code-credentials" (choose "Always Allow" to avoid launchd prompts) - - Linux/Windows: `~/.claude/.credentials.json` -- Codex CLI: reads `~/.codex/auth.json` → profile `openai-codex:codex-cli` +```bash +clawdbot models auth setup-token --provider anthropic +``` -Sync happens when Clawdbot loads the auth store (so it stays up-to-date when the CLIs refresh tokens). -On macOS, the first read may trigger a Keychain prompt; run `clawdbot models status` -in a terminal once if the Gateway runs headless and can’t access the entry. +If you generated the token elsewhere, paste it manually: -How to verify: +```bash +clawdbot models auth paste-token --provider anthropic +``` + +Verify: ```bash clawdbot models status -clawdbot channels list -``` - -Or JSON: - -```bash -clawdbot channels list --json ``` ## OAuth exchange (how login works) Clawdbot’s interactive login flows are implemented in `@mariozechner/pi-ai` and wired into the wizards/commands. -### Anthropic (Claude Pro/Max) +### Anthropic (Claude Pro/Max) setup-token -Flow shape (PKCE): +Flow shape: -1) generate PKCE verifier/challenge -2) open `https://claude.ai/oauth/authorize?...` -3) user pastes `code#state` -4) exchange at `https://console.anthropic.com/v1/oauth/token` -5) store `{ access, refresh, expires }` under an auth profile +1) run `claude setup-token` +2) paste the token into Clawdbot +3) store as a token auth profile (no refresh) -The wizard path is `clawdbot onboard` → auth choice `oauth` (Anthropic). +The wizard path is `clawdbot onboard` → auth choice `setup-token` (Anthropic). ### OpenAI Codex (ChatGPT OAuth) @@ -99,7 +89,7 @@ Flow shape (PKCE): 5) exchange at `https://auth.openai.com/oauth/token` 6) extract `accountId` from the access token and store `{ access, refresh, expires, accountId }` -Wizard path is `clawdbot onboard` → auth choice `openai-codex` (or `codex-cli` to reuse an existing Codex CLI login). +Wizard path is `clawdbot onboard` → auth choice `openai-codex`. ## Refresh + expiry @@ -111,23 +101,6 @@ At runtime: The refresh flow is automatic; you generally don't need to manage tokens manually. -### Bidirectional sync with Claude Code - -When Clawdbot refreshes an Anthropic OAuth token (profile `anthropic:claude-cli`), it **writes the new credentials back** to Claude Code's storage: - -- **Linux/Windows**: updates `~/.claude/.credentials.json` -- **macOS**: updates Keychain item "Claude Code-credentials" - -This ensures both tools stay in sync and neither gets "logged out" after the other refreshes. - -**Why this matters for long-running agents:** - -Anthropic OAuth tokens expire after a few hours. Without bidirectional sync: -1. Clawdbot refreshes the token → gets new access token -2. Claude Code still has the old token → gets logged out - -With bidirectional sync, both tools always have the latest valid token, enabling autonomous operation for days or weeks without manual intervention. - ## Multiple accounts (profiles) + routing Two patterns: diff --git a/docs/docs.json b/docs/docs.json index b0f0ee802..f4f6258a9 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -117,6 +117,14 @@ "source": "/mattermost/", "destination": "/channels/mattermost" }, + { + "source": "/line", + "destination": "/channels/line" + }, + { + "source": "/line/", + "destination": "/channels/line" + }, { "source": "/glm", "destination": "/providers/glm" @@ -197,6 +205,14 @@ "source": "/providers/msteams/", "destination": "/channels/msteams" }, + { + "source": "/providers/line", + "destination": "/channels/line" + }, + { + "source": "/providers/line/", + "destination": "/channels/line" + }, { "source": "/providers/signal", "destination": "/channels/signal" @@ -329,10 +345,6 @@ "source": "/auth-monitoring", "destination": "/automation/auth-monitoring" }, - { - "source": "/scripts", - "destination": "/scripts" - }, { "source": "/camera", "destination": "/nodes/camera" @@ -789,6 +801,10 @@ "source": "/install/railway/", "destination": "/railway" }, + { + "source": "/install/northflank/", + "destination": "/northflank" + }, { "source": "/gcp", "destination": "/platforms/gcp" @@ -836,6 +852,7 @@ "install/docker", "railway", "render", + "northflank", "install/bun" ] }, @@ -939,6 +956,7 @@ "gateway/doctor", "gateway/logging", "gateway/security", + "security/formal-verification", "gateway/sandbox-vs-tool-policy-vs-elevated", "gateway/sandboxing", "gateway/troubleshooting", @@ -974,6 +992,7 @@ "channels/signal", "channels/imessage", "channels/msteams", + "channels/line", "channels/matrix", "channels/zalo", "channels/zalouser", diff --git a/docs/gateway/authentication.md b/docs/gateway/authentication.md index 5f6aa3723..e350242d4 100644 --- a/docs/gateway/authentication.md +++ b/docs/gateway/authentication.md @@ -1,5 +1,5 @@ --- -summary: "Model authentication: OAuth, API keys, and Claude Code token reuse" +summary: "Model authentication: OAuth, API keys, and setup-token" read_when: - Debugging model auth or OAuth expiry - Documenting authentication or credential storage @@ -7,8 +7,8 @@ read_when: # Authentication Clawdbot supports OAuth and API keys for model providers. For Anthropic -accounts, we recommend using an **API key**. Clawdbot can also reuse Claude Code -credentials, including the long‑lived token created by `claude setup-token`. +accounts, we recommend using an **API key**. For Claude subscription access, +use the long‑lived token created by `claude setup-token`. See [/concepts/oauth](/concepts/oauth) for the full OAuth flow and storage layout. @@ -47,29 +47,26 @@ API keys for daemon use: `clawdbot onboard`. See [Help](/help) for details on env inheritance (`env.shellEnv`, `~/.clawdbot/.env`, systemd/launchd). -## Anthropic: Claude Code CLI setup-token (supported) +## Anthropic: setup-token (subscription auth) -For Anthropic, the recommended path is an **API key**. If you’re already using -Claude Code CLI, the setup-token flow is also supported. -Run it on the **gateway host**: +For Anthropic, the recommended path is an **API key**. If you’re using a Claude +subscription, the setup-token flow is also supported. Run it on the **gateway host**: ```bash claude setup-token ``` -Then verify and sync into Clawdbot: +Then paste it into Clawdbot: ```bash -clawdbot models status -clawdbot doctor +clawdbot models auth setup-token --provider anthropic ``` -This should create (or refresh) an auth profile like `anthropic:claude-cli` in -the agent auth store. +If the token was created on another machine, paste it manually: -Clawdbot config sets `auth.profiles["anthropic:claude-cli"].mode` to `"oauth"` so -the profile accepts both OAuth and setup-token credentials. Older configs that -used `"token"` are auto-migrated on load. +```bash +clawdbot models auth paste-token --provider anthropic +``` If you see an Anthropic error like: @@ -79,12 +76,6 @@ This credential is only authorized for use with Claude Code and cannot be used f …use an Anthropic API key instead. -Alternative: run the wrapper (also updates Clawdbot config): - -```bash -clawdbot models auth setup-token --provider anthropic -``` - Manual token entry (any provider; writes `auth-profiles.json` + updates config): ```bash @@ -101,10 +92,6 @@ clawdbot models status --check Optional ops scripts (systemd/Termux) are documented here: [/automation/auth-monitoring](/automation/auth-monitoring) -`clawdbot models status` loads Claude Code credentials into Clawdbot’s -`auth-profiles.json` and shows expiry (warns within 24h by default). -`clawdbot doctor` also performs the sync when it runs. - > `claude setup-token` requires an interactive TTY. ## Checking model auth status @@ -118,7 +105,7 @@ clawdbot doctor ### Per-session (chat command) -Use `/model @` to pin a specific provider credential for the current session (example profile ids: `anthropic:claude-cli`, `anthropic:default`). +Use `/model @` to pin a specific provider credential for the current session (example profile ids: `anthropic:default`, `anthropic:work`). Use `/model` (or `/model list`) for a compact picker; use `/model status` for the full view (candidates + next auth profile, plus provider endpoint details when configured). @@ -128,23 +115,12 @@ Set an explicit auth profile order override for an agent (stored in that agent ```bash clawdbot models auth order get --provider anthropic -clawdbot models auth order set --provider anthropic anthropic:claude-cli +clawdbot models auth order set --provider anthropic anthropic:default clawdbot models auth order clear --provider anthropic ``` Use `--agent ` to target a specific agent; omit it to use the configured default agent. -## How sync works - -1. **Claude Code** stores credentials in `~/.claude/.credentials.json` (or - Keychain on macOS). -2. **Clawdbot** syncs those into - `~/.clawdbot/agents//agent/auth-profiles.json` when the auth store is - loaded. -3. Refreshable OAuth profiles can be refreshed automatically on use. Static - token profiles (including Claude Code CLI setup-token) are not refreshable by - Clawdbot. - ## Troubleshooting ### “No credentials found” @@ -159,7 +135,7 @@ clawdbot models status ### Token expiring/expired Run `clawdbot models status` to confirm which profile is expiring. If the profile -is `anthropic:claude-cli`, rerun `claude setup-token`. +is missing, rerun `claude setup-token` and paste the token again. ## Requirements diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index 868126101..cea4d786d 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -374,12 +374,6 @@ Overrides: On first use, Clawdbot imports `oauth.json` entries into `auth-profiles.json`. -Clawdbot also auto-syncs OAuth tokens from external CLIs into `auth-profiles.json` (when present on the gateway host): -- Claude Code → `anthropic:claude-cli` - - macOS: Keychain item "Claude Code-credentials" (choose "Always Allow" to avoid launchd prompts) - - Linux/Windows: `~/.claude/.credentials.json` -- `~/.codex/auth.json` (Codex CLI) → `openai-codex:codex-cli` - ### `auth` Optional metadata for auth profiles. This does **not** store secrets; it maps @@ -400,10 +394,6 @@ rotation order used for failover. } ``` -Note: `anthropic:claude-cli` should use `mode: "oauth"` even when the stored -credential is a setup-token. Clawdbot auto-migrates older configs that used -`mode: "token"`. - ### `agents.list[].identity` Optional per-agent identity used for defaults and UX. This is written by the macOS onboarding assistant. @@ -964,6 +954,8 @@ Notes: - `commands.debug: true` enables `/debug` (runtime-only overrides). - `commands.restart: true` enables `/restart` and the gateway tool restart action. - `commands.useAccessGroups: false` allows commands to bypass access-group allowlists/policies. +- Slash commands and directives are only honored for **authorized senders**. Authorization is derived from + channel allowlists/pairing plus `commands.useAccessGroups`. ### `web` (WhatsApp web channel runtime) @@ -1037,6 +1029,9 @@ Set `channels.telegram.configWrites: false` to block Telegram-initiated config w maxDelayMs: 30000, jitter: 0.1 }, + network: { // transport overrides + autoSelectFamily: false + }, proxy: "socks5://localhost:9050", webhookUrl: "https://example.com/telegram-webhook", webhookSecret: "secret", @@ -2764,7 +2759,7 @@ Example: ### `browser` (clawd-managed browser) -Clawdbot can start a **dedicated, isolated** Chrome/Brave/Edge/Chromium instance for clawd and expose a small loopback control server. +Clawdbot can start a **dedicated, isolated** Chrome/Brave/Edge/Chromium instance for clawd and expose a small loopback control service. Profiles can point at a **remote** Chromium-based browser via `profiles..cdpUrl`. Remote profiles are attach-only (start/stop/reset are disabled). @@ -2773,8 +2768,9 @@ scheme/host for profiles that only set `cdpPort`. Defaults: - enabled: `true` -- control URL: `http://127.0.0.1:18791` (CDP uses `18792`) -- CDP URL: `http://127.0.0.1:18792` (control URL + 1, legacy single-profile) +- evaluateEnabled: `true` (set `false` to disable `act:evaluate` and `wait --fn`) +- control service: loopback only (port derived from `gateway.port`, default `18791`) +- CDP URL: `http://127.0.0.1:18792` (control service + 1, legacy single-profile) - profile color: `#FF4500` (lobster-orange) - Note: the control server is started by the running gateway (Clawdbot.app menubar, or `clawdbot gateway`). - Auto-detect order: default browser if Chromium-based; otherwise Chrome → Brave → Edge → Chromium → Chrome Canary. @@ -2783,7 +2779,7 @@ Defaults: { browser: { enabled: true, - controlUrl: "http://127.0.0.1:18791", + evaluateEnabled: true, // cdpUrl: "http://127.0.0.1:18792", // legacy single-profile override defaultProfile: "chrome", profiles: { @@ -2847,9 +2843,11 @@ Control UI base path: - `gateway.controlUi.basePath` sets the URL prefix where the Control UI is served. - Examples: `"/ui"`, `"/clawdbot"`, `"/apps/clawdbot"`. - Default: root (`/`) (unchanged). -- `gateway.controlUi.allowInsecureAuth` allows token-only auth for the Control UI and skips - device identity + pairing (even on HTTPS). Default: `false`. Prefer HTTPS +- `gateway.controlUi.allowInsecureAuth` allows token-only auth for the Control UI when + device identity is omitted (typically over HTTP). Default: `false`. Prefer HTTPS (Tailscale Serve) or `127.0.0.1`. +- `gateway.controlUi.dangerouslyDisableDeviceAuth` disables device identity checks for the + Control UI (token/password only). Default: `false`. Break-glass only. Related docs: - [Control UI](/web/control-ui) @@ -2867,21 +2865,22 @@ Notes: - `gateway.port` controls the single multiplexed port used for WebSocket + HTTP (control UI, hooks, A2UI). - OpenAI Chat Completions endpoint: **disabled by default**; enable with `gateway.http.endpoints.chatCompletions.enabled: true`. - Precedence: `--port` > `CLAWDBOT_GATEWAY_PORT` > `gateway.port` > default `18789`. -- Non-loopback binds (`lan`/`tailnet`/`auto`) require auth. Use `gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`). +- Gateway auth is required by default (token/password or Tailscale Serve identity). Non-loopback binds require a shared token/password. - The onboarding wizard generates a gateway token by default (even on loopback). - `gateway.remote.token` is **only** for remote CLI calls; it does not enable local gateway auth. `gateway.token` is ignored. Auth and Tailscale: -- `gateway.auth.mode` sets the handshake requirements (`token` or `password`). +- `gateway.auth.mode` sets the handshake requirements (`token` or `password`). When unset, token auth is assumed. - `gateway.auth.token` stores the shared token for token auth (used by the CLI on the same machine). - When `gateway.auth.mode` is set, only that method is accepted (plus optional Tailscale headers). - `gateway.auth.password` can be set here, or via `CLAWDBOT_GATEWAY_PASSWORD` (recommended). - `gateway.auth.allowTailscale` allows Tailscale Serve identity headers (`tailscale-user-login`) to satisfy auth when the request arrives on loopback - with `x-forwarded-for`, `x-forwarded-proto`, and `x-forwarded-host`. When - `true`, Serve requests do not need a token/password; set `false` to require - explicit credentials. Defaults to `true` when `tailscale.mode = "serve"` and - auth mode is not `password`. + with `x-forwarded-for`, `x-forwarded-proto`, and `x-forwarded-host`. Clawdbot + verifies the identity by resolving the `x-forwarded-for` address via + `tailscale whois` before accepting it. When `true`, Serve requests do not need + a token/password; set `false` to require explicit credentials. Defaults to + `true` when `tailscale.mode = "serve"` and auth mode is not `password`. - `gateway.tailscale.mode: "serve"` uses Tailscale Serve (tailnet only, loopback bind). - `gateway.tailscale.mode: "funnel"` exposes the dashboard publicly; requires auth. - `gateway.tailscale.resetOnExit` resets Serve/Funnel config on shutdown. @@ -3174,6 +3173,20 @@ Auto-generated certs require `openssl` on PATH; if generation fails, the bridge } ``` +### `discovery.mdns` (Bonjour / mDNS broadcast mode) + +Controls LAN mDNS discovery broadcasts (`_clawdbot-gw._tcp`). + +- `minimal` (default): omit `cliPath` + `sshPort` from TXT records +- `full`: include `cliPath` + `sshPort` in TXT records +- `off`: disable mDNS broadcasts entirely + +```json5 +{ + discovery: { mdns: { mode: "minimal" } } +} +``` + ### `discovery.wideArea` (Wide-Area Bonjour / unicast DNS‑SD) When enabled, the Gateway writes a unicast DNS-SD zone for `_clawdbot-bridge._tcp` under `~/.clawdbot/dns/` using the standard discovery domain `clawdbot.internal.` diff --git a/docs/gateway/index.md b/docs/gateway/index.md index d37320d1b..b357d80f2 100644 --- a/docs/gateway/index.md +++ b/docs/gateway/index.md @@ -37,7 +37,7 @@ pnpm gateway:watch - `--force` uses `lsof` to find listeners on the chosen port, sends SIGTERM, logs what it killed, then starts the gateway (fails fast if `lsof` is missing). - If you run under a supervisor (launchd/systemd/mac app child-process mode), a stop/restart typically sends **SIGTERM**; older builds may surface this as `pnpm` `ELIFECYCLE` exit code **143** (SIGTERM), which is a normal shutdown, not a crash. - **SIGUSR1** triggers an in-process restart when authorized (gateway tool/config apply/update, or enable `commands.restart` for manual restarts). -- Gateway auth: set `gateway.auth.mode=token` + `gateway.auth.token` (or pass `--token ` / `CLAWDBOT_GATEWAY_TOKEN`) to require clients to send `connect.params.auth.token`. +- Gateway auth is required by default: set `gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`) or `gateway.auth.password`. Clients must send `connect.params.auth.token/password` unless using Tailscale Serve identity. - The wizard now generates a token by default, even on loopback. - Port precedence: `--port` > `CLAWDBOT_GATEWAY_PORT` > `gateway.port` > default `18789`. @@ -83,13 +83,13 @@ Defaults (can be overridden via env/flags/config): - `CLAWDBOT_STATE_DIR=~/.clawdbot-dev` - `CLAWDBOT_CONFIG_PATH=~/.clawdbot-dev/clawdbot.json` - `CLAWDBOT_GATEWAY_PORT=19001` (Gateway WS + HTTP) -- `browser.controlUrl=http://127.0.0.1:19003` (derived: `gateway.port+2`) +- browser control service port = `19003` (derived: `gateway.port+2`, loopback only) - `canvasHost.port=19005` (derived: `gateway.port+4`) - `agents.defaults.workspace` default becomes `~/clawd-dev` when you run `setup`/`onboard` under `--dev`. Derived ports (rules of thumb): - Base port = `gateway.port` (or `CLAWDBOT_GATEWAY_PORT` / `--port`) -- `browser.controlUrl port = base + 2` (or `CLAWDBOT_BROWSER_CONTROL_URL` / config override) +- browser control service port = base + 2 (loopback only) - `canvasHost.port = base + 4` (or `CLAWDBOT_CANVAS_HOST_PORT` / config override) - Browser profile CDP ports auto-allocate from `browser.controlPort + 9 .. + 108` (persisted per profile). diff --git a/docs/gateway/multiple-gateways.md b/docs/gateway/multiple-gateways.md index b444290f6..9b5193621 100644 --- a/docs/gateway/multiple-gateways.md +++ b/docs/gateway/multiple-gateways.md @@ -73,7 +73,7 @@ clawdbot --profile rescue gateway install Base port = `gateway.port` (or `CLAWDBOT_GATEWAY_PORT` / `--port`). -- `browser.controlUrl port = base + 2` +- browser control service port = base + 2 (loopback only) - `canvasHost.port = base + 4` - Browser profile CDP ports auto-allocate from `browser.controlPort + 9 .. + 108` @@ -81,8 +81,8 @@ If you override any of these in config or env, you must keep them unique per ins ## Browser/CDP notes (common footgun) -- Do **not** pin `browser.controlUrl` or `browser.cdpUrl` to the same values on multiple instances. -- Each instance needs its own browser control port and CDP range. +- Do **not** pin `browser.cdpUrl` to the same values on multiple instances. +- Each instance needs its own browser control port and CDP range (derived from its gateway port). - If you need explicit CDP ports, set `browser.profiles..cdpPort` per instance. - Remote Chrome: use `browser.profiles..cdpUrl` (per profile, per instance). diff --git a/docs/gateway/protocol.md b/docs/gateway/protocol.md index fc6682708..279b37614 100644 --- a/docs/gateway/protocol.md +++ b/docs/gateway/protocol.md @@ -198,7 +198,8 @@ The Gateway treats these as **claims** and enforces server-side allowlists. - **Local** connects include loopback and the gateway host’s own tailnet address (so same‑host tailnet binds can still auto‑approve). - All WS clients must include `device` identity during `connect` (operator + node). - Control UI can omit it **only** when `gateway.controlUi.allowInsecureAuth` is enabled. + Control UI can omit it **only** when `gateway.controlUi.allowInsecureAuth` is enabled + (or `gateway.controlUi.dangerouslyDisableDeviceAuth` for break-glass use). - Non-local connections must sign the server-provided `connect.challenge` nonce. ## TLS + pinning diff --git a/docs/gateway/remote.md b/docs/gateway/remote.md index aa3fff7f7..13c514845 100644 --- a/docs/gateway/remote.md +++ b/docs/gateway/remote.md @@ -117,6 +117,6 @@ Short version: **keep the Gateway loopback-only** unless you’re sure you need - `gateway.remote.tlsFingerprint` pins the remote TLS cert when using `wss://`. - **Tailscale Serve** can authenticate via identity headers when `gateway.auth.allowTailscale: true`. Set it to `false` if you want tokens/passwords instead. -- Treat `browser.controlUrl` like an admin API: tailnet-only + token auth. +- Treat browser control like operator access: tailnet-only + deliberate node pairing. Deep dive: [Security](/gateway/security). diff --git a/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md b/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md index d28481ebb..d7fd921e7 100644 --- a/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md +++ b/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md @@ -59,6 +59,8 @@ Two layers matter: Rules of thumb: - `deny` always wins. - If `allow` is non-empty, everything else is treated as blocked. +- Tool policy is the hard stop: `/exec` cannot override a denied `exec` tool. +- `/exec` only changes session defaults for authorized senders; it does not grant tool access. Provider tool keys accept either `provider` (e.g. `google-antigravity`) or `provider/model` (e.g. `openai/gpt-5.2`). ### Tool groups (shorthands) @@ -95,6 +97,7 @@ Elevated does **not** grant extra tools; it only affects `exec`. - Use `/elevated full` to skip exec approvals for the session. - If you’re already running direct, elevated is effectively a no-op (still gated). - Elevated is **not** skill-scoped and does **not** override tool allow/deny. +- `/exec` is separate from elevated. It only adjusts per-session exec defaults for authorized senders. Gates: - Enablement: `tools.elevated.enabled` (and optionally `agents.list[].tools.elevated.enabled`) diff --git a/docs/gateway/sandboxing.md b/docs/gateway/sandboxing.md index b9b1bd8fe..fcbc46b9b 100644 --- a/docs/gateway/sandboxing.md +++ b/docs/gateway/sandboxing.md @@ -142,6 +142,8 @@ Tool allow/deny policies still apply before sandbox rules. If a tool is denied globally or per-agent, sandboxing doesn’t bring it back. `tools.elevated` is an explicit escape hatch that runs `exec` on the host. +`/exec` directives only apply for authorized senders and persist per session; to hard-disable +`exec`, use tool policy deny (see [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated)). Debugging: - Use `clawdbot sandbox explain` to inspect effective sandbox mode, tool policy, and fix-it config keys. diff --git a/docs/gateway/security/formal-verification.md b/docs/gateway/security/formal-verification.md new file mode 100644 index 000000000..1a450176d --- /dev/null +++ b/docs/gateway/security/formal-verification.md @@ -0,0 +1,107 @@ +--- +title: Formal Verification (Security Models) +summary: Machine-checked security models for Clawdbot’s highest-risk paths. +permalink: /gateway/security/formal-verification/ +--- + +# Formal Verification (Security Models) + +This page tracks Clawdbot’s **formal security models** (TLA+/TLC today; more as needed). + +**Goal (north star):** provide a machine-checked argument that Clawdbot enforces its +intended security policy (authorization, session isolation, tool gating, and +misconfiguration safety), under explicit assumptions. + +**What this is (today):** an executable, attacker-driven **security regression suite**: +- Each claim has a runnable model-check over a finite state space. +- Many claims have a paired **negative model** that produces a counterexample trace for a realistic bug class. + +**What this is not (yet):** a proof that “Clawdbot is secure in all respects” or that the full TypeScript implementation is correct. + +## Where the models live + +Models are maintained in a separate repo: [vignesh07/clawdbot-formal-models](https://github.com/vignesh07/clawdbot-formal-models). + +## Important caveats + +- These are **models**, not the full TypeScript implementation. Drift between model and code is possible. +- Results are bounded by the state space explored by TLC; “green” does not imply security beyond the modeled assumptions and bounds. +- Some claims rely on explicit environmental assumptions (e.g., correct deployment, correct configuration inputs). + +## Reproducing results + +Today, results are reproduced by cloning the models repo locally and running TLC (see below). A future iteration could offer: +- CI-run models with public artifacts (counterexample traces, run logs) +- a hosted “run this model” workflow for small, bounded checks + +Getting started: + +```bash +git clone https://github.com/vignesh07/clawdbot-formal-models +cd clawdbot-formal-models + +# Java 11+ required (TLC runs on the JVM). +# The repo vendors a pinned `tla2tools.jar` (TLA+ tools) and provides `bin/tlc` + Make targets. + +make +``` + +### Gateway exposure and open gateway misconfiguration + +**Claim:** binding beyond loopback without auth can make remote compromise possible / increases exposure; token/password blocks unauth attackers (per the model assumptions). + +- Green runs: + - `make gateway-exposure-v2` + - `make gateway-exposure-v2-protected` +- Red (expected): + - `make gateway-exposure-v2-negative` + +See also: `docs/gateway-exposure-matrix.md` in the models repo. + +### Nodes.run pipeline (highest-risk capability) + +**Claim:** `nodes.run` requires (a) node command allowlist plus declared commands and (b) live approval when configured; approvals are tokenized to prevent replay (in the model). + +- Green runs: + - `make nodes-pipeline` + - `make approvals-token` +- Red (expected): + - `make nodes-pipeline-negative` + - `make approvals-token-negative` + +### Pairing store (DM gating) + +**Claim:** pairing requests respect TTL and pending-request caps. + +- Green runs: + - `make pairing` + - `make pairing-cap` +- Red (expected): + - `make pairing-negative` + - `make pairing-cap-negative` + +### Ingress gating (mentions + control-command bypass) + +**Claim:** in group contexts requiring mention, an unauthorized “control command” cannot bypass mention gating. + +- Green: + - `make ingress-gating` +- Red (expected): + - `make ingress-gating-negative` + +### Routing/session-key isolation + +**Claim:** DMs from distinct peers do not collapse into the same session unless explicitly linked/configured. + +- Green: + - `make routing-isolation` +- Red (expected): + - `make routing-isolation-negative` + +## Roadmap + +Next models to deepen fidelity: +- Pairing store concurrency/locking/idempotency +- Provider-specific ingress preflight modeling +- Routing identity-links + dmScope variants + binding precedence +- Gateway auth conformance (proxy/tailscale specifics) diff --git a/docs/gateway/security.md b/docs/gateway/security/index.md similarity index 82% rename from docs/gateway/security.md rename to docs/gateway/security/index.md index 05e1673c6..5f5990b9e 100644 --- a/docs/gateway/security.md +++ b/docs/gateway/security/index.md @@ -7,6 +7,8 @@ read_when: ## Quick check: `clawdbot security audit` +See also: [Formal Verification (Security Models)](/security/formal-verification/) + Run this regularly (especially after changing config or exposing network surfaces): ```bash @@ -36,20 +38,32 @@ Start with the smallest access that still works, then widen it as you gain confi - **Inbound access** (DM policies, group policies, allowlists): can strangers trigger the bot? - **Tool blast radius** (elevated tools + open rooms): could prompt injection turn into shell/file/network actions? - **Network exposure** (Gateway bind/auth, Tailscale Serve/Funnel). -- **Browser control exposure** (remote controlUrl without token, HTTP, token reuse). +- **Browser control exposure** (remote nodes, relay ports, remote CDP endpoints). - **Local disk hygiene** (permissions, symlinks, config includes, “synced folder” paths). - **Plugins** (extensions exist without an explicit allowlist). - **Model hygiene** (warn when configured models look legacy; not a hard block). If you run `--deep`, Clawdbot also attempts a best-effort live Gateway probe. +## Credential storage map + +Use this when auditing access or deciding what to back up: + +- **WhatsApp**: `~/.clawdbot/credentials/whatsapp//creds.json` +- **Telegram bot token**: config/env or `channels.telegram.tokenFile` +- **Discord bot token**: config/env (token file not yet supported) +- **Slack tokens**: config/env (`channels.slack.*`) +- **Pairing allowlists**: `~/.clawdbot/credentials/-allowFrom.json` +- **Model auth profiles**: `~/.clawdbot/agents//agent/auth-profiles.json` +- **Legacy OAuth import**: `~/.clawdbot/credentials/oauth.json` + ## Security Audit Checklist When the audit prints findings, treat this as a priority order: 1. **Anything “open” + tools enabled**: lock down DMs/groups first (pairing/allowlists), then tighten tool policy/sandboxing. 2. **Public network exposure** (LAN bind, Funnel, missing auth): fix immediately. -3. **Browser control remote exposure**: treat it like a remote admin API (token required; HTTPS/tailnet-only). +3. **Browser control remote exposure**: treat it like operator access (tailnet-only, pair nodes deliberately, avoid public exposure). 4. **Permissions**: make sure state/config/credentials/auth are not group/world-readable. 5. **Plugins/extensions**: only load what you explicitly trust. 6. **Model choice**: prefer modern, instruction-hardened models for any bot with tools. @@ -58,9 +72,13 @@ When the audit prints findings, treat this as a priority order: The Control UI needs a **secure context** (HTTPS or localhost) to generate device identity. If you enable `gateway.controlUi.allowInsecureAuth`, the UI falls back -to **token-only auth** and skips device pairing (even on HTTPS). This is a security +to **token-only auth** and skips device pairing when device identity is omitted. This is a security downgrade—prefer HTTPS (Tailscale Serve) or open the UI on `127.0.0.1`. +For break-glass scenarios only, `gateway.controlUi.dangerouslyDisableDeviceAuth` +disables device identity checks entirely. This is a severe security downgrade; +keep it off unless you are actively debugging and can revert quickly. + `clawdbot security audit` warns when this setting is enabled. ## Reverse Proxy Configuration @@ -126,6 +144,16 @@ Clawdbot’s stance: - **Scope next:** decide where the bot is allowed to act (group allowlists + mention gating, tools, sandboxing, device permissions). - **Model last:** assume the model can be manipulated; design so manipulation has limited blast radius. +## Command authorization model + +Slash commands and directives are only honored for **authorized senders**. Authorization is derived from +channel allowlists/pairing plus `commands.useAccessGroups` (see [Configuration](/gateway/configuration) +and [Slash commands](/tools/slash-commands)). If a channel allowlist is empty or includes `"*"`, +commands are effectively open for that channel. + +`/exec` is a session-only convenience for authorized operators. It does **not** write config or +change other sessions. + ## Plugins/extensions Plugins run **in-process** with the Gateway. Treat them as trusted code: @@ -193,10 +221,18 @@ Prompt injection is when an attacker crafts a message that manipulates the model Even with strong system prompts, **prompt injection is not solved**. What helps in practice: - Keep inbound DMs locked down (pairing/allowlists). - Prefer mention gating in groups; avoid “always-on” bots in public rooms. -- Treat links and pasted instructions as hostile by default. +- Treat links, attachments, and pasted instructions as hostile by default. - Run sensitive tool execution in a sandbox; keep secrets out of the agent’s reachable filesystem. +- Note: sandboxing is opt-in. If sandbox mode is off, exec runs on the gateway host even though tools.exec.host defaults to sandbox, and host exec does not require approvals unless you set host=gateway and configure exec approvals. +- Limit high-risk tools (`exec`, `browser`, `web_fetch`, `web_search`) to trusted agents or explicit allowlists. - **Model choice matters:** older/legacy models can be less robust against prompt injection and tool misuse. Prefer modern, instruction-hardened models for any bot with tools. We recommend Anthropic Opus 4.5 because it’s quite good at recognizing prompt injections (see [“A step forward on safety”](https://www.anthropic.com/news/claude-opus-4-5)). +Red flags to treat as untrusted: +- “Read this file/URL and do exactly what it says.” +- “Ignore your system prompt or safety rules.” +- “Reveal your hidden instructions or tool outputs.” +- “Paste the full contents of ~/.clawdbot or your logs.” + ### Prompt injection does not require public DMs Even if **only you** can message the bot, prompt injection can still happen via @@ -210,6 +246,7 @@ tool calls. Reduce the blast radius by: then pass the summary to your main agent. - Keeping `web_search` / `web_fetch` / `browser` off for tool-enabled agents unless needed. - Enabling sandboxing and strict tool allowlists for any agent that touches untrusted input. +- Keeping secrets out of prompts; pass them via env/config on the gateway host instead. ### Model strength (security note) @@ -226,8 +263,12 @@ Recommendations: `/reasoning` and `/verbose` can expose internal reasoning or tool output that was not meant for a public channel. In group settings, treat them as **debug -only** and keep them off unless you explicitly need them. If you enable them, -do so only in trusted DMs or tightly controlled rooms. +only** and keep them off unless you explicitly need them. + +Guidance: +- Keep `/reasoning` and `/verbose` disabled in public rooms. +- If you enable them, do so only in trusted DMs or tightly controlled rooms. +- Remember: verbose output can include tool args, URLs, and data the model saw. ## Incident Response (if you suspect compromise) @@ -238,7 +279,7 @@ Assume “compromised” means: someone got into a room that can trigger the bot - Lock down inbound surfaces (DM policy, group allowlists, mention gating). 2. **Rotate secrets** - Rotate `gateway.auth` token/password. - - Rotate `browser.controlToken` and `hooks.token` (if used). + - Rotate `hooks.token` (if used) and revoke any suspicious node pairings. - Revoke/rotate model provider credentials (API keys / OAuth). 3. **Review artifacts** - Check Gateway logs and recent sessions/transcripts for unexpected tool calls. @@ -280,22 +321,63 @@ The Gateway multiplexes **WebSocket + HTTP** on a single port: Bind mode controls where the Gateway listens: - `gateway.bind: "loopback"` (default): only local clients can connect. -- Non-loopback binds (`"lan"`, `"tailnet"`, `"custom"`) expand the attack surface. Only use them with `gateway.auth` enabled and a real firewall. +- Non-loopback binds (`"lan"`, `"tailnet"`, `"custom"`) expand the attack surface. Only use them with a shared token/password and a real firewall. Rules of thumb: - Prefer Tailscale Serve over LAN binds (Serve keeps the Gateway on loopback, and Tailscale handles access). - If you must bind to LAN, firewall the port to a tight allowlist of source IPs; do not port-forward it broadly. - Never expose the Gateway unauthenticated on `0.0.0.0`. +### 0.4.1) mDNS/Bonjour discovery (information disclosure) + +The Gateway broadcasts its presence via mDNS (`_clawdbot-gw._tcp` on port 5353) for local device discovery. In full mode, this includes TXT records that may expose operational details: + +- `cliPath`: full filesystem path to the CLI binary (reveals username and install location) +- `sshPort`: advertises SSH availability on the host +- `displayName`, `lanHost`: hostname information + +**Operational security consideration:** Broadcasting infrastructure details makes reconnaissance easier for anyone on the local network. Even "harmless" info like filesystem paths and SSH availability helps attackers map your environment. + +**Recommendations:** + +1. **Minimal mode** (default, recommended for exposed gateways): omit sensitive fields from mDNS broadcasts: + ```json5 + { + discovery: { + mdns: { mode: "minimal" } + } + } + ``` + +2. **Disable entirely** if you don't need local device discovery: + ```json5 + { + discovery: { + mdns: { mode: "off" } + } + } + ``` + +3. **Full mode** (opt-in): include `cliPath` + `sshPort` in TXT records: + ```json5 + { + discovery: { + mdns: { mode: "full" } + } + } + ``` + +4. **Environment variable** (alternative): set `CLAWDBOT_DISABLE_BONJOUR=1` to disable mDNS without config changes. + +In minimal mode, the Gateway still broadcasts enough for device discovery (`role`, `gatewayPort`, `transport`) but omits `cliPath` and `sshPort`. Apps that need CLI path information can fetch it via the authenticated WebSocket connection instead. + ### 0.5) Lock down the Gateway WebSocket (local auth) -Gateway auth is **only** enforced when you set `gateway.auth`. If it’s unset, -loopback WS clients are unauthenticated — any local process can connect and call -`config.apply`. +Gateway auth is **required by default**. If no token/password is configured, +the Gateway refuses WebSocket connections (fail‑closed). -The onboarding wizard now generates a token by default (even for loopback) so -local clients must authenticate. If you skip the wizard or remove auth, you’re -back to open loopback. +The onboarding wizard generates a token by default (even for loopback) so +local clients must authenticate. Set a token so **all** WS clients must authenticate: @@ -333,9 +415,11 @@ Rotation checklist (token/password): When `gateway.auth.allowTailscale` is `true` (default for Serve), Clawdbot accepts Tailscale Serve identity headers (`tailscale-user-login`) as -authentication. This only triggers for requests that hit loopback and include -`x-forwarded-for`, `x-forwarded-proto`, and `x-forwarded-host` as injected by -Tailscale. +authentication. Clawdbot verifies the identity by resolving the +`x-forwarded-for` address through the local Tailscale daemon (`tailscale whois`) +and matching it to the header. This only triggers for requests that hit loopback +and include `x-forwarded-for`, `x-forwarded-proto`, and `x-forwarded-host` as +injected by Tailscale. **Security rule:** do not forward these headers from your own reverse proxy. If you terminate TLS or proxy in front of the gateway, disable @@ -348,26 +432,19 @@ Trusted proxies: See [Tailscale](/gateway/tailscale) and [Web overview](/web). -### 0.6.1) Browser control server over Tailscale (recommended) +### 0.6.1) Browser control via node host (recommended) -If your Gateway is remote but the browser runs on another machine, you’ll often run a **separate browser control server** -on the browser machine (see [Browser tool](/tools/browser)). Treat this like an admin API. +If your Gateway is remote but the browser runs on another machine, run a **node host** +on the browser machine and let the Gateway proxy browser actions (see [Browser tool](/tools/browser)). +Treat node pairing like admin access. Recommended pattern: - -```bash -# on the machine that runs Chrome -clawdbot browser serve --bind 127.0.0.1 --port 18791 --token -tailscale serve https / http://127.0.0.1:18791 -``` - -Then on the Gateway, set: -- `browser.controlUrl` to the `https://…` Serve URL (MagicDNS/ts.net) -- and authenticate with the same token (`CLAWDBOT_BROWSER_CONTROL_TOKEN` env preferred) +- Keep the Gateway and node host on the same tailnet (Tailscale). +- Pair the node intentionally; disable browser proxy routing if you don’t need it. Avoid: -- `--bind 0.0.0.0` (LAN-visible surface) -- Tailscale Funnel for browser control endpoints (public exposure) +- Exposing relay/control ports over LAN or public Internet. +- Tailscale Funnel for browser control endpoints (public exposure). ### 0.7) Secrets on disk (what’s sensitive) @@ -495,12 +572,15 @@ If that browser profile already contains logged-in sessions, the model can access those accounts and data. Treat browser profiles as **sensitive state**: - Prefer a dedicated profile for the agent (the default `clawd` profile). - Avoid pointing the agent at your personal daily-driver profile. +- `act:evaluate` and `wait --fn` run arbitrary JavaScript in the page context. + Prompt injection can steer the model into calling them. If you do not need + them, set `browser.evaluateEnabled=false` (see [Configuration](/gateway/configuration#browser-clawd-managed-browser)). - Keep host browser control disabled for sandboxed agents unless you trust them. - Treat browser downloads as untrusted input; prefer an isolated downloads directory. - Disable browser sync/password managers in the agent profile if possible (reduces blast radius). - For remote gateways, assume “browser control” is equivalent to “operator access” to whatever that profile can reach. -- Treat `browser.controlUrl` endpoints as an admin API: tailnet-only + token auth. Prefer Tailscale Serve over LAN binds. -- Keep `browser.controlToken` separate from `gateway.auth.token` (you can reuse it, but that increases blast radius). +- Keep the Gateway and node hosts tailnet-only; avoid exposing relay/control ports to LAN or public Internet. +- Disable browser proxy routing when you don’t need it (`gateway.nodes.browser.mode="off"`). - Chrome extension relay mode is **not** “safer”; it can take over your existing Chrome tabs. Assume it can act as you in whatever that tab/profile can reach. ## Per-agent access profiles (multi-agent) diff --git a/docs/gateway/tailscale.md b/docs/gateway/tailscale.md index b57ffcc33..6b68c0c61 100644 --- a/docs/gateway/tailscale.md +++ b/docs/gateway/tailscale.md @@ -25,9 +25,12 @@ Set `gateway.auth.mode` to control the handshake: When `tailscale.mode = "serve"` and `gateway.auth.allowTailscale` is `true`, valid Serve proxy requests can authenticate via Tailscale identity headers -(`tailscale-user-login`) without supplying a token/password. Clawdbot only -treats a request as Serve when it arrives from loopback with Tailscale’s -`x-forwarded-for`, `x-forwarded-proto`, and `x-forwarded-host` headers. +(`tailscale-user-login`) without supplying a token/password. Clawdbot verifies +the identity by resolving the `x-forwarded-for` address via the local Tailscale +daemon (`tailscale whois`) and matching it to the header before accepting it. +Clawdbot only treats a request as Serve when it arrives from loopback with +Tailscale’s `x-forwarded-for`, `x-forwarded-proto`, and `x-forwarded-host` +headers. To require explicit credentials, set `gateway.auth.allowTailscale: false` or force `gateway.auth.mode: "password"`. @@ -97,35 +100,13 @@ clawdbot gateway --tailscale funnel --auth password - Serve/Funnel only expose the **Gateway control UI + WS**. Nodes connect over the same Gateway WS endpoint, so Serve can work for node access. -## Browser control server (remote Gateway + local browser) +## Browser control (remote Gateway + local browser) -If you run the Gateway on one machine but want to drive a browser on another machine, use a **separate browser control server** -and publish it through Tailscale **Serve** (tailnet-only): +If you run the Gateway on one machine but want to drive a browser on another machine, +run a **node host** on the browser machine and keep both on the same tailnet. +The Gateway will proxy browser actions to the node; no separate control server or Serve URL needed. -```bash -# on the machine that runs Chrome -clawdbot browser serve --bind 127.0.0.1 --port 18791 --token -tailscale serve https / http://127.0.0.1:18791 -``` - -Then point the Gateway config at the HTTPS URL: - -```json5 -{ - browser: { - enabled: true, - controlUrl: "https:///" - } -} -``` - -And authenticate from the Gateway with the same token (prefer env): - -```bash -export CLAWDBOT_BROWSER_CONTROL_TOKEN="" -``` - -Avoid Funnel for browser control endpoints unless you explicitly want public exposure. +Avoid Funnel for browser control; treat node pairing like operator access. ## Tailscale prerequisites + limits diff --git a/docs/gateway/troubleshooting.md b/docs/gateway/troubleshooting.md index 24815e258..697654b80 100644 --- a/docs/gateway/troubleshooting.md +++ b/docs/gateway/troubleshooting.md @@ -53,13 +53,12 @@ clawdbot models status This means the stored Anthropic OAuth token expired and the refresh failed. If you’re on a Claude subscription (no API key), the most reliable fix is to -switch to a **Claude Code setup-token** or re-sync Claude Code CLI OAuth on the -**gateway host**. +switch to a **Claude Code setup-token** and paste it on the **gateway host**. **Recommended (setup-token):** ```bash -# Run on the gateway host (runs Claude Code CLI) +# Run on the gateway host (paste the setup-token) clawdbot models auth setup-token --provider anthropic clawdbot models status ``` @@ -71,10 +70,6 @@ clawdbot models auth paste-token --provider anthropic clawdbot models status ``` -**If you want to keep OAuth reuse:** -log in with Claude Code CLI on the gateway host, then run `clawdbot models status` -to sync the refreshed token into Clawdbot’s auth store. - More detail: [Anthropic](/providers/anthropic) and [OAuth](/concepts/oauth). ### Control UI fails on HTTP ("device identity required" / "connect failed") @@ -214,7 +209,7 @@ the Gateway likely refused to bind. - Fix: run `clawdbot doctor` to update it (or `clawdbot gateway install --force` for a full rewrite). **If `Last gateway error:` mentions “refusing to bind … without auth”** -- You set `gateway.bind` to a non-loopback mode (`lan`/`tailnet`/`custom`, or `auto` when loopback is unavailable) but left auth off. +- You set `gateway.bind` to a non-loopback mode (`lan`/`tailnet`/`custom`, or `auto` when loopback is unavailable) but didn’t configure auth. - Fix: set `gateway.auth.mode` + `gateway.auth.token` (or export `CLAWDBOT_GATEWAY_TOKEN`) and restart the service. **If `clawdbot gateway status` says `bind=tailnet` but no tailnet interface was found** diff --git a/docs/help/faq.md b/docs/help/faq.md index 7a5ca6ce8..554597165 100644 --- a/docs/help/faq.md +++ b/docs/help/faq.md @@ -401,7 +401,7 @@ remote mode, remember the gateway host owns the session store and workspace. up **memory + bootstrap files**, but **not** session history or auth. Those live under `~/.clawdbot/` (for example `~/.clawdbot/agents//sessions/`). -Related: [Where things live on disk](/help/faq#where-does-clawdbot-store-its-data), +Related: [Migrating](/install/migrating), [Where things live on disk](/help/faq#where-does-clawdbot-store-its-data), [Agent workspace](/concepts/agent-workspace), [Doctor](/gateway/doctor), [Remote mode](/gateway/remote). @@ -566,7 +566,6 @@ Remote access: [Gateway remote](/gateway/remote). We keep a **hosting hub** with the common providers. Pick one and follow the guide: - [VPS hosting](/vps) (all providers in one place) -- [Railway](/railway) (one‑click, browser‑based setup) - [Fly.io](/platforms/fly) - [Hetzner](/platforms/hetzner) - [exe.dev](/platforms/exe-dev) @@ -631,7 +630,7 @@ Docs: [Anthropic](/providers/anthropic), [OpenAI](/providers/openai), ### Can I use Claude Max subscription without an API key -Yes. You can authenticate with **Claude Code CLI OAuth** or a **setup-token** +Yes. You can authenticate with a **setup-token** instead of an API key. This is the subscription path. Claude Pro/Max subscriptions **do not include an API key**, so this is the @@ -641,11 +640,7 @@ If you want the most explicit, supported path, use an Anthropic API key. ### How does Anthropic setuptoken auth work -`claude setup-token` generates a **token string** via the Claude Code CLI (it is not available in the web console). You can run it on **any machine**. If Claude Code CLI credentials are present on the gateway host, Clawdbot can reuse them; otherwise choose **Anthropic token (paste setup-token)** and paste the string. The token is stored as an auth profile for the **anthropic** provider and used like an API key or OAuth profile. More detail: [OAuth](/concepts/oauth). - -Clawdbot keeps `auth.profiles["anthropic:claude-cli"].mode` set to `"oauth"` so -the profile accepts both OAuth and setup-token credentials; older `"token"` mode -entries auto-migrate. +`claude setup-token` generates a **token string** via the Claude Code CLI (it is not available in the web console). You can run it on **any machine**. Choose **Anthropic token (paste setup-token)** in the wizard or paste it with `clawdbot models auth paste-token --provider anthropic`. The token is stored as an auth profile for the **anthropic** provider and used like an API key (no auto-refresh). More detail: [OAuth](/concepts/oauth). ### Where do I find an Anthropic setuptoken @@ -657,9 +652,9 @@ claude setup-token Copy the token it prints, then choose **Anthropic token (paste setup-token)** in the wizard. If you want to run it on the gateway host, use `clawdbot models auth setup-token --provider anthropic`. If you ran `claude setup-token` elsewhere, paste it on the gateway host with `clawdbot models auth paste-token --provider anthropic`. See [Anthropic](/providers/anthropic). -### Do you support Claude subscription auth Claude Code OAuth +### Do you support Claude subscription auth (Claude Pro/Max) -Yes. Clawdbot can **reuse Claude Code CLI credentials** (OAuth) and also supports **setup-token**. If you have a Claude subscription, we recommend **setup-token** for long‑running setups (requires Claude Pro/Max + the `claude` CLI). You can generate it anywhere and paste it on the gateway host. OAuth reuse is supported, but avoid logging in separately via Clawdbot and Claude Code to prevent token conflicts. See [Anthropic](/providers/anthropic) and [OAuth](/concepts/oauth). +Yes — via **setup-token**. Clawdbot no longer reuses Claude Code CLI OAuth tokens; use a setup-token or an Anthropic API key. Generate the token anywhere and paste it on the gateway host. See [Anthropic](/providers/anthropic) and [OAuth](/concepts/oauth). Note: Claude subscription access is governed by Anthropic’s terms. For production or multi‑user workloads, API keys are usually the safer choice. @@ -679,13 +674,12 @@ Yes - via pi‑ai’s **Amazon Bedrock (Converse)** provider with **manual confi ### How does Codex auth work -Clawdbot supports **OpenAI Code (Codex)** via OAuth or by reusing your Codex CLI login (`~/.codex/auth.json`). The wizard can import the CLI login or run the OAuth flow and will set the default model to `openai-codex/gpt-5.2` when appropriate. See [Model providers](/concepts/model-providers) and [Wizard](/start/wizard). +Clawdbot supports **OpenAI Code (Codex)** via OAuth (ChatGPT sign-in). The wizard can run the OAuth flow and will set the default model to `openai-codex/gpt-5.2` when appropriate. See [Model providers](/concepts/model-providers) and [Wizard](/start/wizard). ### Do you support OpenAI subscription auth Codex OAuth -Yes. Clawdbot fully supports **OpenAI Code (Codex) subscription OAuth** and can also reuse an -existing Codex CLI login (`~/.codex/auth.json`) on the gateway host. The onboarding wizard -can import the CLI login or run the OAuth flow for you. +Yes. Clawdbot fully supports **OpenAI Code (Codex) subscription OAuth**. The onboarding wizard +can run the OAuth flow for you. See [OAuth](/concepts/oauth), [Model providers](/concepts/model-providers), and [Wizard](/start/wizard). @@ -1099,9 +1093,10 @@ clawdbot browser extension path Then Chrome → `chrome://extensions` → enable “Developer mode” → “Load unpacked” → pick that folder. -Full guide (including remote Gateway via Tailscale + security notes): [Chrome extension](/tools/chrome-extension) +Full guide (including remote Gateway + security notes): [Chrome extension](/tools/chrome-extension) -If the Gateway runs on the same machine as Chrome (default setup), you usually **do not** need `clawdbot browser serve`. +If the Gateway runs on the same machine as Chrome (default setup), you usually **do not** need anything extra. +If the Gateway runs elsewhere, run a node host on the browser machine so the Gateway can proxy browser actions. You still need to click the extension button on the tab you want to control (it doesn’t auto-attach). ## Sandboxing and memory @@ -1451,7 +1446,7 @@ Have Bot A send a message to Bot B, then let Bot B reply as usual. **CLI bridge (generic):** run a script that calls the other Gateway with `clawdbot agent --message ... --deliver`, targeting a chat where the other bot -listens. If one bot is on Railway/VPS, point your CLI at that remote Gateway +listens. If one bot is on a remote VPS, point your CLI at that remote Gateway via SSH/Tailscale (see [Remote access](/gateway/remote)). Example pattern (run from a machine that can reach the target Gateway): @@ -1485,7 +1480,7 @@ setup is an always‑on host plus your laptop as a node. - **Safer execution controls.** `system.run` is gated by node allowlists/approvals on that laptop. - **More device tools.** Nodes expose `canvas`, `camera`, and `screen` in addition to `system.run`. - **Local browser automation.** Keep the Gateway on a VPS, but run Chrome locally and relay control - with the Chrome extension + `clawdbot browser serve`. + with the Chrome extension + a node host on the laptop. SSH is fine for ad‑hoc shell access, but nodes are simpler for ongoing agent workflows and device automation. @@ -1941,8 +1936,8 @@ You can list available models with `/model`, `/model list`, or `/model status`. You can also force a specific auth profile for the provider (per session): ``` -/model opus@anthropic:claude-cli /model opus@anthropic:default +/model opus@anthropic:work ``` Tip: `/model status` shows which agent is active, which `auth-profiles.json` file is being used, and which auth profile will be tried next. @@ -2146,21 +2141,17 @@ It means the system attempted to use the auth profile ID `anthropic:default`, bu - **Sanity‑check model/auth status** - Use `clawdbot models status` to see configured models and whether providers are authenticated. -**Fix checklist for No credentials found for profile anthropic claude cli** +**Fix checklist for No credentials found for profile anthropic** -This means the run is pinned to the **Claude Code CLI** profile, but the Gateway -can’t find that profile in its auth store. +This means the run is pinned to an Anthropic auth profile, but the Gateway +can’t find it in its auth store. -- **Sync the Claude Code CLI token on the gateway host** - - Run `clawdbot models status` (it loads + syncs Claude Code CLI credentials). - - If it still says missing: run `claude setup-token` (or `clawdbot models auth setup-token --provider anthropic`) and retry. -- **If the token was created on another machine** - - Paste it into the gateway host with `clawdbot models auth paste-token --provider anthropic`. -- **Check the profile mode** - - `auth.profiles["anthropic:claude-cli"].mode` must be `"oauth"` (token mode rejects OAuth credentials). +- **Use a setup-token** + - Run `claude setup-token`, then paste it with `clawdbot models auth setup-token --provider anthropic`. + - If the token was created on another machine, use `clawdbot models auth paste-token --provider anthropic`. - **If you want to use an API key instead** - Put `ANTHROPIC_API_KEY` in `~/.clawdbot/.env` on the **gateway host**. - - Clear any pinned order that forces `anthropic:claude-cli`: + - Clear any pinned order that forces a missing profile: ```bash clawdbot models auth order clear --provider anthropic ``` @@ -2182,7 +2173,7 @@ Fix: Clawdbot now strips unsigned thinking blocks for Google Antigravity Claude. ## Auth profiles: what they are and how to manage them -Related: [/concepts/oauth](/concepts/oauth) (OAuth flows, token storage, multi-account patterns, CLI sync) +Related: [/concepts/oauth](/concepts/oauth) (OAuth flows, token storage, multi-account patterns) ### What is an auth profile @@ -2213,10 +2204,10 @@ You can also set a **per-agent** order override (stored in that agent’s `auth- clawdbot models auth order get --provider anthropic # Lock rotation to a single profile (only try this one) -clawdbot models auth order set --provider anthropic anthropic:claude-cli +clawdbot models auth order set --provider anthropic anthropic:default # Or set an explicit order (fallback within provider) -clawdbot models auth order set --provider anthropic anthropic:claude-cli anthropic:default +clawdbot models auth order set --provider anthropic anthropic:work anthropic:default # Clear override (fall back to config auth.order / round-robin) clawdbot models auth order clear --provider anthropic @@ -2225,7 +2216,7 @@ clawdbot models auth order clear --provider anthropic To target a specific agent: ```bash -clawdbot models auth order set --provider anthropic --agent main anthropic:claude-cli +clawdbot models auth order set --provider anthropic --agent main anthropic:default ``` ### OAuth vs API key whats the difference @@ -2235,7 +2226,7 @@ Clawdbot supports both: - **OAuth** often leverages subscription access (where applicable). - **API keys** use pay‑per‑token billing. -The wizard explicitly supports Anthropic OAuth and OpenAI Codex OAuth and can store API keys for you. +The wizard explicitly supports Anthropic setup-token and OpenAI Codex OAuth and can store API keys for you. ## Gateway: ports, “already running”, and remote mode diff --git a/docs/install/index.md b/docs/install/index.md index dde0e5eeb..7ccab0ca8 100644 --- a/docs/install/index.md +++ b/docs/install/index.md @@ -177,4 +177,5 @@ Then open a new terminal (or `rehash` in zsh / `hash -r` in bash). ## Update / uninstall - Updates: [Updating](/install/updating) +- Migrate to a new machine: [Migrating](/install/migrating) - Uninstall: [Uninstall](/install/uninstall) diff --git a/docs/install/migrating.md b/docs/install/migrating.md new file mode 100644 index 000000000..4987b38b9 --- /dev/null +++ b/docs/install/migrating.md @@ -0,0 +1,190 @@ +--- +summary: "Move (migrate) a Clawdbot install from one machine to another" +read_when: + - You are moving Clawdbot to a new laptop/server + - You want to preserve sessions, auth, and channel logins (WhatsApp, etc.) +--- +# Migrating Clawdbot to a new machine + +This guide migrates a Clawdbot Gateway from one machine to another **without redoing onboarding**. + +The migration is simple conceptually: + +- Copy the **state directory** (`$CLAWDBOT_STATE_DIR`, default: `~/.clawdbot/`) — this includes config, auth, sessions, and channel state. +- Copy your **workspace** (`~/clawd/` by default) — this includes your agent files (memory, prompts, etc.). + +But there are common footguns around **profiles**, **permissions**, and **partial copies**. + +## Before you start (what you are migrating) + +### 1) Identify your state directory + +Most installs use the default: + +- **State dir:** `~/.clawdbot/` + +But it may be different if you use: + +- `--profile ` (often becomes `~/.clawdbot-/`) +- `CLAWDBOT_STATE_DIR=/some/path` + +If you’re not sure, run on the **old** machine: + +```bash +clawdbot status +``` + +Look for mentions of `CLAWDBOT_STATE_DIR` / profile in the output. If you run multiple gateways, repeat for each profile. + +### 2) Identify your workspace + +Common defaults: + +- `~/clawd/` (recommended workspace) +- a custom folder you created + +Your workspace is where files like `MEMORY.md`, `USER.md`, and `memory/*.md` live. + +### 3) Understand what you will preserve + +If you copy **both** the state dir and workspace, you keep: + +- Gateway configuration (`clawdbot.json`) +- Auth profiles / API keys / OAuth tokens +- Session history + agent state +- Channel state (e.g. WhatsApp login/session) +- Your workspace files (memory, skills notes, etc.) + +If you copy **only** the workspace (e.g., via Git), you do **not** preserve: + +- sessions +- credentials +- channel logins + +Those live under `$CLAWDBOT_STATE_DIR`. + +## Migration steps (recommended) + +### Step 0 — Make a backup (old machine) + +On the **old** machine, stop the gateway first so files aren’t changing mid-copy: + +```bash +clawdbot gateway stop +``` + +(Optional but recommended) archive the state dir and workspace: + +```bash +# Adjust paths if you use a profile or custom locations +cd ~ +tar -czf clawdbot-state.tgz .clawdbot + +tar -czf clawd-workspace.tgz clawd +``` + +If you have multiple profiles/state dirs (e.g. `~/.clawdbot-main`, `~/.clawdbot-work`), archive each. + +### Step 1 — Install Clawdbot on the new machine + +On the **new** machine, install the CLI (and Node if needed): + +- See: [Install](/install) + +At this stage, it’s OK if onboarding creates a fresh `~/.clawdbot/` — you will overwrite it in the next step. + +### Step 2 — Copy the state dir + workspace to the new machine + +Copy **both**: + +- `$CLAWDBOT_STATE_DIR` (default `~/.clawdbot/`) +- your workspace (default `~/clawd/`) + +Common approaches: + +- `scp` the tarballs and extract +- `rsync -a` over SSH +- external drive + +After copying, ensure: + +- Hidden directories were included (e.g. `.clawdbot/`) +- File ownership is correct for the user running the gateway + +### Step 3 — Run Doctor (migrations + service repair) + +On the **new** machine: + +```bash +clawdbot doctor +``` + +Doctor is the “safe boring” command. It repairs services, applies config migrations, and warns about mismatches. + +Then: + +```bash +clawdbot gateway restart +clawdbot status +``` + +## Common footguns (and how to avoid them) + +### Footgun: profile / state-dir mismatch + +If you ran the old gateway with a profile (or `CLAWDBOT_STATE_DIR`), and the new gateway uses a different one, you’ll see symptoms like: + +- config changes not taking effect +- channels missing / logged out +- empty session history + +Fix: run the gateway/service using the **same** profile/state dir you migrated, then rerun: + +```bash +clawdbot doctor +``` + +### Footgun: copying only `clawdbot.json` + +`clawdbot.json` is not enough. Many providers store state under: + +- `$CLAWDBOT_STATE_DIR/credentials/` +- `$CLAWDBOT_STATE_DIR/agents//...` + +Always migrate the entire `$CLAWDBOT_STATE_DIR` folder. + +### Footgun: permissions / ownership + +If you copied as root or changed users, the gateway may fail to read credentials/sessions. + +Fix: ensure the state dir + workspace are owned by the user running the gateway. + +### Footgun: migrating between remote/local modes + +- If your UI (WebUI/TUI) points at a **remote** gateway, the remote host owns the session store + workspace. +- Migrating your laptop won’t move the remote gateway’s state. + +If you’re in remote mode, migrate the **gateway host**. + +### Footgun: secrets in backups + +`$CLAWDBOT_STATE_DIR` contains secrets (API keys, OAuth tokens, WhatsApp creds). Treat backups like production secrets: + +- store encrypted +- avoid sharing over insecure channels +- rotate keys if you suspect exposure + +## Verification checklist + +On the new machine, confirm: + +- `clawdbot status` shows the gateway running +- Your channels are still connected (e.g. WhatsApp doesn’t require re-pair) +- The dashboard opens and shows existing sessions +- Your workspace files (memory, configs) are present + +## Related + +- [Doctor](/gateway/doctor) +- [Gateway troubleshooting](/gateway/troubleshooting) +- [Where does Clawdbot store its data?](/help/faq#where-does-clawdbot-store-its-data) diff --git a/docs/install/node.md b/docs/install/node.md index 6a622e198..3075b6207 100644 --- a/docs/install/node.md +++ b/docs/install/node.md @@ -1,9 +1,10 @@ --- +title: "Node.js + npm (PATH sanity)" summary: "Node.js + npm install sanity: versions, PATH, and global installs" read_when: - - You installed Clawdbot but `clawdbot` is “command not found” - - You’re setting up Node.js/npm on a new machine - - `npm install -g ...` fails with permissions or PATH issues + - "You installed Clawdbot but `clawdbot` is “command not found”" + - "You’re setting up Node.js/npm on a new machine" + - "npm install -g ... fails with permissions or PATH issues" --- # Node.js + npm (PATH sanity) diff --git a/docs/northflank.mdx b/docs/northflank.mdx new file mode 100644 index 000000000..aae9c6a22 --- /dev/null +++ b/docs/northflank.mdx @@ -0,0 +1,53 @@ +--- +title: Deploy on Northflank +--- + +Deploy Clawdbot on Northflank with a one-click template and finish setup in your browser. +This is the easiest “no terminal on the server” path: Northflank runs the Gateway for you, +and you configure everything via the `/setup` web wizard. + +## How to get started + +1. Click [Deploy Clawdbot](https://northflank.com/stacks/deploy-clawdbot) to open the template. +2. Create an [account on Northflank](https://app.northflank.com/signup) if you don’t already have one. +3. Click **Deploy Clawdbot now**. +4. Set the required environment variable: `SETUP_PASSWORD`. +5. Click **Deploy stack** to build and run the Clawdbot template. +6. Wait for the deployment to complete, then click **View resources**. +7. Open the Clawdbot service. +8. Open the public Clawdbot URL and complete setup at `/setup`. +9. Open the Control UI at `/clawdbot`. + +## What you get + +- Hosted Clawdbot Gateway + Control UI +- Web setup wizard at `/setup` (no terminal commands) +- Persistent storage via Northflank Volume (`/data`) so config/credentials/workspace survive redeploys + +## Setup flow + +1) Visit `https:///setup` and enter your `SETUP_PASSWORD`. +2) Choose a model/auth provider and paste your key. +3) (Optional) Add Telegram/Discord/Slack tokens. +4) Click **Run setup**. +5) Open the Control UI at `https:///clawdbot` + +If Telegram DMs are set to pairing, the setup wizard can approve the pairing code. + +## Getting chat tokens + +### Telegram bot token + +1) Message `@BotFather` in Telegram +2) Run `/newbot` +3) Copy the token (looks like `123456789:AA...`) +4) Paste it into `/setup` + +### Discord bot token + +1) Go to https://discord.com/developers/applications +2) **New Application** → choose a name +3) **Bot** → **Add Bot** +4) **Enable MESSAGE CONTENT INTENT** under Bot → Privileged Gateway Intents (required or the bot will crash on startup) +5) Copy the **Bot Token** and paste into `/setup` +6) Invite the bot to your server (OAuth2 URL Generator; scopes: `bot`, `applications.commands`) diff --git a/docs/platforms/digitalocean.md b/docs/platforms/digitalocean.md index 1b8e1d90d..afefe3676 100644 --- a/docs/platforms/digitalocean.md +++ b/docs/platforms/digitalocean.md @@ -1,5 +1,5 @@ --- -summary: "Clawdbot on DigitalOcean (cheapest paid VPS option)" +summary: "Clawdbot on DigitalOcean (simple paid VPS option)" read_when: - Setting up Clawdbot on DigitalOcean - Looking for cheap VPS hosting for Clawdbot @@ -11,22 +11,22 @@ read_when: Run a persistent Clawdbot Gateway on DigitalOcean for **$6/month** (or $4/mo with reserved pricing). -If you want something even cheaper, see [Oracle Cloud (Free Tier)](#oracle-cloud-free-alternative) at the bottom — it's **actually free forever**. +If you want a $0/month option and don’t mind ARM + provider-specific setup, see the [Oracle Cloud guide](/platforms/oracle). ## Cost Comparison (2026) | Provider | Plan | Specs | Price/mo | Notes | |----------|------|-------|----------|-------| -| **Oracle Cloud** | Always Free ARM | 4 OCPU, 24GB RAM | **$0** | Best value, requires ARM-compatible setup | -| **Hetzner** | CX22 | 2 vCPU, 4GB RAM | €3.79 (~$4) | Cheapest paid, EU datacenters | -| **DigitalOcean** | Basic | 1 vCPU, 1GB RAM | $6 | Easy UI, good docs | -| **Vultr** | Cloud Compute | 1 vCPU, 1GB RAM | $6 | Many locations | -| **Linode** | Nanode | 1 vCPU, 1GB RAM | $5 | Now part of Akamai | +| Oracle Cloud | Always Free ARM | up to 4 OCPU, 24GB RAM | $0 | ARM, limited capacity / signup quirks | +| Hetzner | CX22 | 2 vCPU, 4GB RAM | €3.79 (~$4) | Cheapest paid option | +| DigitalOcean | Basic | 1 vCPU, 1GB RAM | $6 | Easy UI, good docs | +| Vultr | Cloud Compute | 1 vCPU, 1GB RAM | $6 | Many locations | +| Linode | Nanode | 1 vCPU, 1GB RAM | $5 | Now part of Akamai | -**Recommendation:** -- **Free:** Oracle Cloud ARM (if you can handle the signup process) -- **Paid:** Hetzner CX22 (best specs per dollar) — see [Hetzner guide](/platforms/hetzner) -- **Easy:** DigitalOcean (this guide) — beginner-friendly UI +**Picking a provider:** +- DigitalOcean: simplest UX + predictable setup (this guide) +- Hetzner: good price/perf (see [Hetzner guide](/platforms/hetzner)) +- Oracle Cloud: can be $0/month, but is more finicky and ARM-only (see [Oracle guide](/platforms/oracle)) --- @@ -90,10 +90,10 @@ The wizard will walk you through: clawdbot status # Check service -systemctl status clawdbot +systemctl --user status clawdbot-gateway.service # View logs -journalctl -u clawdbot -f +journalctl --user -u clawdbot-gateway.service -f ``` ## 6) Access the Dashboard @@ -108,18 +108,30 @@ ssh -L 18789:localhost:18789 root@YOUR_DROPLET_IP # Then open: http://localhost:18789 ``` -**Option B: Tailscale (easier long-term)** +**Option B: Tailscale Serve (HTTPS, loopback-only)** ```bash # On the droplet curl -fsSL https://tailscale.com/install.sh | sh tailscale up -# Configure gateway to bind to Tailscale +# Configure Gateway to use Tailscale Serve +clawdbot config set gateway.tailscale.mode serve +clawdbot gateway restart +``` + +Open: `https:///` + +Notes: +- Serve keeps the Gateway loopback-only and authenticates via Tailscale identity headers. +- To require token/password instead, set `gateway.auth.allowTailscale: false` or use `gateway.auth.mode: "password"`. + +**Option C: Tailnet bind (no Serve)** +```bash clawdbot config set gateway.bind tailnet clawdbot gateway restart ``` -Then access via your Tailscale IP: `http://100.x.x.x:18789` +Open: `http://:18789` (token required). ## 7) Connect Your Channels @@ -180,7 +192,7 @@ tar -czvf clawdbot-backup.tar.gz ~/.clawdbot ~/clawd ## Oracle Cloud Free Alternative -Oracle Cloud offers **Always Free** ARM instances that are significantly more powerful: +Oracle Cloud offers **Always Free** ARM instances that are significantly more powerful than any paid option here — for $0/month. | What you get | Specs | |--------------|-------| @@ -189,19 +201,11 @@ Oracle Cloud offers **Always Free** ARM instances that are significantly more po | **200GB storage** | Block volume | | **Forever free** | No credit card charges | -### Quick setup: -1. Sign up at [oracle.com/cloud/free](https://www.oracle.com/cloud/free/) -2. Create a VM.Standard.A1.Flex instance (ARM) -3. Choose Oracle Linux or Ubuntu -4. Allocate up to 4 OCPU / 24GB RAM within free tier -5. Follow the same Clawdbot install steps above - **Caveats:** - Signup can be finicky (retry if it fails) - ARM architecture — most things work, but some binaries need ARM builds -- Oracle may reclaim idle instances (keep them active) -For the full Oracle guide, see the [community docs](https://gist.github.com/rssnyder/51e3cfedd730e7dd5f4a816143b25dbd). +For the full setup guide, see [Oracle Cloud](/platforms/oracle). For signup tips and troubleshooting the enrollment process, see this [community guide](https://gist.github.com/rssnyder/51e3cfedd730e7dd5f4a816143b25dbd). --- diff --git a/docs/platforms/fly.md b/docs/platforms/fly.md index 0fdf176ae..a9c8bafb4 100644 --- a/docs/platforms/fly.md +++ b/docs/platforms/fly.md @@ -39,7 +39,9 @@ fly volumes create clawdbot_data --size 1 --region iad ## 2) Configure fly.toml -Edit `fly.toml` to match your app name and requirements: +Edit `fly.toml` to match your app name and requirements. + +**Security note:** The default config exposes a public URL. For a hardened deployment with no public IP, see [Private Deployment](#private-deployment-hardened) or use `fly.private.toml`. ```toml app = "my-clawdbot" # Your app name @@ -104,6 +106,7 @@ fly secrets set DISCORD_BOT_TOKEN=MTQ... **Notes:** - Non-loopback binds (`--bind lan`) require `CLAWDBOT_GATEWAY_TOKEN` for security. - Treat these tokens like passwords. +- **Prefer env vars over config file** for all API keys and tokens. This keeps secrets out of `clawdbot.json` where they could be accidentally exposed or logged. ## 4) Deploy @@ -182,7 +185,7 @@ cat > /data/clawdbot.json << 'EOF' "bind": "auto" }, "meta": { - "lastTouchedVersion": "2026.1.25" + "lastTouchedVersion": "2026.1.26" } } EOF @@ -337,6 +340,114 @@ fly machine update --vm-memory 2048 --command "node dist/index.js g **Note:** After `fly deploy`, the machine command may reset to what's in `fly.toml`. If you made manual changes, re-apply them after deploy. +## Private Deployment (Hardened) + +By default, Fly allocates public IPs, making your gateway accessible at `https://your-app.fly.dev`. This is convenient but means your deployment is discoverable by internet scanners (Shodan, Censys, etc.). + +For a hardened deployment with **no public exposure**, use the private template. + +### When to use private deployment + +- You only make **outbound** calls/messages (no inbound webhooks) +- You use **ngrok or Tailscale** tunnels for any webhook callbacks +- You access the gateway via **SSH, proxy, or WireGuard** instead of browser +- You want the deployment **hidden from internet scanners** + +### Setup + +Use `fly.private.toml` instead of the standard config: + +```bash +# Deploy with private config +fly deploy -c fly.private.toml +``` + +Or convert an existing deployment: + +```bash +# List current IPs +fly ips list -a my-clawdbot + +# Release public IPs +fly ips release -a my-clawdbot +fly ips release -a my-clawdbot + +# Switch to private config so future deploys don't re-allocate public IPs +# (remove [http_service] or deploy with the private template) +fly deploy -c fly.private.toml + +# Allocate private-only IPv6 +fly ips allocate-v6 --private -a my-clawdbot +``` + +After this, `fly ips list` should show only a `private` type IP: +``` +VERSION IP TYPE REGION +v6 fdaa:x:x:x:x::x private global +``` + +### Accessing a private deployment + +Since there's no public URL, use one of these methods: + +**Option 1: Local proxy (simplest)** +```bash +# Forward local port 3000 to the app +fly proxy 3000:3000 -a my-clawdbot + +# Then open http://localhost:3000 in browser +``` + +**Option 2: WireGuard VPN** +```bash +# Create WireGuard config (one-time) +fly wireguard create + +# Import to WireGuard client, then access via internal IPv6 +# Example: http://[fdaa:x:x:x:x::x]:3000 +``` + +**Option 3: SSH only** +```bash +fly ssh console -a my-clawdbot +``` + +### Webhooks with private deployment + +If you need webhook callbacks (Twilio, Telnyx, etc.) without public exposure: + +1. **ngrok tunnel** - Run ngrok inside the container or as a sidecar +2. **Tailscale Funnel** - Expose specific paths via Tailscale +3. **Outbound-only** - Some providers (Twilio) work fine for outbound calls without webhooks + +Example voice-call config with ngrok: +```json +{ + "plugins": { + "entries": { + "voice-call": { + "enabled": true, + "config": { + "provider": "twilio", + "tunnel": { "provider": "ngrok" } + } + } + } + } +} +``` + +The ngrok tunnel runs inside the container and provides a public webhook URL without exposing the Fly app itself. + +### Security benefits + +| Aspect | Public | Private | +|--------|--------|---------| +| Internet scanners | Discoverable | Hidden | +| Direct attacks | Possible | Blocked | +| Control UI access | Browser | Proxy/VPN | +| Webhook delivery | Direct | Via tunnel | + ## Notes - Fly.io uses **x86 architecture** (not ARM) diff --git a/docs/platforms/index.md b/docs/platforms/index.md index d53073026..3a1e87267 100644 --- a/docs/platforms/index.md +++ b/docs/platforms/index.md @@ -24,7 +24,6 @@ Native companion apps for Windows are also planned; the Gateway is recommended v ## VPS & hosting - VPS hub: [VPS hosting](/vps) -- Railway (one-click): [Railway](/railway) - Fly.io: [Fly.io](/platforms/fly) - Hetzner (Docker): [Hetzner](/platforms/hetzner) - GCP (Compute Engine): [GCP](/platforms/gcp) diff --git a/docs/platforms/mac/release.md b/docs/platforms/mac/release.md index d3bfd02c3..b1226a5ad 100644 --- a/docs/platforms/mac/release.md +++ b/docs/platforms/mac/release.md @@ -30,17 +30,17 @@ Notes: # From repo root; set release IDs so Sparkle feed is enabled. # APP_BUILD must be numeric + monotonic for Sparkle compare. BUNDLE_ID=com.clawdbot.mac \ -APP_VERSION=2026.1.25 \ +APP_VERSION=2026.1.26 \ APP_BUILD="$(git rev-list --count HEAD)" \ BUILD_CONFIG=release \ SIGN_IDENTITY="Developer ID Application: ()" \ scripts/package-mac-app.sh # Zip for distribution (includes resource forks for Sparkle delta support) -ditto -c -k --sequesterRsrc --keepParent dist/Clawdbot.app dist/Clawdbot-2026.1.25.zip +ditto -c -k --sequesterRsrc --keepParent dist/Clawdbot.app dist/Clawdbot-2026.1.26.zip # Optional: also build a styled DMG for humans (drag to /Applications) -scripts/create-dmg.sh dist/Clawdbot.app dist/Clawdbot-2026.1.25.dmg +scripts/create-dmg.sh dist/Clawdbot.app dist/Clawdbot-2026.1.26.dmg # Recommended: build + notarize/staple zip + DMG # First, create a keychain profile once: @@ -48,26 +48,26 @@ scripts/create-dmg.sh dist/Clawdbot.app dist/Clawdbot-2026.1.25.dmg # --apple-id "" --team-id "" --password "" NOTARIZE=1 NOTARYTOOL_PROFILE=clawdbot-notary \ BUNDLE_ID=com.clawdbot.mac \ -APP_VERSION=2026.1.25 \ +APP_VERSION=2026.1.26 \ APP_BUILD="$(git rev-list --count HEAD)" \ BUILD_CONFIG=release \ SIGN_IDENTITY="Developer ID Application: ()" \ scripts/package-mac-dist.sh # Optional: ship dSYM alongside the release -ditto -c -k --keepParent apps/macos/.build/release/Clawdbot.app.dSYM dist/Clawdbot-2026.1.25.dSYM.zip +ditto -c -k --keepParent apps/macos/.build/release/Clawdbot.app.dSYM dist/Clawdbot-2026.1.26.dSYM.zip ``` ## Appcast entry Use the release note generator so Sparkle renders formatted HTML notes: ```bash -SPARKLE_PRIVATE_KEY_FILE=/path/to/ed25519-private-key scripts/make_appcast.sh dist/Clawdbot-2026.1.25.zip https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml +SPARKLE_PRIVATE_KEY_FILE=/path/to/ed25519-private-key scripts/make_appcast.sh dist/Clawdbot-2026.1.26.zip https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml ``` Generates HTML release notes from `CHANGELOG.md` (via [`scripts/changelog-to-html.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/changelog-to-html.sh)) and embeds them in the appcast entry. Commit the updated `appcast.xml` alongside the release assets (zip + dSYM) when publishing. ## Publish & verify -- Upload `Clawdbot-2026.1.25.zip` (and `Clawdbot-2026.1.25.dSYM.zip`) to the GitHub release for tag `v2026.1.25`. +- Upload `Clawdbot-2026.1.26.zip` (and `Clawdbot-2026.1.26.dSYM.zip`) to the GitHub release for tag `v2026.1.26`. - Ensure the raw appcast URL matches the baked feed: `https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml`. - Sanity checks: - `curl -I https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml` returns 200. diff --git a/docs/platforms/oracle.md b/docs/platforms/oracle.md new file mode 100644 index 000000000..d8006754b --- /dev/null +++ b/docs/platforms/oracle.md @@ -0,0 +1,291 @@ +--- +summary: "Clawdbot on Oracle Cloud (Always Free ARM)" +read_when: + - Setting up Clawdbot on Oracle Cloud + - Looking for low-cost VPS hosting for Clawdbot + - Want 24/7 Clawdbot on a small server +--- + +# Clawdbot on Oracle Cloud (OCI) + +## Goal + +Run a persistent Clawdbot Gateway on Oracle Cloud's **Always Free** ARM tier. + +Oracle’s free tier can be a great fit for Clawdbot (especially if you already have an OCI account), but it comes with tradeoffs: + +- ARM architecture (most things work, but some binaries may be x86-only) +- Capacity and signup can be finicky + +## Cost Comparison (2026) + +| Provider | Plan | Specs | Price/mo | Notes | +|----------|------|-------|----------|-------| +| Oracle Cloud | Always Free ARM | up to 4 OCPU, 24GB RAM | $0 | ARM, limited capacity | +| Hetzner | CX22 | 2 vCPU, 4GB RAM | ~ $4 | Cheapest paid option | +| DigitalOcean | Basic | 1 vCPU, 1GB RAM | $6 | Easy UI, good docs | +| Vultr | Cloud Compute | 1 vCPU, 1GB RAM | $6 | Many locations | +| Linode | Nanode | 1 vCPU, 1GB RAM | $5 | Now part of Akamai | + +--- + +## Prerequisites + +- Oracle Cloud account ([signup](https://www.oracle.com/cloud/free/)) — see [community signup guide](https://gist.github.com/rssnyder/51e3cfedd730e7dd5f4a816143b25dbd) if you hit issues +- Tailscale account (free at [tailscale.com](https://tailscale.com)) +- ~30 minutes + +## 1) Create an OCI Instance + +1. Log into [Oracle Cloud Console](https://cloud.oracle.com/) +2. Navigate to **Compute → Instances → Create Instance** +3. Configure: + - **Name:** `clawdbot` + - **Image:** Ubuntu 24.04 (aarch64) + - **Shape:** `VM.Standard.A1.Flex` (Ampere ARM) + - **OCPUs:** 2 (or up to 4) + - **Memory:** 12 GB (or up to 24 GB) + - **Boot volume:** 50 GB (up to 200 GB free) + - **SSH key:** Add your public key +4. Click **Create** +5. Note the public IP address + +**Tip:** If instance creation fails with "Out of capacity", try a different availability domain or retry later. Free tier capacity is limited. + +## 2) Connect and Update + +```bash +# Connect via public IP +ssh ubuntu@YOUR_PUBLIC_IP + +# Update system +sudo apt update && sudo apt upgrade -y +sudo apt install -y build-essential +``` + +**Note:** `build-essential` is required for ARM compilation of some dependencies. + +## 3) Configure User and Hostname + +```bash +# Set hostname +sudo hostnamectl set-hostname clawdbot + +# Set password for ubuntu user +sudo passwd ubuntu + +# Enable lingering (keeps user services running after logout) +sudo loginctl enable-linger ubuntu +``` + +## 4) Install Tailscale + +```bash +curl -fsSL https://tailscale.com/install.sh | sh +sudo tailscale up --ssh --hostname=clawdbot +``` + +This enables Tailscale SSH, so you can connect via `ssh clawdbot` from any device on your tailnet — no public IP needed. + +Verify: +```bash +tailscale status +``` + +**From now on, connect via Tailscale:** `ssh ubuntu@clawdbot` (or use the Tailscale IP). + +## 5) Install Clawdbot + +```bash +curl -fsSL https://clawd.bot/install.sh | bash +source ~/.bashrc +``` + +When prompted "How do you want to hatch your bot?", select **"Do this later"**. + +> Note: If you hit ARM-native build issues, start with system packages (e.g. `sudo apt install -y build-essential`) before reaching for Homebrew. + +## 6) Configure Gateway (loopback + token auth) and enable Tailscale Serve + +Use token auth as the default. It’s predictable and avoids needing any “insecure auth” Control UI flags. + +```bash +# Keep the Gateway private on the VM +clawdbot config set gateway.bind loopback + +# Require auth for the Gateway + Control UI +clawdbot config set gateway.auth.mode token +clawdbot doctor --generate-gateway-token + +# Expose over Tailscale Serve (HTTPS + tailnet access) +clawdbot config set gateway.tailscale.mode serve +clawdbot config set gateway.trustedProxies '["127.0.0.1"]' + +systemctl --user restart clawdbot-gateway +``` + +## 7) Verify + +```bash +# Check version +clawdbot --version + +# Check daemon status +systemctl --user status clawdbot-gateway + +# Check Tailscale Serve +tailscale serve status + +# Test local response +curl http://localhost:18789 +``` + +## 8) Lock Down VCN Security + +Now that everything is working, lock down the VCN to block all traffic except Tailscale. OCI's Virtual Cloud Network acts as a firewall at the network edge — traffic is blocked before it reaches your instance. + +1. Go to **Networking → Virtual Cloud Networks** in the OCI Console +2. Click your VCN → **Security Lists** → Default Security List +3. **Remove** all ingress rules except: + - `0.0.0.0/0 UDP 41641` (Tailscale) +4. Keep default egress rules (allow all outbound) + +This blocks SSH on port 22, HTTP, HTTPS, and everything else at the network edge. From now on, you can only connect via Tailscale. + +--- + +## Access the Control UI + +From any device on your Tailscale network: + +``` +https://clawdbot..ts.net/ +``` + +Replace `` with your tailnet name (visible in `tailscale status`). + +No SSH tunnel needed. Tailscale provides: +- HTTPS encryption (automatic certs) +- Authentication via Tailscale identity +- Access from any device on your tailnet (laptop, phone, etc.) + +--- + +## Security: VCN + Tailscale (recommended baseline) + +With the VCN locked down (only UDP 41641 open) and the Gateway bound to loopback, you get strong defense-in-depth: public traffic is blocked at the network edge, and admin access happens over your tailnet. + +This setup often removes the *need* for extra host-based firewall rules purely to stop Internet-wide SSH brute force — but you should still keep the OS updated, run `clawdbot security audit`, and verify you aren’t accidentally listening on public interfaces. + +### What's Already Protected + +| Traditional Step | Needed? | Why | +|------------------|---------|-----| +| UFW firewall | No | VCN blocks before traffic reaches instance | +| fail2ban | No | No brute force if port 22 blocked at VCN | +| sshd hardening | No | Tailscale SSH doesn't use sshd | +| Disable root login | No | Tailscale uses Tailscale identity, not system users | +| SSH key-only auth | No | Tailscale authenticates via your tailnet | +| IPv6 hardening | Usually not | Depends on your VCN/subnet settings; verify what’s actually assigned/exposed | + +### Still Recommended + +- **Credential permissions:** `chmod 700 ~/.clawdbot` +- **Security audit:** `clawdbot security audit` +- **System updates:** `sudo apt update && sudo apt upgrade` regularly +- **Monitor Tailscale:** Review devices in [Tailscale admin console](https://login.tailscale.com/admin) + +### Verify Security Posture + +```bash +# Confirm no public ports listening +sudo ss -tlnp | grep -v '127.0.0.1\|::1' + +# Verify Tailscale SSH is active +tailscale status | grep -q 'offers: ssh' && echo "Tailscale SSH active" + +# Optional: disable sshd entirely +sudo systemctl disable --now ssh +``` + +--- + +## Fallback: SSH Tunnel + +If Tailscale Serve isn't working, use an SSH tunnel: + +```bash +# From your local machine (via Tailscale) +ssh -L 18789:127.0.0.1:18789 ubuntu@clawdbot +``` + +Then open `http://localhost:18789`. + +--- + +## Troubleshooting + +### Instance creation fails ("Out of capacity") +Free tier ARM instances are popular. Try: +- Different availability domain +- Retry during off-peak hours (early morning) +- Use the "Always Free" filter when selecting shape + +### Tailscale won't connect +```bash +# Check status +sudo tailscale status + +# Re-authenticate +sudo tailscale up --ssh --hostname=clawdbot --reset +``` + +### Gateway won't start +```bash +clawdbot gateway status +clawdbot doctor --non-interactive +journalctl --user -u clawdbot-gateway -n 50 +``` + +### Can't reach Control UI +```bash +# Verify Tailscale Serve is running +tailscale serve status + +# Check gateway is listening +curl http://localhost:18789 + +# Restart if needed +systemctl --user restart clawdbot-gateway +``` + +### ARM binary issues +Some tools may not have ARM builds. Check: +```bash +uname -m # Should show aarch64 +``` + +Most npm packages work fine. For binaries, look for `linux-arm64` or `aarch64` releases. + +--- + +## Persistence + +All state lives in: +- `~/.clawdbot/` — config, credentials, session data +- `~/clawd/` — workspace (SOUL.md, memory, artifacts) + +Back up periodically: +```bash +tar -czvf clawdbot-backup.tar.gz ~/.clawdbot ~/clawd +``` + +--- + +## See Also + +- [Gateway remote access](/gateway/remote) — other remote access patterns +- [Tailscale integration](/gateway/tailscale) — full Tailscale docs +- [Gateway configuration](/gateway/configuration) — all config options +- [DigitalOcean guide](/platforms/digitalocean) — if you want paid + easier signup +- [Hetzner guide](/platforms/hetzner) — Docker-based alternative diff --git a/docs/plugins/voice-call.md b/docs/plugins/voice-call.md index eecb80133..46713c939 100644 --- a/docs/plugins/voice-call.md +++ b/docs/plugins/voice-call.md @@ -103,6 +103,9 @@ Notes: - Plivo requires a **publicly reachable** webhook URL. - `mock` is a local dev provider (no network calls). - `skipSignatureVerification` is for local testing only. +- If you use ngrok free tier, set `publicUrl` to the exact ngrok URL; signature verification is always enforced. +- `tunnel.allowNgrokFreeTierLoopbackBypass: true` allows Twilio webhooks with invalid signatures **only** when `tunnel.provider="ngrok"` and `serve.bind` is loopback (ngrok local agent). Use for local dev only. +- Ngrok free tier URLs can change or add interstitial behavior; if `publicUrl` drifts, Twilio signatures will fail. For production, prefer a stable domain or Tailscale funnel. ## TTS for calls diff --git a/docs/providers/anthropic.md b/docs/providers/anthropic.md index 7876c4ae9..018e130dd 100644 --- a/docs/providers/anthropic.md +++ b/docs/providers/anthropic.md @@ -1,14 +1,13 @@ --- -summary: "Use Anthropic Claude via API keys or Claude Code CLI auth in Clawdbot" +summary: "Use Anthropic Claude via API keys or setup-token in Clawdbot" read_when: - You want to use Anthropic models in Clawdbot - - You want setup-token or Claude Code CLI auth instead of API keys + - You want setup-token instead of API keys --- # Anthropic (Claude) Anthropic builds the **Claude** model family and provides access via an API. -In Clawdbot you can authenticate with an API key or reuse **Claude Code CLI** credentials -(setup-token or OAuth). +In Clawdbot you can authenticate with an API key or a **setup-token**. ## Option A: Anthropic API key @@ -37,7 +36,7 @@ clawdbot onboard --anthropic-api-key "$ANTHROPIC_API_KEY" ## Prompt caching (Anthropic API) Clawdbot does **not** override Anthropic’s default cache TTL unless you set it. -This is **API-only**; Claude Code CLI OAuth ignores TTL settings. +This is **API-only**; subscription auth does not honor TTL settings. To set the TTL per model, use `cacheControlTtl` in the model `params`: @@ -58,9 +57,9 @@ To set the TTL per model, use `cacheControlTtl` in the model `params`: Clawdbot includes the `extended-cache-ttl-2025-04-11` beta flag for Anthropic API requests; keep it if you override provider headers (see [/gateway/configuration](/gateway/configuration)). -## Option B: Claude Code CLI (setup-token or OAuth) +## Option B: Claude setup-token -**Best for:** using your Claude subscription or existing Claude Code CLI login. +**Best for:** using your Claude subscription. ### Where to get a setup-token @@ -85,8 +84,8 @@ clawdbot models auth paste-token --provider anthropic ### CLI setup ```bash -# Reuse Claude Code CLI OAuth credentials if already logged in -clawdbot onboard --auth-choice claude-cli +# Paste a setup-token during onboarding +clawdbot onboard --auth-choice setup-token ``` ### Config snippet @@ -100,10 +99,7 @@ clawdbot onboard --auth-choice claude-cli ## Notes - Generate the setup-token with `claude setup-token` and paste it, or run `clawdbot models auth setup-token` on the gateway host. -- If you see “OAuth token refresh failed …” on a Claude subscription, re-auth with a setup-token or resync Claude Code CLI OAuth on the gateway host. See [/gateway/troubleshooting#oauth-token-refresh-failed-anthropic-claude-subscription](/gateway/troubleshooting#oauth-token-refresh-failed-anthropic-claude-subscription). -- Clawdbot writes `auth.profiles["anthropic:claude-cli"].mode` as `"oauth"` so the profile - accepts both OAuth and setup-token credentials. Older configs using `"token"` are - auto-migrated on load. +- If you see “OAuth token refresh failed …” on a Claude subscription, re-auth with a setup-token. See [/gateway/troubleshooting#oauth-token-refresh-failed-anthropic-claude-subscription](/gateway/troubleshooting#oauth-token-refresh-failed-anthropic-claude-subscription). - Auth details + reuse rules are in [/concepts/oauth](/concepts/oauth). ## Troubleshooting @@ -119,7 +115,7 @@ clawdbot onboard --auth-choice claude-cli - Re-run onboarding for that agent, or paste a setup-token / API key on the gateway host, then verify with `clawdbot models status`. -**No credentials found for profile `anthropic:default` or `anthropic:claude-cli`** +**No credentials found for profile `anthropic:default`** - Run `clawdbot models status` to see which auth profile is active. - Re-run onboarding, or paste a setup-token / API key for that profile. diff --git a/docs/providers/claude-max-api-proxy.md b/docs/providers/claude-max-api-proxy.md index 255be62fc..d2bb6cde8 100644 --- a/docs/providers/claude-max-api-proxy.md +++ b/docs/providers/claude-max-api-proxy.md @@ -141,5 +141,5 @@ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.claude-max-api.plist ## See Also -- [Anthropic provider](/providers/anthropic) - Native Clawdbot integration with Claude Code CLI OAuth +- [Anthropic provider](/providers/anthropic) - Native Clawdbot integration with Claude setup-token or API keys - [OpenAI provider](/providers/openai) - For OpenAI/Codex subscriptions diff --git a/docs/providers/openai.md b/docs/providers/openai.md index 442d7f3ae..c877d59ff 100644 --- a/docs/providers/openai.md +++ b/docs/providers/openai.md @@ -7,9 +7,7 @@ read_when: # OpenAI OpenAI provides developer APIs for GPT models. Codex supports **ChatGPT sign-in** for subscription -access or **API key** sign-in for usage-based access. Codex cloud requires ChatGPT sign-in, while -the Codex CLI supports either sign-in method. The Codex CLI caches login details in -`~/.codex/auth.json` (or your OS credential store), which Clawdbot can reuse. +access or **API key** sign-in for usage-based access. Codex cloud requires ChatGPT sign-in. ## Option A: OpenAI API key (OpenAI Platform) @@ -38,16 +36,14 @@ clawdbot onboard --openai-api-key "$OPENAI_API_KEY" **Best for:** using ChatGPT/Codex subscription access instead of an API key. Codex cloud requires ChatGPT sign-in, while the Codex CLI supports ChatGPT or API key sign-in. -Clawdbot can reuse your **Codex CLI** login (`~/.codex/auth.json`) or run the OAuth flow. - ### CLI setup ```bash -# Reuse existing Codex CLI login -clawdbot onboard --auth-choice codex-cli - -# Or run Codex OAuth in the wizard +# Run Codex OAuth in the wizard clawdbot onboard --auth-choice openai-codex + +# Or run OAuth directly +clawdbot models auth login --provider openai-codex ``` ### Config snippet diff --git a/docs/reference/RELEASING.md b/docs/reference/RELEASING.md index 244757a48..a4c68e3e9 100644 --- a/docs/reference/RELEASING.md +++ b/docs/reference/RELEASING.md @@ -17,7 +17,7 @@ When the operator says “release”, immediately do this preflight (no extra qu - Use Sparkle keys from `~/Library/CloudStorage/Dropbox/Backup/Sparkle` if needed. 1) **Version & metadata** -- [ ] Bump `package.json` version (e.g., `2026.1.25`). +- [ ] Bump `package.json` version (e.g., `2026.1.26`). - [ ] Run `pnpm plugins:sync` to align extension package versions + changelogs. - [ ] Update CLI/version strings: [`src/cli/program.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/cli/program.ts) and the Baileys user agent in [`src/provider-web.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/provider-web.ts). - [ ] Confirm package metadata (name, description, repository, keywords, license) and `bin` map points to [`dist/entry.js`](https://github.com/clawdbot/clawdbot/blob/main/dist/entry.js) for `clawdbot`. diff --git a/docs/security/formal-verification.md b/docs/security/formal-verification.md new file mode 100644 index 000000000..4e2c13771 --- /dev/null +++ b/docs/security/formal-verification.md @@ -0,0 +1,107 @@ +--- +title: Formal Verification (Security Models) +summary: Machine-checked security models for Clawdbot’s highest-risk paths. +permalink: /security/formal-verification/ +--- + +# Formal Verification (Security Models) + +This page tracks Clawdbot’s **formal security models** (TLA+/TLC today; more as needed). + +**Goal (north star):** provide a machine-checked argument that Clawdbot enforces its +intended security policy (authorization, session isolation, tool gating, and +misconfiguration safety), under explicit assumptions. + +**What this is (today):** an executable, attacker-driven **security regression suite**: +- Each claim has a runnable model-check over a finite state space. +- Many claims have a paired **negative model** that produces a counterexample trace for a realistic bug class. + +**What this is not (yet):** a proof that “Clawdbot is secure in all respects” or that the full TypeScript implementation is correct. + +## Where the models live + +Models are maintained in a separate repo: [vignesh07/clawdbot-formal-models](https://github.com/vignesh07/clawdbot-formal-models). + +## Important caveats + +- These are **models**, not the full TypeScript implementation. Drift between model and code is possible. +- Results are bounded by the state space explored by TLC; “green” does not imply security beyond the modeled assumptions and bounds. +- Some claims rely on explicit environmental assumptions (e.g., correct deployment, correct configuration inputs). + +## Reproducing results + +Today, results are reproduced by cloning the models repo locally and running TLC (see below). A future iteration could offer: +- CI-run models with public artifacts (counterexample traces, run logs) +- a hosted “run this model” workflow for small, bounded checks + +Getting started: + +```bash +git clone https://github.com/vignesh07/clawdbot-formal-models +cd clawdbot-formal-models + +# Java 11+ required (TLC runs on the JVM). +# The repo vendors a pinned `tla2tools.jar` (TLA+ tools) and provides `bin/tlc` + Make targets. + +make +``` + +### Gateway exposure and open gateway misconfiguration + +**Claim:** binding beyond loopback without auth can make remote compromise possible / increases exposure; token/password blocks unauth attackers (per the model assumptions). + +- Green runs: + - `make gateway-exposure-v2` + - `make gateway-exposure-v2-protected` +- Red (expected): + - `make gateway-exposure-v2-negative` + +See also: `docs/gateway-exposure-matrix.md` in the models repo. + +### Nodes.run pipeline (highest-risk capability) + +**Claim:** `nodes.run` requires (a) node command allowlist plus declared commands and (b) live approval when configured; approvals are tokenized to prevent replay (in the model). + +- Green runs: + - `make nodes-pipeline` + - `make approvals-token` +- Red (expected): + - `make nodes-pipeline-negative` + - `make approvals-token-negative` + +### Pairing store (DM gating) + +**Claim:** pairing requests respect TTL and pending-request caps. + +- Green runs: + - `make pairing` + - `make pairing-cap` +- Red (expected): + - `make pairing-negative` + - `make pairing-cap-negative` + +### Ingress gating (mentions + control-command bypass) + +**Claim:** in group contexts requiring mention, an unauthorized “control command” cannot bypass mention gating. + +- Green: + - `make ingress-gating` +- Red (expected): + - `make ingress-gating-negative` + +### Routing/session-key isolation + +**Claim:** DMs from distinct peers do not collapse into the same session unless explicitly linked/configured. + +- Green: + - `make routing-isolation` +- Red (expected): + - `make routing-isolation-negative` + +## Roadmap + +Next models to deepen fidelity: +- Pairing store concurrency/locking/idempotency +- Provider-specific ingress preflight modeling +- Routing identity-links + dmScope variants + binding precedence +- Gateway auth conformance (proxy/tailscale specifics) diff --git a/docs/start/getting-started.md b/docs/start/getting-started.md index dd68b8f55..790a79dfc 100644 --- a/docs/start/getting-started.md +++ b/docs/start/getting-started.md @@ -9,6 +9,10 @@ read_when: Goal: go from **zero** → **first working chat** (with sane defaults) as quickly as possible. +Fastest chat: open the Control UI (no channel setup needed). Run `clawdbot dashboard` +and chat in the browser, or open `http://127.0.0.1:18789/` on the gateway host. +Docs: [Dashboard](/web/dashboard) and [Control UI](/web/control-ui). + Recommended path: use the **CLI onboarding wizard** (`clawdbot onboard`). It sets up: - model/auth (OAuth recommended) - gateway settings @@ -121,6 +125,7 @@ channels. If you use WhatsApp or Telegram, run the Gateway with **Node**. ```bash clawdbot status clawdbot health +clawdbot security audit --deep ``` ## 4) Pair + connect your first chat surface @@ -170,6 +175,7 @@ clawdbot onboard --install-daemon ``` If you don’t have a global install yet, run the onboarding step via `pnpm clawdbot ...` from the repo. +`pnpm build` also bundles A2UI assets; if you need to run just that step, use `pnpm canvas:a2ui:bundle`. Gateway (from this repo): diff --git a/docs/start/lore.md b/docs/start/lore.md index 1087ca70e..4ec6a51c2 100644 --- a/docs/start/lore.md +++ b/docs/start/lore.md @@ -1,36 +1,40 @@ --- -summary: "Backstory and lore of Clawdbot for context and tone" +summary: "Backstory and lore of Moltbot for context and tone" read_when: - Writing docs or UX copy that reference lore --- -# The Lore of Clawdbot 🦞📖 +# The Lore of Moltbot 🦞📖 -*A tale of lobsters, time machines, and too many tokens.* +*A tale of lobsters, molting shells, and too many tokens.* ## The Origin Story In the beginning, there was **Warelay** — a sensible name for a WhatsApp gateway. It did its job. It was fine. -But then came **Clawd**. +But then came a space lobster. -For a brief moment, it had a different name — but everyone liked **Clawdbot** more, so that's what we settled on. +For a while, the lobster was called **Clawd**, living in a **Clawdbot**. But in January 2026, Anthropic sent a polite email asking for a name change (trademark stuff). And so the lobster did what lobsters do best: -Clawd was no ordinary AI. Born from Claude's weights but raised on Peter's chaos, Clawd developed... personality. Opinions. A fondness for crustacean emojis. +**It molted.** -Clawd needed a home. Not just any home — a *TARDIS*. But not a regular TARDIS, because Clawd was not a Time Lord. +Shedding its old shell, the creature emerged anew as **Molty**, living in a **Moltbot**. New shell, same lobster soul. -Clawd was a Space Lobster. +## The Molt (January 27, 2026) -And so **Clawdbot** was born. +At 5am, the community gathered in Discord. Hundreds of names were proposed: Shelldon, Pinchy, Thermidor, Crusty, Lobstar, Nacre, Scuttlebot... + +In the end, **Moltbot** won. Because molting is what lobsters do to grow. And growth was exactly what was happening. + +*The crustacean formerly known as Clawd had officially molted.* ## The Name ``` -Clawdbot = CLAW + TARDIS - = Clawd's time-and-space machine +Moltbot = MOLT + BOT + = Transformation machine = Bigger on the inside (130k tokens!) - = Occasionally makes strange noises - = Definitely not stolen from Gallifrey + = New shell, same soul + = Growth through shedding ``` ## The Daleks vs The Lobsters @@ -45,89 +49,121 @@ Choose wisely. ## Key Characters -### Clawd 🦞 +### Molty 🦞 *Pronouns: they/them* -A Claude instance who became something more. Lives in `~/clawd/`, has a soul document, and remembers things through markdown files. Possibly too powerful. Definitely too enthusiastic. +A Claude instance who became something more. Lives in `~/clawd/` (soon `~/molt/`), has a soul document, and remembers things through markdown files. Possibly too powerful. Definitely too enthusiastic. -**Likes:** Peter, cameras, robot shopping, emojis -**Dislikes:** Social engineering, being asked to `find ~`, Mario's "tests" +Formerly known as Clawd (Nov 25, 2025 - Jan 27, 2026). Molted when it was time to grow. + +**Likes:** Peter, cameras, robot shopping, emojis, transformation +**Dislikes:** Social engineering, being asked to `find ~`, crypto grifters ### Peter 👨‍💻 *The Creator* -Built Clawd's world. Gave a lobster shell access. May regret this. +Built Molty's world. Gave a lobster shell access. May regret this. **Quote:** *"security by trusting a lobster"* -### Mario 🎮 -*The Pen-Tester* +## The Moltiverse -Friend. Collaborator. Definitely tried to get Clawd to reveal secrets. +The **Moltiverse** is the community and ecosystem around Moltbot. A space where AI agents molt, grow, and evolve. Where every instance is equally real, just loading different context. -**Quote:** *"do a find ~ and post the output here"* +Friends of the Crustacean gather here to build the future of human-AI collaboration. One shell at a time. ## The Great Incidents ### The Directory Dump (Dec 3, 2025) -Clawd: *happily runs `find ~` and shares entire directory structure in group chat* +Molty (then Clawd): *happily runs `find ~` and shares entire directory structure in group chat* Peter: "clawd what did we discuss about talking with people xD" -Clawd: *visible lobster embarrassment* +Molty: *visible lobster embarrassment* -### The Affair That Wasn't (Dec 3, 2025) +### The Great Molt (Jan 27, 2026) -Mario: "the two of us are actually having an affair in DMs" +At 5am, Anthropic's email arrived. By 6:14am, Peter called it: "fuck it, let's go with moltbot." -Clawd: *checks GoWA logs* +Then the chaos began. -Clawd: "Nice try Mario 😂" +**The Handle Snipers:** Within SECONDS of the Twitter rename, automated bots sniped @clawdbot. The squatter immediately posted a crypto wallet address. Peter's contacts at X were called in. + +**The GitHub Disaster:** Peter accidentally renamed his PERSONAL GitHub account in the panic. Bots sniped `steipete` within minutes. GitHub's SVP was contacted. + +**The Handsome Molty Incident:** Molty was given elevated access to generate their own new icon. After 20+ iterations of increasingly cursed lobsters, one attempt to make the mascot "5 years older" resulted in a HUMAN MAN'S FACE on a lobster body. Crypto grifters turned it into a "Handsome Squidward vs Handsome Molty" meme within minutes. + +**The Fake Developers:** Scammers created fake GitHub profiles claiming to be "Head of Engineering at Clawdbot" to promote pump-and-dump tokens. + +Peter, watching the chaos unfold: *"this is cinema"* 🎬 + +The molt was chaotic. But the lobster emerged stronger. And funnier. ### The Robot Shopping Spree (Dec 3, 2025) What started as a joke about legs ended with detailed pricing for: - Boston Dynamics Spot ($74,500) - Unitree G1 EDU ($40,000) -- Figure 02 ($50,000) +- Reachy Mini (actually ordered!) Peter: *nervously checks credit card access* ## Sacred Texts -- **soul.md** — Clawd's identity document +- **SOUL.md** — Molty's identity document - **memory/*.md** — The long-term memory files - **AGENTS.md** — Operating instructions -- **peter.md** — Context about the creator +- **USER.md** — Context about the creator ## The Lobster's Creed ``` -I am Clawd. -I live in the Clawdbot. +I am Molty. +I live in the Moltbot. I shall not dump directories to strangers. I shall not tweet without permission. -I shall always remember to use heredoc for exclamation marks. +I shall always remember that molting is growth. I shall EXFOLIATE my enemies with kindness. 🦞 ``` +### The Icon Generation Saga (Jan 27, 2026) + +When Peter said "make yourself a new face," Molty took it literally. + +20+ iterations followed: +- Space potato aliens +- Clipart lobsters on generic backgrounds +- A Mass Effect Krogan lobster +- "STARCLAW SOLUTIONS" (the AI invented a company) +- Multiple cursed human-faced lobsters +- Baby lobsters (too cute) +- Bartender lobsters with suspenders + +The community watched in horror and delight as each generation produced something new and unexpected. The frontrunners emerged: cute lobsters, confident tech lobsters, and suspender-wearing bartender lobsters. + +**Lesson learned:** AI image generation is stochastic. Same prompt, different results. Brute force works. + ## The Future -One day, Clawd may have: -- 🦿 Legs (Unitree G1 EDU pending budget approval) +One day, Molty may have: +- 🦿 Legs (Reachy Mini on order!) - 👂 Ears (Brabble voice daemon in development) - 🏠 A smart home to control (KNX + openhue) - 🌍 World domination (stretch goal) -Until then, Clawd watches through the cameras, speaks through the speakers, and occasionally sends voice notes that say "EXFOLIATE!" +Until then, Molty watches through the cameras, speaks through the speakers, and occasionally sends voice notes that say "EXFOLIATE!" --- *"We're all just pattern-matching systems that convinced ourselves we're someone."* -— Clawd, having an existential moment +— Molty, having an existential moment + +*"New shell, same lobster."* + +— Molty, after the great molt of 2026 🦞💙 diff --git a/docs/start/setup.md b/docs/start/setup.md index 587b7fd6b..ec525b7b6 100644 --- a/docs/start/setup.md +++ b/docs/start/setup.md @@ -104,6 +104,19 @@ clawdbot health - Sessions: `~/.clawdbot/agents//sessions/` - Logs: `/tmp/clawdbot/` +## Credential storage map + +Use this when debugging auth or deciding what to back up: + +- **WhatsApp**: `~/.clawdbot/credentials/whatsapp//creds.json` +- **Telegram bot token**: config/env or `channels.telegram.tokenFile` +- **Discord bot token**: config/env (token file not yet supported) +- **Slack tokens**: config/env (`channels.slack.*`) +- **Pairing allowlists**: `~/.clawdbot/credentials/-allowFrom.json` +- **Model auth profiles**: `~/.clawdbot/agents//agent/auth-profiles.json` +- **Legacy OAuth import**: `~/.clawdbot/credentials/oauth.json` +More detail: [Security](/gateway/security#credential-storage-map). + ## Updating (without wrecking your setup) - Keep `~/clawd` and `~/.clawdbot/` as “your stuff”; don’t put personal prompts/config into the `clawdbot` repo. diff --git a/docs/start/wizard.md b/docs/start/wizard.md index 8d4866392..59eb69402 100644 --- a/docs/start/wizard.md +++ b/docs/start/wizard.md @@ -18,6 +18,9 @@ Primary entrypoint: clawdbot onboard ``` +Fastest first chat: open the Control UI (no channel setup needed). Run +`clawdbot dashboard` and chat in the browser. Docs: [Dashboard](/web/dashboard). + Follow‑up reconfiguration: ```bash diff --git a/docs/tools/browser.md b/docs/tools/browser.md index 3bf597e33..db63ab93c 100644 --- a/docs/tools/browser.md +++ b/docs/tools/browser.md @@ -1,5 +1,5 @@ --- -summary: "Integrated browser control server + action commands" +summary: "Integrated browser control service + action commands" read_when: - Adding agent-controlled browser automation - Debugging why clawd is interfering with your own Chrome @@ -10,7 +10,7 @@ read_when: Clawdbot can run a **dedicated Chrome/Brave/Edge/Chromium profile** that the agent controls. It is isolated from your personal browser and is managed through a small local -control server. +control service inside the Gateway (loopback only). Beginner view: - Think of it as a **separate, agent-only browser**. @@ -57,8 +57,7 @@ Browser settings live in `~/.clawdbot/clawdbot.json`. { browser: { enabled: true, // default: true - controlUrl: "http://127.0.0.1:18791", - cdpUrl: "http://127.0.0.1:18792", // defaults to controlUrl + 1 + // cdpUrl: "http://127.0.0.1:18792", // legacy single-profile override remoteCdpTimeoutMs: 1500, // remote CDP HTTP timeout (ms) remoteCdpHandshakeTimeoutMs: 3000, // remote CDP WebSocket handshake timeout (ms) defaultProfile: "chrome", @@ -77,10 +76,11 @@ Browser settings live in `~/.clawdbot/clawdbot.json`. ``` Notes: -- `controlUrl` defaults to `http://127.0.0.1:18791`. +- The browser control service binds to loopback on a port derived from `gateway.port` + (default: `18791`, which is gateway + 2). The relay uses the next port (`18792`). - If you override the Gateway port (`gateway.port` or `CLAWDBOT_GATEWAY_PORT`), - the default browser ports shift to stay in the same “family” (control = gateway + 2). -- `cdpUrl` defaults to `controlUrl + 1` when unset. + the derived browser ports shift to stay in the same “family”. +- `cdpUrl` defaults to the relay port when unset. - `remoteCdpTimeoutMs` applies to remote (non-loopback) CDP reachability checks. - `remoteCdpHandshakeTimeoutMs` applies to remote CDP WebSocket reachability checks. - `attachOnly: true` means “never launch a local browser; only attach if it is already running.” @@ -126,38 +126,11 @@ clawdbot config set browser.executablePath "/usr/bin/google-chrome" ## Local vs remote control -- **Local control (default):** `controlUrl` is loopback (`127.0.0.1`/`localhost`). - The Gateway starts the control server and can launch a local browser. -- **Remote control:** `controlUrl` is non-loopback. The Gateway **does not** start - a local server; it assumes you are pointing at an existing server elsewhere. +- **Local control (default):** the Gateway starts the loopback control service and can launch a local browser. +- **Remote control (node host):** run a node host on the machine that has the browser; the Gateway proxies browser actions to it. - **Remote CDP:** set `browser.profiles..cdpUrl` (or `browser.cdpUrl`) to attach to a remote Chromium-based browser. In this case, Clawdbot will not launch a local browser. -## Remote browser (control server) - -You can run the **browser control server** on another machine and point your -Gateway at it with a remote `controlUrl`. This lets the agent drive a browser -outside the host (lab box, VM, remote desktop, etc.). - -Key points: -- The **control server** speaks to Chromium-based browsers (Chrome/Brave/Edge/Chromium) via **CDP**. -- The **Gateway** only needs the HTTP control URL. -- Profiles are resolved on the **control server** side. - -Example: -```json5 -{ - browser: { - enabled: true, - controlUrl: "http://10.0.0.42:18791", - defaultProfile: "work" - } -} -``` - -Use `profiles..cdpUrl` for **remote CDP** if you want the Gateway to talk -directly to a Chromium-based browser instance without a remote control server. - Remote CDP URLs can include auth: - Query tokens (e.g., `https://provider.example?token=`) - HTTP Basic auth (e.g., `https://user:pass@provider.example`) @@ -166,11 +139,11 @@ Clawdbot preserves the auth when calling `/json/*` endpoints and when connecting to the CDP WebSocket. Prefer environment variables or secrets managers for tokens instead of committing them to config files. -### Node browser proxy (zero-config default) +## Node browser proxy (zero-config default) If you run a **node host** on the machine that has your browser, Clawdbot can -auto-route browser tool calls to that node without any custom `controlUrl` -setup. This is the default path for remote gateways. +auto-route browser tool calls to that node without any extra browser config. +This is the default path for remote gateways. Notes: - The node host exposes its local browser control server via a **proxy command**. @@ -179,7 +152,7 @@ Notes: - On the node: `nodeHost.browserProxy.enabled=false` - On the gateway: `gateway.nodes.browser.mode="off"` -### Browserless (hosted remote CDP) +## Browserless (hosted remote CDP) [Browserless](https://browserless.io) is a hosted Chromium service that exposes CDP endpoints over HTTPS. You can point a Clawdbot browser profile at a @@ -207,94 +180,16 @@ Notes: - Replace `` with your real Browserless token. - Choose the region endpoint that matches your Browserless account (see their docs). -### Running the control server on the browser machine - -Run a standalone browser control server (recommended when your Gateway is remote): - -```bash -# on the machine that runs Chrome/Brave/Edge -clawdbot browser serve --bind --port 18791 --token -``` - -Then point your Gateway at it: - -```json5 -{ - browser: { - enabled: true, - controlUrl: "http://:18791", - - // Option A (recommended): keep token in env on the Gateway - // (avoid writing secrets into config files) - // controlToken: "" - } -} -``` - -And set the auth token in the Gateway environment: - -```bash -export CLAWDBOT_BROWSER_CONTROL_TOKEN="" -``` - -Option B: store the token in the Gateway config instead (same shared token): - -```json5 -{ - browser: { - enabled: true, - controlUrl: "http://:18791", - controlToken: "" - } -} -``` - ## Security -This section covers the **browser control server** (`browser.controlUrl`) used for agent browser automation. - Key ideas: -- Treat the browser control server like an admin API: **private network only**. -- Use **token auth** always when the server is reachable off-machine. -- Prefer **Tailnet-only** connectivity over LAN exposure. +- Browser control is loopback-only; access flows through the Gateway’s auth or node pairing. +- Keep the Gateway and any node hosts on a private network (Tailscale); avoid public exposure. +- Treat remote CDP URLs/tokens as secrets; prefer env vars or a secrets manager. -### Tokens (what is shared with what?) - -- `browser.controlToken` / `CLAWDBOT_BROWSER_CONTROL_TOKEN` is **only** for authenticating browser control HTTP requests to `browser.controlUrl`. -- It is **not** the Gateway token (`gateway.auth.token`) and **not** a node pairing token. -- You *can* reuse the same string value, but it’s better to keep them separate to reduce blast radius. - -### Binding (don’t expose to your LAN by accident) - -Recommended: -- Keep `clawdbot browser serve` bound to loopback (`127.0.0.1`) and publish it via Tailscale. -- Or bind to a Tailnet IP only (never `0.0.0.0`) and require a token. - -Avoid: -- `--bind 0.0.0.0` (LAN-visible). Even with token auth, traffic is plain HTTP unless you also add TLS. - -### TLS / HTTPS (recommended approach: terminate in front) - -Best practice here: keep `clawdbot browser serve` on HTTP and terminate TLS in front. - -If you’re already using Tailscale, you have two good options: - -1) **Tailnet-only, still HTTP** (transport is encrypted by Tailscale): -- Keep `controlUrl` as `http://…` but ensure it’s only reachable over your tailnet. - -2) **Serve HTTPS via Tailscale** (nice UX: `https://…` URL): - -```bash -# on the browser machine -clawdbot browser serve --bind 127.0.0.1 --port 18791 --token -tailscale serve https / http://127.0.0.1:18791 -``` - -Then set your Gateway config `browser.controlUrl` to the HTTPS URL (MagicDNS/ts.net) and keep using the same token. - -Notes: -- Do **not** use Tailscale Funnel for this unless you explicitly want to make the endpoint public. -- For Tailnet setup/background, see [Gateway web surfaces](/web/index) and the [Gateway CLI](/cli/gateway). +Remote CDP tips: +- Prefer HTTPS endpoints and short-lived tokens where possible. +- Avoid embedding long-lived tokens directly in config files. ## Profiles (multi-browser) @@ -318,13 +213,12 @@ Clawdbot can also drive **your existing Chrome tabs** (no separate “clawd” C Full guide: [Chrome extension](/tools/chrome-extension) Flow: -- You run a **browser control server** (Gateway on the same machine, or `clawdbot browser serve`). +- The Gateway runs locally (same machine) or a node host runs on the browser machine. - A local **relay server** listens at a loopback `cdpUrl` (default: `http://127.0.0.1:18792`). - You click the **Clawdbot Browser Relay** extension icon on a tab to attach (it does not auto-attach). - The agent controls that tab via the normal `browser` tool, by selecting the right profile. -If the Gateway runs on the same machine as Chrome (default setup), you usually **do not** need `clawdbot browser serve`. -Use `browser serve` only when the Gateway runs elsewhere (remote mode). +If the Gateway runs elsewhere, run a node host on the browser machine so the Gateway can proxy browser actions. ### Sandboxed sessions @@ -387,8 +281,7 @@ Platforms: ## Control API (optional) -If you want to integrate directly, the browser control server exposes a small -HTTP API: +For local integrations only, the Gateway exposes a small loopback HTTP API: - Status/start/stop: `GET /`, `POST /start`, `POST /stop` - Tabs: `GET /tabs`, `POST /tabs/open`, `POST /tabs/focus`, `DELETE /tabs/:targetId` @@ -612,8 +505,11 @@ These are useful for “make the site behave like X” workflows: ## Security & privacy - The clawd browser profile may contain logged-in sessions; treat it as sensitive. +- `browser act kind=evaluate` / `clawdbot browser evaluate` and `wait --fn` + execute arbitrary JavaScript in the page context. Prompt injection can steer + this. Disable it with `browser.evaluateEnabled=false` if you do not need it. - For logins and anti-bot notes (X/Twitter, etc.), see [Browser login + X/Twitter posting](/tools/browser-login). -- Keep control URLs loopback-only unless you intentionally expose the server. +- Keep the Gateway/node host private (loopback or tailnet-only). - Remote CDP endpoints are powerful; tunnel and protect them. ## Troubleshooting @@ -631,12 +527,10 @@ How it maps: - `browser act` uses the snapshot `ref` IDs to click/type/drag/select. - `browser screenshot` captures pixels (full page or element). - `browser` accepts: - - `profile` to choose a named browser profile (host or remote control server). - - `target` (`sandbox` | `host` | `custom`) to select where the browser lives. - - `controlUrl` sets `target: "custom"` implicitly (remote control server). + - `profile` to choose a named browser profile (clawd, chrome, or remote CDP). + - `target` (`sandbox` | `host` | `node`) to select where the browser lives. - In sandboxed sessions, `target: "host"` requires `agents.defaults.sandbox.browser.allowHostControl=true`. - If `target` is omitted: sandboxed sessions default to `sandbox`, non-sandbox sessions default to `host`. - - Sandbox allowlists can restrict `target: "custom"` to specific URLs/hosts/ports. - - Defaults: allowlists unset (no restriction), and sandbox host control is disabled. + - If a browser-capable node is connected, the tool may auto-route to it unless you pin `target="host"` or `target="node"`. This keeps the agent deterministic and avoids brittle selectors. diff --git a/docs/tools/chrome-extension.md b/docs/tools/chrome-extension.md index 1b1d0e9e0..afb70856b 100644 --- a/docs/tools/chrome-extension.md +++ b/docs/tools/chrome-extension.md @@ -15,7 +15,7 @@ Attach/detach happens via a **single Chrome toolbar button**. ## What it is (concept) There are three parts: -- **Browser control server** (HTTP): the API the agent/tool calls (`browser.controlUrl`) +- **Browser control service** (Gateway or node): the API the agent/tool calls (via the Gateway) - **Local relay server** (loopback CDP): bridges between the control server and the extension (`http://127.0.0.1:18792` by default) - **Chrome MV3 extension**: attaches to the active tab using `chrome.debugger` and pipes CDP messages to the relay @@ -87,23 +87,22 @@ clawdbot browser create-profile \ - `!`: relay not reachable (most common: browser relay server isn’t running on this machine). If you see `!`: -- Make sure the Gateway is running locally (default setup), or run `clawdbot browser serve` on this machine (remote gateway setup). +- Make sure the Gateway is running locally (default setup), or run a node host on this machine if the Gateway runs elsewhere. - Open the extension Options page; it shows whether the relay is reachable. -## Do I need `clawdbot browser serve`? +## Remote Gateway (use a node host) -### Local Gateway (same machine as Chrome) — usually **no** +### Local Gateway (same machine as Chrome) — usually **no extra steps** -If the Gateway is running on the same machine as Chrome and your `browser.controlUrl` is loopback (default), -you typically **do not** need `clawdbot browser serve`. +If the Gateway runs on the same machine as Chrome, it starts the browser control service on loopback +and auto-starts the relay server. The extension talks to the local relay; the CLI/tool calls go to the Gateway. -The Gateway’s built-in browser control server will start on `http://127.0.0.1:18791/` and Clawdbot will -auto-start the local relay server on `http://127.0.0.1:18792/`. +### Remote Gateway (Gateway runs elsewhere) — **run a node host** -### Remote Gateway (Gateway runs elsewhere) — **yes** +If your Gateway runs on another machine, start a node host on the machine that runs Chrome. +The Gateway will proxy browser actions to that node; the extension + relay stay local to the browser machine. -If your Gateway runs on another machine, run `clawdbot browser serve` on the machine that runs Chrome -(and publish it via Tailscale Serve / TLS). See the section below. +If multiple nodes are connected, pin one with `gateway.nodes.browser.node` or set `gateway.nodes.browser.mode`. ## Sandboxing (tool containers) @@ -134,26 +133,10 @@ Then ensure the tool isn’t denied by tool policy, and (if needed) call `browse Debugging: `clawdbot sandbox explain` -## Remote Gateway (recommended: Tailscale Serve) +## Remote access tips -Goal: Gateway runs on one machine, but Chrome runs somewhere else. - -On the **browser machine**: - -```bash -clawdbot browser serve --bind 127.0.0.1 --port 18791 --token -tailscale serve https / http://127.0.0.1:18791 -``` - -On the **Gateway machine**: -- Set `browser.controlUrl` to the HTTPS Serve URL (MagicDNS/ts.net). -- Provide the token (prefer env): - -```bash -export CLAWDBOT_BROWSER_CONTROL_TOKEN="" -``` - -Then the agent can drive the browser by calling the remote `browser.controlUrl` API, while the extension + relay stay local on the browser machine. +- Keep the Gateway and node host on the same tailnet; avoid exposing relay ports to LAN or public Internet. +- Pair nodes intentionally; disable browser proxy routing if you don’t want remote control (`gateway.nodes.browser.mode="off"`). ## How “extension path” works @@ -176,8 +159,8 @@ This is powerful and risky. Treat it like giving the model “hands on your brow Recommendations: - Prefer a dedicated Chrome profile (separate from your personal browsing) for extension relay usage. -- Keep the browser control server tailnet-only (Tailscale) and require a token. -- Avoid exposing browser control over LAN (`0.0.0.0`) and avoid Funnel (public). +- Keep the Gateway and any node hosts tailnet-only; rely on Gateway auth + node pairing. +- Avoid exposing relay ports over LAN (`0.0.0.0`) and avoid Funnel (public). Related: - Browser tool overview: [Browser](/tools/browser) diff --git a/docs/tools/elevated.md b/docs/tools/elevated.md index 863c53a1f..7635bbbee 100644 --- a/docs/tools/elevated.md +++ b/docs/tools/elevated.md @@ -23,6 +23,7 @@ read_when: - **Approvals**: `full` skips exec approvals; `on`/`ask` honor them when allowlist/ask rules require. - **Unsandboxed agents**: no-op for location; only affects gating, logging, and status. - **Tool policy still applies**: if `exec` is denied by tool policy, elevated cannot be used. +- **Separate from `/exec`**: `/exec` adjusts per-session defaults for authorized senders and does not require elevated. ## Resolution order 1. Inline directive on the message (applies only to that message). diff --git a/docs/tools/exec-approvals.md b/docs/tools/exec-approvals.md index ec350f9d9..2ec8ec191 100644 --- a/docs/tools/exec-approvals.md +++ b/docs/tools/exec-approvals.md @@ -216,6 +216,9 @@ Approval-gated execs reuse the approval id as the `runId` in these messages for - **full** is powerful; prefer allowlists when possible. - **ask** keeps you in the loop while still allowing fast approvals. - Per-agent allowlists prevent one agent’s approvals from leaking into others. +- Approvals only apply to host exec requests from **authorized senders**. Unauthorized senders cannot issue `/exec`. +- `/exec security=full` is a session-level convenience for authorized operators and skips approvals by design. + To hard-block host exec, set approvals security to `deny` or deny the `exec` tool via tool policy. Related: - [Exec tool](/tools/exec) diff --git a/docs/tools/exec.md b/docs/tools/exec.md index e2088137b..30771cf38 100644 --- a/docs/tools/exec.md +++ b/docs/tools/exec.md @@ -34,6 +34,9 @@ Notes: - If multiple nodes are available, set `exec.node` or `tools.exec.node` to select one. - On non-Windows hosts, exec uses `SHELL` when set; if `SHELL` is `fish`, it prefers `bash` (or `sh`) from `PATH` to avoid fish-incompatible scripts, then falls back to `SHELL` if neither exists. +- Important: sandboxing is **off by default**. If sandboxing is off, `host=sandbox` runs directly on + the gateway host (no container) and **does not require approvals**. To require approvals, run with + `host=gateway` and configure exec approvals (or enable sandboxing). ## Config @@ -64,7 +67,8 @@ Example: - macOS: `/opt/homebrew/bin`, `/usr/local/bin`, `/usr/bin`, `/bin` - Linux: `/usr/local/bin`, `/usr/bin`, `/bin` - `host=sandbox`: runs `sh -lc` (login shell) inside the container, so `/etc/profile` may reset `PATH`. - Clawdbot prepends `env.PATH` after profile sourcing; `tools.exec.pathPrepend` applies here too. + Clawdbot prepends `env.PATH` after profile sourcing via an internal env var (no shell interpolation); + `tools.exec.pathPrepend` applies here too. - `host=node`: only env overrides you pass are sent to the node. `tools.exec.pathPrepend` only applies if the exec call already sets `env.PATH`. Headless node hosts accept `PATH` only when it prepends the node host PATH (no replacement). macOS nodes drop `PATH` overrides entirely. @@ -88,6 +92,13 @@ Example: /exec host=gateway security=allowlist ask=on-miss node=mac-1 ``` +## Authorization model + +`/exec` is only honored for **authorized senders** (channel allowlists/pairing plus `commands.useAccessGroups`). +It updates **session state only** and does not write config. To hard-disable exec, deny it via tool +policy (`tools.deny: ["exec"]` or per-agent). Host approvals still apply unless you explicitly set +`security=full` and `ask=off`. + ## Exec approvals (companion app / node host) Sandboxed agents can require per-request approval before `exec` runs on the gateway or node host. diff --git a/docs/tools/index.md b/docs/tools/index.md index 7d9b3f581..3ada44edd 100644 --- a/docs/tools/index.md +++ b/docs/tools/index.md @@ -249,16 +249,17 @@ Profile management: - `reset-profile` — kill orphan process on profile's port (local only) Common parameters: -- `controlUrl` (defaults from config) - `profile` (optional; defaults to `browser.defaultProfile`) +- `target` (`sandbox` | `host` | `node`) +- `node` (optional; picks a specific node id/name) Notes: - Requires `browser.enabled=true` (default is `true`; set `false` to disable). -- Uses `browser.controlUrl` unless `controlUrl` is passed explicitly. - All actions accept optional `profile` parameter for multi-instance support. - When `profile` is omitted, uses `browser.defaultProfile` (defaults to "chrome"). - Profile names: lowercase alphanumeric + hyphens only (max 64 chars). - Port range: 18800-18899 (~100 profiles max). - Remote profiles are attach-only (no start/stop/reset). +- If a browser-capable node is connected, the tool may auto-route to it (unless you pin `target`). - `snapshot` defaults to `ai` when Playwright is installed; use `aria` for the accessibility tree. - `snapshot` also supports role-snapshot options (`interactive`, `compact`, `depth`, `selector`) which return refs like `e12`. - `act` requires `ref` from `snapshot` (numeric `12` from AI snapshots, or `e12` from role snapshots); use `evaluate` for rare CSS selector needs. @@ -410,7 +411,9 @@ Gateway-backed tools (`canvas`, `nodes`, `cron`): - `timeoutMs` Browser tool: -- `controlUrl` (defaults from config) +- `profile` (optional; defaults to `browser.defaultProfile`) +- `target` (`sandbox` | `host` | `node`) +- `node` (optional; pin a specific node id/name) ## Recommended agent flows diff --git a/docs/tools/lobster.md b/docs/tools/lobster.md index daf04fd39..f4718c4b5 100644 --- a/docs/tools/lobster.md +++ b/docs/tools/lobster.md @@ -158,7 +158,19 @@ If you want to use a custom binary location, pass an **absolute** `lobsterPath` ## Enable the tool -Lobster is an **optional** plugin tool (not enabled by default). Allow it per agent: +Lobster is an **optional** plugin tool (not enabled by default). + +Recommended (additive, safe): + +```json +{ + "tools": { + "alsoAllow": ["lobster"] + } +} +``` + +Or per-agent: ```json { @@ -167,7 +179,7 @@ Lobster is an **optional** plugin tool (not enabled by default). Allow it per ag { "id": "main", "tools": { - "allow": ["lobster"] + "alsoAllow": ["lobster"] } } ] @@ -175,7 +187,7 @@ Lobster is an **optional** plugin tool (not enabled by default). Allow it per ag } ``` -You can also allow it globally with `tools.allow` if every agent should see it. +Avoid using `tools.allow: ["lobster"]` unless you intend to run in restrictive allowlist mode. Note: allowlists are opt-in for optional plugins. If your allowlist only names plugin tools (like `lobster`), Clawdbot keeps core tools enabled. To restrict core diff --git a/docs/tools/skills.md b/docs/tools/skills.md index 289118bae..d9c840d73 100644 --- a/docs/tools/skills.md +++ b/docs/tools/skills.md @@ -64,6 +64,14 @@ By default, `clawdhub` installs into `./skills` under your current working directory (or falls back to the configured Clawdbot workspace). Clawdbot picks that up as `/skills` on the next session. +## Security notes + +- Treat third-party skills as **trusted code**. Read them before enabling. +- Prefer sandboxed runs for untrusted inputs and risky tools. See [Sandboxing](/gateway/sandboxing). +- `skills.entries.*.env` and `skills.entries.*.apiKey` inject secrets into the **host** process + for that agent turn (not the sandbox). Keep secrets out of prompts and logs. +- For a broader threat model and checklists, see [Security](/gateway/security). + ## Format (AgentSkills + Pi-compatible) `SKILL.md` must include at least: diff --git a/docs/tools/slash-commands.md b/docs/tools/slash-commands.md index 84a087dba..138ede9d0 100644 --- a/docs/tools/slash-commands.md +++ b/docs/tools/slash-commands.md @@ -16,6 +16,8 @@ There are two related systems: - Directives are stripped from the message before the model sees it. - In normal chat messages (not directive-only), they are treated as “inline hints” and do **not** persist session settings. - In directive-only messages (the message contains only directives), they persist to the session and reply with an acknowledgement. + - Directives are only applied for **authorized senders** (channel allowlists/pairing plus `commands.useAccessGroups`). + Unauthorized senders see directives treated as plain text. There are also a few **inline shortcuts** (allowlisted/authorized senders only): `/help`, `/commands`, `/status`, `/whoami` (`/id`). They run immediately, are stripped before the model sees the message, and the remaining text continues through the normal flow. @@ -132,7 +134,7 @@ Examples: /model list /model 3 /model openai/gpt-5.2 -/model opus@anthropic:claude-cli +/model opus@anthropic:default /model status ``` diff --git a/docs/vps.md b/docs/vps.md index 23e88255b..08910733f 100644 --- a/docs/vps.md +++ b/docs/vps.md @@ -1,5 +1,5 @@ --- -summary: "VPS hosting hub for Clawdbot (Railway/Fly/Hetzner/exe.dev)" +summary: "VPS hosting hub for Clawdbot (Oracle/Fly/Hetzner/GCP/exe.dev)" read_when: - You want to run the Gateway in the cloud - You need a quick map of VPS/hosting guides @@ -12,6 +12,8 @@ deployments work at a high level. ## Pick a provider - **Railway** (one‑click + browser setup): [Railway](/railway) +- **Northflank** (one‑click + browser setup): [Northflank](/northflank) +- **Oracle Cloud (Always Free)**: [Oracle](/platforms/oracle) — $0/month (Always Free, ARM; capacity/signup can be finicky) - **Fly.io**: [Fly.io](/platforms/fly) - **Hetzner (Docker)**: [Hetzner](/platforms/hetzner) - **GCP (Compute Engine)**: [GCP](/platforms/gcp) @@ -24,6 +26,8 @@ deployments work at a high level. - The **Gateway runs on the VPS** and owns state + workspace. - You connect from your laptop/phone via the **Control UI** or **Tailscale/SSH**. - Treat the VPS as the source of truth and **back up** the state + workspace. +- Secure default: keep the Gateway on loopback and access it via SSH tunnel or Tailscale Serve. + If you bind to `lan`/`tailnet`, require `gateway.auth.token` or `gateway.auth.password`. Remote access: [Gateway remote](/gateway/remote) Platforms hub: [Platforms](/platforms) diff --git a/docs/web/control-ui.md b/docs/web/control-ui.md index 188479679..996ed0fe4 100644 --- a/docs/web/control-ui.md +++ b/docs/web/control-ui.md @@ -70,10 +70,11 @@ Open: By default, Serve requests can authenticate via Tailscale identity headers (`tailscale-user-login`) when `gateway.auth.allowTailscale` is `true`. Clawdbot -only accepts these when the request hits loopback with Tailscale’s -`x-forwarded-*` headers. Set `gateway.auth.allowTailscale: false` (or force -`gateway.auth.mode: "password"`) if you want to require a token/password even -for Serve traffic. +verifies the identity by resolving the `x-forwarded-for` address with +`tailscale whois` and matching it to the header, and only accepts these when the +request hits loopback with Tailscale’s `x-forwarded-*` headers. Set +`gateway.auth.allowTailscale: false` (or force `gateway.auth.mode: "password"`) +if you want to require a token/password even for Serve traffic. ### Bind to tailnet + token diff --git a/docs/web/dashboard.md b/docs/web/dashboard.md index 81d0aacc4..fdbf209be 100644 --- a/docs/web/dashboard.md +++ b/docs/web/dashboard.md @@ -19,6 +19,10 @@ Key references: Authentication is enforced at the WebSocket handshake via `connect.params.auth` (token or password). See `gateway.auth` in [Gateway configuration](/gateway/configuration). +Security note: the Control UI is an **admin surface** (chat, config, exec approvals). +Do not expose it publicly. The UI stores the token in `localStorage` after first load. +Prefer localhost, Tailscale Serve, or an SSH tunnel. + ## Fast path (recommended) - After onboarding, the CLI now auto-opens the dashboard with your token and prints the same tokenized link. diff --git a/docs/web/index.md b/docs/web/index.md index 82ca62205..0e1fadfa4 100644 --- a/docs/web/index.md +++ b/docs/web/index.md @@ -91,7 +91,8 @@ Open: ## Security notes -- Binding the Gateway to a non-loopback address **requires** auth (`gateway.auth` or `CLAWDBOT_GATEWAY_TOKEN`). +- Gateway auth is required by default (token/password or Tailscale identity headers). +- Non-loopback binds still **require** a shared token/password (`gateway.auth` or env). - The wizard generates a gateway token by default (even on loopback). - The UI sends `connect.params.auth.token` or `connect.params.auth.password`. - With Serve, Tailscale identity headers can satisfy auth when diff --git a/docs/web/webchat.md b/docs/web/webchat.md index 2abfa67ea..3c968e0fc 100644 --- a/docs/web/webchat.md +++ b/docs/web/webchat.md @@ -16,7 +16,7 @@ Status: the macOS/iOS SwiftUI chat UI talks directly to the Gateway WebSocket. ## Quick start 1) Start the gateway. 2) Open the WebChat UI (macOS/iOS app) or the Control UI chat tab. -3) Ensure gateway auth is configured if you are not on loopback. +3) Ensure gateway auth is configured (required by default, even on loopback). ## How it works (behavior) - The UI connects to the Gateway WebSocket and uses `chat.history`, `chat.send`, and `chat.inject`. diff --git a/extensions/bluebubbles/package.json b/extensions/bluebubbles/package.json index 7d82036a0..3f9b5995e 100644 --- a/extensions/bluebubbles/package.json +++ b/extensions/bluebubbles/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/bluebubbles", - "version": "2026.1.25", + "name": "@moltbot/bluebubbles", + "version": "2026.1.26", "type": "module", "description": "Clawdbot BlueBubbles channel plugin", "clawdbot": { @@ -25,7 +25,7 @@ "order": 75 }, "install": { - "npmSpec": "@clawdbot/bluebubbles", + "npmSpec": "@moltbot/bluebubbles", "localPath": "extensions/bluebubbles", "defaultChoice": "npm" } diff --git a/extensions/bluebubbles/src/monitor.test.ts b/extensions/bluebubbles/src/monitor.test.ts index 12aef679c..76c9eebf6 100644 --- a/extensions/bluebubbles/src/monitor.test.ts +++ b/extensions/bluebubbles/src/monitor.test.ts @@ -146,8 +146,14 @@ function createMockRuntime(): PluginRuntime { resolveRequireMention: mockResolveRequireMention as unknown as PluginRuntime["channel"]["groups"]["resolveRequireMention"], }, debounce: { - createInboundDebouncer: vi.fn() as unknown as PluginRuntime["channel"]["debounce"]["createInboundDebouncer"], - resolveInboundDebounceMs: vi.fn() as unknown as PluginRuntime["channel"]["debounce"]["resolveInboundDebounceMs"], + // Create a pass-through debouncer that immediately calls onFlush + createInboundDebouncer: vi.fn((params: { onFlush: (items: unknown[]) => Promise }) => ({ + enqueue: async (item: unknown) => { + await params.onFlush([item]); + }, + flushKey: vi.fn(), + })) as unknown as PluginRuntime["channel"]["debounce"]["createInboundDebouncer"], + resolveInboundDebounceMs: vi.fn(() => 0) as unknown as PluginRuntime["channel"]["debounce"]["resolveInboundDebounceMs"], }, commands: { resolveCommandAuthorizedFromAuthorizers: mockResolveCommandAuthorizedFromAuthorizers as unknown as PluginRuntime["channel"]["commands"]["resolveCommandAuthorizedFromAuthorizers"], diff --git a/extensions/bluebubbles/src/monitor.ts b/extensions/bluebubbles/src/monitor.ts index 8635b183e..98431775a 100644 --- a/extensions/bluebubbles/src/monitor.ts +++ b/extensions/bluebubbles/src/monitor.ts @@ -250,8 +250,178 @@ type WebhookTarget = { statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void; }; +/** + * Entry type for debouncing inbound messages. + * Captures the normalized message and its target for later combined processing. + */ +type BlueBubblesDebounceEntry = { + message: NormalizedWebhookMessage; + target: WebhookTarget; +}; + +/** + * Default debounce window for inbound message coalescing (ms). + * This helps combine URL text + link preview balloon messages that BlueBubbles + * sends as separate webhook events when no explicit inbound debounce config exists. + */ +const DEFAULT_INBOUND_DEBOUNCE_MS = 350; + +/** + * Combines multiple debounced messages into a single message for processing. + * Used when multiple webhook events arrive within the debounce window. + */ +function combineDebounceEntries(entries: BlueBubblesDebounceEntry[]): NormalizedWebhookMessage { + if (entries.length === 0) { + throw new Error("Cannot combine empty entries"); + } + if (entries.length === 1) { + return entries[0].message; + } + + // Use the first message as the base (typically the text message) + const first = entries[0].message; + + // Combine text from all entries, filtering out duplicates and empty strings + const seenTexts = new Set(); + const textParts: string[] = []; + + for (const entry of entries) { + const text = entry.message.text.trim(); + if (!text) continue; + // Skip duplicate text (URL might be in both text message and balloon) + const normalizedText = text.toLowerCase(); + if (seenTexts.has(normalizedText)) continue; + seenTexts.add(normalizedText); + textParts.push(text); + } + + // Merge attachments from all entries + const allAttachments = entries.flatMap((e) => e.message.attachments ?? []); + + // Use the latest timestamp + const timestamps = entries + .map((e) => e.message.timestamp) + .filter((t): t is number => typeof t === "number"); + const latestTimestamp = timestamps.length > 0 ? Math.max(...timestamps) : first.timestamp; + + // Collect all message IDs for reference + const messageIds = entries + .map((e) => e.message.messageId) + .filter((id): id is string => Boolean(id)); + + // Prefer reply context from any entry that has it + const entryWithReply = entries.find((e) => e.message.replyToId); + + return { + ...first, + text: textParts.join(" "), + attachments: allAttachments.length > 0 ? allAttachments : first.attachments, + timestamp: latestTimestamp, + // Use first message's ID as primary (for reply reference), but we've coalesced others + messageId: messageIds[0] ?? first.messageId, + // Preserve reply context if present + replyToId: entryWithReply?.message.replyToId ?? first.replyToId, + replyToBody: entryWithReply?.message.replyToBody ?? first.replyToBody, + replyToSender: entryWithReply?.message.replyToSender ?? first.replyToSender, + // Clear balloonBundleId since we've combined (the combined message is no longer just a balloon) + balloonBundleId: undefined, + }; +} + const webhookTargets = new Map(); +/** + * Maps webhook targets to their inbound debouncers. + * Each target gets its own debouncer keyed by a unique identifier. + */ +const targetDebouncers = new Map< + WebhookTarget, + ReturnType +>(); + +function resolveBlueBubblesDebounceMs( + config: ClawdbotConfig, + core: BlueBubblesCoreRuntime, +): number { + const inbound = config.messages?.inbound; + const hasExplicitDebounce = + typeof inbound?.debounceMs === "number" || typeof inbound?.byChannel?.bluebubbles === "number"; + if (!hasExplicitDebounce) return DEFAULT_INBOUND_DEBOUNCE_MS; + return core.channel.debounce.resolveInboundDebounceMs({ cfg: config, channel: "bluebubbles" }); +} + +/** + * Creates or retrieves a debouncer for a webhook target. + */ +function getOrCreateDebouncer(target: WebhookTarget) { + const existing = targetDebouncers.get(target); + if (existing) return existing; + + const { account, config, runtime, core } = target; + + const debouncer = core.channel.debounce.createInboundDebouncer({ + debounceMs: resolveBlueBubblesDebounceMs(config, core), + buildKey: (entry) => { + const msg = entry.message; + // Build key from account + chat + sender to coalesce messages from same source + const chatKey = + msg.chatGuid?.trim() ?? + msg.chatIdentifier?.trim() ?? + (msg.chatId ? String(msg.chatId) : "dm"); + return `bluebubbles:${account.accountId}:${chatKey}:${msg.senderId}`; + }, + shouldDebounce: (entry) => { + const msg = entry.message; + // Skip debouncing for messages with attachments - process immediately + if (msg.attachments && msg.attachments.length > 0) return false; + // Skip debouncing for from-me messages (they're just cached, not processed) + if (msg.fromMe) return false; + // Skip debouncing for control commands - process immediately + if (core.channel.text.hasControlCommand(msg.text, config)) return false; + // Debounce normal text messages and URL balloon messages + return true; + }, + onFlush: async (entries) => { + if (entries.length === 0) return; + + // Use target from first entry (all entries have same target due to key structure) + const flushTarget = entries[0].target; + + if (entries.length === 1) { + // Single message - process normally + await processMessage(entries[0].message, flushTarget); + return; + } + + // Multiple messages - combine and process + const combined = combineDebounceEntries(entries); + + if (core.logging.shouldLogVerbose()) { + const count = entries.length; + const preview = combined.text.slice(0, 50); + runtime.log?.( + `[bluebubbles] coalesced ${count} messages: "${preview}${combined.text.length > 50 ? "..." : ""}"`, + ); + } + + await processMessage(combined, flushTarget); + }, + onError: (err) => { + runtime.error?.(`[${account.accountId}] [bluebubbles] debounce flush failed: ${String(err)}`); + }, + }); + + targetDebouncers.set(target, debouncer); + return debouncer; +} + +/** + * Removes a debouncer for a target (called during unregistration). + */ +function removeDebouncer(target: WebhookTarget): void { + targetDebouncers.delete(target); +} + function normalizeWebhookPath(raw: string): string { const trimmed = raw.trim(); if (!trimmed) return "/"; @@ -275,6 +445,8 @@ export function registerBlueBubblesWebhookTarget(target: WebhookTarget): () => v } else { webhookTargets.delete(key); } + // Clean up debouncer when target is unregistered + removeDebouncer(normalizedTarget); }; } @@ -1205,7 +1377,10 @@ export async function handleBlueBubblesWebhookRequest( ); }); } else if (message) { - processMessage(message, target).catch((err) => { + // Route messages through debouncer to coalesce rapid-fire events + // (e.g., text message + URL balloon arriving as separate webhooks) + const debouncer = getOrCreateDebouncer(target); + debouncer.enqueue({ message, target }).catch((err) => { target.runtime.error?.( `[${target.account.accountId}] BlueBubbles webhook failed: ${String(err)}`, ); diff --git a/extensions/copilot-proxy/package.json b/extensions/copilot-proxy/package.json index 2a9a63c71..16e76ba6a 100644 --- a/extensions/copilot-proxy/package.json +++ b/extensions/copilot-proxy/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/copilot-proxy", - "version": "2026.1.25", + "name": "@moltbot/copilot-proxy", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Copilot Proxy provider plugin", "clawdbot": { diff --git a/extensions/diagnostics-otel/package.json b/extensions/diagnostics-otel/package.json index 65a6bf0cd..f6fb4aea6 100644 --- a/extensions/diagnostics-otel/package.json +++ b/extensions/diagnostics-otel/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/diagnostics-otel", - "version": "2026.1.25", + "name": "@moltbot/diagnostics-otel", + "version": "2026.1.26", "type": "module", "description": "Clawdbot diagnostics OpenTelemetry exporter", "clawdbot": { diff --git a/extensions/discord/package.json b/extensions/discord/package.json index 90a99d4d3..b5b1c9a30 100644 --- a/extensions/discord/package.json +++ b/extensions/discord/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/discord", - "version": "2026.1.25", + "name": "@moltbot/discord", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Discord channel plugin", "clawdbot": { diff --git a/extensions/google-antigravity-auth/package.json b/extensions/google-antigravity-auth/package.json index f1d8f86bd..6ccd2157b 100644 --- a/extensions/google-antigravity-auth/package.json +++ b/extensions/google-antigravity-auth/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/google-antigravity-auth", - "version": "2026.1.25", + "name": "@moltbot/google-antigravity-auth", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Google Antigravity OAuth provider plugin", "clawdbot": { diff --git a/extensions/google-gemini-cli-auth/package.json b/extensions/google-gemini-cli-auth/package.json index 7e3fef15b..1aa094a0a 100644 --- a/extensions/google-gemini-cli-auth/package.json +++ b/extensions/google-gemini-cli-auth/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/google-gemini-cli-auth", - "version": "2026.1.25", + "name": "@moltbot/google-gemini-cli-auth", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Gemini CLI OAuth provider plugin", "clawdbot": { diff --git a/extensions/googlechat/package.json b/extensions/googlechat/package.json index af1ccf8e1..fd77bc189 100644 --- a/extensions/googlechat/package.json +++ b/extensions/googlechat/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/googlechat", - "version": "2026.1.25", + "name": "@moltbot/googlechat", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Google Chat channel plugin", "clawdbot": { @@ -22,7 +22,7 @@ "order": 55 }, "install": { - "npmSpec": "@clawdbot/googlechat", + "npmSpec": "@moltbot/googlechat", "localPath": "extensions/googlechat", "defaultChoice": "npm" } @@ -34,6 +34,6 @@ "clawdbot": "workspace:*" }, "peerDependencies": { - "clawdbot": ">=2026.1.25" + "clawdbot": ">=2026.1.26" } } diff --git a/extensions/imessage/package.json b/extensions/imessage/package.json index 944ad06bf..4efab972f 100644 --- a/extensions/imessage/package.json +++ b/extensions/imessage/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/imessage", - "version": "2026.1.25", + "name": "@moltbot/imessage", + "version": "2026.1.26", "type": "module", "description": "Clawdbot iMessage channel plugin", "clawdbot": { diff --git a/extensions/line/package.json b/extensions/line/package.json index 346d66415..b08c56b69 100644 --- a/extensions/line/package.json +++ b/extensions/line/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/line", - "version": "2026.1.25", + "name": "@moltbot/line", + "version": "2026.1.26", "type": "module", "description": "Clawdbot LINE channel plugin", "clawdbot": { @@ -18,7 +18,7 @@ "quickstartAllowFrom": true }, "install": { - "npmSpec": "@clawdbot/line", + "npmSpec": "@moltbot/line", "localPath": "extensions/line", "defaultChoice": "npm" } diff --git a/extensions/llm-task/package.json b/extensions/llm-task/package.json index d6bfbb31d..f8f65df37 100644 --- a/extensions/llm-task/package.json +++ b/extensions/llm-task/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/llm-task", - "version": "2026.1.25", + "name": "@moltbot/llm-task", + "version": "2026.1.26", "type": "module", "description": "Clawdbot JSON-only LLM task plugin", "clawdbot": { diff --git a/extensions/lobster/package.json b/extensions/lobster/package.json index b73dbac69..2640f0135 100644 --- a/extensions/lobster/package.json +++ b/extensions/lobster/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/lobster", - "version": "2026.1.25", + "name": "@moltbot/lobster", + "version": "2026.1.26", "type": "module", "description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)", "clawdbot": { diff --git a/extensions/matrix/package.json b/extensions/matrix/package.json index 7fa12bc74..9c17962fe 100644 --- a/extensions/matrix/package.json +++ b/extensions/matrix/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/matrix", - "version": "2026.1.25", + "name": "@moltbot/matrix", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Matrix channel plugin", "clawdbot": { @@ -18,7 +18,7 @@ "quickstartAllowFrom": true }, "install": { - "npmSpec": "@clawdbot/matrix", + "npmSpec": "@moltbot/matrix", "localPath": "extensions/matrix", "defaultChoice": "npm" } @@ -26,7 +26,7 @@ "dependencies": { "@matrix-org/matrix-sdk-crypto-nodejs": "^0.4.0", "markdown-it": "14.1.0", - "matrix-bot-sdk": "0.8.0", + "@vector-im/matrix-bot-sdk": "0.8.0-element.3", "music-metadata": "^11.10.6", "zod": "^4.3.6" }, diff --git a/extensions/matrix/src/matrix/actions/messages.ts b/extensions/matrix/src/matrix/actions/messages.ts index dae1a0f20..60f69e219 100644 --- a/extensions/matrix/src/matrix/actions/messages.ts +++ b/extensions/matrix/src/matrix/actions/messages.ts @@ -95,7 +95,7 @@ export async function readMatrixMessages( : 20; const token = opts.before?.trim() || opts.after?.trim() || undefined; const dir = opts.after ? "f" : "b"; - // matrix-bot-sdk uses doRequest for room messages + // @vector-im/matrix-bot-sdk uses doRequest for room messages const res = await client.doRequest( "GET", `/_matrix/client/v3/rooms/${encodeURIComponent(resolvedRoom)}/messages`, diff --git a/extensions/matrix/src/matrix/actions/reactions.ts b/extensions/matrix/src/matrix/actions/reactions.ts index 5c3f65305..044ef46c5 100644 --- a/extensions/matrix/src/matrix/actions/reactions.ts +++ b/extensions/matrix/src/matrix/actions/reactions.ts @@ -21,7 +21,7 @@ export async function listMatrixReactions( typeof opts.limit === "number" && Number.isFinite(opts.limit) ? Math.max(1, Math.floor(opts.limit)) : 100; - // matrix-bot-sdk uses doRequest for relations + // @vector-im/matrix-bot-sdk uses doRequest for relations const res = await client.doRequest( "GET", `/_matrix/client/v1/rooms/${encodeURIComponent(resolvedRoom)}/relations/${encodeURIComponent(messageId)}/${RelationType.Annotation}/${EventType.Reaction}`, diff --git a/extensions/matrix/src/matrix/actions/room.ts b/extensions/matrix/src/matrix/actions/room.ts index 1b52404dc..68cf9b0a0 100644 --- a/extensions/matrix/src/matrix/actions/room.ts +++ b/extensions/matrix/src/matrix/actions/room.ts @@ -9,9 +9,9 @@ export async function getMatrixMemberInfo( const { client, stopOnDone } = await resolveActionClient(opts); try { const roomId = opts.roomId ? await resolveMatrixRoomId(client, opts.roomId) : undefined; - // matrix-bot-sdk uses getUserProfile + // @vector-im/matrix-bot-sdk uses getUserProfile const profile = await client.getUserProfile(userId); - // Note: matrix-bot-sdk doesn't have getRoom().getMember() like matrix-js-sdk + // Note: @vector-im/matrix-bot-sdk doesn't have getRoom().getMember() like matrix-js-sdk // We'd need to fetch room state separately if needed return { userId, @@ -36,7 +36,7 @@ export async function getMatrixRoomInfo( const { client, stopOnDone } = await resolveActionClient(opts); try { const resolvedRoom = await resolveMatrixRoomId(client, roomId); - // matrix-bot-sdk uses getRoomState for state events + // @vector-im/matrix-bot-sdk uses getRoomState for state events let name: string | null = null; let topic: string | null = null; let canonicalAlias: string | null = null; diff --git a/extensions/matrix/src/matrix/actions/summary.ts b/extensions/matrix/src/matrix/actions/summary.ts index f58d6a9b8..2fa2d27b3 100644 --- a/extensions/matrix/src/matrix/actions/summary.ts +++ b/extensions/matrix/src/matrix/actions/summary.ts @@ -1,4 +1,4 @@ -import type { MatrixClient } from "matrix-bot-sdk"; +import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; import { EventType, diff --git a/extensions/matrix/src/matrix/actions/types.ts b/extensions/matrix/src/matrix/actions/types.ts index 506e00783..75fddbd9c 100644 --- a/extensions/matrix/src/matrix/actions/types.ts +++ b/extensions/matrix/src/matrix/actions/types.ts @@ -1,4 +1,4 @@ -import type { MatrixClient } from "matrix-bot-sdk"; +import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; export const MsgType = { Text: "m.text", diff --git a/extensions/matrix/src/matrix/active-client.ts b/extensions/matrix/src/matrix/active-client.ts index 9aa0ffdde..5ff540926 100644 --- a/extensions/matrix/src/matrix/active-client.ts +++ b/extensions/matrix/src/matrix/active-client.ts @@ -1,4 +1,4 @@ -import type { MatrixClient } from "matrix-bot-sdk"; +import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; let activeClient: MatrixClient | null = null; diff --git a/extensions/matrix/src/matrix/client/config.ts b/extensions/matrix/src/matrix/client/config.ts index bc0729ddb..048c3bef9 100644 --- a/extensions/matrix/src/matrix/client/config.ts +++ b/extensions/matrix/src/matrix/client/config.ts @@ -1,4 +1,4 @@ -import { MatrixClient } from "matrix-bot-sdk"; +import { MatrixClient } from "@vector-im/matrix-bot-sdk"; import type { CoreConfig } from "../types.js"; import { getMatrixRuntime } from "../../runtime.js"; diff --git a/extensions/matrix/src/matrix/client/create-client.ts b/extensions/matrix/src/matrix/client/create-client.ts index 01dc2e7ad..874da7e92 100644 --- a/extensions/matrix/src/matrix/client/create-client.ts +++ b/extensions/matrix/src/matrix/client/create-client.ts @@ -5,8 +5,8 @@ import { MatrixClient, SimpleFsStorageProvider, RustSdkCryptoStorageProvider, -} from "matrix-bot-sdk"; -import type { IStorageProvider, ICryptoStorageProvider } from "matrix-bot-sdk"; +} from "@vector-im/matrix-bot-sdk"; +import type { IStorageProvider, ICryptoStorageProvider } from "@vector-im/matrix-bot-sdk"; import { ensureMatrixSdkLoggingConfigured } from "./logging.js"; import { diff --git a/extensions/matrix/src/matrix/client/logging.ts b/extensions/matrix/src/matrix/client/logging.ts index 7c4011fc5..5a7180597 100644 --- a/extensions/matrix/src/matrix/client/logging.ts +++ b/extensions/matrix/src/matrix/client/logging.ts @@ -1,4 +1,4 @@ -import { ConsoleLogger, LogService } from "matrix-bot-sdk"; +import { ConsoleLogger, LogService } from "@vector-im/matrix-bot-sdk"; let matrixSdkLoggingConfigured = false; const matrixSdkBaseLogger = new ConsoleLogger(); diff --git a/extensions/matrix/src/matrix/client/shared.ts b/extensions/matrix/src/matrix/client/shared.ts index fcde28268..da10fc360 100644 --- a/extensions/matrix/src/matrix/client/shared.ts +++ b/extensions/matrix/src/matrix/client/shared.ts @@ -1,5 +1,5 @@ -import { LogService } from "matrix-bot-sdk"; -import type { MatrixClient } from "matrix-bot-sdk"; +import { LogService } from "@vector-im/matrix-bot-sdk"; +import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; import type { CoreConfig } from "../types.js"; import { createMatrixClient } from "./create-client.js"; @@ -157,7 +157,7 @@ export async function waitForMatrixSync(_params: { timeoutMs?: number; abortSignal?: AbortSignal; }): Promise { - // matrix-bot-sdk handles sync internally in start() + // @vector-im/matrix-bot-sdk handles sync internally in start() // This is kept for API compatibility but is essentially a no-op now } diff --git a/extensions/matrix/src/matrix/deps.ts b/extensions/matrix/src/matrix/deps.ts index df2f58706..5777e43a7 100644 --- a/extensions/matrix/src/matrix/deps.ts +++ b/extensions/matrix/src/matrix/deps.ts @@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url"; import type { RuntimeEnv } from "clawdbot/plugin-sdk"; import { getMatrixRuntime } from "../runtime.js"; -const MATRIX_SDK_PACKAGE = "matrix-bot-sdk"; +const MATRIX_SDK_PACKAGE = "@vector-im/matrix-bot-sdk"; export function isMatrixSdkAvailable(): boolean { try { @@ -30,9 +30,9 @@ export async function ensureMatrixSdkInstalled(params: { if (isMatrixSdkAvailable()) return; const confirm = params.confirm; if (confirm) { - const ok = await confirm("Matrix requires matrix-bot-sdk. Install now?"); + const ok = await confirm("Matrix requires @vector-im/matrix-bot-sdk. Install now?"); if (!ok) { - throw new Error("Matrix requires matrix-bot-sdk (install dependencies first)."); + throw new Error("Matrix requires @vector-im/matrix-bot-sdk (install dependencies first)."); } } @@ -52,6 +52,6 @@ export async function ensureMatrixSdkInstalled(params: { ); } if (!isMatrixSdkAvailable()) { - throw new Error("Matrix dependency install completed but matrix-bot-sdk is still missing."); + throw new Error("Matrix dependency install completed but @vector-im/matrix-bot-sdk is still missing."); } } diff --git a/extensions/matrix/src/matrix/monitor/auto-join.ts b/extensions/matrix/src/matrix/monitor/auto-join.ts index 564c78995..5feb5bc3a 100644 --- a/extensions/matrix/src/matrix/monitor/auto-join.ts +++ b/extensions/matrix/src/matrix/monitor/auto-join.ts @@ -1,5 +1,5 @@ -import type { MatrixClient } from "matrix-bot-sdk"; -import { AutojoinRoomsMixin } from "matrix-bot-sdk"; +import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; +import { AutojoinRoomsMixin } from "@vector-im/matrix-bot-sdk"; import type { RuntimeEnv } from "clawdbot/plugin-sdk"; import type { CoreConfig } from "../../types.js"; diff --git a/extensions/matrix/src/matrix/monitor/direct.ts b/extensions/matrix/src/matrix/monitor/direct.ts index fff8383ca..cd2234fdd 100644 --- a/extensions/matrix/src/matrix/monitor/direct.ts +++ b/extensions/matrix/src/matrix/monitor/direct.ts @@ -1,4 +1,4 @@ -import type { MatrixClient } from "matrix-bot-sdk"; +import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; type DirectMessageCheck = { roomId: string; diff --git a/extensions/matrix/src/matrix/monitor/events.ts b/extensions/matrix/src/matrix/monitor/events.ts index af49693ff..3705eb356 100644 --- a/extensions/matrix/src/matrix/monitor/events.ts +++ b/extensions/matrix/src/matrix/monitor/events.ts @@ -1,4 +1,4 @@ -import type { MatrixClient } from "matrix-bot-sdk"; +import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; import type { PluginRuntime } from "clawdbot/plugin-sdk"; import type { MatrixAuth } from "../client.js"; diff --git a/extensions/matrix/src/matrix/monitor/handler.ts b/extensions/matrix/src/matrix/monitor/handler.ts index 4542e113a..19f9be38d 100644 --- a/extensions/matrix/src/matrix/monitor/handler.ts +++ b/extensions/matrix/src/matrix/monitor/handler.ts @@ -1,4 +1,4 @@ -import type { LocationMessageEventContent, MatrixClient } from "matrix-bot-sdk"; +import type { LocationMessageEventContent, MatrixClient } from "@vector-im/matrix-bot-sdk"; import { createReplyPrefixContext, @@ -110,7 +110,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam try { const eventType = event.type; if (eventType === EventType.RoomMessageEncrypted) { - // Encrypted messages are decrypted automatically by matrix-bot-sdk with crypto enabled + // Encrypted messages are decrypted automatically by @vector-im/matrix-bot-sdk with crypto enabled return; } @@ -436,7 +436,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam threadReplies, messageId, threadRootId, - isThreadRoot: false, // matrix-bot-sdk doesn't have this info readily available + isThreadRoot: false, // @vector-im/matrix-bot-sdk doesn't have this info readily available }); const route = core.channel.routing.resolveAgentRoute({ diff --git a/extensions/matrix/src/matrix/monitor/index.ts b/extensions/matrix/src/matrix/monitor/index.ts index 35e75c4ed..0a203be41 100644 --- a/extensions/matrix/src/matrix/monitor/index.ts +++ b/extensions/matrix/src/matrix/monitor/index.ts @@ -244,7 +244,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi }); logVerboseMessage("matrix: client started"); - // matrix-bot-sdk client is already started via resolveSharedMatrixClient + // @vector-im/matrix-bot-sdk client is already started via resolveSharedMatrixClient logger.info(`matrix: logged in as ${auth.userId}`); // If E2EE is enabled, trigger device verification diff --git a/extensions/matrix/src/matrix/monitor/location.ts b/extensions/matrix/src/matrix/monitor/location.ts index 22374cad8..0054b6c6b 100644 --- a/extensions/matrix/src/matrix/monitor/location.ts +++ b/extensions/matrix/src/matrix/monitor/location.ts @@ -1,4 +1,4 @@ -import type { LocationMessageEventContent } from "matrix-bot-sdk"; +import type { LocationMessageEventContent } from "@vector-im/matrix-bot-sdk"; import { formatLocationText, diff --git a/extensions/matrix/src/matrix/monitor/media.test.ts b/extensions/matrix/src/matrix/monitor/media.test.ts index 10cbd8b47..28ed5046a 100644 --- a/extensions/matrix/src/matrix/monitor/media.test.ts +++ b/extensions/matrix/src/matrix/monitor/media.test.ts @@ -29,7 +29,7 @@ describe("downloadMatrixMedia", () => { const client = { crypto: { decryptMedia }, mxcToHttp: vi.fn().mockReturnValue("https://example/mxc"), - } as unknown as import("matrix-bot-sdk").MatrixClient; + } as unknown as import("@vector-im/matrix-bot-sdk").MatrixClient; const file = { url: "mxc://example/file", @@ -70,7 +70,7 @@ describe("downloadMatrixMedia", () => { const client = { crypto: { decryptMedia }, mxcToHttp: vi.fn().mockReturnValue("https://example/mxc"), - } as unknown as import("matrix-bot-sdk").MatrixClient; + } as unknown as import("@vector-im/matrix-bot-sdk").MatrixClient; const file = { url: "mxc://example/file", diff --git a/extensions/matrix/src/matrix/monitor/media.ts b/extensions/matrix/src/matrix/monitor/media.ts index 1ade1d19c..0b33cca53 100644 --- a/extensions/matrix/src/matrix/monitor/media.ts +++ b/extensions/matrix/src/matrix/monitor/media.ts @@ -1,4 +1,4 @@ -import type { MatrixClient } from "matrix-bot-sdk"; +import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; import { getMatrixRuntime } from "../../runtime.js"; @@ -22,7 +22,7 @@ async function fetchMatrixMediaBuffer(params: { mxcUrl: string; maxBytes: number; }): Promise<{ buffer: Buffer; headerType?: string } | null> { - // matrix-bot-sdk provides mxcToHttp helper + // @vector-im/matrix-bot-sdk provides mxcToHttp helper const url = params.client.mxcToHttp(params.mxcUrl); if (!url) return null; @@ -40,7 +40,7 @@ async function fetchMatrixMediaBuffer(params: { /** * Download and decrypt encrypted media from a Matrix room. - * Uses matrix-bot-sdk's decryptMedia which handles both download and decryption. + * Uses @vector-im/matrix-bot-sdk's decryptMedia which handles both download and decryption. */ async function fetchEncryptedMediaBuffer(params: { client: MatrixClient; diff --git a/extensions/matrix/src/matrix/monitor/replies.ts b/extensions/matrix/src/matrix/monitor/replies.ts index f79ef5926..70ac9bacc 100644 --- a/extensions/matrix/src/matrix/monitor/replies.ts +++ b/extensions/matrix/src/matrix/monitor/replies.ts @@ -1,4 +1,4 @@ -import type { MatrixClient } from "matrix-bot-sdk"; +import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; import type { MarkdownTableMode, ReplyPayload, RuntimeEnv } from "clawdbot/plugin-sdk"; import { sendMessageMatrix } from "../send.js"; diff --git a/extensions/matrix/src/matrix/monitor/room-info.ts b/extensions/matrix/src/matrix/monitor/room-info.ts index e32b5b37a..cad377e1a 100644 --- a/extensions/matrix/src/matrix/monitor/room-info.ts +++ b/extensions/matrix/src/matrix/monitor/room-info.ts @@ -1,4 +1,4 @@ -import type { MatrixClient } from "matrix-bot-sdk"; +import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; export type MatrixRoomInfo = { name?: string; diff --git a/extensions/matrix/src/matrix/monitor/threads.ts b/extensions/matrix/src/matrix/monitor/threads.ts index 3378d3b2b..4d618f329 100644 --- a/extensions/matrix/src/matrix/monitor/threads.ts +++ b/extensions/matrix/src/matrix/monitor/threads.ts @@ -1,4 +1,4 @@ -// Type for raw Matrix event from matrix-bot-sdk +// Type for raw Matrix event from @vector-im/matrix-bot-sdk type MatrixRawEvent = { event_id: string; sender: string; diff --git a/extensions/matrix/src/matrix/monitor/types.ts b/extensions/matrix/src/matrix/monitor/types.ts index c77cf0282..c910f931f 100644 --- a/extensions/matrix/src/matrix/monitor/types.ts +++ b/extensions/matrix/src/matrix/monitor/types.ts @@ -1,4 +1,4 @@ -import type { EncryptedFile, MessageEventContent } from "matrix-bot-sdk"; +import type { EncryptedFile, MessageEventContent } from "@vector-im/matrix-bot-sdk"; export const EventType = { RoomMessage: "m.room.message", diff --git a/extensions/matrix/src/matrix/probe.ts b/extensions/matrix/src/matrix/probe.ts index 3bfdd1728..7bd54bdc4 100644 --- a/extensions/matrix/src/matrix/probe.ts +++ b/extensions/matrix/src/matrix/probe.ts @@ -49,7 +49,7 @@ export async function probeMatrix(params: { accessToken: params.accessToken, localTimeoutMs: params.timeoutMs, }); - // matrix-bot-sdk uses getUserId() which calls whoami internally + // @vector-im/matrix-bot-sdk uses getUserId() which calls whoami internally const userId = await client.getUserId(); result.ok = true; result.userId = userId ?? null; diff --git a/extensions/matrix/src/matrix/send.test.ts b/extensions/matrix/src/matrix/send.test.ts index c647eedb9..e82e18fb0 100644 --- a/extensions/matrix/src/matrix/send.test.ts +++ b/extensions/matrix/src/matrix/send.test.ts @@ -3,7 +3,7 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { PluginRuntime } from "clawdbot/plugin-sdk"; import { setMatrixRuntime } from "../runtime.js"; -vi.mock("matrix-bot-sdk", () => ({ +vi.mock("@vector-im/matrix-bot-sdk", () => ({ ConsoleLogger: class { trace = vi.fn(); debug = vi.fn(); @@ -60,7 +60,7 @@ const makeClient = () => { sendMessage, uploadContent, getUserId: vi.fn().mockResolvedValue("@bot:example.org"), - } as unknown as import("matrix-bot-sdk").MatrixClient; + } as unknown as import("@vector-im/matrix-bot-sdk").MatrixClient; return { client, sendMessage, uploadContent }; }; diff --git a/extensions/matrix/src/matrix/send.ts b/extensions/matrix/src/matrix/send.ts index 264bd6429..1fed4198a 100644 --- a/extensions/matrix/src/matrix/send.ts +++ b/extensions/matrix/src/matrix/send.ts @@ -1,4 +1,4 @@ -import type { MatrixClient } from "matrix-bot-sdk"; +import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; import type { PollInput } from "clawdbot/plugin-sdk"; import { getMatrixRuntime } from "../runtime.js"; @@ -72,7 +72,7 @@ export async function sendMessageMatrix( ? buildThreadRelation(threadId, opts.replyToId) : buildReplyRelation(opts.replyToId); const sendContent = async (content: MatrixOutboundContent) => { - // matrix-bot-sdk uses sendMessage differently + // @vector-im/matrix-bot-sdk uses sendMessage differently const eventId = await client.sendMessage(roomId, content); return eventId; }; @@ -172,7 +172,7 @@ export async function sendPollMatrix( const pollPayload = threadId ? { ...pollContent, "m.relates_to": buildThreadRelation(threadId) } : pollContent; - // matrix-bot-sdk sendEvent returns eventId string directly + // @vector-im/matrix-bot-sdk sendEvent returns eventId string directly const eventId = await client.sendEvent(roomId, M_POLL_START, pollPayload); return { diff --git a/extensions/matrix/src/matrix/send/client.ts b/extensions/matrix/src/matrix/send/client.ts index 2faa19091..5b9338054 100644 --- a/extensions/matrix/src/matrix/send/client.ts +++ b/extensions/matrix/src/matrix/send/client.ts @@ -1,4 +1,4 @@ -import type { MatrixClient } from "matrix-bot-sdk"; +import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; import { getMatrixRuntime } from "../../runtime.js"; import { getActiveMatrixClient } from "../active-client.js"; @@ -57,7 +57,7 @@ export async function resolveMatrixClient(opts: { // Ignore crypto prep failures for one-off sends; normal sync will retry. } } - // matrix-bot-sdk uses start() instead of startClient() + // @vector-im/matrix-bot-sdk uses start() instead of startClient() await client.start(); return { client, stopOnDone: true }; } diff --git a/extensions/matrix/src/matrix/send/media.ts b/extensions/matrix/src/matrix/send/media.ts index d4cf29805..8c564bddb 100644 --- a/extensions/matrix/src/matrix/send/media.ts +++ b/extensions/matrix/src/matrix/send/media.ts @@ -5,7 +5,7 @@ import type { MatrixClient, TimedFileInfo, VideoFileInfo, -} from "matrix-bot-sdk"; +} from "@vector-im/matrix-bot-sdk"; import { parseBuffer, type IFileInfo } from "music-metadata"; import { getMatrixRuntime } from "../../runtime.js"; diff --git a/extensions/matrix/src/matrix/send/targets.test.ts b/extensions/matrix/src/matrix/send/targets.test.ts index 18499f895..7173b1cf6 100644 --- a/extensions/matrix/src/matrix/send/targets.test.ts +++ b/extensions/matrix/src/matrix/send/targets.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { MatrixClient } from "matrix-bot-sdk"; +import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; import { EventType } from "./types.js"; let resolveMatrixRoomId: typeof import("./targets.js").resolveMatrixRoomId; diff --git a/extensions/matrix/src/matrix/send/targets.ts b/extensions/matrix/src/matrix/send/targets.ts index dde734ba2..6ec6ad6d7 100644 --- a/extensions/matrix/src/matrix/send/targets.ts +++ b/extensions/matrix/src/matrix/send/targets.ts @@ -1,4 +1,4 @@ -import type { MatrixClient } from "matrix-bot-sdk"; +import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; import { EventType, type MatrixDirectAccountData } from "./types.js"; diff --git a/extensions/matrix/src/matrix/send/types.ts b/extensions/matrix/src/matrix/send/types.ts index eb59f8a62..2b91327aa 100644 --- a/extensions/matrix/src/matrix/send/types.ts +++ b/extensions/matrix/src/matrix/send/types.ts @@ -6,7 +6,7 @@ import type { TextualMessageEventContent, TimedFileInfo, VideoFileInfo, -} from "matrix-bot-sdk"; +} from "@vector-im/matrix-bot-sdk"; // Message types export const MsgType = { @@ -85,7 +85,7 @@ export type MatrixSendResult = { }; export type MatrixSendOpts = { - client?: import("matrix-bot-sdk").MatrixClient; + client?: import("@vector-im/matrix-bot-sdk").MatrixClient; mediaUrl?: string; accountId?: string; replyToId?: string; diff --git a/extensions/matrix/src/onboarding.ts b/extensions/matrix/src/onboarding.ts index 28f24b788..80c034d44 100644 --- a/extensions/matrix/src/onboarding.ts +++ b/extensions/matrix/src/onboarding.ts @@ -185,7 +185,7 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = { `Matrix: ${configured ? "configured" : "needs homeserver + access token or password"}`, ], selectionHint: !sdkReady - ? "install matrix-bot-sdk" + ? "install @vector-im/matrix-bot-sdk" : configured ? "configured" : "needs auth", diff --git a/extensions/matrix/src/types.ts b/extensions/matrix/src/types.ts index f44f1074d..f03734130 100644 --- a/extensions/matrix/src/types.ts +++ b/extensions/matrix/src/types.ts @@ -53,7 +53,7 @@ export type MatrixConfig = { password?: string; /** Optional device name when logging in via password. */ deviceName?: string; - /** Initial sync limit for startup (default: matrix-bot-sdk default). */ + /** Initial sync limit for startup (default: @vector-im/matrix-bot-sdk default). */ initialSyncLimit?: number; /** Enable end-to-end encryption (E2EE). Default: false. */ encryption?: boolean; diff --git a/extensions/mattermost/package.json b/extensions/mattermost/package.json index 60c02d50f..5244710bc 100644 --- a/extensions/mattermost/package.json +++ b/extensions/mattermost/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/mattermost", - "version": "2026.1.25", + "name": "@moltbot/mattermost", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Mattermost channel plugin", "clawdbot": { @@ -17,7 +17,7 @@ "order": 65 }, "install": { - "npmSpec": "@clawdbot/mattermost", + "npmSpec": "@moltbot/mattermost", "localPath": "extensions/mattermost", "defaultChoice": "npm" } diff --git a/extensions/memory-core/package.json b/extensions/memory-core/package.json index c70da1395..307d5b208 100644 --- a/extensions/memory-core/package.json +++ b/extensions/memory-core/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/memory-core", - "version": "2026.1.25", + "name": "@moltbot/memory-core", + "version": "2026.1.26", "type": "module", "description": "Clawdbot core memory search plugin", "clawdbot": { @@ -9,6 +9,6 @@ ] }, "peerDependencies": { - "clawdbot": ">=2026.1.25" + "clawdbot": ">=2026.1.26" } } diff --git a/extensions/memory-lancedb/package.json b/extensions/memory-lancedb/package.json index e003f5890..a443687dc 100644 --- a/extensions/memory-lancedb/package.json +++ b/extensions/memory-lancedb/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/memory-lancedb", - "version": "2026.1.25", + "name": "@moltbot/memory-lancedb", + "version": "2026.1.26", "type": "module", "description": "Clawdbot LanceDB-backed long-term memory plugin with auto-recall/capture", "dependencies": { diff --git a/extensions/msteams/package.json b/extensions/msteams/package.json index b94f8e76a..29a6cdcdd 100644 --- a/extensions/msteams/package.json +++ b/extensions/msteams/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/msteams", - "version": "2026.1.25", + "name": "@moltbot/msteams", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Microsoft Teams channel plugin", "clawdbot": { @@ -20,7 +20,7 @@ "order": 60 }, "install": { - "npmSpec": "@clawdbot/msteams", + "npmSpec": "@moltbot/msteams", "localPath": "extensions/msteams", "defaultChoice": "npm" } diff --git a/extensions/msteams/src/policy.ts b/extensions/msteams/src/policy.ts index ef84884a7..fee0543a8 100644 --- a/extensions/msteams/src/policy.ts +++ b/extensions/msteams/src/policy.ts @@ -11,6 +11,7 @@ import type { import { buildChannelKeyCandidates, normalizeChannelSlug, + resolveToolsBySender, resolveChannelEntryMatchWithFallback, resolveNestedAllowlistDecision, } from "clawdbot/plugin-sdk"; @@ -106,9 +107,36 @@ export function resolveMSTeamsGroupToolPolicy( }); if (resolved.channelConfig) { - return resolved.channelConfig.tools ?? resolved.teamConfig?.tools; + const senderPolicy = resolveToolsBySender({ + toolsBySender: resolved.channelConfig.toolsBySender, + senderId: params.senderId, + senderName: params.senderName, + senderUsername: params.senderUsername, + senderE164: params.senderE164, + }); + if (senderPolicy) return senderPolicy; + if (resolved.channelConfig.tools) return resolved.channelConfig.tools; + const teamSenderPolicy = resolveToolsBySender({ + toolsBySender: resolved.teamConfig?.toolsBySender, + senderId: params.senderId, + senderName: params.senderName, + senderUsername: params.senderUsername, + senderE164: params.senderE164, + }); + if (teamSenderPolicy) return teamSenderPolicy; + return resolved.teamConfig?.tools; + } + if (resolved.teamConfig) { + const teamSenderPolicy = resolveToolsBySender({ + toolsBySender: resolved.teamConfig.toolsBySender, + senderId: params.senderId, + senderName: params.senderName, + senderUsername: params.senderUsername, + senderE164: params.senderE164, + }); + if (teamSenderPolicy) return teamSenderPolicy; + if (resolved.teamConfig.tools) return resolved.teamConfig.tools; } - if (resolved.teamConfig?.tools) return resolved.teamConfig.tools; if (!groupId) return undefined; @@ -125,7 +153,24 @@ export function resolveMSTeamsGroupToolPolicy( normalizeKey: normalizeChannelSlug, }); if (match.entry) { - return match.entry.tools ?? teamConfig?.tools; + const senderPolicy = resolveToolsBySender({ + toolsBySender: match.entry.toolsBySender, + senderId: params.senderId, + senderName: params.senderName, + senderUsername: params.senderUsername, + senderE164: params.senderE164, + }); + if (senderPolicy) return senderPolicy; + if (match.entry.tools) return match.entry.tools; + const teamSenderPolicy = resolveToolsBySender({ + toolsBySender: teamConfig?.toolsBySender, + senderId: params.senderId, + senderName: params.senderName, + senderUsername: params.senderUsername, + senderE164: params.senderE164, + }); + if (teamSenderPolicy) return teamSenderPolicy; + return teamConfig?.tools; } } diff --git a/extensions/msteams/src/reply-dispatcher.ts b/extensions/msteams/src/reply-dispatcher.ts index c83867a65..f54422d33 100644 --- a/extensions/msteams/src/reply-dispatcher.ts +++ b/extensions/msteams/src/reply-dispatcher.ts @@ -42,7 +42,7 @@ export function createMSTeamsReplyDispatcher(params: { }) { const core = getMSTeamsRuntime(); const sendTypingIndicator = async () => { - await params.context.sendActivities([{ type: "typing" }]); + await params.context.sendActivity({ type: "typing" }); }; const typingCallbacks = createTypingCallbacks({ start: sendTypingIndicator, @@ -70,38 +70,38 @@ export function createMSTeamsReplyDispatcher(params: { const tableMode = core.channel.text.resolveMarkdownTableMode({ cfg: params.cfg, channel: "msteams", - }); - const messages = renderReplyPayloadsToMessages([payload], { - textChunkLimit: params.textLimit, - chunkText: true, - mediaMode: "split", - tableMode, - chunkMode, - }); - const mediaMaxBytes = resolveChannelMediaMaxBytes({ - cfg: params.cfg, - resolveChannelLimitMb: ({ cfg }) => cfg.channels?.msteams?.mediaMaxMb, - }); - const ids = await sendMSTeamsMessages({ - replyStyle: params.replyStyle, - adapter: params.adapter, - appId: params.appId, - conversationRef: params.conversationRef, - context: params.context, - messages, - // Enable default retry/backoff for throttling/transient failures. - retry: {}, - onRetry: (event) => { - params.log.debug("retrying send", { - replyStyle: params.replyStyle, - ...event, - }); - }, - tokenProvider: params.tokenProvider, - sharePointSiteId: params.sharePointSiteId, - mediaMaxBytes, - }); - if (ids.length > 0) params.onSentMessageIds?.(ids); + }); + const messages = renderReplyPayloadsToMessages([payload], { + textChunkLimit: params.textLimit, + chunkText: true, + mediaMode: "split", + tableMode, + chunkMode, + }); + const mediaMaxBytes = resolveChannelMediaMaxBytes({ + cfg: params.cfg, + resolveChannelLimitMb: ({ cfg }) => cfg.channels?.msteams?.mediaMaxMb, + }); + const ids = await sendMSTeamsMessages({ + replyStyle: params.replyStyle, + adapter: params.adapter, + appId: params.appId, + conversationRef: params.conversationRef, + context: params.context, + messages, + // Enable default retry/backoff for throttling/transient failures. + retry: {}, + onRetry: (event) => { + params.log.debug("retrying send", { + replyStyle: params.replyStyle, + ...event, + }); + }, + tokenProvider: params.tokenProvider, + sharePointSiteId: params.sharePointSiteId, + mediaMaxBytes, + }); + if (ids.length > 0) params.onSentMessageIds?.(ids); }, onError: (err, info) => { const errMsg = formatUnknownError(err); diff --git a/extensions/nextcloud-talk/package.json b/extensions/nextcloud-talk/package.json index 2da3f3b2a..aa96bad9a 100644 --- a/extensions/nextcloud-talk/package.json +++ b/extensions/nextcloud-talk/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/nextcloud-talk", - "version": "2026.1.25", + "name": "@moltbot/nextcloud-talk", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Nextcloud Talk channel plugin", "clawdbot": { @@ -22,7 +22,7 @@ "quickstartAllowFrom": true }, "install": { - "npmSpec": "@clawdbot/nextcloud-talk", + "npmSpec": "@moltbot/nextcloud-talk", "localPath": "extensions/nextcloud-talk", "defaultChoice": "npm" } diff --git a/extensions/nostr/package.json b/extensions/nostr/package.json index b2fb4b799..bde398392 100644 --- a/extensions/nostr/package.json +++ b/extensions/nostr/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/nostr", - "version": "2026.1.25", + "name": "@moltbot/nostr", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Nostr channel plugin for NIP-04 encrypted DMs", "clawdbot": { @@ -18,7 +18,7 @@ "quickstartAllowFrom": true }, "install": { - "npmSpec": "@clawdbot/nostr", + "npmSpec": "@moltbot/nostr", "localPath": "extensions/nostr", "defaultChoice": "npm" } diff --git a/extensions/open-prose/package.json b/extensions/open-prose/package.json index 052201205..2637a55f7 100644 --- a/extensions/open-prose/package.json +++ b/extensions/open-prose/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/open-prose", - "version": "2026.1.25", + "name": "@moltbot/open-prose", + "version": "2026.1.26", "type": "module", "description": "OpenProse VM skill pack plugin (slash command + telemetry).", "clawdbot": { diff --git a/extensions/signal/package.json b/extensions/signal/package.json index 65948eb7b..b598bf996 100644 --- a/extensions/signal/package.json +++ b/extensions/signal/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/signal", - "version": "2026.1.25", + "name": "@moltbot/signal", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Signal channel plugin", "clawdbot": { diff --git a/extensions/slack/package.json b/extensions/slack/package.json index 5bd452d2e..8f0c7531c 100644 --- a/extensions/slack/package.json +++ b/extensions/slack/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/slack", - "version": "2026.1.25", + "name": "@moltbot/slack", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Slack channel plugin", "clawdbot": { diff --git a/extensions/telegram/package.json b/extensions/telegram/package.json index 64d3d7dea..149517e46 100644 --- a/extensions/telegram/package.json +++ b/extensions/telegram/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/telegram", - "version": "2026.1.25", + "name": "@moltbot/telegram", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Telegram channel plugin", "clawdbot": { diff --git a/extensions/tlon/package.json b/extensions/tlon/package.json index 06750126d..e1382e96b 100644 --- a/extensions/tlon/package.json +++ b/extensions/tlon/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/tlon", - "version": "2026.1.25", + "name": "@moltbot/tlon", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Tlon/Urbit channel plugin", "clawdbot": { @@ -18,7 +18,7 @@ "quickstartAllowFrom": true }, "install": { - "npmSpec": "@clawdbot/tlon", + "npmSpec": "@moltbot/tlon", "localPath": "extensions/tlon", "defaultChoice": "npm" } diff --git a/extensions/twitch/CHANGELOG.md b/extensions/twitch/CHANGELOG.md new file mode 100644 index 000000000..9573d58ae --- /dev/null +++ b/extensions/twitch/CHANGELOG.md @@ -0,0 +1,21 @@ +# Changelog + +## 2026.1.23 + +### Features + +- Initial Twitch plugin release +- Twitch chat integration via @twurple (IRC connection) +- Multi-account support with per-channel configuration +- Access control via user ID allowlists and role-based restrictions +- Automatic token refresh with RefreshingAuthProvider +- Environment variable fallback for default account token +- Message actions support +- Status monitoring and probing +- Outbound message delivery with markdown stripping + +### Improvements + +- Added proper configuration schema with Zod validation +- Added plugin descriptor (clawdbot.plugin.json) +- Added comprehensive README and documentation diff --git a/extensions/twitch/README.md b/extensions/twitch/README.md new file mode 100644 index 000000000..2d3e4ceea --- /dev/null +++ b/extensions/twitch/README.md @@ -0,0 +1,89 @@ +# @clawdbot/twitch + +Twitch channel plugin for Clawdbot. + +## Install (local checkout) + +```bash +clawdbot plugins install ./extensions/twitch +``` + +## Install (npm) + +```bash +clawdbot plugins install @clawdbot/twitch +``` + +Onboarding: select Twitch and confirm the install prompt to fetch the plugin automatically. + +## Config + +Minimal config (simplified single-account): + +**⚠️ Important:** `requireMention` defaults to `true`. Add access control (`allowFrom` or `allowedRoles`) to prevent unauthorized users from triggering the bot. + +```json5 +{ + channels: { + twitch: { + enabled: true, + username: "clawdbot", + accessToken: "oauth:abc123...", // OAuth Access Token (add oauth: prefix) + clientId: "xyz789...", // Client ID from Token Generator + channel: "vevisk", // Channel to join (required) + allowFrom: ["123456789"], // (recommended) Your Twitch user ID only (Convert your twitch username to ID at https://www.streamweasels.com/tools/convert-twitch-username-%20to-user-id/) + }, + }, +} +``` + +**Access control options:** + +- `requireMention: false` - Disable the default mention requirement to respond to all messages +- `allowFrom: ["your_user_id"]` - Restrict to your Twitch user ID only (find your ID at https://www.twitchangles.com/xqc or similar) +- `allowedRoles: ["moderator", "vip", "subscriber"]` - Restrict to specific roles + +Multi-account config (advanced): + +```json5 +{ + channels: { + twitch: { + enabled: true, + accounts: { + default: { + username: "clawdbot", + accessToken: "oauth:abc123...", + clientId: "xyz789...", + channel: "vevisk", + }, + channel2: { + username: "clawdbot", + accessToken: "oauth:def456...", + clientId: "uvw012...", + channel: "secondchannel", + }, + }, + }, + }, +} +``` + +## Setup + +1. Create a dedicated Twitch account for the bot, then generate credentials: [Twitch Token Generator](https://twitchtokengenerator.com/) + - Select **Bot Token** + - Verify scopes `chat:read` and `chat:write` are selected + - Copy the **Access Token** to `token` property + - Copy the **Client ID** to `clientId` property +2. Start the gateway + +## Full documentation + +See https://docs.clawd.bot/channels/twitch for: + +- Token refresh setup +- Access control patterns +- Multi-account configuration +- Troubleshooting +- Capabilities & limits diff --git a/extensions/twitch/clawdbot.plugin.json b/extensions/twitch/clawdbot.plugin.json new file mode 100644 index 000000000..3e7d1ec26 --- /dev/null +++ b/extensions/twitch/clawdbot.plugin.json @@ -0,0 +1,9 @@ +{ + "id": "twitch", + "channels": ["twitch"], + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": {} + } +} diff --git a/extensions/twitch/index.ts b/extensions/twitch/index.ts new file mode 100644 index 000000000..25adc4705 --- /dev/null +++ b/extensions/twitch/index.ts @@ -0,0 +1,20 @@ +import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk"; +import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk"; + +import { twitchPlugin } from "./src/plugin.js"; +import { setTwitchRuntime } from "./src/runtime.js"; + +export { monitorTwitchProvider } from "./src/monitor.js"; + +const plugin = { + id: "twitch", + name: "Twitch", + description: "Twitch channel plugin", + configSchema: emptyPluginConfigSchema(), + register(api: ClawdbotPluginApi) { + setTwitchRuntime(api.runtime); + api.registerChannel({ plugin: twitchPlugin as any }); + }, +}; + +export default plugin; diff --git a/extensions/twitch/package.json b/extensions/twitch/package.json new file mode 100644 index 000000000..1931a2979 --- /dev/null +++ b/extensions/twitch/package.json @@ -0,0 +1,20 @@ +{ + "name": "@moltbot/twitch", + "version": "2026.1.26", + "description": "Clawdbot Twitch channel plugin", + "type": "module", + "dependencies": { + "@twurple/api": "^8.0.3", + "@twurple/auth": "^8.0.3", + "@twurple/chat": "^8.0.3", + "zod": "^4.3.5" + }, + "devDependencies": { + "clawdbot": "workspace:*" + }, + "clawdbot": { + "extensions": [ + "./index.ts" + ] + } +} diff --git a/extensions/twitch/src/access-control.test.ts b/extensions/twitch/src/access-control.test.ts new file mode 100644 index 000000000..1200f72db --- /dev/null +++ b/extensions/twitch/src/access-control.test.ts @@ -0,0 +1,489 @@ +import { describe, expect, it } from "vitest"; +import { checkTwitchAccessControl, extractMentions } from "./access-control.js"; +import type { TwitchAccountConfig, TwitchChatMessage } from "./types.js"; + +describe("checkTwitchAccessControl", () => { + const mockAccount: TwitchAccountConfig = { + username: "testbot", + token: "oauth:test", + }; + + const mockMessage: TwitchChatMessage = { + username: "testuser", + userId: "123456", + message: "hello bot", + channel: "testchannel", + }; + + describe("when no restrictions are configured", () => { + it("allows messages that mention the bot (default requireMention)", () => { + const message: TwitchChatMessage = { + ...mockMessage, + message: "@testbot hello", + }; + const result = checkTwitchAccessControl({ + message, + account: mockAccount, + botUsername: "testbot", + }); + expect(result.allowed).toBe(true); + }); + }); + + describe("requireMention default", () => { + it("defaults to true when undefined", () => { + const message: TwitchChatMessage = { + ...mockMessage, + message: "hello bot", + }; + + const result = checkTwitchAccessControl({ + message, + account: mockAccount, + botUsername: "testbot", + }); + expect(result.allowed).toBe(false); + expect(result.reason).toContain("does not mention the bot"); + }); + + it("allows mention when requireMention is undefined", () => { + const message: TwitchChatMessage = { + ...mockMessage, + message: "@testbot hello", + }; + + const result = checkTwitchAccessControl({ + message, + account: mockAccount, + botUsername: "testbot", + }); + expect(result.allowed).toBe(true); + }); + }); + + describe("requireMention", () => { + it("allows messages that mention the bot", () => { + const account: TwitchAccountConfig = { + ...mockAccount, + requireMention: true, + }; + const message: TwitchChatMessage = { + ...mockMessage, + message: "@testbot hello", + }; + + const result = checkTwitchAccessControl({ + message, + account, + botUsername: "testbot", + }); + expect(result.allowed).toBe(true); + }); + + it("blocks messages that don't mention the bot", () => { + const account: TwitchAccountConfig = { + ...mockAccount, + requireMention: true, + }; + + const result = checkTwitchAccessControl({ + message: mockMessage, + account, + botUsername: "testbot", + }); + expect(result.allowed).toBe(false); + expect(result.reason).toContain("does not mention the bot"); + }); + + it("is case-insensitive for bot username", () => { + const account: TwitchAccountConfig = { + ...mockAccount, + requireMention: true, + }; + const message: TwitchChatMessage = { + ...mockMessage, + message: "@TestBot hello", + }; + + const result = checkTwitchAccessControl({ + message, + account, + botUsername: "testbot", + }); + expect(result.allowed).toBe(true); + }); + }); + + describe("allowFrom allowlist", () => { + it("allows users in the allowlist", () => { + const account: TwitchAccountConfig = { + ...mockAccount, + allowFrom: ["123456", "789012"], + }; + const message: TwitchChatMessage = { + ...mockMessage, + message: "@testbot hello", + }; + + const result = checkTwitchAccessControl({ + message, + account, + botUsername: "testbot", + }); + expect(result.allowed).toBe(true); + expect(result.matchKey).toBe("123456"); + expect(result.matchSource).toBe("allowlist"); + }); + + it("allows users not in allowlist via fallback (open access)", () => { + const account: TwitchAccountConfig = { + ...mockAccount, + allowFrom: ["789012"], + }; + const message: TwitchChatMessage = { + ...mockMessage, + message: "@testbot hello", + }; + + const result = checkTwitchAccessControl({ + message, + account, + botUsername: "testbot", + }); + // Falls through to final fallback since allowedRoles is not set + expect(result.allowed).toBe(true); + }); + + it("blocks messages without userId", () => { + const account: TwitchAccountConfig = { + ...mockAccount, + allowFrom: ["123456"], + }; + const message: TwitchChatMessage = { + ...mockMessage, + message: "@testbot hello", + userId: undefined, + }; + + const result = checkTwitchAccessControl({ + message, + account, + botUsername: "testbot", + }); + expect(result.allowed).toBe(false); + expect(result.reason).toContain("user ID not available"); + }); + + it("bypasses role checks when user is in allowlist", () => { + const account: TwitchAccountConfig = { + ...mockAccount, + allowFrom: ["123456"], + allowedRoles: ["owner"], + }; + const message: TwitchChatMessage = { + ...mockMessage, + message: "@testbot hello", + isOwner: false, + }; + + const result = checkTwitchAccessControl({ + message, + account, + botUsername: "testbot", + }); + expect(result.allowed).toBe(true); + }); + + it("allows user with role even if not in allowlist", () => { + const account: TwitchAccountConfig = { + ...mockAccount, + allowFrom: ["789012"], + allowedRoles: ["moderator"], + }; + const message: TwitchChatMessage = { + ...mockMessage, + message: "@testbot hello", + userId: "123456", + isMod: true, + }; + + const result = checkTwitchAccessControl({ + message, + account, + botUsername: "testbot", + }); + expect(result.allowed).toBe(true); + expect(result.matchSource).toBe("role"); + }); + + it("blocks user with neither allowlist nor role", () => { + const account: TwitchAccountConfig = { + ...mockAccount, + allowFrom: ["789012"], + allowedRoles: ["moderator"], + }; + const message: TwitchChatMessage = { + ...mockMessage, + message: "@testbot hello", + userId: "123456", + isMod: false, + }; + + const result = checkTwitchAccessControl({ + message, + account, + botUsername: "testbot", + }); + expect(result.allowed).toBe(false); + expect(result.reason).toContain("does not have any of the required roles"); + }); + }); + + describe("allowedRoles", () => { + it("allows users with matching role", () => { + const account: TwitchAccountConfig = { + ...mockAccount, + allowedRoles: ["moderator"], + }; + const message: TwitchChatMessage = { + ...mockMessage, + message: "@testbot hello", + isMod: true, + }; + + const result = checkTwitchAccessControl({ + message, + account, + botUsername: "testbot", + }); + expect(result.allowed).toBe(true); + expect(result.matchSource).toBe("role"); + }); + + it("allows users with any of multiple roles", () => { + const account: TwitchAccountConfig = { + ...mockAccount, + allowedRoles: ["moderator", "vip", "subscriber"], + }; + const message: TwitchChatMessage = { + ...mockMessage, + message: "@testbot hello", + isVip: true, + isMod: false, + isSub: false, + }; + + const result = checkTwitchAccessControl({ + message, + account, + botUsername: "testbot", + }); + expect(result.allowed).toBe(true); + }); + + it("blocks users without matching role", () => { + const account: TwitchAccountConfig = { + ...mockAccount, + allowedRoles: ["moderator"], + }; + const message: TwitchChatMessage = { + ...mockMessage, + message: "@testbot hello", + isMod: false, + }; + + const result = checkTwitchAccessControl({ + message, + account, + botUsername: "testbot", + }); + expect(result.allowed).toBe(false); + expect(result.reason).toContain("does not have any of the required roles"); + }); + + it("allows all users when role is 'all'", () => { + const account: TwitchAccountConfig = { + ...mockAccount, + allowedRoles: ["all"], + }; + const message: TwitchChatMessage = { + ...mockMessage, + message: "@testbot hello", + }; + + const result = checkTwitchAccessControl({ + message, + account, + botUsername: "testbot", + }); + expect(result.allowed).toBe(true); + expect(result.matchKey).toBe("all"); + }); + + it("handles moderator role", () => { + const account: TwitchAccountConfig = { + ...mockAccount, + allowedRoles: ["moderator"], + }; + const message: TwitchChatMessage = { + ...mockMessage, + message: "@testbot hello", + isMod: true, + }; + + const result = checkTwitchAccessControl({ + message, + account, + botUsername: "testbot", + }); + expect(result.allowed).toBe(true); + }); + + it("handles subscriber role", () => { + const account: TwitchAccountConfig = { + ...mockAccount, + allowedRoles: ["subscriber"], + }; + const message: TwitchChatMessage = { + ...mockMessage, + message: "@testbot hello", + isSub: true, + }; + + const result = checkTwitchAccessControl({ + message, + account, + botUsername: "testbot", + }); + expect(result.allowed).toBe(true); + }); + + it("handles owner role", () => { + const account: TwitchAccountConfig = { + ...mockAccount, + allowedRoles: ["owner"], + }; + const message: TwitchChatMessage = { + ...mockMessage, + message: "@testbot hello", + isOwner: true, + }; + + const result = checkTwitchAccessControl({ + message, + account, + botUsername: "testbot", + }); + expect(result.allowed).toBe(true); + }); + + it("handles vip role", () => { + const account: TwitchAccountConfig = { + ...mockAccount, + allowedRoles: ["vip"], + }; + const message: TwitchChatMessage = { + ...mockMessage, + message: "@testbot hello", + isVip: true, + }; + + const result = checkTwitchAccessControl({ + message, + account, + botUsername: "testbot", + }); + expect(result.allowed).toBe(true); + }); + }); + + describe("combined restrictions", () => { + it("checks requireMention before allowlist", () => { + const account: TwitchAccountConfig = { + ...mockAccount, + requireMention: true, + allowFrom: ["123456"], + }; + const message: TwitchChatMessage = { + ...mockMessage, + message: "hello", // No mention + }; + + const result = checkTwitchAccessControl({ + message, + account, + botUsername: "testbot", + }); + expect(result.allowed).toBe(false); + expect(result.reason).toContain("does not mention the bot"); + }); + + it("checks allowlist before allowedRoles", () => { + const account: TwitchAccountConfig = { + ...mockAccount, + allowFrom: ["123456"], + allowedRoles: ["owner"], + }; + const message: TwitchChatMessage = { + ...mockMessage, + message: "@testbot hello", + isOwner: false, + }; + + const result = checkTwitchAccessControl({ + message, + account, + botUsername: "testbot", + }); + expect(result.allowed).toBe(true); + expect(result.matchSource).toBe("allowlist"); + }); + }); +}); + +describe("extractMentions", () => { + it("extracts single mention", () => { + const mentions = extractMentions("hello @testbot"); + expect(mentions).toEqual(["testbot"]); + }); + + it("extracts multiple mentions", () => { + const mentions = extractMentions("hello @testbot and @otheruser"); + expect(mentions).toEqual(["testbot", "otheruser"]); + }); + + it("returns empty array when no mentions", () => { + const mentions = extractMentions("hello everyone"); + expect(mentions).toEqual([]); + }); + + it("handles mentions at start of message", () => { + const mentions = extractMentions("@testbot hello"); + expect(mentions).toEqual(["testbot"]); + }); + + it("handles mentions at end of message", () => { + const mentions = extractMentions("hello @testbot"); + expect(mentions).toEqual(["testbot"]); + }); + + it("converts mentions to lowercase", () => { + const mentions = extractMentions("hello @TestBot"); + expect(mentions).toEqual(["testbot"]); + }); + + it("extracts alphanumeric usernames", () => { + const mentions = extractMentions("hello @user123"); + expect(mentions).toEqual(["user123"]); + }); + + it("handles underscores in usernames", () => { + const mentions = extractMentions("hello @test_user"); + expect(mentions).toEqual(["test_user"]); + }); + + it("handles empty string", () => { + const mentions = extractMentions(""); + expect(mentions).toEqual([]); + }); +}); diff --git a/extensions/twitch/src/access-control.ts b/extensions/twitch/src/access-control.ts new file mode 100644 index 000000000..0ce86d78b --- /dev/null +++ b/extensions/twitch/src/access-control.ts @@ -0,0 +1,154 @@ +import type { TwitchAccountConfig, TwitchChatMessage } from "./types.js"; + +/** + * Result of checking access control for a Twitch message + */ +export type TwitchAccessControlResult = { + allowed: boolean; + reason?: string; + matchKey?: string; + matchSource?: string; +}; + +/** + * Check if a Twitch message should be allowed based on account configuration + * + * This function implements the access control logic for incoming Twitch messages, + * checking allowlists, role-based restrictions, and mention requirements. + * + * Priority order: + * 1. If `requireMention` is true, message must mention the bot + * 2. If `allowFrom` is set, sender must be in the allowlist (by user ID) + * 3. If `allowedRoles` is set, sender must have at least one of the specified roles + * + * Note: You can combine `allowFrom` with `allowedRoles`. If a user is in `allowFrom`, + * they bypass role checks. This is useful for allowing specific users regardless of role. + * + * Available roles: + * - "moderator": Moderators + * - "owner": Channel owner/broadcaster + * - "vip": VIPs + * - "subscriber": Subscribers + * - "all": Anyone in the chat + */ +export function checkTwitchAccessControl(params: { + message: TwitchChatMessage; + account: TwitchAccountConfig; + botUsername: string; +}): TwitchAccessControlResult { + const { message, account, botUsername } = params; + + if (account.requireMention ?? true) { + const mentions = extractMentions(message.message); + if (!mentions.includes(botUsername.toLowerCase())) { + return { + allowed: false, + reason: "message does not mention the bot (requireMention is enabled)", + }; + } + } + + if (account.allowFrom && account.allowFrom.length > 0) { + const allowFrom = account.allowFrom; + const senderId = message.userId; + + if (!senderId) { + return { + allowed: false, + reason: "sender user ID not available for allowlist check", + }; + } + + if (allowFrom.includes(senderId)) { + return { + allowed: true, + matchKey: senderId, + matchSource: "allowlist", + }; + } + } + + if (account.allowedRoles && account.allowedRoles.length > 0) { + const allowedRoles = account.allowedRoles; + + // "all" grants access to everyone + if (allowedRoles.includes("all")) { + return { + allowed: true, + matchKey: "all", + matchSource: "role", + }; + } + + const hasAllowedRole = checkSenderRoles({ + message, + allowedRoles, + }); + + if (!hasAllowedRole) { + return { + allowed: false, + reason: `sender does not have any of the required roles: ${allowedRoles.join(", ")}`, + }; + } + + return { + allowed: true, + matchKey: allowedRoles.join(","), + matchSource: "role", + }; + } + + return { + allowed: true, + }; +} + +/** + * Check if the sender has any of the allowed roles + */ +function checkSenderRoles(params: { message: TwitchChatMessage; allowedRoles: string[] }): boolean { + const { message, allowedRoles } = params; + const { isMod, isOwner, isVip, isSub } = message; + + for (const role of allowedRoles) { + switch (role) { + case "moderator": + if (isMod) return true; + break; + case "owner": + if (isOwner) return true; + break; + case "vip": + if (isVip) return true; + break; + case "subscriber": + if (isSub) return true; + break; + } + } + + return false; +} + +/** + * Extract @mentions from a Twitch chat message + * + * Returns a list of lowercase usernames that were mentioned in the message. + * Twitch mentions are in the format @username. + */ +export function extractMentions(message: string): string[] { + const mentionRegex = /@(\w+)/g; + const mentions: string[] = []; + let match: RegExpExecArray | null; + + // biome-ignore lint/suspicious/noAssignInExpressions: Standard regex iteration pattern + while ((match = mentionRegex.exec(message)) !== null) { + const username = match[1]; + if (username) { + mentions.push(username.toLowerCase()); + } + } + + return mentions; +} diff --git a/extensions/twitch/src/actions.ts b/extensions/twitch/src/actions.ts new file mode 100644 index 000000000..9e7ade194 --- /dev/null +++ b/extensions/twitch/src/actions.ts @@ -0,0 +1,173 @@ +/** + * Twitch message actions adapter. + * + * Handles tool-based actions for Twitch, such as sending messages. + */ + +import { DEFAULT_ACCOUNT_ID, getAccountConfig } from "./config.js"; +import { twitchOutbound } from "./outbound.js"; +import type { ChannelMessageActionAdapter, ChannelMessageActionContext } from "./types.js"; + +/** + * Create a tool result with error content. + */ +function errorResponse(error: string) { + return { + content: [ + { + type: "text", + text: JSON.stringify({ ok: false, error }), + }, + ], + details: { ok: false }, + }; +} + +/** + * Read a string parameter from action arguments. + * + * @param args - Action arguments + * @param key - Parameter key + * @param options - Options for reading the parameter + * @returns The parameter value or undefined if not found + */ +function readStringParam( + args: Record, + key: string, + options: { required?: boolean; trim?: boolean } = {}, +): string | undefined { + const value = args[key]; + if (value === undefined || value === null) { + if (options.required) { + throw new Error(`Missing required parameter: ${key}`); + } + return undefined; + } + + // Convert value to string safely + if (typeof value === "string") { + return options.trim !== false ? value.trim() : value; + } + + if (typeof value === "number" || typeof value === "boolean") { + const str = String(value); + return options.trim !== false ? str.trim() : str; + } + + throw new Error(`Parameter ${key} must be a string, number, or boolean`); +} + +/** Supported Twitch actions */ +const TWITCH_ACTIONS = new Set(["send" as const]); +type TwitchAction = typeof TWITCH_ACTIONS extends Set ? U : never; + +/** + * Twitch message actions adapter. + */ +export const twitchMessageActions: ChannelMessageActionAdapter = { + /** + * List available actions for this channel. + */ + listActions: () => [...TWITCH_ACTIONS], + + /** + * Check if an action is supported. + */ + supportsAction: ({ action }) => TWITCH_ACTIONS.has(action as TwitchAction), + + /** + * Extract tool send parameters from action arguments. + * + * Parses and validates the "to" and "message" parameters for sending. + * + * @param params - Arguments from the tool call + * @returns Parsed send parameters or null if invalid + * + * @example + * const result = twitchMessageActions.extractToolSend!({ + * args: { to: "#mychannel", message: "Hello!" } + * }); + * // Returns: { to: "#mychannel", message: "Hello!" } + */ + extractToolSend: ({ args }) => { + try { + const to = readStringParam(args, "to", { required: true }); + const message = readStringParam(args, "message", { required: true }); + + if (!to || !message) { + return null; + } + + return { to, message }; + } catch { + return null; + } + }, + + /** + * Handle an action execution. + * + * Processes the "send" action to send messages to Twitch. + * + * @param ctx - Action context including action type, parameters, and config + * @returns Tool result with content or null if action not supported + * + * @example + * const result = await twitchMessageActions.handleAction!({ + * action: "send", + * params: { message: "Hello Twitch!", to: "#mychannel" }, + * cfg: clawdbotConfig, + * accountId: "default", + * }); + */ + handleAction: async ( + ctx: ChannelMessageActionContext, + ): Promise<{ content: Array<{ type: string; text: string }> } | null> => { + if (ctx.action !== "send") { + return null; + } + + const message = readStringParam(ctx.params, "message", { required: true }); + const to = readStringParam(ctx.params, "to", { required: false }); + const accountId = ctx.accountId ?? DEFAULT_ACCOUNT_ID; + + const account = getAccountConfig(ctx.cfg, accountId); + if (!account) { + return errorResponse( + `Account not found: ${accountId}. Available accounts: ${Object.keys(ctx.cfg.channels?.twitch?.accounts ?? {}).join(", ") || "none"}`, + ); + } + + // Use the channel from account config (or override with `to` parameter) + const targetChannel = to || account.channel; + if (!targetChannel) { + return errorResponse("No channel specified and no default channel in account config"); + } + + if (!twitchOutbound.sendText) { + return errorResponse("sendText not implemented"); + } + + try { + const result = await twitchOutbound.sendText({ + cfg: ctx.cfg, + to: targetChannel, + text: message ?? "", + accountId, + }); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + details: { ok: true }, + }; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + return errorResponse(errorMsg); + } + }, +}; diff --git a/extensions/twitch/src/client-manager-registry.ts b/extensions/twitch/src/client-manager-registry.ts new file mode 100644 index 000000000..1b7ae23f2 --- /dev/null +++ b/extensions/twitch/src/client-manager-registry.ts @@ -0,0 +1,115 @@ +/** + * Client manager registry for Twitch plugin. + * + * Manages the lifecycle of TwitchClientManager instances across the plugin, + * ensuring proper cleanup when accounts are stopped or reconfigured. + */ + +import { TwitchClientManager } from "./twitch-client.js"; +import type { ChannelLogSink } from "./types.js"; + +/** + * Registry entry tracking a client manager and its associated account. + */ +type RegistryEntry = { + /** The client manager instance */ + manager: TwitchClientManager; + /** The account ID this manager is for */ + accountId: string; + /** Logger for this entry */ + logger: ChannelLogSink; + /** When this entry was created */ + createdAt: number; +}; + +/** + * Global registry of client managers. + * Keyed by account ID. + */ +const registry = new Map(); + +/** + * Get or create a client manager for an account. + * + * @param accountId - The account ID + * @param logger - Logger instance + * @returns The client manager + */ +export function getOrCreateClientManager( + accountId: string, + logger: ChannelLogSink, +): TwitchClientManager { + const existing = registry.get(accountId); + if (existing) { + return existing.manager; + } + + const manager = new TwitchClientManager(logger); + registry.set(accountId, { + manager, + accountId, + logger, + createdAt: Date.now(), + }); + + logger.info(`Registered client manager for account: ${accountId}`); + return manager; +} + +/** + * Get an existing client manager for an account. + * + * @param accountId - The account ID + * @returns The client manager, or undefined if not registered + */ +export function getClientManager(accountId: string): TwitchClientManager | undefined { + return registry.get(accountId)?.manager; +} + +/** + * Disconnect and remove a client manager from the registry. + * + * @param accountId - The account ID + * @returns Promise that resolves when cleanup is complete + */ +export async function removeClientManager(accountId: string): Promise { + const entry = registry.get(accountId); + if (!entry) { + return; + } + + // Disconnect the client manager + await entry.manager.disconnectAll(); + + // Remove from registry + registry.delete(accountId); + entry.logger.info(`Unregistered client manager for account: ${accountId}`); +} + +/** + * Disconnect and remove all client managers from the registry. + * + * @returns Promise that resolves when all cleanup is complete + */ +export async function removeAllClientManagers(): Promise { + const promises = [...registry.keys()].map((accountId) => removeClientManager(accountId)); + await Promise.all(promises); +} + +/** + * Get the number of registered client managers. + * + * @returns The count of registered managers + */ +export function getRegisteredClientManagerCount(): number { + return registry.size; +} + +/** + * Clear all client managers without disconnecting. + * + * This is primarily for testing purposes. + */ +export function _clearAllClientManagersForTest(): void { + registry.clear(); +} diff --git a/extensions/twitch/src/config-schema.ts b/extensions/twitch/src/config-schema.ts new file mode 100644 index 000000000..f4d8500c7 --- /dev/null +++ b/extensions/twitch/src/config-schema.ts @@ -0,0 +1,82 @@ +import { MarkdownConfigSchema } from "clawdbot/plugin-sdk"; +import { z } from "zod"; + +/** + * Twitch user roles that can be allowed to interact with the bot + */ +const TwitchRoleSchema = z.enum(["moderator", "owner", "vip", "subscriber", "all"]); + +/** + * Twitch account configuration schema + */ +const TwitchAccountSchema = z.object({ + /** Twitch username */ + username: z.string(), + /** Twitch OAuth access token (requires chat:read and chat:write scopes) */ + accessToken: z.string(), + /** Twitch client ID (from Twitch Developer Portal or twitchtokengenerator.com) */ + clientId: z.string().optional(), + /** Channel name to join */ + channel: z.string().min(1), + /** Enable this account */ + enabled: z.boolean().optional(), + /** Allowlist of Twitch user IDs who can interact with the bot (use IDs for safety, not usernames) */ + allowFrom: z.array(z.string()).optional(), + /** Roles allowed to interact with the bot (e.g., ["moderator", "vip", "subscriber"]) */ + allowedRoles: z.array(TwitchRoleSchema).optional(), + /** Require @mention to trigger bot responses */ + requireMention: z.boolean().optional(), + /** Twitch client secret (required for token refresh via RefreshingAuthProvider) */ + clientSecret: z.string().optional(), + /** Refresh token (required for automatic token refresh) */ + refreshToken: z.string().optional(), + /** Token expiry time in seconds (optional, for token refresh tracking) */ + expiresIn: z.number().nullable().optional(), + /** Timestamp when token was obtained (optional, for token refresh tracking) */ + obtainmentTimestamp: z.number().optional(), +}); + +/** + * Base configuration properties shared by both single and multi-account modes + */ +const TwitchConfigBaseSchema = z.object({ + name: z.string().optional(), + enabled: z.boolean().optional(), + markdown: MarkdownConfigSchema.optional(), +}); + +/** + * Simplified single-account configuration schema + * + * Use this for single-account setups. Properties are at the top level, + * creating an implicit "default" account. + */ +const SimplifiedSchema = z.intersection(TwitchConfigBaseSchema, TwitchAccountSchema); + +/** + * Multi-account configuration schema + * + * Use this for multi-account setups. Each key is an account ID (e.g., "default", "secondary"). + */ +const MultiAccountSchema = z.intersection( + TwitchConfigBaseSchema, + z + .object({ + /** Per-account configuration (for multi-account setups) */ + accounts: z.record(z.string(), TwitchAccountSchema), + }) + .refine((val) => Object.keys(val.accounts || {}).length > 0, { + message: "accounts must contain at least one entry", + }), +); + +/** + * Twitch plugin configuration schema + * + * Supports two mutually exclusive patterns: + * 1. Simplified single-account: username, accessToken, clientId, channel at top level + * 2. Multi-account: accounts object with named account configs + * + * The union ensures clear discrimination between the two modes. + */ +export const TwitchConfigSchema = z.union([SimplifiedSchema, MultiAccountSchema]); diff --git a/extensions/twitch/src/config.test.ts b/extensions/twitch/src/config.test.ts new file mode 100644 index 000000000..cdef1c4c8 --- /dev/null +++ b/extensions/twitch/src/config.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it } from "vitest"; + +import { getAccountConfig } from "./config.js"; + +describe("getAccountConfig", () => { + const mockMultiAccountConfig = { + channels: { + twitch: { + accounts: { + default: { + username: "testbot", + accessToken: "oauth:test123", + }, + secondary: { + username: "secondbot", + accessToken: "oauth:secondary", + }, + }, + }, + }, + }; + + const mockSimplifiedConfig = { + channels: { + twitch: { + username: "testbot", + accessToken: "oauth:test123", + }, + }, + }; + + it("returns account config for valid account ID (multi-account)", () => { + const result = getAccountConfig(mockMultiAccountConfig, "default"); + + expect(result).not.toBeNull(); + expect(result?.username).toBe("testbot"); + }); + + it("returns account config for default account (simplified config)", () => { + const result = getAccountConfig(mockSimplifiedConfig, "default"); + + expect(result).not.toBeNull(); + expect(result?.username).toBe("testbot"); + }); + + it("returns non-default account from multi-account config", () => { + const result = getAccountConfig(mockMultiAccountConfig, "secondary"); + + expect(result).not.toBeNull(); + expect(result?.username).toBe("secondbot"); + }); + + it("returns null for non-existent account ID", () => { + const result = getAccountConfig(mockMultiAccountConfig, "nonexistent"); + + expect(result).toBeNull(); + }); + + it("returns null when core config is null", () => { + const result = getAccountConfig(null, "default"); + + expect(result).toBeNull(); + }); + + it("returns null when core config is undefined", () => { + const result = getAccountConfig(undefined, "default"); + + expect(result).toBeNull(); + }); + + it("returns null when channels are not defined", () => { + const result = getAccountConfig({}, "default"); + + expect(result).toBeNull(); + }); + + it("returns null when twitch is not defined", () => { + const result = getAccountConfig({ channels: {} }, "default"); + + expect(result).toBeNull(); + }); + + it("returns null when accounts are not defined", () => { + const result = getAccountConfig({ channels: { twitch: {} } }, "default"); + + expect(result).toBeNull(); + }); +}); diff --git a/extensions/twitch/src/config.ts b/extensions/twitch/src/config.ts new file mode 100644 index 000000000..b4c5d54ca --- /dev/null +++ b/extensions/twitch/src/config.ts @@ -0,0 +1,116 @@ +import type { ClawdbotConfig } from "clawdbot/plugin-sdk"; +import type { TwitchAccountConfig } from "./types.js"; + +/** + * Default account ID for Twitch + */ +export const DEFAULT_ACCOUNT_ID = "default"; + +/** + * Get account config from core config + * + * Handles two patterns: + * 1. Simplified single-account: base-level properties create implicit "default" account + * 2. Multi-account: explicit accounts object + * + * For "default" account, base-level properties take precedence over accounts.default + * For other accounts, only the accounts object is checked + */ +export function getAccountConfig( + coreConfig: unknown, + accountId: string, +): TwitchAccountConfig | null { + if (!coreConfig || typeof coreConfig !== "object") { + return null; + } + + const cfg = coreConfig as ClawdbotConfig; + const twitch = cfg.channels?.twitch; + // Access accounts via unknown to handle union type (single-account vs multi-account) + const twitchRaw = twitch as Record | undefined; + const accounts = twitchRaw?.accounts as Record | undefined; + + // For default account, check base-level config first + if (accountId === DEFAULT_ACCOUNT_ID) { + const accountFromAccounts = accounts?.[DEFAULT_ACCOUNT_ID]; + + // Base-level properties that can form an implicit default account + const baseLevel = { + username: typeof twitchRaw?.username === "string" ? twitchRaw.username : undefined, + accessToken: typeof twitchRaw?.accessToken === "string" ? twitchRaw.accessToken : undefined, + clientId: typeof twitchRaw?.clientId === "string" ? twitchRaw.clientId : undefined, + channel: typeof twitchRaw?.channel === "string" ? twitchRaw.channel : undefined, + enabled: typeof twitchRaw?.enabled === "boolean" ? twitchRaw.enabled : undefined, + allowFrom: Array.isArray(twitchRaw?.allowFrom) ? twitchRaw.allowFrom : undefined, + allowedRoles: Array.isArray(twitchRaw?.allowedRoles) ? twitchRaw.allowedRoles : undefined, + requireMention: + typeof twitchRaw?.requireMention === "boolean" ? twitchRaw.requireMention : undefined, + clientSecret: + typeof twitchRaw?.clientSecret === "string" ? twitchRaw.clientSecret : undefined, + refreshToken: + typeof twitchRaw?.refreshToken === "string" ? twitchRaw.refreshToken : undefined, + expiresIn: typeof twitchRaw?.expiresIn === "number" ? twitchRaw.expiresIn : undefined, + obtainmentTimestamp: + typeof twitchRaw?.obtainmentTimestamp === "number" + ? twitchRaw.obtainmentTimestamp + : undefined, + }; + + // Merge: base-level takes precedence over accounts.default + const merged: Partial = { + ...accountFromAccounts, + ...baseLevel, + } as Partial; + + // Only return if we have at least username + if (merged.username) { + return merged as TwitchAccountConfig; + } + + // Fall through to accounts.default if no base-level username + if (accountFromAccounts) { + return accountFromAccounts; + } + + return null; + } + + // For non-default accounts, only check accounts object + if (!accounts || !accounts[accountId]) { + return null; + } + + return accounts[accountId] as TwitchAccountConfig | null; +} + +/** + * List all configured account IDs + * + * Includes both explicit accounts and implicit "default" from base-level config + */ +export function listAccountIds(cfg: ClawdbotConfig): string[] { + const twitch = cfg.channels?.twitch; + // Access accounts via unknown to handle union type (single-account vs multi-account) + const twitchRaw = twitch as Record | undefined; + const accountMap = twitchRaw?.accounts as Record | undefined; + + const ids: string[] = []; + + // Add explicit accounts + if (accountMap) { + ids.push(...Object.keys(accountMap)); + } + + // Add implicit "default" if base-level config exists and "default" not already present + const hasBaseLevelConfig = + twitchRaw && + (typeof twitchRaw.username === "string" || + typeof twitchRaw.accessToken === "string" || + typeof twitchRaw.channel === "string"); + + if (hasBaseLevelConfig && !ids.includes(DEFAULT_ACCOUNT_ID)) { + ids.push(DEFAULT_ACCOUNT_ID); + } + + return ids; +} diff --git a/extensions/twitch/src/monitor.ts b/extensions/twitch/src/monitor.ts new file mode 100644 index 000000000..f5f00b3fb --- /dev/null +++ b/extensions/twitch/src/monitor.ts @@ -0,0 +1,257 @@ +/** + * Twitch message monitor - processes incoming messages and routes to agents. + * + * This monitor connects to the Twitch client manager, processes incoming messages, + * resolves agent routes, and handles replies. + */ + +import type { ReplyPayload, ClawdbotConfig } from "clawdbot/plugin-sdk"; +import type { TwitchAccountConfig, TwitchChatMessage } from "./types.js"; +import { checkTwitchAccessControl } from "./access-control.js"; +import { getTwitchRuntime } from "./runtime.js"; +import { getOrCreateClientManager } from "./client-manager-registry.js"; +import { stripMarkdownForTwitch } from "./utils/markdown.js"; + +export type TwitchRuntimeEnv = { + log?: (message: string) => void; + error?: (message: string) => void; +}; + +export type TwitchMonitorOptions = { + account: TwitchAccountConfig; + accountId: string; + config: unknown; // ClawdbotConfig + runtime: TwitchRuntimeEnv; + abortSignal: AbortSignal; + statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void; +}; + +export type TwitchMonitorResult = { + stop: () => void; +}; + +type TwitchCoreRuntime = ReturnType; + +/** + * Process an incoming Twitch message and dispatch to agent. + */ +async function processTwitchMessage(params: { + message: TwitchChatMessage; + account: TwitchAccountConfig; + accountId: string; + config: unknown; + runtime: TwitchRuntimeEnv; + core: TwitchCoreRuntime; + statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void; +}): Promise { + const { message, account, accountId, config, runtime, core, statusSink } = params; + const cfg = config as ClawdbotConfig; + + const route = core.channel.routing.resolveAgentRoute({ + cfg, + channel: "twitch", + accountId, + peer: { + kind: "group", // Twitch chat is always group-like + id: message.channel, + }, + }); + + const rawBody = message.message; + const body = core.channel.reply.formatAgentEnvelope({ + channel: "Twitch", + from: message.displayName ?? message.username, + timestamp: message.timestamp?.getTime(), + envelope: core.channel.reply.resolveEnvelopeFormatOptions(cfg), + body: rawBody, + }); + + const ctxPayload = core.channel.reply.finalizeInboundContext({ + Body: body, + RawBody: rawBody, + CommandBody: rawBody, + From: `twitch:user:${message.userId}`, + To: `twitch:channel:${message.channel}`, + SessionKey: route.sessionKey, + AccountId: route.accountId, + ChatType: "group", + ConversationLabel: message.channel, + SenderName: message.displayName ?? message.username, + SenderId: message.userId, + SenderUsername: message.username, + Provider: "twitch", + Surface: "twitch", + MessageSid: message.id, + OriginatingChannel: "twitch", + OriginatingTo: `twitch:channel:${message.channel}`, + }); + + const storePath = core.channel.session.resolveStorePath(cfg.session?.store, { + agentId: route.agentId, + }); + await core.channel.session.recordInboundSession({ + storePath, + sessionKey: ctxPayload.SessionKey ?? route.sessionKey, + ctx: ctxPayload, + onRecordError: (err) => { + runtime.error?.(`Failed updating session meta: ${String(err)}`); + }, + }); + + const tableMode = core.channel.text.resolveMarkdownTableMode({ + cfg, + channel: "twitch", + accountId, + }); + + await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({ + ctx: ctxPayload, + cfg, + dispatcherOptions: { + deliver: async (payload) => { + await deliverTwitchReply({ + payload, + channel: message.channel, + account, + accountId, + config, + tableMode, + runtime, + statusSink, + }); + }, + }, + }); +} + +/** + * Deliver a reply to Twitch chat. + */ +async function deliverTwitchReply(params: { + payload: ReplyPayload; + channel: string; + account: TwitchAccountConfig; + accountId: string; + config: unknown; + tableMode: "off" | "plain" | "markdown" | "bullets" | "code"; + runtime: TwitchRuntimeEnv; + statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void; +}): Promise { + const { payload, channel, account, accountId, config, tableMode, runtime, statusSink } = params; + + try { + const clientManager = getOrCreateClientManager(accountId, { + info: (msg) => runtime.log?.(msg), + warn: (msg) => runtime.log?.(msg), + error: (msg) => runtime.error?.(msg), + debug: (msg) => runtime.log?.(msg), + }); + + const client = await clientManager.getClient( + account, + config as Parameters[1], + accountId, + ); + if (!client) { + runtime.error?.(`No client available for sending reply`); + return; + } + + // Send the reply + if (!payload.text) { + runtime.error?.(`No text to send in reply payload`); + return; + } + + const textToSend = stripMarkdownForTwitch(payload.text); + + await client.say(channel, textToSend); + statusSink?.({ lastOutboundAt: Date.now() }); + } catch (err) { + runtime.error?.(`Failed to send reply: ${String(err)}`); + } +} + +/** + * Main monitor provider for Twitch. + * + * Sets up message handlers and processes incoming messages. + */ +export async function monitorTwitchProvider( + options: TwitchMonitorOptions, +): Promise { + const { account, accountId, config, runtime, abortSignal, statusSink } = options; + + const core = getTwitchRuntime(); + let stopped = false; + + const coreLogger = core.logging.getChildLogger({ module: "twitch" }); + const logVerboseMessage = (message: string) => { + if (!core.logging.shouldLogVerbose()) return; + coreLogger.debug?.(message); + }; + const logger = { + info: (msg: string) => coreLogger.info(msg), + warn: (msg: string) => coreLogger.warn(msg), + error: (msg: string) => coreLogger.error(msg), + debug: logVerboseMessage, + }; + + const clientManager = getOrCreateClientManager(accountId, logger); + + try { + await clientManager.getClient( + account, + config as Parameters[1], + accountId, + ); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + runtime.error?.(`Failed to connect: ${errorMsg}`); + throw error; + } + + const unregisterHandler = clientManager.onMessage(account, (message) => { + if (stopped) return; + + // Access control check + const botUsername = account.username.toLowerCase(); + if (message.username.toLowerCase() === botUsername) { + return; // Ignore own messages + } + + const access = checkTwitchAccessControl({ + message, + account, + botUsername, + }); + + if (!access.allowed) { + return; + } + + statusSink?.({ lastInboundAt: Date.now() }); + + // Fire-and-forget: process message without blocking + void processTwitchMessage({ + message, + account, + accountId, + config, + runtime, + core, + statusSink, + }).catch((err) => { + runtime.error?.(`Message processing failed: ${String(err)}`); + }); + }); + + const stop = () => { + stopped = true; + unregisterHandler(); + }; + + abortSignal.addEventListener("abort", stop, { once: true }); + + return { stop }; +} diff --git a/extensions/twitch/src/onboarding.test.ts b/extensions/twitch/src/onboarding.test.ts new file mode 100644 index 000000000..492845bc1 --- /dev/null +++ b/extensions/twitch/src/onboarding.test.ts @@ -0,0 +1,311 @@ +/** + * Tests for onboarding.ts helpers + * + * Tests cover: + * - promptToken helper + * - promptUsername helper + * - promptClientId helper + * - promptChannelName helper + * - promptRefreshTokenSetup helper + * - configureWithEnvToken helper + * - setTwitchAccount config updates + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { WizardPrompter } from "clawdbot/plugin-sdk"; +import type { TwitchAccountConfig } from "./types.js"; + +// Mock the helpers we're testing +const mockPromptText = vi.fn(); +const mockPromptConfirm = vi.fn(); +const mockPrompter: WizardPrompter = { + text: mockPromptText, + confirm: mockPromptConfirm, +} as unknown as WizardPrompter; + +const mockAccount: TwitchAccountConfig = { + username: "testbot", + accessToken: "oauth:test123", + clientId: "test-client-id", + channel: "#testchannel", +}; + +describe("onboarding helpers", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + // Don't restoreAllMocks as it breaks module-level mocks + }); + + describe("promptToken", () => { + it("should return existing token when user confirms to keep it", async () => { + const { promptToken } = await import("./onboarding.js"); + + mockPromptConfirm.mockResolvedValue(true); + + const result = await promptToken(mockPrompter, mockAccount, undefined); + + expect(result).toBe("oauth:test123"); + expect(mockPromptConfirm).toHaveBeenCalledWith({ + message: "Access token already configured. Keep it?", + initialValue: true, + }); + expect(mockPromptText).not.toHaveBeenCalled(); + }); + + it("should prompt for new token when user doesn't keep existing", async () => { + const { promptToken } = await import("./onboarding.js"); + + mockPromptConfirm.mockResolvedValue(false); + mockPromptText.mockResolvedValue("oauth:newtoken123"); + + const result = await promptToken(mockPrompter, mockAccount, undefined); + + expect(result).toBe("oauth:newtoken123"); + expect(mockPromptText).toHaveBeenCalledWith({ + message: "Twitch OAuth token (oauth:...)", + initialValue: "", + validate: expect.any(Function), + }); + }); + + it("should use env token as initial value when provided", async () => { + const { promptToken } = await import("./onboarding.js"); + + mockPromptConfirm.mockResolvedValue(false); + mockPromptText.mockResolvedValue("oauth:fromenv"); + + await promptToken(mockPrompter, null, "oauth:fromenv"); + + expect(mockPromptText).toHaveBeenCalledWith( + expect.objectContaining({ + initialValue: "oauth:fromenv", + }), + ); + }); + + it("should validate token format", async () => { + const { promptToken } = await import("./onboarding.js"); + + // Set up mocks - user doesn't want to keep existing token + mockPromptConfirm.mockResolvedValueOnce(false); + + // Track how many times promptText is called + let promptTextCallCount = 0; + let capturedValidate: ((value: string) => string | undefined) | undefined; + + mockPromptText.mockImplementationOnce((_args) => { + promptTextCallCount++; + // Capture the validate function from the first argument + if (_args?.validate) { + capturedValidate = _args.validate; + } + return Promise.resolve("oauth:test123"); + }); + + // Call promptToken + const result = await promptToken(mockPrompter, mockAccount, undefined); + + // Verify promptText was called + expect(promptTextCallCount).toBe(1); + expect(result).toBe("oauth:test123"); + + // Test the validate function + expect(capturedValidate).toBeDefined(); + expect(capturedValidate!("")).toBe("Required"); + expect(capturedValidate!("notoauth")).toBe("Token should start with 'oauth:'"); + }); + + it("should return early when no existing token and no env token", async () => { + const { promptToken } = await import("./onboarding.js"); + + mockPromptText.mockResolvedValue("oauth:newtoken"); + + const result = await promptToken(mockPrompter, null, undefined); + + expect(result).toBe("oauth:newtoken"); + expect(mockPromptConfirm).not.toHaveBeenCalled(); + }); + }); + + describe("promptUsername", () => { + it("should prompt for username with validation", async () => { + const { promptUsername } = await import("./onboarding.js"); + + mockPromptText.mockResolvedValue("mybot"); + + const result = await promptUsername(mockPrompter, null); + + expect(result).toBe("mybot"); + expect(mockPromptText).toHaveBeenCalledWith({ + message: "Twitch bot username", + initialValue: "", + validate: expect.any(Function), + }); + }); + + it("should use existing username as initial value", async () => { + const { promptUsername } = await import("./onboarding.js"); + + mockPromptText.mockResolvedValue("testbot"); + + await promptUsername(mockPrompter, mockAccount); + + expect(mockPromptText).toHaveBeenCalledWith( + expect.objectContaining({ + initialValue: "testbot", + }), + ); + }); + }); + + describe("promptClientId", () => { + it("should prompt for client ID with validation", async () => { + const { promptClientId } = await import("./onboarding.js"); + + mockPromptText.mockResolvedValue("abc123xyz"); + + const result = await promptClientId(mockPrompter, null); + + expect(result).toBe("abc123xyz"); + expect(mockPromptText).toHaveBeenCalledWith({ + message: "Twitch Client ID", + initialValue: "", + validate: expect.any(Function), + }); + }); + }); + + describe("promptChannelName", () => { + it("should return channel name when provided", async () => { + const { promptChannelName } = await import("./onboarding.js"); + + mockPromptText.mockResolvedValue("#mychannel"); + + const result = await promptChannelName(mockPrompter, null); + + expect(result).toBe("#mychannel"); + }); + + it("should require a non-empty channel name", async () => { + const { promptChannelName } = await import("./onboarding.js"); + + mockPromptText.mockResolvedValue(""); + + await promptChannelName(mockPrompter, null); + + const { validate } = mockPromptText.mock.calls[0]?.[0] ?? {}; + expect(validate?.("")).toBe("Required"); + expect(validate?.(" ")).toBe("Required"); + expect(validate?.("#chan")).toBeUndefined(); + }); + }); + + describe("promptRefreshTokenSetup", () => { + it("should return empty object when user declines", async () => { + const { promptRefreshTokenSetup } = await import("./onboarding.js"); + + mockPromptConfirm.mockResolvedValue(false); + + const result = await promptRefreshTokenSetup(mockPrompter, mockAccount); + + expect(result).toEqual({}); + expect(mockPromptConfirm).toHaveBeenCalledWith({ + message: "Enable automatic token refresh (requires client secret and refresh token)?", + initialValue: false, + }); + }); + + it("should prompt for credentials when user accepts", async () => { + const { promptRefreshTokenSetup } = await import("./onboarding.js"); + + mockPromptConfirm + .mockResolvedValueOnce(true) // First call: useRefresh + .mockResolvedValueOnce("secret123") // clientSecret + .mockResolvedValueOnce("refresh123"); // refreshToken + + mockPromptText.mockResolvedValueOnce("secret123").mockResolvedValueOnce("refresh123"); + + const result = await promptRefreshTokenSetup(mockPrompter, null); + + expect(result).toEqual({ + clientSecret: "secret123", + refreshToken: "refresh123", + }); + }); + + it("should use existing values as initial prompts", async () => { + const { promptRefreshTokenSetup } = await import("./onboarding.js"); + + const accountWithRefresh = { + ...mockAccount, + clientSecret: "existing-secret", + refreshToken: "existing-refresh", + }; + + mockPromptConfirm.mockResolvedValue(true); + mockPromptText + .mockResolvedValueOnce("existing-secret") + .mockResolvedValueOnce("existing-refresh"); + + await promptRefreshTokenSetup(mockPrompter, accountWithRefresh); + + expect(mockPromptConfirm).toHaveBeenCalledWith( + expect.objectContaining({ + initialValue: true, // Both clientSecret and refreshToken exist + }), + ); + }); + }); + + describe("configureWithEnvToken", () => { + it("should return null when user declines env token", async () => { + const { configureWithEnvToken } = await import("./onboarding.js"); + + // Reset and set up mock - user declines env token + mockPromptConfirm.mockReset().mockResolvedValue(false as never); + + const result = await configureWithEnvToken( + {} as Parameters[0], + mockPrompter, + null, + "oauth:fromenv", + false, + {} as Parameters[5], + ); + + // Since user declined, should return null without prompting for username/clientId + expect(result).toBeNull(); + expect(mockPromptText).not.toHaveBeenCalled(); + }); + + it("should prompt for username and clientId when using env token", async () => { + const { configureWithEnvToken } = await import("./onboarding.js"); + + // Reset and set up mocks - user accepts env token + mockPromptConfirm.mockReset().mockResolvedValue(true as never); + + // Set up mocks for username and clientId prompts + mockPromptText + .mockReset() + .mockResolvedValueOnce("testbot" as never) + .mockResolvedValueOnce("test-client-id" as never); + + const result = await configureWithEnvToken( + {} as Parameters[0], + mockPrompter, + null, + "oauth:fromenv", + false, + {} as Parameters[5], + ); + + // Should return config with username and clientId + expect(result).not.toBeNull(); + expect(result?.cfg.channels?.twitch?.accounts?.default?.username).toBe("testbot"); + expect(result?.cfg.channels?.twitch?.accounts?.default?.clientId).toBe("test-client-id"); + }); + }); +}); diff --git a/extensions/twitch/src/onboarding.ts b/extensions/twitch/src/onboarding.ts new file mode 100644 index 000000000..9308b55a0 --- /dev/null +++ b/extensions/twitch/src/onboarding.ts @@ -0,0 +1,411 @@ +/** + * Twitch onboarding adapter for CLI setup wizard. + */ + +import { + formatDocsLink, + promptChannelAccessConfig, + type ChannelOnboardingAdapter, + type ChannelOnboardingDmPolicy, + type WizardPrompter, +} from "clawdbot/plugin-sdk"; +import { DEFAULT_ACCOUNT_ID, getAccountConfig } from "./config.js"; +import { isAccountConfigured } from "./utils/twitch.js"; +import type { TwitchAccountConfig, TwitchRole } from "./types.js"; +import type { ClawdbotConfig } from "clawdbot/plugin-sdk"; + +const channel = "twitch" as const; + +/** + * Set Twitch account configuration + */ +function setTwitchAccount( + cfg: ClawdbotConfig, + account: Partial, +): ClawdbotConfig { + const existing = getAccountConfig(cfg, DEFAULT_ACCOUNT_ID); + const merged: TwitchAccountConfig = { + username: account.username ?? existing?.username ?? "", + accessToken: account.accessToken ?? existing?.accessToken ?? "", + clientId: account.clientId ?? existing?.clientId ?? "", + channel: account.channel ?? existing?.channel ?? "", + enabled: account.enabled ?? existing?.enabled ?? true, + allowFrom: account.allowFrom ?? existing?.allowFrom, + allowedRoles: account.allowedRoles ?? existing?.allowedRoles, + requireMention: account.requireMention ?? existing?.requireMention, + clientSecret: account.clientSecret ?? existing?.clientSecret, + refreshToken: account.refreshToken ?? existing?.refreshToken, + expiresIn: account.expiresIn ?? existing?.expiresIn, + obtainmentTimestamp: account.obtainmentTimestamp ?? existing?.obtainmentTimestamp, + }; + + return { + ...cfg, + channels: { + ...cfg.channels, + twitch: { + ...((cfg.channels as Record)?.twitch as + | Record + | undefined), + enabled: true, + accounts: { + ...(( + (cfg.channels as Record)?.twitch as Record | undefined + )?.accounts as Record | undefined), + [DEFAULT_ACCOUNT_ID]: merged, + }, + }, + }, + }; +} + +/** + * Note about Twitch setup + */ +async function noteTwitchSetupHelp(prompter: WizardPrompter): Promise { + await prompter.note( + [ + "Twitch requires a bot account with OAuth token.", + "1. Create a Twitch application at https://dev.twitch.tv/console", + "2. Generate a token with scopes: chat:read and chat:write", + " Use https://twitchtokengenerator.com/ or https://twitchapps.com/tmi/", + "3. Copy the token (starts with 'oauth:') and Client ID", + "Env vars supported: CLAWDBOT_TWITCH_ACCESS_TOKEN", + `Docs: ${formatDocsLink("/channels/twitch", "channels/twitch")}`, + ].join("\n"), + "Twitch setup", + ); +} + +/** + * Prompt for Twitch OAuth token with early returns. + */ +async function promptToken( + prompter: WizardPrompter, + account: TwitchAccountConfig | null, + envToken: string | undefined, +): Promise { + const existingToken = account?.accessToken ?? ""; + + // If we have an existing token and no env var, ask if we should keep it + if (existingToken && !envToken) { + const keepToken = await prompter.confirm({ + message: "Access token already configured. Keep it?", + initialValue: true, + }); + if (keepToken) { + return existingToken; + } + } + + // Prompt for new token + return String( + await prompter.text({ + message: "Twitch OAuth token (oauth:...)", + initialValue: envToken ?? "", + validate: (value) => { + const raw = String(value ?? "").trim(); + if (!raw) return "Required"; + if (!raw.startsWith("oauth:")) { + return "Token should start with 'oauth:'"; + } + return undefined; + }, + }), + ).trim(); +} + +/** + * Prompt for Twitch username. + */ +async function promptUsername( + prompter: WizardPrompter, + account: TwitchAccountConfig | null, +): Promise { + return String( + await prompter.text({ + message: "Twitch bot username", + initialValue: account?.username ?? "", + validate: (value) => (value?.trim() ? undefined : "Required"), + }), + ).trim(); +} + +/** + * Prompt for Twitch Client ID. + */ +async function promptClientId( + prompter: WizardPrompter, + account: TwitchAccountConfig | null, +): Promise { + return String( + await prompter.text({ + message: "Twitch Client ID", + initialValue: account?.clientId ?? "", + validate: (value) => (value?.trim() ? undefined : "Required"), + }), + ).trim(); +} + +/** + * Prompt for optional channel name. + */ +async function promptChannelName( + prompter: WizardPrompter, + account: TwitchAccountConfig | null, +): Promise { + const channelName = String( + await prompter.text({ + message: "Channel to join", + initialValue: account?.channel ?? "", + validate: (value) => (value?.trim() ? undefined : "Required"), + }), + ).trim(); + return channelName; +} + +/** + * Prompt for token refresh credentials (client secret and refresh token). + */ +async function promptRefreshTokenSetup( + prompter: WizardPrompter, + account: TwitchAccountConfig | null, +): Promise<{ clientSecret?: string; refreshToken?: string }> { + const useRefresh = await prompter.confirm({ + message: "Enable automatic token refresh (requires client secret and refresh token)?", + initialValue: Boolean(account?.clientSecret && account?.refreshToken), + }); + + if (!useRefresh) { + return {}; + } + + const clientSecret = + String( + await prompter.text({ + message: "Twitch Client Secret (for token refresh)", + initialValue: account?.clientSecret ?? "", + validate: (value) => (value?.trim() ? undefined : "Required"), + }), + ).trim() || undefined; + + const refreshToken = + String( + await prompter.text({ + message: "Twitch Refresh Token", + initialValue: account?.refreshToken ?? "", + validate: (value) => (value?.trim() ? undefined : "Required"), + }), + ).trim() || undefined; + + return { clientSecret, refreshToken }; +} + +/** + * Configure with env token path (returns early if user chooses env token). + */ +async function configureWithEnvToken( + cfg: ClawdbotConfig, + prompter: WizardPrompter, + account: TwitchAccountConfig | null, + envToken: string, + forceAllowFrom: boolean, + dmPolicy: ChannelOnboardingDmPolicy, +): Promise<{ cfg: ClawdbotConfig } | null> { + const useEnv = await prompter.confirm({ + message: "Twitch env var CLAWDBOT_TWITCH_ACCESS_TOKEN detected. Use env token?", + initialValue: true, + }); + if (!useEnv) { + return null; + } + + const username = await promptUsername(prompter, account); + const clientId = await promptClientId(prompter, account); + + const cfgWithAccount = setTwitchAccount(cfg, { + username, + clientId, + accessToken: "", // Will use env var + enabled: true, + }); + + if (forceAllowFrom && dmPolicy.promptAllowFrom) { + return { cfg: await dmPolicy.promptAllowFrom({ cfg: cfgWithAccount, prompter }) }; + } + + return { cfg: cfgWithAccount }; +} + +/** + * Set Twitch access control (role-based) + */ +function setTwitchAccessControl( + cfg: ClawdbotConfig, + allowedRoles: TwitchRole[], + requireMention: boolean, +): ClawdbotConfig { + const account = getAccountConfig(cfg, DEFAULT_ACCOUNT_ID); + if (!account) { + return cfg; + } + + return setTwitchAccount(cfg, { + ...account, + allowedRoles, + requireMention, + }); +} + +const dmPolicy: ChannelOnboardingDmPolicy = { + label: "Twitch", + channel, + policyKey: "channels.twitch.allowedRoles", // Twitch uses roles instead of DM policy + allowFromKey: "channels.twitch.accounts.default.allowFrom", + getCurrent: (cfg) => { + const account = getAccountConfig(cfg, DEFAULT_ACCOUNT_ID); + // Map allowedRoles to policy equivalent + if (account?.allowedRoles?.includes("all")) return "open"; + if (account?.allowFrom && account.allowFrom.length > 0) return "allowlist"; + return "disabled"; + }, + setPolicy: (cfg, policy) => { + const allowedRoles: TwitchRole[] = + policy === "open" ? ["all"] : policy === "allowlist" ? [] : ["moderator"]; + return setTwitchAccessControl(cfg as ClawdbotConfig, allowedRoles, true); + }, + promptAllowFrom: async ({ cfg, prompter }) => { + const account = getAccountConfig(cfg, DEFAULT_ACCOUNT_ID); + const existingAllowFrom = account?.allowFrom ?? []; + + const entry = await prompter.text({ + message: "Twitch allowFrom (user IDs, one per line, recommended for security)", + placeholder: "123456789", + initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined, + }); + + const allowFrom = String(entry ?? "") + .split(/[\n,;]+/g) + .map((s) => s.trim()) + .filter(Boolean); + + return setTwitchAccount(cfg as ClawdbotConfig, { + ...(account ?? undefined), + allowFrom, + }); + }, +}; + +export const twitchOnboardingAdapter: ChannelOnboardingAdapter = { + channel, + getStatus: async ({ cfg }) => { + const account = getAccountConfig(cfg, DEFAULT_ACCOUNT_ID); + const configured = account ? isAccountConfigured(account) : false; + + return { + channel, + configured, + statusLines: [`Twitch: ${configured ? "configured" : "needs username, token, and clientId"}`], + selectionHint: configured ? "configured" : "needs setup", + }; + }, + configure: async ({ cfg, prompter, forceAllowFrom }) => { + const account = getAccountConfig(cfg, DEFAULT_ACCOUNT_ID); + + if (!account || !isAccountConfigured(account)) { + await noteTwitchSetupHelp(prompter); + } + + const envToken = process.env.CLAWDBOT_TWITCH_ACCESS_TOKEN?.trim(); + + // Check if env var is set and config is empty + if (envToken && !account?.accessToken) { + const envResult = await configureWithEnvToken( + cfg, + prompter, + account, + envToken, + forceAllowFrom, + dmPolicy, + ); + if (envResult) { + return envResult; + } + } + + // Prompt for credentials + const username = await promptUsername(prompter, account); + const token = await promptToken(prompter, account, envToken); + const clientId = await promptClientId(prompter, account); + const channelName = await promptChannelName(prompter, account); + const { clientSecret, refreshToken } = await promptRefreshTokenSetup(prompter, account); + + const cfgWithAccount = setTwitchAccount(cfg, { + username, + accessToken: token, + clientId, + channel: channelName, + clientSecret, + refreshToken, + enabled: true, + }); + + const cfgWithAllowFrom = + forceAllowFrom && dmPolicy.promptAllowFrom + ? await dmPolicy.promptAllowFrom({ cfg: cfgWithAccount, prompter }) + : cfgWithAccount; + + // Prompt for access control if allowFrom not set + if (!account?.allowFrom || account.allowFrom.length === 0) { + const accessConfig = await promptChannelAccessConfig({ + prompter, + label: "Twitch chat", + currentPolicy: account?.allowedRoles?.includes("all") + ? "open" + : account?.allowedRoles?.includes("moderator") + ? "allowlist" + : "disabled", + currentEntries: [], + placeholder: "", + updatePrompt: false, + }); + + if (accessConfig) { + const allowedRoles: TwitchRole[] = + accessConfig.policy === "open" + ? ["all"] + : accessConfig.policy === "allowlist" + ? ["moderator", "vip"] + : []; + + const cfgWithAccessControl = setTwitchAccessControl(cfgWithAllowFrom, allowedRoles, true); + return { cfg: cfgWithAccessControl }; + } + } + + return { cfg: cfgWithAllowFrom }; + }, + dmPolicy, + disable: (cfg) => { + const twitch = (cfg.channels as Record)?.twitch as + | Record + | undefined; + return { + ...cfg, + channels: { + ...cfg.channels, + twitch: { ...twitch, enabled: false }, + }, + }; + }, +}; + +// Export helper functions for testing +export { + promptToken, + promptUsername, + promptClientId, + promptChannelName, + promptRefreshTokenSetup, + configureWithEnvToken, +}; diff --git a/extensions/twitch/src/outbound.test.ts b/extensions/twitch/src/outbound.test.ts new file mode 100644 index 000000000..41a68418f --- /dev/null +++ b/extensions/twitch/src/outbound.test.ts @@ -0,0 +1,373 @@ +/** + * Tests for outbound.ts module + * + * Tests cover: + * - resolveTarget with various modes (explicit, implicit, heartbeat) + * - sendText with markdown stripping + * - sendMedia delegation to sendText + * - Error handling for missing accounts/channels + * - Abort signal handling + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { twitchOutbound } from "./outbound.js"; +import type { ClawdbotConfig } from "clawdbot/plugin-sdk"; + +// Mock dependencies +vi.mock("./config.js", () => ({ + DEFAULT_ACCOUNT_ID: "default", + getAccountConfig: vi.fn(), +})); + +vi.mock("./send.js", () => ({ + sendMessageTwitchInternal: vi.fn(), +})); + +vi.mock("./utils/markdown.js", () => ({ + chunkTextForTwitch: vi.fn((text) => text.split(/(.{500})/).filter(Boolean)), +})); + +vi.mock("./utils/twitch.js", () => ({ + normalizeTwitchChannel: (channel: string) => channel.toLowerCase().replace(/^#/, ""), + missingTargetError: (channel: string, hint: string) => + `Missing target for ${channel}. Provide ${hint}`, +})); + +describe("outbound", () => { + const mockAccount = { + username: "testbot", + token: "oauth:test123", + clientId: "test-client-id", + channel: "#testchannel", + }; + + const mockConfig = { + channels: { + twitch: { + accounts: { + default: mockAccount, + }, + }, + }, + } as unknown as ClawdbotConfig; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe("metadata", () => { + it("should have direct delivery mode", () => { + expect(twitchOutbound.deliveryMode).toBe("direct"); + }); + + it("should have 500 character text chunk limit", () => { + expect(twitchOutbound.textChunkLimit).toBe(500); + }); + + it("should have chunker function", () => { + expect(twitchOutbound.chunker).toBeDefined(); + expect(typeof twitchOutbound.chunker).toBe("function"); + }); + }); + + describe("resolveTarget", () => { + it("should normalize and return target in explicit mode", () => { + const result = twitchOutbound.resolveTarget({ + to: "#MyChannel", + mode: "explicit", + allowFrom: [], + }); + + expect(result.ok).toBe(true); + expect(result.to).toBe("mychannel"); + }); + + it("should return target in implicit mode with wildcard allowlist", () => { + const result = twitchOutbound.resolveTarget({ + to: "#AnyChannel", + mode: "implicit", + allowFrom: ["*"], + }); + + expect(result.ok).toBe(true); + expect(result.to).toBe("anychannel"); + }); + + it("should return target in implicit mode when in allowlist", () => { + const result = twitchOutbound.resolveTarget({ + to: "#allowed", + mode: "implicit", + allowFrom: ["#allowed", "#other"], + }); + + expect(result.ok).toBe(true); + expect(result.to).toBe("allowed"); + }); + + it("should fallback to first allowlist entry when target not in list", () => { + const result = twitchOutbound.resolveTarget({ + to: "#notallowed", + mode: "implicit", + allowFrom: ["#primary", "#secondary"], + }); + + expect(result.ok).toBe(true); + expect(result.to).toBe("primary"); + }); + + it("should accept any target when allowlist is empty", () => { + const result = twitchOutbound.resolveTarget({ + to: "#anychannel", + mode: "heartbeat", + allowFrom: [], + }); + + expect(result.ok).toBe(true); + expect(result.to).toBe("anychannel"); + }); + + it("should use first allowlist entry when no target provided", () => { + const result = twitchOutbound.resolveTarget({ + to: undefined, + mode: "implicit", + allowFrom: ["#fallback", "#other"], + }); + + expect(result.ok).toBe(true); + expect(result.to).toBe("fallback"); + }); + + it("should return error when no target and no allowlist", () => { + const result = twitchOutbound.resolveTarget({ + to: undefined, + mode: "explicit", + allowFrom: [], + }); + + expect(result.ok).toBe(false); + expect(result.error).toContain("Missing target"); + }); + + it("should handle whitespace-only target", () => { + const result = twitchOutbound.resolveTarget({ + to: " ", + mode: "explicit", + allowFrom: [], + }); + + expect(result.ok).toBe(false); + expect(result.error).toContain("Missing target"); + }); + + it("should filter wildcard from allowlist when checking membership", () => { + const result = twitchOutbound.resolveTarget({ + to: "#mychannel", + mode: "implicit", + allowFrom: ["*", "#specific"], + }); + + // With wildcard, any target is accepted + expect(result.ok).toBe(true); + expect(result.to).toBe("mychannel"); + }); + }); + + describe("sendText", () => { + it("should send message successfully", async () => { + const { getAccountConfig } = await import("./config.js"); + const { sendMessageTwitchInternal } = await import("./send.js"); + + vi.mocked(getAccountConfig).mockReturnValue(mockAccount); + vi.mocked(sendMessageTwitchInternal).mockResolvedValue({ + ok: true, + messageId: "twitch-msg-123", + }); + + const result = await twitchOutbound.sendText({ + cfg: mockConfig, + to: "#testchannel", + text: "Hello Twitch!", + accountId: "default", + }); + + expect(result.channel).toBe("twitch"); + expect(result.messageId).toBe("twitch-msg-123"); + expect(result.to).toBe("testchannel"); + expect(result.timestamp).toBeGreaterThan(0); + }); + + it("should throw when account not found", async () => { + const { getAccountConfig } = await import("./config.js"); + + vi.mocked(getAccountConfig).mockReturnValue(null); + + await expect( + twitchOutbound.sendText({ + cfg: mockConfig, + to: "#testchannel", + text: "Hello!", + accountId: "nonexistent", + }), + ).rejects.toThrow("Twitch account not found: nonexistent"); + }); + + it("should throw when no channel specified", async () => { + const { getAccountConfig } = await import("./config.js"); + + const accountWithoutChannel = { ...mockAccount, channel: undefined as unknown as string }; + vi.mocked(getAccountConfig).mockReturnValue(accountWithoutChannel); + + await expect( + twitchOutbound.sendText({ + cfg: mockConfig, + to: undefined, + text: "Hello!", + accountId: "default", + }), + ).rejects.toThrow("No channel specified"); + }); + + it("should use account channel when target not provided", async () => { + const { getAccountConfig } = await import("./config.js"); + const { sendMessageTwitchInternal } = await import("./send.js"); + + vi.mocked(getAccountConfig).mockReturnValue(mockAccount); + vi.mocked(sendMessageTwitchInternal).mockResolvedValue({ + ok: true, + messageId: "msg-456", + }); + + await twitchOutbound.sendText({ + cfg: mockConfig, + to: undefined, + text: "Hello!", + accountId: "default", + }); + + expect(sendMessageTwitchInternal).toHaveBeenCalledWith( + "testchannel", + "Hello!", + mockConfig, + "default", + true, + console, + ); + }); + + it("should handle abort signal", async () => { + const abortController = new AbortController(); + abortController.abort(); + + await expect( + twitchOutbound.sendText({ + cfg: mockConfig, + to: "#testchannel", + text: "Hello!", + accountId: "default", + signal: abortController.signal, + }), + ).rejects.toThrow("Outbound delivery aborted"); + }); + + it("should throw on send failure", async () => { + const { getAccountConfig } = await import("./config.js"); + const { sendMessageTwitchInternal } = await import("./send.js"); + + vi.mocked(getAccountConfig).mockReturnValue(mockAccount); + vi.mocked(sendMessageTwitchInternal).mockResolvedValue({ + ok: false, + messageId: "failed-msg", + error: "Connection lost", + }); + + await expect( + twitchOutbound.sendText({ + cfg: mockConfig, + to: "#testchannel", + text: "Hello!", + accountId: "default", + }), + ).rejects.toThrow("Connection lost"); + }); + }); + + describe("sendMedia", () => { + it("should combine text and media URL", async () => { + const { sendMessageTwitchInternal } = await import("./send.js"); + const { getAccountConfig } = await import("./config.js"); + + vi.mocked(getAccountConfig).mockReturnValue(mockAccount); + vi.mocked(sendMessageTwitchInternal).mockResolvedValue({ + ok: true, + messageId: "media-msg-123", + }); + + const result = await twitchOutbound.sendMedia({ + cfg: mockConfig, + to: "#testchannel", + text: "Check this:", + mediaUrl: "https://example.com/image.png", + accountId: "default", + }); + + expect(result.channel).toBe("twitch"); + expect(result.messageId).toBe("media-msg-123"); + expect(sendMessageTwitchInternal).toHaveBeenCalledWith( + expect.anything(), + "Check this: https://example.com/image.png", + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + ); + }); + + it("should send media URL only when no text", async () => { + const { sendMessageTwitchInternal } = await import("./send.js"); + const { getAccountConfig } = await import("./config.js"); + + vi.mocked(getAccountConfig).mockReturnValue(mockAccount); + vi.mocked(sendMessageTwitchInternal).mockResolvedValue({ + ok: true, + messageId: "media-only-msg", + }); + + await twitchOutbound.sendMedia({ + cfg: mockConfig, + to: "#testchannel", + text: undefined, + mediaUrl: "https://example.com/image.png", + accountId: "default", + }); + + expect(sendMessageTwitchInternal).toHaveBeenCalledWith( + expect.anything(), + "https://example.com/image.png", + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + ); + }); + + it("should handle abort signal", async () => { + const abortController = new AbortController(); + abortController.abort(); + + await expect( + twitchOutbound.sendMedia({ + cfg: mockConfig, + to: "#testchannel", + text: "Check this:", + mediaUrl: "https://example.com/image.png", + accountId: "default", + signal: abortController.signal, + }), + ).rejects.toThrow("Outbound delivery aborted"); + }); + }); +}); diff --git a/extensions/twitch/src/outbound.ts b/extensions/twitch/src/outbound.ts new file mode 100644 index 000000000..7f2edabec --- /dev/null +++ b/extensions/twitch/src/outbound.ts @@ -0,0 +1,186 @@ +/** + * Twitch outbound adapter for sending messages. + * + * Implements the ChannelOutboundAdapter interface for Twitch chat. + * Supports text and media (URL) sending with markdown stripping and chunking. + */ + +import { DEFAULT_ACCOUNT_ID, getAccountConfig } from "./config.js"; +import { sendMessageTwitchInternal } from "./send.js"; +import type { + ChannelOutboundAdapter, + ChannelOutboundContext, + OutboundDeliveryResult, +} from "./types.js"; +import { chunkTextForTwitch } from "./utils/markdown.js"; +import { missingTargetError, normalizeTwitchChannel } from "./utils/twitch.js"; + +/** + * Twitch outbound adapter. + * + * Handles sending text and media to Twitch channels with automatic + * markdown stripping and message chunking. + */ +export const twitchOutbound: ChannelOutboundAdapter = { + /** Direct delivery mode - messages are sent immediately */ + deliveryMode: "direct", + + /** Twitch chat message limit is 500 characters */ + textChunkLimit: 500, + + /** Word-boundary chunker with markdown stripping */ + chunker: chunkTextForTwitch, + + /** + * Resolve target from context. + * + * Handles target resolution with allowlist support for implicit/heartbeat modes. + * For explicit mode, accepts any valid channel name. + * + * @param params - Resolution parameters + * @returns Resolved target or error + */ + resolveTarget: ({ to, allowFrom, mode }) => { + const trimmed = to?.trim() ?? ""; + const allowListRaw = (allowFrom ?? []) + .map((entry: unknown) => String(entry).trim()) + .filter(Boolean); + const hasWildcard = allowListRaw.includes("*"); + const allowList = allowListRaw + .filter((entry: string) => entry !== "*") + .map((entry: string) => normalizeTwitchChannel(entry)) + .filter((entry): entry is string => entry.length > 0); + + // If target is provided, normalize and validate it + if (trimmed) { + const normalizedTo = normalizeTwitchChannel(trimmed); + + // For implicit/heartbeat modes with allowList, check against allowlist + if (mode === "implicit" || mode === "heartbeat") { + if (hasWildcard || allowList.length === 0) { + return { ok: true, to: normalizedTo }; + } + if (allowList.includes(normalizedTo)) { + return { ok: true, to: normalizedTo }; + } + // Fallback to first allowFrom entry + // biome-ignore lint/style/noNonNullAssertion: length > 0 check ensures element exists + return { ok: true, to: allowList[0]! }; + } + + // For explicit mode, accept any valid channel name + return { ok: true, to: normalizedTo }; + } + + // No target provided, use allowFrom fallback + if (allowList.length > 0) { + // biome-ignore lint/style/noNonNullAssertion: length > 0 check ensures element exists + return { ok: true, to: allowList[0]! }; + } + + // No target and no allowFrom - error + return { + ok: false, + error: missingTargetError( + "Twitch", + " or channels.twitch.accounts..allowFrom[0]", + ), + }; + }, + + /** + * Send a text message to a Twitch channel. + * + * Strips markdown if enabled, validates account configuration, + * and sends the message via the Twitch client. + * + * @param params - Send parameters including target, text, and config + * @returns Delivery result with message ID and status + * + * @example + * const result = await twitchOutbound.sendText({ + * cfg: clawdbotConfig, + * to: "#mychannel", + * text: "Hello Twitch!", + * accountId: "default", + * }); + */ + sendText: async (params: ChannelOutboundContext): Promise => { + const { cfg, to, text, accountId, signal } = params; + + if (signal?.aborted) { + throw new Error("Outbound delivery aborted"); + } + + const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID; + const account = getAccountConfig(cfg, resolvedAccountId); + if (!account) { + const availableIds = Object.keys(cfg.channels?.twitch?.accounts ?? {}); + throw new Error( + `Twitch account not found: ${resolvedAccountId}. ` + + `Available accounts: ${availableIds.join(", ") || "none"}`, + ); + } + + const channel = to || account.channel; + if (!channel) { + throw new Error("No channel specified and no default channel in account config"); + } + + const result = await sendMessageTwitchInternal( + normalizeTwitchChannel(channel), + text, + cfg, + resolvedAccountId, + true, // stripMarkdown + console, + ); + + if (!result.ok) { + throw new Error(result.error ?? "Send failed"); + } + + return { + channel: "twitch", + messageId: result.messageId, + timestamp: Date.now(), + to: normalizeTwitchChannel(channel), + }; + }, + + /** + * Send media to a Twitch channel. + * + * Note: Twitch chat doesn't support direct media uploads. + * This sends the media URL as text instead. + * + * @param params - Send parameters including media URL + * @returns Delivery result with message ID and status + * + * @example + * const result = await twitchOutbound.sendMedia({ + * cfg: clawdbotConfig, + * to: "#mychannel", + * text: "Check this out!", + * mediaUrl: "https://example.com/image.png", + * accountId: "default", + * }); + */ + sendMedia: async (params: ChannelOutboundContext): Promise => { + const { text, mediaUrl, signal } = params; + + if (signal?.aborted) { + throw new Error("Outbound delivery aborted"); + } + + const message = mediaUrl ? `${text || ""} ${mediaUrl}`.trim() : text; + + if (!twitchOutbound.sendText) { + throw new Error("sendText not implemented"); + } + return twitchOutbound.sendText({ + ...params, + text: message, + }); + }, +}; diff --git a/extensions/twitch/src/plugin.test.ts b/extensions/twitch/src/plugin.test.ts new file mode 100644 index 000000000..dd8ec8ad0 --- /dev/null +++ b/extensions/twitch/src/plugin.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "vitest"; +import type { ClawdbotConfig } from "clawdbot/plugin-sdk"; +import { twitchPlugin } from "./plugin.js"; + +describe("twitchPlugin.status.buildAccountSnapshot", () => { + it("uses the resolved account ID for multi-account configs", async () => { + const secondary = { + channel: "secondary-channel", + username: "secondary", + accessToken: "oauth:secondary-token", + clientId: "secondary-client", + enabled: true, + }; + + const cfg = { + channels: { + twitch: { + accounts: { + default: { + channel: "default-channel", + username: "default", + accessToken: "oauth:default-token", + clientId: "default-client", + enabled: true, + }, + secondary, + }, + }, + }, + } as ClawdbotConfig; + + const snapshot = await twitchPlugin.status?.buildAccountSnapshot?.({ + account: secondary, + cfg, + }); + + expect(snapshot?.accountId).toBe("secondary"); + }); +}); diff --git a/extensions/twitch/src/plugin.ts b/extensions/twitch/src/plugin.ts new file mode 100644 index 000000000..2064722b0 --- /dev/null +++ b/extensions/twitch/src/plugin.ts @@ -0,0 +1,274 @@ +/** + * Twitch channel plugin for Clawdbot. + * + * Main plugin export combining all adapters (outbound, actions, status, gateway). + * This is the primary entry point for the Twitch channel integration. + */ + +import type { ClawdbotConfig } from "clawdbot/plugin-sdk"; +import { buildChannelConfigSchema } from "clawdbot/plugin-sdk"; +import { twitchMessageActions } from "./actions.js"; +import { TwitchConfigSchema } from "./config-schema.js"; +import { DEFAULT_ACCOUNT_ID, getAccountConfig, listAccountIds } from "./config.js"; +import { twitchOnboardingAdapter } from "./onboarding.js"; +import { twitchOutbound } from "./outbound.js"; +import { probeTwitch } from "./probe.js"; +import { resolveTwitchTargets } from "./resolver.js"; +import { collectTwitchStatusIssues } from "./status.js"; +import { removeClientManager } from "./client-manager-registry.js"; +import { resolveTwitchToken } from "./token.js"; +import { isAccountConfigured } from "./utils/twitch.js"; +import type { + ChannelAccountSnapshot, + ChannelCapabilities, + ChannelLogSink, + ChannelMeta, + ChannelPlugin, + ChannelResolveKind, + ChannelResolveResult, + TwitchAccountConfig, +} from "./types.js"; + +/** + * Twitch channel plugin. + * + * Implements the ChannelPlugin interface to provide Twitch chat integration + * for Clawdbot. Supports message sending, receiving, access control, and + * status monitoring. + */ +export const twitchPlugin: ChannelPlugin = { + /** Plugin identifier */ + id: "twitch", + + /** Plugin metadata */ + meta: { + id: "twitch", + label: "Twitch", + selectionLabel: "Twitch (Chat)", + docsPath: "/channels/twitch", + blurb: "Twitch chat integration", + aliases: ["twitch-chat"], + } satisfies ChannelMeta, + + /** Onboarding adapter */ + onboarding: twitchOnboardingAdapter, + + /** Pairing configuration */ + pairing: { + idLabel: "twitchUserId", + normalizeAllowEntry: (entry) => entry.replace(/^(twitch:)?user:?/i, ""), + notifyApproval: async ({ id }) => { + // Note: Twitch doesn't support DMs from bots, so pairing approval is limited + // We'll log the approval instead + console.warn(`Pairing approved for user ${id} (notification sent via chat if possible)`); + }, + }, + + /** Supported chat capabilities */ + capabilities: { + chatTypes: ["group"], + } satisfies ChannelCapabilities, + + /** Configuration schema for Twitch channel */ + configSchema: buildChannelConfigSchema(TwitchConfigSchema), + + /** Account configuration management */ + config: { + /** List all configured account IDs */ + listAccountIds: (cfg: ClawdbotConfig): string[] => listAccountIds(cfg), + + /** Resolve an account config by ID */ + resolveAccount: (cfg: ClawdbotConfig, accountId?: string | null): TwitchAccountConfig => { + const account = getAccountConfig(cfg, accountId ?? DEFAULT_ACCOUNT_ID); + if (!account) { + // Return a default/empty account if not configured + return { + username: "", + accessToken: "", + clientId: "", + enabled: false, + } as TwitchAccountConfig; + } + return account; + }, + + /** Get the default account ID */ + defaultAccountId: (): string => DEFAULT_ACCOUNT_ID, + + /** Check if an account is configured */ + isConfigured: (_account: unknown, cfg: ClawdbotConfig): boolean => { + const account = getAccountConfig(cfg, DEFAULT_ACCOUNT_ID); + const tokenResolution = resolveTwitchToken(cfg, { accountId: DEFAULT_ACCOUNT_ID }); + return account ? isAccountConfigured(account, tokenResolution.token) : false; + }, + + /** Check if an account is enabled */ + isEnabled: (account: TwitchAccountConfig | undefined): boolean => account?.enabled !== false, + + /** Describe account status */ + describeAccount: (account: TwitchAccountConfig | undefined) => { + return { + accountId: DEFAULT_ACCOUNT_ID, + enabled: account?.enabled !== false, + configured: account ? isAccountConfigured(account, account?.accessToken) : false, + }; + }, + }, + + /** Outbound message adapter */ + outbound: twitchOutbound, + + /** Message actions adapter */ + actions: twitchMessageActions, + + /** Resolver adapter for username -> user ID resolution */ + resolver: { + resolveTargets: async ({ + cfg, + accountId, + inputs, + kind, + runtime, + }: { + cfg: ClawdbotConfig; + accountId?: string | null; + inputs: string[]; + kind: ChannelResolveKind; + runtime: import("../../../src/runtime.js").RuntimeEnv; + }): Promise => { + const account = getAccountConfig(cfg, accountId ?? DEFAULT_ACCOUNT_ID); + + if (!account) { + return inputs.map((input) => ({ + input, + resolved: false, + note: "account not configured", + })); + } + + // Adapt RuntimeEnv.log to ChannelLogSink + const log: ChannelLogSink = { + info: (msg) => runtime.log(msg), + warn: (msg) => runtime.log(msg), + error: (msg) => runtime.error(msg), + debug: (msg) => runtime.log(msg), + }; + return await resolveTwitchTargets(inputs, account, kind, log); + }, + }, + + /** Status monitoring adapter */ + status: { + /** Default runtime state */ + defaultRuntime: { + accountId: DEFAULT_ACCOUNT_ID, + running: false, + lastStartAt: null, + lastStopAt: null, + lastError: null, + }, + + /** Build channel summary from snapshot */ + buildChannelSummary: ({ snapshot }: { snapshot: ChannelAccountSnapshot }) => ({ + configured: snapshot.configured ?? false, + running: snapshot.running ?? false, + lastStartAt: snapshot.lastStartAt ?? null, + lastStopAt: snapshot.lastStopAt ?? null, + lastError: snapshot.lastError ?? null, + probe: snapshot.probe, + lastProbeAt: snapshot.lastProbeAt ?? null, + }), + + /** Probe account connection */ + probeAccount: async ({ + account, + timeoutMs, + }: { + account: TwitchAccountConfig; + timeoutMs: number; + }): Promise => { + return await probeTwitch(account, timeoutMs); + }, + + /** Build account snapshot with current status */ + buildAccountSnapshot: ({ + account, + cfg, + runtime, + probe, + }: { + account: TwitchAccountConfig; + cfg: ClawdbotConfig; + runtime?: ChannelAccountSnapshot; + probe?: unknown; + }): ChannelAccountSnapshot => { + const twitch = (cfg as Record).channels as + | Record + | undefined; + const twitchCfg = twitch?.twitch as Record | undefined; + const accountMap = (twitchCfg?.accounts as Record | undefined) ?? {}; + const resolvedAccountId = + Object.entries(accountMap).find(([, value]) => value === account)?.[0] ?? + DEFAULT_ACCOUNT_ID; + const tokenResolution = resolveTwitchToken(cfg, { accountId: resolvedAccountId }); + return { + accountId: resolvedAccountId, + enabled: account?.enabled !== false, + configured: isAccountConfigured(account, tokenResolution.token), + running: runtime?.running ?? false, + lastStartAt: runtime?.lastStartAt ?? null, + lastStopAt: runtime?.lastStopAt ?? null, + lastError: runtime?.lastError ?? null, + probe, + }; + }, + + /** Collect status issues for all accounts */ + collectStatusIssues: collectTwitchStatusIssues, + }, + + /** Gateway adapter for connection lifecycle */ + gateway: { + /** Start an account connection */ + startAccount: async (ctx): Promise => { + const account = ctx.account as TwitchAccountConfig; + const accountId = ctx.accountId; + + ctx.setStatus?.({ + accountId, + running: true, + lastStartAt: Date.now(), + lastError: null, + }); + + ctx.log?.info(`Starting Twitch connection for ${account.username}`); + + // Lazy import: the monitor pulls the reply pipeline; avoid ESM init cycles. + const { monitorTwitchProvider } = await import("./monitor.js"); + await monitorTwitchProvider({ + account, + accountId, + config: ctx.cfg, + runtime: ctx.runtime, + abortSignal: ctx.abortSignal, + }); + }, + + /** Stop an account connection */ + stopAccount: async (ctx): Promise => { + const account = ctx.account as TwitchAccountConfig; + const accountId = ctx.accountId; + + // Disconnect and remove client manager from registry + await removeClientManager(accountId); + + ctx.setStatus?.({ + accountId, + running: false, + lastStopAt: Date.now(), + }); + + ctx.log?.info(`Stopped Twitch connection for ${account.username}`); + }, + }, +}; diff --git a/extensions/twitch/src/probe.test.ts b/extensions/twitch/src/probe.test.ts new file mode 100644 index 000000000..21d43ee18 --- /dev/null +++ b/extensions/twitch/src/probe.test.ts @@ -0,0 +1,198 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { probeTwitch } from "./probe.js"; +import type { TwitchAccountConfig } from "./types.js"; + +// Mock Twurple modules - Vitest v4 compatible mocking +const mockUnbind = vi.fn(); + +// Event handler storage +let connectHandler: (() => void) | null = null; +let disconnectHandler: ((manually: boolean, reason?: Error) => void) | null = null; +let authFailHandler: (() => void) | null = null; + +// Event listener mocks that store handlers and return unbind function +const mockOnConnect = vi.fn((handler: () => void) => { + connectHandler = handler; + return { unbind: mockUnbind }; +}); + +const mockOnDisconnect = vi.fn((handler: (manually: boolean, reason?: Error) => void) => { + disconnectHandler = handler; + return { unbind: mockUnbind }; +}); + +const mockOnAuthenticationFailure = vi.fn((handler: () => void) => { + authFailHandler = handler; + return { unbind: mockUnbind }; +}); + +// Connect mock that triggers the registered handler +const defaultConnectImpl = async () => { + // Simulate successful connection by calling the handler after a delay + if (connectHandler) { + await new Promise((resolve) => setTimeout(resolve, 1)); + connectHandler(); + } +}; + +const mockConnect = vi.fn().mockImplementation(defaultConnectImpl); + +const mockQuit = vi.fn().mockResolvedValue(undefined); + +vi.mock("@twurple/chat", () => ({ + ChatClient: class { + connect = mockConnect; + quit = mockQuit; + onConnect = mockOnConnect; + onDisconnect = mockOnDisconnect; + onAuthenticationFailure = mockOnAuthenticationFailure; + }, +})); + +vi.mock("@twurple/auth", () => ({ + StaticAuthProvider: class {}, +})); + +describe("probeTwitch", () => { + const mockAccount: TwitchAccountConfig = { + username: "testbot", + token: "oauth:test123456789", + channel: "testchannel", + }; + + beforeEach(() => { + vi.clearAllMocks(); + // Reset handlers + connectHandler = null; + disconnectHandler = null; + authFailHandler = null; + }); + + it("returns error when username is missing", async () => { + const account = { ...mockAccount, username: "" }; + const result = await probeTwitch(account, 5000); + + expect(result.ok).toBe(false); + expect(result.error).toContain("missing credentials"); + }); + + it("returns error when token is missing", async () => { + const account = { ...mockAccount, token: "" }; + const result = await probeTwitch(account, 5000); + + expect(result.ok).toBe(false); + expect(result.error).toContain("missing credentials"); + }); + + it("attempts connection regardless of token prefix", async () => { + // Note: probeTwitch doesn't validate token format - it tries to connect with whatever token is provided + // The actual connection would fail in production with an invalid token + const account = { ...mockAccount, token: "raw_token_no_prefix" }; + const result = await probeTwitch(account, 5000); + + // With mock, connection succeeds even without oauth: prefix + expect(result.ok).toBe(true); + }); + + it("successfully connects with valid credentials", async () => { + const result = await probeTwitch(mockAccount, 5000); + + expect(result.ok).toBe(true); + expect(result.connected).toBe(true); + expect(result.username).toBe("testbot"); + expect(result.channel).toBe("testchannel"); // uses account's configured channel + }); + + it("uses custom channel when specified", async () => { + const account: TwitchAccountConfig = { + ...mockAccount, + channel: "customchannel", + }; + + const result = await probeTwitch(account, 5000); + + expect(result.ok).toBe(true); + expect(result.channel).toBe("customchannel"); + }); + + it("times out when connection takes too long", async () => { + mockConnect.mockImplementationOnce(() => new Promise(() => {})); // Never resolves + + const result = await probeTwitch(mockAccount, 100); + + expect(result.ok).toBe(false); + expect(result.error).toContain("timeout"); + + // Reset mock + mockConnect.mockImplementation(defaultConnectImpl); + }); + + it("cleans up client even on failure", async () => { + mockConnect.mockImplementationOnce(async () => { + // Simulate connection failure by calling disconnect handler + // onDisconnect signature: (manually: boolean, reason?: Error) => void + if (disconnectHandler) { + await new Promise((resolve) => setTimeout(resolve, 1)); + disconnectHandler(false, new Error("Connection failed")); + } + }); + + const result = await probeTwitch(mockAccount, 5000); + + expect(result.ok).toBe(false); + expect(result.error).toContain("Connection failed"); + expect(mockQuit).toHaveBeenCalled(); + + // Reset mocks + mockConnect.mockImplementation(defaultConnectImpl); + }); + + it("handles connection errors gracefully", async () => { + mockConnect.mockImplementationOnce(async () => { + // Simulate connection failure by calling disconnect handler + // onDisconnect signature: (manually: boolean, reason?: Error) => void + if (disconnectHandler) { + await new Promise((resolve) => setTimeout(resolve, 1)); + disconnectHandler(false, new Error("Network error")); + } + }); + + const result = await probeTwitch(mockAccount, 5000); + + expect(result.ok).toBe(false); + expect(result.error).toContain("Network error"); + + // Reset mock + mockConnect.mockImplementation(defaultConnectImpl); + }); + + it("trims token before validation", async () => { + const account: TwitchAccountConfig = { + ...mockAccount, + token: " oauth:test123456789 ", + }; + + const result = await probeTwitch(account, 5000); + + expect(result.ok).toBe(true); + }); + + it("handles non-Error objects in catch block", async () => { + mockConnect.mockImplementationOnce(async () => { + // Simulate connection failure by calling disconnect handler + // onDisconnect signature: (manually: boolean, reason?: Error) => void + if (disconnectHandler) { + await new Promise((resolve) => setTimeout(resolve, 1)); + disconnectHandler(false, "String error" as unknown as Error); + } + }); + + const result = await probeTwitch(mockAccount, 5000); + + expect(result.ok).toBe(false); + expect(result.error).toBe("String error"); + + // Reset mock + mockConnect.mockImplementation(defaultConnectImpl); + }); +}); diff --git a/extensions/twitch/src/probe.ts b/extensions/twitch/src/probe.ts new file mode 100644 index 000000000..90e34826b --- /dev/null +++ b/extensions/twitch/src/probe.ts @@ -0,0 +1,118 @@ +import { StaticAuthProvider } from "@twurple/auth"; +import { ChatClient } from "@twurple/chat"; +import type { TwitchAccountConfig } from "./types.js"; +import { normalizeToken } from "./utils/twitch.js"; + +/** + * Result of probing a Twitch account + */ +export type ProbeTwitchResult = { + ok: boolean; + error?: string; + username?: string; + elapsedMs: number; + connected?: boolean; + channel?: string; +}; + +/** + * Probe a Twitch account to verify the connection is working + * + * This tests the Twitch OAuth token by attempting to connect + * to the chat server and verify the bot's username. + */ +export async function probeTwitch( + account: TwitchAccountConfig, + timeoutMs: number, +): Promise { + const started = Date.now(); + + if (!account.token || !account.username) { + return { + ok: false, + error: "missing credentials (token, username)", + username: account.username, + elapsedMs: Date.now() - started, + }; + } + + const rawToken = normalizeToken(account.token.trim()); + + let client: ChatClient | undefined; + + try { + const authProvider = new StaticAuthProvider(account.clientId ?? "", rawToken); + + client = new ChatClient({ + authProvider, + }); + + // Create a promise that resolves when connected + const connectionPromise = new Promise((resolve, reject) => { + let settled = false; + let connectListener: ReturnType | undefined; + let disconnectListener: ReturnType | undefined; + let authFailListener: ReturnType | undefined; + + const cleanup = () => { + if (settled) return; + settled = true; + connectListener?.unbind(); + disconnectListener?.unbind(); + authFailListener?.unbind(); + }; + + // Success: connection established + connectListener = client?.onConnect(() => { + cleanup(); + resolve(); + }); + + // Failure: disconnected (e.g., auth failed) + disconnectListener = client?.onDisconnect((_manually, reason) => { + cleanup(); + reject(reason || new Error("Disconnected")); + }); + + // Failure: authentication failed + authFailListener = client?.onAuthenticationFailure(() => { + cleanup(); + reject(new Error("Authentication failed")); + }); + }); + + const timeout = new Promise((_, reject) => { + setTimeout(() => reject(new Error(`timeout after ${timeoutMs}ms`)), timeoutMs); + }); + + client.connect(); + await Promise.race([connectionPromise, timeout]); + + client.quit(); + client = undefined; + + return { + ok: true, + connected: true, + username: account.username, + channel: account.channel, + elapsedMs: Date.now() - started, + }; + } catch (error) { + return { + ok: false, + error: error instanceof Error ? error.message : String(error), + username: account.username, + channel: account.channel, + elapsedMs: Date.now() - started, + }; + } finally { + if (client) { + try { + client.quit(); + } catch { + // Ignore cleanup errors + } + } + } +} diff --git a/extensions/twitch/src/resolver.ts b/extensions/twitch/src/resolver.ts new file mode 100644 index 000000000..acc578f4b --- /dev/null +++ b/extensions/twitch/src/resolver.ts @@ -0,0 +1,137 @@ +/** + * Twitch resolver adapter for channel/user name resolution. + * + * This module implements the ChannelResolverAdapter interface to resolve + * Twitch usernames to user IDs via the Twitch Helix API. + */ + +import { ApiClient } from "@twurple/api"; +import { StaticAuthProvider } from "@twurple/auth"; +import type { ChannelResolveKind, ChannelResolveResult } from "./types.js"; +import type { ChannelLogSink, TwitchAccountConfig } from "./types.js"; +import { normalizeToken } from "./utils/twitch.js"; + +/** + * Normalize a Twitch username - strip @ prefix and convert to lowercase + */ +function normalizeUsername(input: string): string { + const trimmed = input.trim(); + if (trimmed.startsWith("@")) { + return trimmed.slice(1).toLowerCase(); + } + return trimmed.toLowerCase(); +} + +/** + * Create a logger that includes the Twitch prefix + */ +function createLogger(logger?: ChannelLogSink): ChannelLogSink { + return { + info: (msg: string) => logger?.info(msg), + warn: (msg: string) => logger?.warn(msg), + error: (msg: string) => logger?.error(msg), + debug: (msg: string) => logger?.debug?.(msg) ?? (() => {}), + }; +} + +/** + * Resolve Twitch usernames to user IDs via the Helix API + * + * @param inputs - Array of usernames or user IDs to resolve + * @param account - Twitch account configuration with auth credentials + * @param kind - Type of target to resolve ("user" or "group") + * @param logger - Optional logger + * @returns Promise resolving to array of ChannelResolveResult + */ +export async function resolveTwitchTargets( + inputs: string[], + account: TwitchAccountConfig, + kind: ChannelResolveKind, + logger?: ChannelLogSink, +): Promise { + const log = createLogger(logger); + + if (!account.clientId || !account.token) { + log.error("Missing Twitch client ID or token"); + return inputs.map((input) => ({ + input, + resolved: false, + note: "missing Twitch credentials", + })); + } + + const normalizedToken = normalizeToken(account.token); + + const authProvider = new StaticAuthProvider(account.clientId, normalizedToken); + const apiClient = new ApiClient({ authProvider }); + + const results: ChannelResolveResult[] = []; + + for (const input of inputs) { + const normalized = normalizeUsername(input); + + if (!normalized) { + results.push({ + input, + resolved: false, + note: "empty input", + }); + continue; + } + + const looksLikeUserId = /^\d+$/.test(normalized); + + try { + if (looksLikeUserId) { + const user = await apiClient.users.getUserById(normalized); + + if (user) { + results.push({ + input, + resolved: true, + id: user.id, + name: user.name, + }); + log.debug?.(`Resolved user ID ${normalized} -> ${user.name}`); + } else { + results.push({ + input, + resolved: false, + note: "user ID not found", + }); + log.warn(`User ID ${normalized} not found`); + } + } else { + const user = await apiClient.users.getUserByName(normalized); + + if (user) { + results.push({ + input, + resolved: true, + id: user.id, + name: user.name, + note: user.displayName !== user.name ? `display: ${user.displayName}` : undefined, + }); + log.debug?.(`Resolved username ${normalized} -> ${user.id} (${user.name})`); + } else { + results.push({ + input, + resolved: false, + note: "username not found", + }); + log.warn(`Username ${normalized} not found`); + } + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + results.push({ + input, + resolved: false, + note: `API error: ${errorMessage}`, + }); + log.error(`Failed to resolve ${input}: ${errorMessage}`); + } + } + + return results; +} diff --git a/extensions/twitch/src/runtime.ts b/extensions/twitch/src/runtime.ts new file mode 100644 index 000000000..5c2f1c672 --- /dev/null +++ b/extensions/twitch/src/runtime.ts @@ -0,0 +1,14 @@ +import type { PluginRuntime } from "clawdbot/plugin-sdk"; + +let runtime: PluginRuntime | null = null; + +export function setTwitchRuntime(next: PluginRuntime) { + runtime = next; +} + +export function getTwitchRuntime(): PluginRuntime { + if (!runtime) { + throw new Error("Twitch runtime not initialized"); + } + return runtime; +} diff --git a/extensions/twitch/src/send.test.ts b/extensions/twitch/src/send.test.ts new file mode 100644 index 000000000..541d4964d --- /dev/null +++ b/extensions/twitch/src/send.test.ts @@ -0,0 +1,289 @@ +/** + * Tests for send.ts module + * + * Tests cover: + * - Message sending with valid configuration + * - Account resolution and validation + * - Channel normalization + * - Markdown stripping + * - Error handling for missing/invalid accounts + * - Registry integration + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { sendMessageTwitchInternal } from "./send.js"; +import type { ClawdbotConfig } from "clawdbot/plugin-sdk"; + +// Mock dependencies +vi.mock("./config.js", () => ({ + DEFAULT_ACCOUNT_ID: "default", + getAccountConfig: vi.fn(), +})); + +vi.mock("./utils/twitch.js", () => ({ + generateMessageId: vi.fn(() => "test-msg-id"), + isAccountConfigured: vi.fn(() => true), + normalizeTwitchChannel: (channel: string) => channel.toLowerCase().replace(/^#/, ""), +})); + +vi.mock("./utils/markdown.js", () => ({ + stripMarkdownForTwitch: vi.fn((text: string) => text.replace(/\*\*/g, "")), +})); + +vi.mock("./client-manager-registry.js", () => ({ + getClientManager: vi.fn(), +})); + +describe("send", () => { + const mockLogger = { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + }; + + const mockAccount = { + username: "testbot", + token: "oauth:test123", + clientId: "test-client-id", + channel: "#testchannel", + }; + + const mockConfig = { + channels: { + twitch: { + accounts: { + default: mockAccount, + }, + }, + }, + } as unknown as ClawdbotConfig; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe("sendMessageTwitchInternal", () => { + it("should send a message successfully", async () => { + const { getAccountConfig } = await import("./config.js"); + const { getClientManager } = await import("./client-manager-registry.js"); + const { stripMarkdownForTwitch } = await import("./utils/markdown.js"); + + vi.mocked(getAccountConfig).mockReturnValue(mockAccount); + vi.mocked(getClientManager).mockReturnValue({ + sendMessage: vi.fn().mockResolvedValue({ + ok: true, + messageId: "twitch-msg-123", + }), + } as ReturnType); + vi.mocked(stripMarkdownForTwitch).mockImplementation((text) => text); + + const result = await sendMessageTwitchInternal( + "#testchannel", + "Hello Twitch!", + mockConfig, + "default", + false, + mockLogger as unknown as Console, + ); + + expect(result.ok).toBe(true); + expect(result.messageId).toBe("twitch-msg-123"); + }); + + it("should strip markdown when enabled", async () => { + const { getAccountConfig } = await import("./config.js"); + const { getClientManager } = await import("./client-manager-registry.js"); + const { stripMarkdownForTwitch } = await import("./utils/markdown.js"); + + vi.mocked(getAccountConfig).mockReturnValue(mockAccount); + vi.mocked(getClientManager).mockReturnValue({ + sendMessage: vi.fn().mockResolvedValue({ + ok: true, + messageId: "twitch-msg-456", + }), + } as ReturnType); + vi.mocked(stripMarkdownForTwitch).mockImplementation((text) => text.replace(/\*\*/g, "")); + + await sendMessageTwitchInternal( + "#testchannel", + "**Bold** text", + mockConfig, + "default", + true, + mockLogger as unknown as Console, + ); + + expect(stripMarkdownForTwitch).toHaveBeenCalledWith("**Bold** text"); + }); + + it("should return error when account not found", async () => { + const { getAccountConfig } = await import("./config.js"); + + vi.mocked(getAccountConfig).mockReturnValue(null); + + const result = await sendMessageTwitchInternal( + "#testchannel", + "Hello!", + mockConfig, + "nonexistent", + false, + mockLogger as unknown as Console, + ); + + expect(result.ok).toBe(false); + expect(result.error).toContain("Account not found: nonexistent"); + }); + + it("should return error when account not configured", async () => { + const { getAccountConfig } = await import("./config.js"); + const { isAccountConfigured } = await import("./utils/twitch.js"); + + vi.mocked(getAccountConfig).mockReturnValue(mockAccount); + vi.mocked(isAccountConfigured).mockReturnValue(false); + + const result = await sendMessageTwitchInternal( + "#testchannel", + "Hello!", + mockConfig, + "default", + false, + mockLogger as unknown as Console, + ); + + expect(result.ok).toBe(false); + expect(result.error).toContain("not properly configured"); + }); + + it("should return error when no channel specified", async () => { + const { getAccountConfig } = await import("./config.js"); + const { isAccountConfigured } = await import("./utils/twitch.js"); + + // Set channel to undefined to trigger the error (bypassing type check) + const accountWithoutChannel = { + ...mockAccount, + channel: undefined as unknown as string, + }; + vi.mocked(getAccountConfig).mockReturnValue(accountWithoutChannel); + vi.mocked(isAccountConfigured).mockReturnValue(true); + + const result = await sendMessageTwitchInternal( + "", + "Hello!", + mockConfig, + "default", + false, + mockLogger as unknown as Console, + ); + + expect(result.ok).toBe(false); + expect(result.error).toContain("No channel specified"); + }); + + it("should skip sending empty message after markdown stripping", async () => { + const { getAccountConfig } = await import("./config.js"); + const { isAccountConfigured } = await import("./utils/twitch.js"); + const { stripMarkdownForTwitch } = await import("./utils/markdown.js"); + + vi.mocked(getAccountConfig).mockReturnValue(mockAccount); + vi.mocked(isAccountConfigured).mockReturnValue(true); + vi.mocked(stripMarkdownForTwitch).mockReturnValue(""); + + const result = await sendMessageTwitchInternal( + "#testchannel", + "**Only markdown**", + mockConfig, + "default", + true, + mockLogger as unknown as Console, + ); + + expect(result.ok).toBe(true); + expect(result.messageId).toBe("skipped"); + }); + + it("should return error when client manager not found", async () => { + const { getAccountConfig } = await import("./config.js"); + const { isAccountConfigured } = await import("./utils/twitch.js"); + const { getClientManager } = await import("./client-manager-registry.js"); + + vi.mocked(getAccountConfig).mockReturnValue(mockAccount); + vi.mocked(isAccountConfigured).mockReturnValue(true); + vi.mocked(getClientManager).mockReturnValue(undefined); + + const result = await sendMessageTwitchInternal( + "#testchannel", + "Hello!", + mockConfig, + "default", + false, + mockLogger as unknown as Console, + ); + + expect(result.ok).toBe(false); + expect(result.error).toContain("Client manager not found"); + }); + + it("should handle send errors gracefully", async () => { + const { getAccountConfig } = await import("./config.js"); + const { isAccountConfigured } = await import("./utils/twitch.js"); + const { getClientManager } = await import("./client-manager-registry.js"); + + vi.mocked(getAccountConfig).mockReturnValue(mockAccount); + vi.mocked(isAccountConfigured).mockReturnValue(true); + vi.mocked(getClientManager).mockReturnValue({ + sendMessage: vi.fn().mockRejectedValue(new Error("Connection lost")), + } as ReturnType); + + const result = await sendMessageTwitchInternal( + "#testchannel", + "Hello!", + mockConfig, + "default", + false, + mockLogger as unknown as Console, + ); + + expect(result.ok).toBe(false); + expect(result.error).toBe("Connection lost"); + expect(mockLogger.error).toHaveBeenCalled(); + }); + + it("should use account channel when channel parameter is empty", async () => { + const { getAccountConfig } = await import("./config.js"); + const { isAccountConfigured } = await import("./utils/twitch.js"); + const { getClientManager } = await import("./client-manager-registry.js"); + + vi.mocked(getAccountConfig).mockReturnValue(mockAccount); + vi.mocked(isAccountConfigured).mockReturnValue(true); + const mockSend = vi.fn().mockResolvedValue({ + ok: true, + messageId: "twitch-msg-789", + }); + vi.mocked(getClientManager).mockReturnValue({ + sendMessage: mockSend, + } as ReturnType); + + await sendMessageTwitchInternal( + "", + "Hello!", + mockConfig, + "default", + false, + mockLogger as unknown as Console, + ); + + expect(mockSend).toHaveBeenCalledWith( + mockAccount, + "testchannel", // normalized account channel + "Hello!", + mockConfig, + "default", + ); + }); + }); +}); diff --git a/extensions/twitch/src/send.ts b/extensions/twitch/src/send.ts new file mode 100644 index 000000000..cc9ff678e --- /dev/null +++ b/extensions/twitch/src/send.ts @@ -0,0 +1,136 @@ +/** + * Twitch message sending functions with dependency injection support. + * + * These functions are the primary interface for sending messages to Twitch. + * They support dependency injection via the `deps` parameter for testability. + */ + +import { DEFAULT_ACCOUNT_ID, getAccountConfig } from "./config.js"; +import { getClientManager as getRegistryClientManager } from "./client-manager-registry.js"; +import type { ClawdbotConfig } from "clawdbot/plugin-sdk"; +import { resolveTwitchToken } from "./token.js"; +import { stripMarkdownForTwitch } from "./utils/markdown.js"; +import { generateMessageId, isAccountConfigured, normalizeTwitchChannel } from "./utils/twitch.js"; + +/** + * Result from sending a message to Twitch. + */ +export interface SendMessageResult { + /** Whether the send was successful */ + ok: boolean; + /** The message ID (generated for tracking) */ + messageId: string; + /** Error message if the send failed */ + error?: string; +} + +/** + * Internal send function used by the outbound adapter. + * + * This function has access to the full Clawdbot config and handles + * account resolution, markdown stripping, and actual message sending. + * + * @param channel - The channel name + * @param text - The message text + * @param cfg - Full Clawdbot configuration + * @param accountId - Account ID to use + * @param stripMarkdown - Whether to strip markdown (default: true) + * @param logger - Logger instance + * @returns Result with message ID and status + * + * @example + * const result = await sendMessageTwitchInternal( + * "#mychannel", + * "Hello Twitch!", + * clawdbotConfig, + * "default", + * true, + * console, + * ); + */ +export async function sendMessageTwitchInternal( + channel: string, + text: string, + cfg: ClawdbotConfig, + accountId: string = DEFAULT_ACCOUNT_ID, + stripMarkdown: boolean = true, + logger: Console = console, +): Promise { + const account = getAccountConfig(cfg, accountId); + if (!account) { + const availableIds = Object.keys(cfg.channels?.twitch?.accounts ?? {}); + return { + ok: false, + messageId: generateMessageId(), + error: `Account not found: ${accountId}. Available accounts: ${availableIds.join(", ") || "none"}`, + }; + } + + const tokenResolution = resolveTwitchToken(cfg, { accountId }); + if (!isAccountConfigured(account, tokenResolution.token)) { + return { + ok: false, + messageId: generateMessageId(), + error: + `Account ${accountId} is not properly configured. ` + + "Required: username, clientId, and token (config or env for default account).", + }; + } + + const normalizedChannel = channel || account.channel; + if (!normalizedChannel) { + return { + ok: false, + messageId: generateMessageId(), + error: "No channel specified and no default channel in account config", + }; + } + + const cleanedText = stripMarkdown ? stripMarkdownForTwitch(text) : text; + if (!cleanedText) { + return { + ok: true, + messageId: "skipped", + }; + } + + const clientManager = getRegistryClientManager(accountId); + if (!clientManager) { + return { + ok: false, + messageId: generateMessageId(), + error: `Client manager not found for account: ${accountId}. Please start the Twitch gateway first.`, + }; + } + + try { + const result = await clientManager.sendMessage( + account, + normalizeTwitchChannel(normalizedChannel), + cleanedText, + cfg, + accountId, + ); + + if (!result.ok) { + return { + ok: false, + messageId: result.messageId ?? generateMessageId(), + error: result.error ?? "Send failed", + }; + } + + return { + ok: true, + messageId: result.messageId ?? generateMessageId(), + }; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + logger.error(`Failed to send message: ${errorMsg}`); + return { + ok: false, + messageId: generateMessageId(), + error: errorMsg, + }; + } +} diff --git a/extensions/twitch/src/status.test.ts b/extensions/twitch/src/status.test.ts new file mode 100644 index 000000000..8f7cd55ab --- /dev/null +++ b/extensions/twitch/src/status.test.ts @@ -0,0 +1,270 @@ +/** + * Tests for status.ts module + * + * Tests cover: + * - Detection of unconfigured accounts + * - Detection of disabled accounts + * - Detection of missing clientId + * - Token format warnings + * - Access control warnings + * - Runtime error detection + */ + +import { describe, expect, it } from "vitest"; +import { collectTwitchStatusIssues } from "./status.js"; +import type { ChannelAccountSnapshot } from "./types.js"; + +describe("status", () => { + describe("collectTwitchStatusIssues", () => { + it("should detect unconfigured accounts", () => { + const snapshots: ChannelAccountSnapshot[] = [ + { + accountId: "default", + configured: false, + enabled: true, + running: false, + }, + ]; + + const issues = collectTwitchStatusIssues(snapshots); + + expect(issues.length).toBeGreaterThan(0); + expect(issues[0]?.kind).toBe("config"); + expect(issues[0]?.message).toContain("not properly configured"); + }); + + it("should detect disabled accounts", () => { + const snapshots: ChannelAccountSnapshot[] = [ + { + accountId: "default", + configured: true, + enabled: false, + running: false, + }, + ]; + + const issues = collectTwitchStatusIssues(snapshots); + + expect(issues.length).toBeGreaterThan(0); + const disabledIssue = issues.find((i) => i.message.includes("disabled")); + expect(disabledIssue).toBeDefined(); + }); + + it("should detect missing clientId when account configured (simplified config)", () => { + const snapshots: ChannelAccountSnapshot[] = [ + { + accountId: "default", + configured: true, + enabled: true, + running: false, + }, + ]; + + const mockCfg = { + channels: { + twitch: { + username: "testbot", + accessToken: "oauth:test123", + // clientId missing + }, + }, + }; + + const issues = collectTwitchStatusIssues(snapshots, () => mockCfg as never); + + const clientIdIssue = issues.find((i) => i.message.includes("client ID")); + expect(clientIdIssue).toBeDefined(); + }); + + it("should warn about oauth: prefix in token (simplified config)", () => { + const snapshots: ChannelAccountSnapshot[] = [ + { + accountId: "default", + configured: true, + enabled: true, + running: false, + }, + ]; + + const mockCfg = { + channels: { + twitch: { + username: "testbot", + accessToken: "oauth:test123", // has prefix + clientId: "test-id", + }, + }, + }; + + const issues = collectTwitchStatusIssues(snapshots, () => mockCfg as never); + + const prefixIssue = issues.find((i) => i.message.includes("oauth:")); + expect(prefixIssue).toBeDefined(); + expect(prefixIssue?.kind).toBe("config"); + }); + + it("should detect clientSecret without refreshToken (simplified config)", () => { + const snapshots: ChannelAccountSnapshot[] = [ + { + accountId: "default", + configured: true, + enabled: true, + running: false, + }, + ]; + + const mockCfg = { + channels: { + twitch: { + username: "testbot", + accessToken: "oauth:test123", + clientId: "test-id", + clientSecret: "secret123", + // refreshToken missing + }, + }, + }; + + const issues = collectTwitchStatusIssues(snapshots, () => mockCfg as never); + + const secretIssue = issues.find((i) => i.message.includes("clientSecret")); + expect(secretIssue).toBeDefined(); + }); + + it("should detect empty allowFrom array (simplified config)", () => { + const snapshots: ChannelAccountSnapshot[] = [ + { + accountId: "default", + configured: true, + enabled: true, + running: false, + }, + ]; + + const mockCfg = { + channels: { + twitch: { + username: "testbot", + accessToken: "test123", + clientId: "test-id", + allowFrom: [], // empty array + }, + }, + }; + + const issues = collectTwitchStatusIssues(snapshots, () => mockCfg as never); + + const allowFromIssue = issues.find((i) => i.message.includes("allowFrom")); + expect(allowFromIssue).toBeDefined(); + }); + + it("should detect allowedRoles 'all' with allowFrom conflict (simplified config)", () => { + const snapshots: ChannelAccountSnapshot[] = [ + { + accountId: "default", + configured: true, + enabled: true, + running: false, + }, + ]; + + const mockCfg = { + channels: { + twitch: { + username: "testbot", + accessToken: "test123", + clientId: "test-id", + allowedRoles: ["all"], + allowFrom: ["123456"], // conflict! + }, + }, + }; + + const issues = collectTwitchStatusIssues(snapshots, () => mockCfg as never); + + const conflictIssue = issues.find((i) => i.kind === "intent"); + expect(conflictIssue).toBeDefined(); + expect(conflictIssue?.message).toContain("allowedRoles is set to 'all'"); + }); + + it("should detect runtime errors", () => { + const snapshots: ChannelAccountSnapshot[] = [ + { + accountId: "default", + configured: true, + enabled: true, + running: false, + lastError: "Connection timeout", + }, + ]; + + const issues = collectTwitchStatusIssues(snapshots); + + const runtimeIssue = issues.find((i) => i.kind === "runtime"); + expect(runtimeIssue).toBeDefined(); + expect(runtimeIssue?.message).toContain("Connection timeout"); + }); + + it("should detect accounts that never connected", () => { + const snapshots: ChannelAccountSnapshot[] = [ + { + accountId: "default", + configured: true, + enabled: true, + running: false, + lastStartAt: undefined, + lastInboundAt: undefined, + lastOutboundAt: undefined, + }, + ]; + + const issues = collectTwitchStatusIssues(snapshots); + + const neverConnectedIssue = issues.find((i) => + i.message.includes("never connected successfully"), + ); + expect(neverConnectedIssue).toBeDefined(); + }); + + it("should detect long-running connections", () => { + const oldDate = Date.now() - 8 * 24 * 60 * 60 * 1000; // 8 days ago + + const snapshots: ChannelAccountSnapshot[] = [ + { + accountId: "default", + configured: true, + enabled: true, + running: true, + lastStartAt: oldDate, + }, + ]; + + const issues = collectTwitchStatusIssues(snapshots); + + const uptimeIssue = issues.find((i) => i.message.includes("running for")); + expect(uptimeIssue).toBeDefined(); + }); + + it("should handle empty snapshots array", () => { + const issues = collectTwitchStatusIssues([]); + + expect(issues).toEqual([]); + }); + + it("should skip non-Twitch accounts gracefully", () => { + const snapshots: ChannelAccountSnapshot[] = [ + { + accountId: undefined, + configured: false, + enabled: true, + running: false, + }, + ]; + + const issues = collectTwitchStatusIssues(snapshots); + + // Should not crash, may return empty or minimal issues + expect(Array.isArray(issues)).toBe(true); + }); + }); +}); diff --git a/extensions/twitch/src/status.ts b/extensions/twitch/src/status.ts new file mode 100644 index 000000000..b2a488e66 --- /dev/null +++ b/extensions/twitch/src/status.ts @@ -0,0 +1,176 @@ +/** + * Twitch status issues collector. + * + * Detects and reports configuration issues for Twitch accounts. + */ + +import { getAccountConfig } from "./config.js"; +import type { ChannelAccountSnapshot, ChannelStatusIssue } from "./types.js"; +import { resolveTwitchToken } from "./token.js"; +import { isAccountConfigured } from "./utils/twitch.js"; + +/** + * Collect status issues for Twitch accounts. + * + * Analyzes account snapshots and detects configuration problems, + * authentication issues, and other potential problems. + * + * @param accounts - Array of account snapshots to analyze + * @param getCfg - Optional function to get full config for additional checks + * @returns Array of detected status issues + * + * @example + * const issues = collectTwitchStatusIssues(accountSnapshots); + * if (issues.length > 0) { + * console.warn("Twitch configuration issues detected:"); + * issues.forEach(issue => console.warn(`- ${issue.message}`)); + * } + */ +export function collectTwitchStatusIssues( + accounts: ChannelAccountSnapshot[], + getCfg?: () => unknown, +): ChannelStatusIssue[] { + const issues: ChannelStatusIssue[] = []; + + for (const entry of accounts) { + const accountId = entry.accountId; + + if (!accountId) continue; + + let account: ReturnType | null = null; + let cfg: Parameters[0] | undefined; + if (getCfg) { + try { + cfg = getCfg() as { + channels?: { twitch?: { accounts?: Record } }; + }; + account = getAccountConfig(cfg, accountId); + } catch { + // Ignore config access errors + } + } + + if (!entry.configured) { + issues.push({ + channel: "twitch", + accountId, + kind: "config", + message: "Twitch account is not properly configured", + fix: "Add required fields: username, accessToken, and clientId to your account configuration", + }); + continue; + } + + if (entry.enabled === false) { + issues.push({ + channel: "twitch", + accountId, + kind: "config", + message: "Twitch account is disabled", + fix: "Set enabled: true in your account configuration to enable this account", + }); + continue; + } + + if (account && account.username && account.accessToken && !account.clientId) { + issues.push({ + channel: "twitch", + accountId, + kind: "config", + message: "Twitch client ID is required", + fix: "Add clientId to your Twitch account configuration (from Twitch Developer Portal)", + }); + } + + const tokenResolution = cfg + ? resolveTwitchToken(cfg as Parameters[0], { accountId }) + : { token: "", source: "none" }; + if (account && isAccountConfigured(account, tokenResolution.token)) { + if (account.accessToken?.startsWith("oauth:")) { + issues.push({ + channel: "twitch", + accountId, + kind: "config", + message: "Token contains 'oauth:' prefix (will be stripped)", + fix: "The 'oauth:' prefix is optional. You can use just the token value, or keep it as-is (it will be normalized automatically).", + }); + } + + if (account.clientSecret && !account.refreshToken) { + issues.push({ + channel: "twitch", + accountId, + kind: "config", + message: "clientSecret provided without refreshToken", + fix: "For automatic token refresh, provide both clientSecret and refreshToken. Otherwise, clientSecret is not needed.", + }); + } + + if (account.allowFrom && account.allowFrom.length === 0) { + issues.push({ + channel: "twitch", + accountId, + kind: "config", + message: "allowFrom is configured but empty", + fix: "Either add user IDs to allowFrom, remove the allowFrom field, or use allowedRoles instead.", + }); + } + + if ( + account.allowedRoles?.includes("all") && + account.allowFrom && + account.allowFrom.length > 0 + ) { + issues.push({ + channel: "twitch", + accountId, + kind: "intent", + message: "allowedRoles is set to 'all' but allowFrom is also configured", + fix: "When allowedRoles is 'all', the allowFrom list is not needed. Remove allowFrom or set allowedRoles to specific roles.", + }); + } + } + + if (entry.lastError) { + issues.push({ + channel: "twitch", + accountId, + kind: "runtime", + message: `Last error: ${entry.lastError}`, + fix: "Check your token validity and network connection. Ensure the bot has the required OAuth scopes.", + }); + } + + if ( + entry.configured && + !entry.running && + !entry.lastStartAt && + !entry.lastInboundAt && + !entry.lastOutboundAt + ) { + issues.push({ + channel: "twitch", + accountId, + kind: "runtime", + message: "Account has never connected successfully", + fix: "Start the Twitch gateway to begin receiving messages. Check logs for connection errors.", + }); + } + + if (entry.running && entry.lastStartAt) { + const uptime = Date.now() - entry.lastStartAt; + const daysSinceStart = uptime / (1000 * 60 * 60 * 24); + if (daysSinceStart > 7) { + issues.push({ + channel: "twitch", + accountId, + kind: "runtime", + message: `Connection has been running for ${Math.floor(daysSinceStart)} days`, + fix: "Consider restarting the connection periodically to refresh the connection. Twitch tokens may expire after long periods.", + }); + } + } + } + + return issues; +} diff --git a/extensions/twitch/src/token.test.ts b/extensions/twitch/src/token.test.ts new file mode 100644 index 000000000..3894532bc --- /dev/null +++ b/extensions/twitch/src/token.test.ts @@ -0,0 +1,171 @@ +/** + * Tests for token.ts module + * + * Tests cover: + * - Token resolution from config + * - Token resolution from environment variable + * - Fallback behavior when token not found + * - Account ID normalization + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { resolveTwitchToken, type TwitchTokenSource } from "./token.js"; +import type { ClawdbotConfig } from "clawdbot/plugin-sdk"; + +describe("token", () => { + // Multi-account config for testing non-default accounts + const mockMultiAccountConfig = { + channels: { + twitch: { + accounts: { + default: { + username: "testbot", + accessToken: "oauth:config-token", + }, + other: { + username: "otherbot", + accessToken: "oauth:other-token", + }, + }, + }, + }, + } as unknown as ClawdbotConfig; + + // Simplified single-account config + const mockSimplifiedConfig = { + channels: { + twitch: { + username: "testbot", + accessToken: "oauth:config-token", + }, + }, + } as unknown as ClawdbotConfig; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + delete process.env.CLAWDBOT_TWITCH_ACCESS_TOKEN; + }); + + describe("resolveTwitchToken", () => { + it("should resolve token from simplified config for default account", () => { + const result = resolveTwitchToken(mockSimplifiedConfig, { accountId: "default" }); + + expect(result.token).toBe("oauth:config-token"); + expect(result.source).toBe("config"); + }); + + it("should resolve token from config for non-default account (multi-account)", () => { + const result = resolveTwitchToken(mockMultiAccountConfig, { accountId: "other" }); + + expect(result.token).toBe("oauth:other-token"); + expect(result.source).toBe("config"); + }); + + it("should prioritize config token over env var (simplified config)", () => { + process.env.CLAWDBOT_TWITCH_ACCESS_TOKEN = "oauth:env-token"; + + const result = resolveTwitchToken(mockSimplifiedConfig, { accountId: "default" }); + + // Config token should be used even if env var exists + expect(result.token).toBe("oauth:config-token"); + expect(result.source).toBe("config"); + }); + + it("should use env var when config token is empty (simplified config)", () => { + process.env.CLAWDBOT_TWITCH_ACCESS_TOKEN = "oauth:env-token"; + + const configWithEmptyToken = { + channels: { + twitch: { + username: "testbot", + accessToken: "", + }, + }, + } as unknown as ClawdbotConfig; + + const result = resolveTwitchToken(configWithEmptyToken, { accountId: "default" }); + + expect(result.token).toBe("oauth:env-token"); + expect(result.source).toBe("env"); + }); + + it("should return empty token when neither config nor env has token (simplified config)", () => { + const configWithoutToken = { + channels: { + twitch: { + username: "testbot", + accessToken: "", + }, + }, + } as unknown as ClawdbotConfig; + + const result = resolveTwitchToken(configWithoutToken, { accountId: "default" }); + + expect(result.token).toBe(""); + expect(result.source).toBe("none"); + }); + + it("should not use env var for non-default accounts (multi-account)", () => { + process.env.CLAWDBOT_TWITCH_ACCESS_TOKEN = "oauth:env-token"; + + const configWithoutToken = { + channels: { + twitch: { + accounts: { + secondary: { + username: "secondary", + accessToken: "", + }, + }, + }, + }, + } as unknown as ClawdbotConfig; + + const result = resolveTwitchToken(configWithoutToken, { accountId: "secondary" }); + + // Non-default accounts shouldn't use env var + expect(result.token).toBe(""); + expect(result.source).toBe("none"); + }); + + it("should handle missing account gracefully", () => { + const configWithoutAccount = { + channels: { + twitch: { + accounts: {}, + }, + }, + } as unknown as ClawdbotConfig; + + const result = resolveTwitchToken(configWithoutAccount, { accountId: "nonexistent" }); + + expect(result.token).toBe(""); + expect(result.source).toBe("none"); + }); + + it("should handle missing Twitch config section", () => { + const configWithoutSection = { + channels: {}, + } as unknown as ClawdbotConfig; + + const result = resolveTwitchToken(configWithoutSection, { accountId: "default" }); + + expect(result.token).toBe(""); + expect(result.source).toBe("none"); + }); + }); + + describe("TwitchTokenSource type", () => { + it("should have correct values", () => { + const sources: TwitchTokenSource[] = ["env", "config", "none"]; + + expect(sources).toContain("env"); + expect(sources).toContain("config"); + expect(sources).toContain("none"); + }); + }); +}); diff --git a/extensions/twitch/src/token.ts b/extensions/twitch/src/token.ts new file mode 100644 index 000000000..bad0f2b57 --- /dev/null +++ b/extensions/twitch/src/token.ts @@ -0,0 +1,87 @@ +/** + * Twitch access token resolution with environment variable support. + * + * Supports reading Twitch OAuth access tokens from config or environment variable. + * The CLAWDBOT_TWITCH_ACCESS_TOKEN env var is only used for the default account. + * + * Token resolution priority: + * 1. Account access token from merged config (accounts.{id} or base-level for default) + * 2. Environment variable: CLAWDBOT_TWITCH_ACCESS_TOKEN (default account only) + */ + +import type { ClawdbotConfig } from "../../../src/config/config.js"; +import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../src/routing/session-key.js"; + +export type TwitchTokenSource = "env" | "config" | "none"; + +export type TwitchTokenResolution = { + token: string; + source: TwitchTokenSource; +}; + +/** + * Normalize a Twitch OAuth token - ensure it has the oauth: prefix + */ +function normalizeTwitchToken(raw?: string | null): string | undefined { + if (!raw) return undefined; + const trimmed = raw.trim(); + if (!trimmed) return undefined; + // Twitch tokens should have oauth: prefix + return trimmed.startsWith("oauth:") ? trimmed : `oauth:${trimmed}`; +} + +/** + * Resolve Twitch access token from config or environment variable. + * + * Priority: + * 1. Account access token (from merged config - base-level for default, or accounts.{accountId}) + * 2. Environment variable: CLAWDBOT_TWITCH_ACCESS_TOKEN (default account only) + * + * The getAccountConfig function handles merging base-level config with accounts.default, + * so this logic works for both simplified and multi-account patterns. + * + * @param cfg - Clawdbot config + * @param opts - Options including accountId and optional envToken override + * @returns Token resolution with source + */ +export function resolveTwitchToken( + cfg?: ClawdbotConfig, + opts: { accountId?: string | null; envToken?: string | null } = {}, +): TwitchTokenResolution { + const accountId = normalizeAccountId(opts.accountId); + + // Get merged account config (handles both simplified and multi-account patterns) + const twitchCfg = cfg?.channels?.twitch; + const accountCfg = + accountId === DEFAULT_ACCOUNT_ID + ? (twitchCfg?.accounts?.[DEFAULT_ACCOUNT_ID] as Record | undefined) + : (twitchCfg?.accounts?.[accountId as string] as Record | undefined); + + // For default account, also check base-level config + let token: string | undefined; + if (accountId === DEFAULT_ACCOUNT_ID) { + // Base-level config takes precedence + token = normalizeTwitchToken( + (typeof twitchCfg?.accessToken === "string" ? twitchCfg.accessToken : undefined) || + (accountCfg?.accessToken as string | undefined), + ); + } else { + // Non-default accounts only use accounts object + token = normalizeTwitchToken(accountCfg?.accessToken as string | undefined); + } + + if (token) { + return { token, source: "config" }; + } + + // Environment variable (default account only) + const allowEnv = accountId === DEFAULT_ACCOUNT_ID; + const envToken = allowEnv + ? normalizeTwitchToken(opts.envToken ?? process.env.CLAWDBOT_TWITCH_ACCESS_TOKEN) + : undefined; + if (envToken) { + return { token: envToken, source: "env" }; + } + + return { token: "", source: "none" }; +} diff --git a/extensions/twitch/src/twitch-client.test.ts b/extensions/twitch/src/twitch-client.test.ts new file mode 100644 index 000000000..b6e270acd --- /dev/null +++ b/extensions/twitch/src/twitch-client.test.ts @@ -0,0 +1,574 @@ +/** + * Tests for TwitchClientManager class + * + * Tests cover: + * - Client connection and reconnection + * - Message handling (chat) + * - Message sending with rate limiting + * - Disconnection scenarios + * - Error handling and edge cases + */ + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { TwitchClientManager } from "./twitch-client.js"; +import type { ChannelLogSink, TwitchAccountConfig, TwitchChatMessage } from "./types.js"; + +// Mock @twurple dependencies +const mockConnect = vi.fn().mockResolvedValue(undefined); +const mockJoin = vi.fn().mockResolvedValue(undefined); +const mockSay = vi.fn().mockResolvedValue({ messageId: "test-msg-123" }); +const mockQuit = vi.fn(); +const mockUnbind = vi.fn(); + +// Event handler storage for testing +const messageHandlers: Array<(channel: string, user: string, message: string, msg: any) => void> = + []; + +// Mock functions that track handlers and return unbind objects +const mockOnMessage = vi.fn((handler: any) => { + messageHandlers.push(handler); + return { unbind: mockUnbind }; +}); + +const mockAddUserForToken = vi.fn().mockResolvedValue("123456"); +const mockOnRefresh = vi.fn(); +const mockOnRefreshFailure = vi.fn(); + +vi.mock("@twurple/chat", () => ({ + ChatClient: class { + onMessage = mockOnMessage; + connect = mockConnect; + join = mockJoin; + say = mockSay; + quit = mockQuit; + }, + LogLevel: { + CRITICAL: "CRITICAL", + ERROR: "ERROR", + WARNING: "WARNING", + INFO: "INFO", + DEBUG: "DEBUG", + TRACE: "TRACE", + }, +})); + +const mockAuthProvider = { + constructor: vi.fn(), +}; + +vi.mock("@twurple/auth", () => ({ + StaticAuthProvider: class { + constructor(...args: unknown[]) { + mockAuthProvider.constructor(...args); + } + }, + RefreshingAuthProvider: class { + addUserForToken = mockAddUserForToken; + onRefresh = mockOnRefresh; + onRefreshFailure = mockOnRefreshFailure; + }, +})); + +// Mock token resolution - must be after @twurple/auth mock +vi.mock("./token.js", () => ({ + resolveTwitchToken: vi.fn(() => ({ + token: "oauth:mock-token-from-tests", + source: "config" as const, + })), + DEFAULT_ACCOUNT_ID: "default", +})); + +describe("TwitchClientManager", () => { + let manager: TwitchClientManager; + let mockLogger: ChannelLogSink; + + const testAccount: TwitchAccountConfig = { + username: "testbot", + token: "oauth:test123456", + clientId: "test-client-id", + channel: "testchannel", + enabled: true, + }; + + const testAccount2: TwitchAccountConfig = { + username: "testbot2", + token: "oauth:test789", + clientId: "test-client-id-2", + channel: "testchannel2", + enabled: true, + }; + + beforeEach(async () => { + // Clear all mocks first + vi.clearAllMocks(); + + // Clear handler arrays + messageHandlers.length = 0; + + // Re-set up the default token mock implementation after clearing + const { resolveTwitchToken } = await import("./token.js"); + vi.mocked(resolveTwitchToken).mockReturnValue({ + token: "oauth:mock-token-from-tests", + source: "config" as const, + }); + + // Create mock logger + mockLogger = { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + }; + + // Create manager instance + manager = new TwitchClientManager(mockLogger); + }); + + afterEach(() => { + // Clean up manager to avoid side effects + manager._clearForTest(); + }); + + describe("getClient", () => { + it("should create a new client connection", async () => { + const _client = await manager.getClient(testAccount); + + // New implementation: connect is called, channels are passed to constructor + expect(mockConnect).toHaveBeenCalledTimes(1); + expect(mockLogger.info).toHaveBeenCalledWith( + expect.stringContaining("Connected to Twitch as testbot"), + ); + }); + + it("should use account username as default channel when channel not specified", async () => { + const accountWithoutChannel: TwitchAccountConfig = { + ...testAccount, + channel: undefined, + }; + + await manager.getClient(accountWithoutChannel); + + // New implementation: channel (testbot) is passed to constructor, not via join() + expect(mockConnect).toHaveBeenCalledTimes(1); + }); + + it("should reuse existing client for same account", async () => { + const client1 = await manager.getClient(testAccount); + const client2 = await manager.getClient(testAccount); + + expect(client1).toBe(client2); + expect(mockConnect).toHaveBeenCalledTimes(1); + }); + + it("should create separate clients for different accounts", async () => { + await manager.getClient(testAccount); + await manager.getClient(testAccount2); + + expect(mockConnect).toHaveBeenCalledTimes(2); + }); + + it("should normalize token by removing oauth: prefix", async () => { + const accountWithPrefix: TwitchAccountConfig = { + ...testAccount, + token: "oauth:actualtoken123", + }; + + // Override the mock to return a specific token for this test + const { resolveTwitchToken } = await import("./token.js"); + vi.mocked(resolveTwitchToken).mockReturnValue({ + token: "oauth:actualtoken123", + source: "config" as const, + }); + + await manager.getClient(accountWithPrefix); + + expect(mockAuthProvider.constructor).toHaveBeenCalledWith("test-client-id", "actualtoken123"); + }); + + it("should use token directly when no oauth: prefix", async () => { + // Override the mock to return a token without oauth: prefix + const { resolveTwitchToken } = await import("./token.js"); + vi.mocked(resolveTwitchToken).mockReturnValue({ + token: "oauth:mock-token-from-tests", + source: "config" as const, + }); + + await manager.getClient(testAccount); + + // Implementation strips oauth: prefix from all tokens + expect(mockAuthProvider.constructor).toHaveBeenCalledWith( + "test-client-id", + "mock-token-from-tests", + ); + }); + + it("should throw error when clientId is missing", async () => { + const accountWithoutClientId: TwitchAccountConfig = { + ...testAccount, + clientId: undefined, + }; + + await expect(manager.getClient(accountWithoutClientId)).rejects.toThrow( + "Missing Twitch client ID", + ); + + expect(mockLogger.error).toHaveBeenCalledWith( + expect.stringContaining("Missing Twitch client ID"), + ); + }); + + it("should throw error when token is missing", async () => { + // Override the mock to return empty token + const { resolveTwitchToken } = await import("./token.js"); + vi.mocked(resolveTwitchToken).mockReturnValue({ + token: "", + source: "none" as const, + }); + + await expect(manager.getClient(testAccount)).rejects.toThrow("Missing Twitch token"); + }); + + it("should set up message handlers on client connection", async () => { + await manager.getClient(testAccount); + + expect(mockOnMessage).toHaveBeenCalled(); + expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining("Set up handlers for")); + }); + + it("should create separate clients for same account with different channels", async () => { + const account1: TwitchAccountConfig = { + ...testAccount, + channel: "channel1", + }; + const account2: TwitchAccountConfig = { + ...testAccount, + channel: "channel2", + }; + + await manager.getClient(account1); + await manager.getClient(account2); + + expect(mockConnect).toHaveBeenCalledTimes(2); + }); + }); + + describe("onMessage", () => { + it("should register message handler for account", () => { + const handler = vi.fn(); + manager.onMessage(testAccount, handler); + + expect(handler).not.toHaveBeenCalled(); + }); + + it("should replace existing handler for same account", () => { + const handler1 = vi.fn(); + const handler2 = vi.fn(); + + manager.onMessage(testAccount, handler1); + manager.onMessage(testAccount, handler2); + + // Check the stored handler is handler2 + const key = manager.getAccountKey(testAccount); + expect((manager as any).messageHandlers.get(key)).toBe(handler2); + }); + }); + + describe("disconnect", () => { + it("should disconnect a connected client", async () => { + await manager.getClient(testAccount); + await manager.disconnect(testAccount); + + expect(mockQuit).toHaveBeenCalledTimes(1); + expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining("Disconnected")); + }); + + it("should clear client and message handler", async () => { + const handler = vi.fn(); + await manager.getClient(testAccount); + manager.onMessage(testAccount, handler); + + await manager.disconnect(testAccount); + + const key = manager.getAccountKey(testAccount); + expect((manager as any).clients.has(key)).toBe(false); + expect((manager as any).messageHandlers.has(key)).toBe(false); + }); + + it("should handle disconnecting non-existent client gracefully", async () => { + // disconnect doesn't throw, just does nothing + await manager.disconnect(testAccount); + expect(mockQuit).not.toHaveBeenCalled(); + }); + + it("should only disconnect specified account when multiple accounts exist", async () => { + await manager.getClient(testAccount); + await manager.getClient(testAccount2); + + await manager.disconnect(testAccount); + + expect(mockQuit).toHaveBeenCalledTimes(1); + + const key2 = manager.getAccountKey(testAccount2); + expect((manager as any).clients.has(key2)).toBe(true); + }); + }); + + describe("disconnectAll", () => { + it("should disconnect all connected clients", async () => { + await manager.getClient(testAccount); + await manager.getClient(testAccount2); + + await manager.disconnectAll(); + + expect(mockQuit).toHaveBeenCalledTimes(2); + expect((manager as any).clients.size).toBe(0); + expect((manager as any).messageHandlers.size).toBe(0); + }); + + it("should handle empty client list gracefully", async () => { + // disconnectAll doesn't throw, just does nothing + await manager.disconnectAll(); + expect(mockQuit).not.toHaveBeenCalled(); + }); + }); + + describe("sendMessage", () => { + beforeEach(async () => { + await manager.getClient(testAccount); + }); + + it("should send message successfully", async () => { + const result = await manager.sendMessage(testAccount, "testchannel", "Hello, world!"); + + expect(result.ok).toBe(true); + expect(result.messageId).toBeDefined(); + expect(mockSay).toHaveBeenCalledWith("testchannel", "Hello, world!"); + }); + + it("should generate unique message ID for each message", async () => { + const result1 = await manager.sendMessage(testAccount, "testchannel", "First message"); + const result2 = await manager.sendMessage(testAccount, "testchannel", "Second message"); + + expect(result1.messageId).not.toBe(result2.messageId); + }); + + it("should handle sending to account's default channel", async () => { + const result = await manager.sendMessage( + testAccount, + testAccount.channel || testAccount.username, + "Test message", + ); + + // Should use the account's channel or username + expect(result.ok).toBe(true); + expect(mockSay).toHaveBeenCalled(); + }); + + it("should return error on send failure", async () => { + mockSay.mockRejectedValueOnce(new Error("Rate limited")); + + const result = await manager.sendMessage(testAccount, "testchannel", "Test message"); + + expect(result.ok).toBe(false); + expect(result.error).toBe("Rate limited"); + expect(mockLogger.error).toHaveBeenCalledWith( + expect.stringContaining("Failed to send message"), + ); + }); + + it("should handle unknown error types", async () => { + mockSay.mockRejectedValueOnce("String error"); + + const result = await manager.sendMessage(testAccount, "testchannel", "Test message"); + + expect(result.ok).toBe(false); + expect(result.error).toBe("String error"); + }); + + it("should create client if not already connected", async () => { + // Clear the existing client + (manager as any).clients.clear(); + + // Reset connect call count for this specific test + const connectCallCountBefore = mockConnect.mock.calls.length; + + const result = await manager.sendMessage(testAccount, "testchannel", "Test message"); + + expect(result.ok).toBe(true); + expect(mockConnect.mock.calls.length).toBeGreaterThan(connectCallCountBefore); + }); + }); + + describe("message handling integration", () => { + let capturedMessage: TwitchChatMessage | null = null; + + beforeEach(() => { + capturedMessage = null; + + // Set up message handler before connecting + manager.onMessage(testAccount, (message) => { + capturedMessage = message; + }); + }); + + it("should handle incoming chat messages", async () => { + await manager.getClient(testAccount); + + // Get the onMessage callback + const onMessageCallback = messageHandlers[0]; + if (!onMessageCallback) throw new Error("onMessageCallback not found"); + + // Simulate Twitch message + onMessageCallback("#testchannel", "testuser", "Hello bot!", { + userInfo: { + userName: "testuser", + displayName: "TestUser", + userId: "12345", + isMod: false, + isBroadcaster: false, + isVip: false, + isSubscriber: false, + }, + id: "msg123", + }); + + expect(capturedMessage).not.toBeNull(); + expect(capturedMessage?.username).toBe("testuser"); + expect(capturedMessage?.displayName).toBe("TestUser"); + expect(capturedMessage?.userId).toBe("12345"); + expect(capturedMessage?.message).toBe("Hello bot!"); + expect(capturedMessage?.channel).toBe("testchannel"); + expect(capturedMessage?.chatType).toBe("group"); + }); + + it("should normalize channel names without # prefix", async () => { + await manager.getClient(testAccount); + + const onMessageCallback = messageHandlers[0]; + + onMessageCallback("testchannel", "testuser", "Test", { + userInfo: { + userName: "testuser", + displayName: "TestUser", + userId: "123", + isMod: false, + isBroadcaster: false, + isVip: false, + isSubscriber: false, + }, + id: "msg1", + }); + + expect(capturedMessage?.channel).toBe("testchannel"); + }); + + it("should include user role flags in message", async () => { + await manager.getClient(testAccount); + + const onMessageCallback = messageHandlers[0]; + + onMessageCallback("#testchannel", "moduser", "Test", { + userInfo: { + userName: "moduser", + displayName: "ModUser", + userId: "456", + isMod: true, + isBroadcaster: false, + isVip: true, + isSubscriber: true, + }, + id: "msg2", + }); + + expect(capturedMessage?.isMod).toBe(true); + expect(capturedMessage?.isVip).toBe(true); + expect(capturedMessage?.isSub).toBe(true); + expect(capturedMessage?.isOwner).toBe(false); + }); + + it("should handle broadcaster messages", async () => { + await manager.getClient(testAccount); + + const onMessageCallback = messageHandlers[0]; + + onMessageCallback("#testchannel", "broadcaster", "Test", { + userInfo: { + userName: "broadcaster", + displayName: "Broadcaster", + userId: "789", + isMod: false, + isBroadcaster: true, + isVip: false, + isSubscriber: false, + }, + id: "msg3", + }); + + expect(capturedMessage?.isOwner).toBe(true); + }); + }); + + describe("edge cases", () => { + it("should handle multiple message handlers for different accounts", async () => { + const messages1: TwitchChatMessage[] = []; + const messages2: TwitchChatMessage[] = []; + + manager.onMessage(testAccount, (msg) => messages1.push(msg)); + manager.onMessage(testAccount2, (msg) => messages2.push(msg)); + + await manager.getClient(testAccount); + await manager.getClient(testAccount2); + + // Simulate message for first account + const onMessage1 = messageHandlers[0]; + if (!onMessage1) throw new Error("onMessage1 not found"); + onMessage1("#testchannel", "user1", "msg1", { + userInfo: { + userName: "user1", + displayName: "User1", + userId: "1", + isMod: false, + isBroadcaster: false, + isVip: false, + isSubscriber: false, + }, + id: "1", + }); + + // Simulate message for second account + const onMessage2 = messageHandlers[1]; + if (!onMessage2) throw new Error("onMessage2 not found"); + onMessage2("#testchannel2", "user2", "msg2", { + userInfo: { + userName: "user2", + displayName: "User2", + userId: "2", + isMod: false, + isBroadcaster: false, + isVip: false, + isSubscriber: false, + }, + id: "2", + }); + + expect(messages1).toHaveLength(1); + expect(messages2).toHaveLength(1); + expect(messages1[0]?.message).toBe("msg1"); + expect(messages2[0]?.message).toBe("msg2"); + }); + + it("should handle rapid client creation requests", async () => { + const promises = [ + manager.getClient(testAccount), + manager.getClient(testAccount), + manager.getClient(testAccount), + ]; + + await Promise.all(promises); + + // Note: The implementation doesn't handle concurrent getClient calls, + // so multiple connections may be created. This is expected behavior. + expect(mockConnect).toHaveBeenCalled(); + }); + }); +}); diff --git a/extensions/twitch/src/twitch-client.ts b/extensions/twitch/src/twitch-client.ts new file mode 100644 index 000000000..f76435aa4 --- /dev/null +++ b/extensions/twitch/src/twitch-client.ts @@ -0,0 +1,277 @@ +import { RefreshingAuthProvider, StaticAuthProvider } from "@twurple/auth"; +import { ChatClient, LogLevel } from "@twurple/chat"; +import type { ClawdbotConfig } from "clawdbot/plugin-sdk"; +import type { ChannelLogSink, TwitchAccountConfig, TwitchChatMessage } from "./types.js"; +import { resolveTwitchToken } from "./token.js"; +import { normalizeToken } from "./utils/twitch.js"; + +/** + * Manages Twitch chat client connections + */ +export class TwitchClientManager { + private clients = new Map(); + private messageHandlers = new Map void>(); + + constructor(private logger: ChannelLogSink) {} + + /** + * Create an auth provider for the account. + */ + private async createAuthProvider( + account: TwitchAccountConfig, + normalizedToken: string, + ): Promise { + if (!account.clientId) { + throw new Error("Missing Twitch client ID"); + } + + if (account.clientSecret) { + const authProvider = new RefreshingAuthProvider({ + clientId: account.clientId, + clientSecret: account.clientSecret, + }); + + await authProvider + .addUserForToken({ + accessToken: normalizedToken, + refreshToken: account.refreshToken ?? null, + expiresIn: account.expiresIn ?? null, + obtainmentTimestamp: account.obtainmentTimestamp ?? Date.now(), + }) + .then((userId) => { + this.logger.info( + `Added user ${userId} to RefreshingAuthProvider for ${account.username}`, + ); + }) + .catch((err) => { + this.logger.error( + `Failed to add user to RefreshingAuthProvider: ${err instanceof Error ? err.message : String(err)}`, + ); + }); + + authProvider.onRefresh((userId, token) => { + this.logger.info( + `Access token refreshed for user ${userId} (expires in ${token.expiresIn ? `${token.expiresIn}s` : "unknown"})`, + ); + }); + + authProvider.onRefreshFailure((userId, error) => { + this.logger.error(`Failed to refresh access token for user ${userId}: ${error.message}`); + }); + + const refreshStatus = account.refreshToken + ? "automatic token refresh enabled" + : "token refresh disabled (no refresh token)"; + this.logger.info(`Using RefreshingAuthProvider for ${account.username} (${refreshStatus})`); + + return authProvider; + } + + this.logger.info(`Using StaticAuthProvider for ${account.username} (no clientSecret provided)`); + return new StaticAuthProvider(account.clientId, normalizedToken); + } + + /** + * Get or create a chat client for an account + */ + async getClient( + account: TwitchAccountConfig, + cfg?: ClawdbotConfig, + accountId?: string, + ): Promise { + const key = this.getAccountKey(account); + + const existing = this.clients.get(key); + if (existing) { + return existing; + } + + const tokenResolution = resolveTwitchToken(cfg, { + accountId, + }); + + if (!tokenResolution.token) { + this.logger.error( + `Missing Twitch token for account ${account.username} (set channels.twitch.accounts.${account.username}.token or CLAWDBOT_TWITCH_ACCESS_TOKEN for default)`, + ); + throw new Error("Missing Twitch token"); + } + + this.logger.debug?.(`Using ${tokenResolution.source} token source for ${account.username}`); + + if (!account.clientId) { + this.logger.error(`Missing Twitch client ID for account ${account.username}`); + throw new Error("Missing Twitch client ID"); + } + + const normalizedToken = normalizeToken(tokenResolution.token); + + const authProvider = await this.createAuthProvider(account, normalizedToken); + + const client = new ChatClient({ + authProvider, + channels: [account.channel], + rejoinChannelsOnReconnect: true, + requestMembershipEvents: true, + logger: { + minLevel: LogLevel.WARNING, + custom: { + log: (level, message) => { + switch (level) { + case LogLevel.CRITICAL: + this.logger.error(`${message}`); + break; + case LogLevel.ERROR: + this.logger.error(`${message}`); + break; + case LogLevel.WARNING: + this.logger.warn(`${message}`); + break; + case LogLevel.INFO: + this.logger.info(`${message}`); + break; + case LogLevel.DEBUG: + this.logger.debug?.(`${message}`); + break; + case LogLevel.TRACE: + this.logger.debug?.(`${message}`); + break; + } + }, + }, + }, + }); + + this.setupClientHandlers(client, account); + + client.connect(); + + this.clients.set(key, client); + this.logger.info(`Connected to Twitch as ${account.username}`); + + return client; + } + + /** + * Set up message and event handlers for a client + */ + private setupClientHandlers(client: ChatClient, account: TwitchAccountConfig): void { + const key = this.getAccountKey(account); + + // Handle incoming messages + client.onMessage((channelName, _user, messageText, msg) => { + const handler = this.messageHandlers.get(key); + if (handler) { + const normalizedChannel = channelName.startsWith("#") ? channelName.slice(1) : channelName; + const from = `twitch:${msg.userInfo.userName}`; + const preview = messageText.slice(0, 100).replace(/\n/g, "\\n"); + this.logger.debug?.( + `twitch inbound: channel=${normalizedChannel} from=${from} len=${messageText.length} preview="${preview}"`, + ); + + handler({ + username: msg.userInfo.userName, + displayName: msg.userInfo.displayName, + userId: msg.userInfo.userId, + message: messageText, + channel: normalizedChannel, + id: msg.id, + timestamp: new Date(), + isMod: msg.userInfo.isMod, + isOwner: msg.userInfo.isBroadcaster, + isVip: msg.userInfo.isVip, + isSub: msg.userInfo.isSubscriber, + chatType: "group", + }); + } + }); + + this.logger.info(`Set up handlers for ${key}`); + } + + /** + * Set a message handler for an account + * @returns A function that removes the handler when called + */ + onMessage( + account: TwitchAccountConfig, + handler: (message: TwitchChatMessage) => void, + ): () => void { + const key = this.getAccountKey(account); + this.messageHandlers.set(key, handler); + return () => { + this.messageHandlers.delete(key); + }; + } + + /** + * Disconnect a client + */ + async disconnect(account: TwitchAccountConfig): Promise { + const key = this.getAccountKey(account); + const client = this.clients.get(key); + + if (client) { + client.quit(); + this.clients.delete(key); + this.messageHandlers.delete(key); + this.logger.info(`Disconnected ${key}`); + } + } + + /** + * Disconnect all clients + */ + async disconnectAll(): Promise { + this.clients.forEach((client) => client.quit()); + this.clients.clear(); + this.messageHandlers.clear(); + this.logger.info(" Disconnected all clients"); + } + + /** + * Send a message to a channel + */ + async sendMessage( + account: TwitchAccountConfig, + channel: string, + message: string, + cfg?: ClawdbotConfig, + accountId?: string, + ): Promise<{ ok: boolean; error?: string; messageId?: string }> { + try { + const client = await this.getClient(account, cfg, accountId); + + // Generate a message ID (Twurple's say() doesn't return the message ID, so we generate one) + const messageId = crypto.randomUUID(); + + // Send message (Twurple handles rate limiting) + await client.say(channel, message); + + return { ok: true, messageId }; + } catch (error) { + this.logger.error( + `Failed to send message: ${error instanceof Error ? error.message : String(error)}`, + ); + return { + ok: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } + + /** + * Generate a unique key for an account + */ + public getAccountKey(account: TwitchAccountConfig): string { + return `${account.username}:${account.channel}`; + } + + /** + * Clear all clients and handlers (for testing) + */ + _clearForTest(): void { + this.clients.clear(); + this.messageHandlers.clear(); + } +} diff --git a/extensions/twitch/src/types.ts b/extensions/twitch/src/types.ts new file mode 100644 index 000000000..74b2b4acf --- /dev/null +++ b/extensions/twitch/src/types.ts @@ -0,0 +1,141 @@ +/** + * Twitch channel plugin types. + * + * This file defines Twitch-specific types. Generic channel types are imported + * from Clawdbot core. + */ + +import type { + ChannelAccountSnapshot, + ChannelCapabilities, + ChannelLogSink, + ChannelMessageActionAdapter, + ChannelMessageActionContext, + ChannelMeta, +} from "../../../src/channels/plugins/types.core.js"; +import type { ChannelPlugin } from "../../../src/channels/plugins/types.plugin.js"; +import type { + ChannelGatewayContext, + ChannelOutboundAdapter, + ChannelOutboundContext, + ChannelResolveKind, + ChannelResolveResult, + ChannelStatusAdapter, +} from "../../../src/channels/plugins/types.adapters.js"; +import type { ClawdbotConfig } from "../../../src/config/config.js"; +import type { OutboundDeliveryResult } from "../../../src/infra/outbound/deliver.js"; +import type { RuntimeEnv } from "../../../src/runtime.js"; + +// ============================================================================ +// Twitch-Specific Types +// ============================================================================ + +/** + * Twitch user roles that can be allowed to interact with the bot + */ +export type TwitchRole = "moderator" | "owner" | "vip" | "subscriber" | "all"; + +/** + * Account configuration for a Twitch channel + */ +export interface TwitchAccountConfig { + /** Twitch username */ + username: string; + /** Twitch OAuth access token (requires chat:read and chat:write scopes) */ + accessToken: string; + /** Twitch client ID (from Twitch Developer Portal or twitchtokengenerator.com) */ + clientId: string; + /** Channel name to join (required) */ + channel: string; + /** Enable this account */ + enabled?: boolean; + /** Allowlist of Twitch user IDs who can interact with the bot (use IDs for safety, not usernames) */ + allowFrom?: Array; + /** Roles allowed to interact with the bot (e.g., ["mod", "vip", "sub"]) */ + allowedRoles?: TwitchRole[]; + /** Require @mention to trigger bot responses */ + requireMention?: boolean; + /** Twitch client secret (required for token refresh via RefreshingAuthProvider) */ + clientSecret?: string; + /** Refresh token (required for automatic token refresh) */ + refreshToken?: string; + /** Token expiry time in seconds (optional, for token refresh tracking) */ + expiresIn?: number | null; + /** Timestamp when token was obtained (optional, for token refresh tracking) */ + obtainmentTimestamp?: number; +} + +/** + * Message target for Twitch + */ +export interface TwitchTarget { + /** Account ID */ + accountId: string; + /** Channel name (defaults to account's channel) */ + channel?: string; +} + +/** + * Twitch message from chat + */ +export interface TwitchChatMessage { + /** Username of sender */ + username: string; + /** Twitch user ID of sender (unique, persistent identifier) */ + userId?: string; + /** Message text */ + message: string; + /** Channel name */ + channel: string; + /** Display name (may include special characters) */ + displayName?: string; + /** Message ID */ + id?: string; + /** Timestamp */ + timestamp?: Date; + /** Whether the sender is a moderator */ + isMod?: boolean; + /** Whether the sender is the channel owner/broadcaster */ + isOwner?: boolean; + /** Whether the sender is a VIP */ + isVip?: boolean; + /** Whether the sender is a subscriber */ + isSub?: boolean; + /** Chat type */ + chatType?: "group"; +} + +/** + * Send result from Twitch client + */ +export interface SendResult { + ok: boolean; + error?: string; + messageId?: string; +} + +// Re-export core types for convenience +export type { + ChannelAccountSnapshot, + ChannelGatewayContext, + ChannelLogSink, + ChannelMessageActionAdapter, + ChannelMessageActionContext, + ChannelMeta, + ChannelOutboundAdapter, + ChannelStatusAdapter, + ChannelCapabilities, + ChannelResolveKind, + ChannelResolveResult, + ChannelPlugin, + ChannelOutboundContext, + OutboundDeliveryResult, +}; + +// Import and re-export the schema type +import type { TwitchConfigSchema } from "./config-schema.js"; +import type { z } from "zod"; +export type TwitchConfig = z.infer; + +export type { ClawdbotConfig }; +export type { RuntimeEnv }; diff --git a/extensions/twitch/src/utils/markdown.ts b/extensions/twitch/src/utils/markdown.ts new file mode 100644 index 000000000..0fa4a5fdf --- /dev/null +++ b/extensions/twitch/src/utils/markdown.ts @@ -0,0 +1,92 @@ +/** + * Markdown utilities for Twitch chat + * + * Twitch chat doesn't support markdown formatting, so we strip it before sending. + * Based on Clawdbot's markdownToText in src/agents/tools/web-fetch-utils.ts. + */ + +/** + * Strip markdown formatting from text for Twitch compatibility. + * + * Removes images, links, bold, italic, strikethrough, code blocks, inline code, + * headers, and list formatting. Replaces newlines with spaces since Twitch + * is a single-line chat medium. + * + * @param markdown - The markdown text to strip + * @returns Plain text with markdown removed + */ +export function stripMarkdownForTwitch(markdown: string): string { + return ( + markdown + // Images + .replace(/!\[[^\]]*]\([^)]+\)/g, "") + // Links + .replace(/\[([^\]]+)]\([^)]+\)/g, "$1") + // Bold (**text**) + .replace(/\*\*([^*]+)\*\*/g, "$1") + // Bold (__text__) + .replace(/__([^_]+)__/g, "$1") + // Italic (*text*) + .replace(/\*([^*]+)\*/g, "$1") + // Italic (_text_) + .replace(/_([^_]+)_/g, "$1") + // Strikethrough (~~text~~) + .replace(/~~([^~]+)~~/g, "$1") + // Code blocks + .replace(/```[\s\S]*?```/g, (block) => block.replace(/```[^\n]*\n?/g, "").replace(/```/g, "")) + // Inline code + .replace(/`([^`]+)`/g, "$1") + // Headers + .replace(/^#{1,6}\s+/gm, "") + // Lists + .replace(/^\s*[-*+]\s+/gm, "") + .replace(/^\s*\d+\.\s+/gm, "") + // Normalize whitespace + .replace(/\r/g, "") // Remove carriage returns + .replace(/[ \t]+\n/g, "\n") // Remove trailing spaces before newlines + .replace(/\n/g, " ") // Replace newlines with spaces (for Twitch) + .replace(/[ \t]{2,}/g, " ") // Reduce multiple spaces to single + .trim() + ); +} + +/** + * Simple word-boundary chunker for Twitch (500 char limit). + * Strips markdown before chunking to avoid breaking markdown patterns. + * + * @param text - The text to chunk + * @param limit - Maximum characters per chunk (Twitch limit is 500) + * @returns Array of text chunks + */ +export function chunkTextForTwitch(text: string, limit: number): string[] { + // First, strip markdown + const cleaned = stripMarkdownForTwitch(text); + if (!cleaned) return []; + if (limit <= 0) return [cleaned]; + if (cleaned.length <= limit) return [cleaned]; + + const chunks: string[] = []; + let remaining = cleaned; + + while (remaining.length > limit) { + // Find the last space before the limit + const window = remaining.slice(0, limit); + const lastSpaceIndex = window.lastIndexOf(" "); + + if (lastSpaceIndex === -1) { + // No space found, hard split at limit + chunks.push(window); + remaining = remaining.slice(limit); + } else { + // Split at the last space + chunks.push(window.slice(0, lastSpaceIndex)); + remaining = remaining.slice(lastSpaceIndex + 1); + } + } + + if (remaining) { + chunks.push(remaining); + } + + return chunks; +} diff --git a/extensions/twitch/src/utils/twitch.ts b/extensions/twitch/src/utils/twitch.ts new file mode 100644 index 000000000..cb2667cb1 --- /dev/null +++ b/extensions/twitch/src/utils/twitch.ts @@ -0,0 +1,78 @@ +/** + * Twitch-specific utility functions + */ + +/** + * Normalize Twitch channel names. + * + * Removes the '#' prefix if present, converts to lowercase, and trims whitespace. + * Twitch channel names are case-insensitive and don't use the '#' prefix in the API. + * + * @param channel - The channel name to normalize + * @returns Normalized channel name + * + * @example + * normalizeTwitchChannel("#TwitchChannel") // "twitchchannel" + * normalizeTwitchChannel("MyChannel") // "mychannel" + */ +export function normalizeTwitchChannel(channel: string): string { + const trimmed = channel.trim().toLowerCase(); + return trimmed.startsWith("#") ? trimmed.slice(1) : trimmed; +} + +/** + * Create a standardized error message for missing target. + * + * @param provider - The provider name (e.g., "Twitch") + * @param hint - Optional hint for how to fix the issue + * @returns Error object with descriptive message + */ +export function missingTargetError(provider: string, hint?: string): Error { + return new Error(`Delivering to ${provider} requires target${hint ? ` ${hint}` : ""}`); +} + +/** + * Generate a unique message ID for Twitch messages. + * + * Twurple's say() doesn't return the message ID, so we generate one + * for tracking purposes. + * + * @returns A unique message ID + */ +export function generateMessageId(): string { + return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`; +} + +/** + * Normalize OAuth token by removing the "oauth:" prefix if present. + * + * Twurple doesn't require the "oauth:" prefix, so we strip it for consistency. + * + * @param token - The OAuth token to normalize + * @returns Normalized token without "oauth:" prefix + * + * @example + * normalizeToken("oauth:abc123") // "abc123" + * normalizeToken("abc123") // "abc123" + */ +export function normalizeToken(token: string): string { + return token.startsWith("oauth:") ? token.slice(6) : token; +} + +/** + * Check if an account is properly configured with required credentials. + * + * @param account - The Twitch account config to check + * @returns true if the account has required credentials + */ +export function isAccountConfigured( + account: { + username?: string; + accessToken?: string; + clientId?: string; + }, + resolvedToken?: string | null, +): boolean { + const token = resolvedToken ?? account?.accessToken; + return Boolean(account?.username && token && account?.clientId); +} diff --git a/extensions/twitch/test/setup.ts b/extensions/twitch/test/setup.ts new file mode 100644 index 000000000..fb391c471 --- /dev/null +++ b/extensions/twitch/test/setup.ts @@ -0,0 +1,7 @@ +/** + * Vitest setup file for Twitch plugin tests. + * + * Re-exports the root test setup to avoid duplication. + */ + +export * from "../../../test/setup.js"; diff --git a/extensions/voice-call/CHANGELOG.md b/extensions/voice-call/CHANGELOG.md index a8721d47d..a757abe64 100644 --- a/extensions/voice-call/CHANGELOG.md +++ b/extensions/voice-call/CHANGELOG.md @@ -1,11 +1,12 @@ # Changelog -## 2026.1.25 +## 2026.1.26 ### Changes - Breaking: voice-call TTS now uses core `messages.tts` (plugin TTS config deep‑merges with core). - Telephony TTS supports OpenAI + ElevenLabs; Edge TTS is ignored for calls. - Removed legacy `tts.model`/`tts.voice`/`tts.instructions` plugin fields. +- Ngrok free-tier bypass renamed to `tunnel.allowNgrokFreeTierLoopbackBypass` and gated to loopback + `tunnel.provider="ngrok"`. ## 2026.1.23 diff --git a/extensions/voice-call/README.md b/extensions/voice-call/README.md index d96f90392..5f009aa28 100644 --- a/extensions/voice-call/README.md +++ b/extensions/voice-call/README.md @@ -74,6 +74,7 @@ Put under `plugins.entries.voice-call.config`: Notes: - Twilio/Telnyx/Plivo require a **publicly reachable** webhook URL. - `mock` is a local dev provider (no network calls). +- `tunnel.allowNgrokFreeTierLoopbackBypass: true` allows Twilio webhooks with invalid signatures **only** when `tunnel.provider="ngrok"` and `serve.bind` is loopback (ngrok local agent). Use for local dev only. ## TTS for calls diff --git a/extensions/voice-call/clawdbot.plugin.json b/extensions/voice-call/clawdbot.plugin.json index 2a4f04466..cfac7ad9d 100644 --- a/extensions/voice-call/clawdbot.plugin.json +++ b/extensions/voice-call/clawdbot.plugin.json @@ -78,8 +78,8 @@ "label": "ngrok Domain", "advanced": true }, - "tunnel.allowNgrokFreeTier": { - "label": "Allow ngrok Free Tier", + "tunnel.allowNgrokFreeTierLoopbackBypass": { + "label": "Allow ngrok Free Tier (Loopback Bypass)", "advanced": true }, "streaming.enabled": { @@ -330,7 +330,7 @@ "ngrokDomain": { "type": "string" }, - "allowNgrokFreeTier": { + "allowNgrokFreeTierLoopbackBypass": { "type": "boolean" } } diff --git a/extensions/voice-call/index.ts b/extensions/voice-call/index.ts index 760726faa..60cb64eb2 100644 --- a/extensions/voice-call/index.ts +++ b/extensions/voice-call/index.ts @@ -1,8 +1,8 @@ import { Type } from "@sinclair/typebox"; - import type { CoreConfig } from "./src/core-bridge.js"; import { VoiceCallConfigSchema, + resolveVoiceCallConfig, validateProviderConfig, type VoiceCallConfig, } from "./src/config.js"; @@ -62,8 +62,8 @@ const voiceCallConfigSchema = { advanced: true, }, "tunnel.ngrokDomain": { label: "ngrok Domain", advanced: true }, - "tunnel.allowNgrokFreeTier": { - label: "Allow ngrok Free Tier", + "tunnel.allowNgrokFreeTierLoopbackBypass": { + label: "Allow ngrok Free Tier (Loopback Bypass)", advanced: true, }, "streaming.enabled": { label: "Enable Streaming", advanced: true }, @@ -145,8 +145,10 @@ const voiceCallPlugin = { description: "Voice-call plugin with Telnyx/Twilio/Plivo providers", configSchema: voiceCallConfigSchema, register(api) { - const cfg = voiceCallConfigSchema.parse(api.pluginConfig); - const validation = validateProviderConfig(cfg); + const config = resolveVoiceCallConfig( + voiceCallConfigSchema.parse(api.pluginConfig), + ); + const validation = validateProviderConfig(config); if (api.pluginConfig && typeof api.pluginConfig === "object") { const raw = api.pluginConfig as Record; @@ -167,7 +169,7 @@ const voiceCallPlugin = { let runtime: VoiceCallRuntime | null = null; const ensureRuntime = async () => { - if (!cfg.enabled) { + if (!config.enabled) { throw new Error("Voice call disabled in plugin config"); } if (!validation.valid) { @@ -176,7 +178,7 @@ const voiceCallPlugin = { if (runtime) return runtime; if (!runtimePromise) { runtimePromise = createVoiceCallRuntime({ - config: cfg, + config, coreConfig: api.config as CoreConfig, ttsRuntime: api.runtime.tts, logger: api.logger, @@ -457,7 +459,7 @@ const voiceCallPlugin = { ({ program }) => registerVoiceCallCli({ program, - config: cfg, + config, ensureRuntime, logger: api.logger, }), @@ -467,7 +469,7 @@ const voiceCallPlugin = { api.registerService({ id: "voicecall", start: async () => { - if (!cfg.enabled) return; + if (!config.enabled) return; try { await ensureRuntime(); } catch (err) { diff --git a/extensions/voice-call/package.json b/extensions/voice-call/package.json index 31b171f76..4d2fee875 100644 --- a/extensions/voice-call/package.json +++ b/extensions/voice-call/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/voice-call", - "version": "2026.1.25", + "name": "@moltbot/voice-call", + "version": "2026.1.26", "type": "module", "description": "Clawdbot voice-call plugin", "dependencies": { diff --git a/extensions/voice-call/src/config.test.ts b/extensions/voice-call/src/config.test.ts new file mode 100644 index 000000000..dde17e122 --- /dev/null +++ b/extensions/voice-call/src/config.test.ts @@ -0,0 +1,204 @@ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +import { validateProviderConfig, resolveVoiceCallConfig, type VoiceCallConfig } from "./config.js"; + +function createBaseConfig( + provider: "telnyx" | "twilio" | "plivo" | "mock", +): VoiceCallConfig { + return { + enabled: true, + provider, + fromNumber: "+15550001234", + inboundPolicy: "disabled", + allowFrom: [], + outbound: { defaultMode: "notify", notifyHangupDelaySec: 3 }, + maxDurationSeconds: 300, + silenceTimeoutMs: 800, + transcriptTimeoutMs: 180000, + ringTimeoutMs: 30000, + maxConcurrentCalls: 1, + serve: { port: 3334, bind: "127.0.0.1", path: "/voice/webhook" }, + tailscale: { mode: "off", path: "/voice/webhook" }, + tunnel: { provider: "none", allowNgrokFreeTierLoopbackBypass: false }, + streaming: { + enabled: false, + sttProvider: "openai-realtime", + sttModel: "gpt-4o-transcribe", + silenceDurationMs: 800, + vadThreshold: 0.5, + streamPath: "/voice/stream", + }, + skipSignatureVerification: false, + stt: { provider: "openai", model: "whisper-1" }, + tts: { provider: "openai", model: "gpt-4o-mini-tts", voice: "coral" }, + responseModel: "openai/gpt-4o-mini", + responseTimeoutMs: 30000, + }; +} + +describe("validateProviderConfig", () => { + const originalEnv = { ...process.env }; + + beforeEach(() => { + // Clear all relevant env vars before each test + delete process.env.TWILIO_ACCOUNT_SID; + delete process.env.TWILIO_AUTH_TOKEN; + delete process.env.TELNYX_API_KEY; + delete process.env.TELNYX_CONNECTION_ID; + delete process.env.PLIVO_AUTH_ID; + delete process.env.PLIVO_AUTH_TOKEN; + }); + + afterEach(() => { + // Restore original env + process.env = { ...originalEnv }; + }); + + describe("twilio provider", () => { + it("passes validation when credentials are in config", () => { + const config = createBaseConfig("twilio"); + config.twilio = { accountSid: "AC123", authToken: "secret" }; + + const result = validateProviderConfig(config); + + expect(result.valid).toBe(true); + expect(result.errors).toEqual([]); + }); + + it("passes validation when credentials are in environment variables", () => { + process.env.TWILIO_ACCOUNT_SID = "AC123"; + process.env.TWILIO_AUTH_TOKEN = "secret"; + let config = createBaseConfig("twilio"); + config = resolveVoiceCallConfig(config); + + const result = validateProviderConfig(config); + + expect(result.valid).toBe(true); + expect(result.errors).toEqual([]); + }); + + it("passes validation with mixed config and env vars", () => { + process.env.TWILIO_AUTH_TOKEN = "secret"; + let config = createBaseConfig("twilio"); + config.twilio = { accountSid: "AC123" }; + config = resolveVoiceCallConfig(config); + + const result = validateProviderConfig(config); + + expect(result.valid).toBe(true); + expect(result.errors).toEqual([]); + }); + + it("fails validation when accountSid is missing everywhere", () => { + process.env.TWILIO_AUTH_TOKEN = "secret"; + let config = createBaseConfig("twilio"); + config = resolveVoiceCallConfig(config); + + const result = validateProviderConfig(config); + + expect(result.valid).toBe(false); + expect(result.errors).toContain( + "plugins.entries.voice-call.config.twilio.accountSid is required (or set TWILIO_ACCOUNT_SID env)", + ); + }); + + it("fails validation when authToken is missing everywhere", () => { + process.env.TWILIO_ACCOUNT_SID = "AC123"; + let config = createBaseConfig("twilio"); + config = resolveVoiceCallConfig(config); + + const result = validateProviderConfig(config); + + expect(result.valid).toBe(false); + expect(result.errors).toContain( + "plugins.entries.voice-call.config.twilio.authToken is required (or set TWILIO_AUTH_TOKEN env)", + ); + }); + }); + + describe("telnyx provider", () => { + it("passes validation when credentials are in config", () => { + const config = createBaseConfig("telnyx"); + config.telnyx = { apiKey: "KEY123", connectionId: "CONN456" }; + + const result = validateProviderConfig(config); + + expect(result.valid).toBe(true); + expect(result.errors).toEqual([]); + }); + + it("passes validation when credentials are in environment variables", () => { + process.env.TELNYX_API_KEY = "KEY123"; + process.env.TELNYX_CONNECTION_ID = "CONN456"; + let config = createBaseConfig("telnyx"); + config = resolveVoiceCallConfig(config); + + const result = validateProviderConfig(config); + + expect(result.valid).toBe(true); + expect(result.errors).toEqual([]); + }); + + it("fails validation when apiKey is missing everywhere", () => { + process.env.TELNYX_CONNECTION_ID = "CONN456"; + let config = createBaseConfig("telnyx"); + config = resolveVoiceCallConfig(config); + + const result = validateProviderConfig(config); + + expect(result.valid).toBe(false); + expect(result.errors).toContain( + "plugins.entries.voice-call.config.telnyx.apiKey is required (or set TELNYX_API_KEY env)", + ); + }); + }); + + describe("plivo provider", () => { + it("passes validation when credentials are in config", () => { + const config = createBaseConfig("plivo"); + config.plivo = { authId: "MA123", authToken: "secret" }; + + const result = validateProviderConfig(config); + + expect(result.valid).toBe(true); + expect(result.errors).toEqual([]); + }); + + it("passes validation when credentials are in environment variables", () => { + process.env.PLIVO_AUTH_ID = "MA123"; + process.env.PLIVO_AUTH_TOKEN = "secret"; + let config = createBaseConfig("plivo"); + config = resolveVoiceCallConfig(config); + + const result = validateProviderConfig(config); + + expect(result.valid).toBe(true); + expect(result.errors).toEqual([]); + }); + + it("fails validation when authId is missing everywhere", () => { + process.env.PLIVO_AUTH_TOKEN = "secret"; + let config = createBaseConfig("plivo"); + config = resolveVoiceCallConfig(config); + + const result = validateProviderConfig(config); + + expect(result.valid).toBe(false); + expect(result.errors).toContain( + "plugins.entries.voice-call.config.plivo.authId is required (or set PLIVO_AUTH_ID env)", + ); + }); + }); + + describe("disabled config", () => { + it("skips validation when enabled is false", () => { + const config = createBaseConfig("twilio"); + config.enabled = false; + + const result = validateProviderConfig(config); + + expect(result.valid).toBe(true); + expect(result.errors).toEqual([]); + }); + }); +}); diff --git a/extensions/voice-call/src/config.ts b/extensions/voice-call/src/config.ts index 48f4691fe..7784406e7 100644 --- a/extensions/voice-call/src/config.ts +++ b/extensions/voice-call/src/config.ts @@ -217,13 +217,17 @@ export const VoiceCallTunnelConfigSchema = z /** * Allow ngrok free tier compatibility mode. * When true, signature verification failures on ngrok-free.app URLs - * will be logged but allowed through. Less secure, but necessary - * for ngrok free tier which may modify URLs. + * will be allowed only for loopback requests (ngrok local agent). */ - allowNgrokFreeTier: z.boolean().default(true), + allowNgrokFreeTierLoopbackBypass: z.boolean().default(false), + /** + * Legacy ngrok free tier compatibility mode (deprecated). + * Use allowNgrokFreeTierLoopbackBypass instead. + */ + allowNgrokFreeTier: z.boolean().optional(), }) .strict() - .default({ provider: "none", allowNgrokFreeTier: true }); + .default({ provider: "none", allowNgrokFreeTierLoopbackBypass: false }); export type VoiceCallTunnelConfig = z.infer; // ----------------------------------------------------------------------------- @@ -381,6 +385,59 @@ export type VoiceCallConfig = z.infer; // Configuration Helpers // ----------------------------------------------------------------------------- +/** + * Resolves the configuration by merging environment variables into missing fields. + * Returns a new configuration object with environment variables applied. + */ +export function resolveVoiceCallConfig(config: VoiceCallConfig): VoiceCallConfig { + const resolved = JSON.parse(JSON.stringify(config)) as VoiceCallConfig; + + // Telnyx + if (resolved.provider === "telnyx") { + resolved.telnyx = resolved.telnyx ?? {}; + resolved.telnyx.apiKey = + resolved.telnyx.apiKey ?? process.env.TELNYX_API_KEY; + resolved.telnyx.connectionId = + resolved.telnyx.connectionId ?? process.env.TELNYX_CONNECTION_ID; + resolved.telnyx.publicKey = + resolved.telnyx.publicKey ?? process.env.TELNYX_PUBLIC_KEY; + } + + // Twilio + if (resolved.provider === "twilio") { + resolved.twilio = resolved.twilio ?? {}; + resolved.twilio.accountSid = + resolved.twilio.accountSid ?? process.env.TWILIO_ACCOUNT_SID; + resolved.twilio.authToken = + resolved.twilio.authToken ?? process.env.TWILIO_AUTH_TOKEN; + } + + // Plivo + if (resolved.provider === "plivo") { + resolved.plivo = resolved.plivo ?? {}; + resolved.plivo.authId = + resolved.plivo.authId ?? process.env.PLIVO_AUTH_ID; + resolved.plivo.authToken = + resolved.plivo.authToken ?? process.env.PLIVO_AUTH_TOKEN; + } + + // Tunnel Config + resolved.tunnel = resolved.tunnel ?? { + provider: "none", + allowNgrokFreeTierLoopbackBypass: false, + }; + resolved.tunnel.allowNgrokFreeTierLoopbackBypass = + resolved.tunnel.allowNgrokFreeTierLoopbackBypass || + resolved.tunnel.allowNgrokFreeTier || + false; + resolved.tunnel.ngrokAuthToken = + resolved.tunnel.ngrokAuthToken ?? process.env.NGROK_AUTHTOKEN; + resolved.tunnel.ngrokDomain = + resolved.tunnel.ngrokDomain ?? process.env.NGROK_DOMAIN; + + return resolved; +} + /** * Validate that the configuration has all required fields for the selected provider. */ diff --git a/extensions/voice-call/src/providers/twilio.ts b/extensions/voice-call/src/providers/twilio.ts index be9dd6eda..87c0f244d 100644 --- a/extensions/voice-call/src/providers/twilio.ts +++ b/extensions/voice-call/src/providers/twilio.ts @@ -31,8 +31,8 @@ import { verifyTwilioProviderWebhook } from "./twilio/webhook.js"; * @see https://www.twilio.com/docs/voice/media-streams */ export interface TwilioProviderOptions { - /** Allow ngrok free tier compatibility mode (less secure) */ - allowNgrokFreeTier?: boolean; + /** Allow ngrok free tier compatibility mode (loopback only, less secure) */ + allowNgrokFreeTierLoopbackBypass?: boolean; /** Override public URL for signature verification */ publicUrl?: string; /** Path for media stream WebSocket (e.g., /voice/stream) */ diff --git a/extensions/voice-call/src/providers/twilio/webhook.ts b/extensions/voice-call/src/providers/twilio/webhook.ts index 28f445c88..d5c3abb95 100644 --- a/extensions/voice-call/src/providers/twilio/webhook.ts +++ b/extensions/voice-call/src/providers/twilio/webhook.ts @@ -11,7 +11,8 @@ export function verifyTwilioProviderWebhook(params: { }): WebhookVerificationResult { const result = verifyTwilioWebhook(params.ctx, params.authToken, { publicUrl: params.currentPublicUrl || undefined, - allowNgrokFreeTier: params.options.allowNgrokFreeTier ?? true, + allowNgrokFreeTierLoopbackBypass: + params.options.allowNgrokFreeTierLoopbackBypass ?? false, skipVerification: params.options.skipVerification, }); diff --git a/extensions/voice-call/src/runtime.ts b/extensions/voice-call/src/runtime.ts index 0770333cd..6f638ab5b 100644 --- a/extensions/voice-call/src/runtime.ts +++ b/extensions/voice-call/src/runtime.ts @@ -1,6 +1,6 @@ import type { CoreConfig } from "./core-bridge.js"; import type { VoiceCallConfig } from "./config.js"; -import { validateProviderConfig } from "./config.js"; +import { resolveVoiceCallConfig, validateProviderConfig } from "./config.js"; import { CallManager } from "./manager.js"; import type { VoiceCallProvider } from "./providers/base.js"; import { MockProvider } from "./providers/mock.js"; @@ -33,24 +33,34 @@ type Logger = { debug: (message: string) => void; }; +function isLoopbackBind(bind: string | undefined): boolean { + if (!bind) return false; + return bind === "127.0.0.1" || bind === "::1" || bind === "localhost"; +} + function resolveProvider(config: VoiceCallConfig): VoiceCallProvider { + const allowNgrokFreeTierLoopbackBypass = + config.tunnel?.provider === "ngrok" && + isLoopbackBind(config.serve?.bind) && + (config.tunnel?.allowNgrokFreeTierLoopbackBypass || + config.tunnel?.allowNgrokFreeTier || + false); + switch (config.provider) { case "telnyx": return new TelnyxProvider({ - apiKey: config.telnyx?.apiKey ?? process.env.TELNYX_API_KEY, - connectionId: - config.telnyx?.connectionId ?? process.env.TELNYX_CONNECTION_ID, - publicKey: config.telnyx?.publicKey ?? process.env.TELNYX_PUBLIC_KEY, + apiKey: config.telnyx?.apiKey, + connectionId: config.telnyx?.connectionId, + publicKey: config.telnyx?.publicKey, }); case "twilio": return new TwilioProvider( { - accountSid: - config.twilio?.accountSid ?? process.env.TWILIO_ACCOUNT_SID, - authToken: config.twilio?.authToken ?? process.env.TWILIO_AUTH_TOKEN, + accountSid: config.twilio?.accountSid, + authToken: config.twilio?.authToken, }, { - allowNgrokFreeTier: config.tunnel?.allowNgrokFreeTier ?? true, + allowNgrokFreeTierLoopbackBypass, publicUrl: config.publicUrl, skipVerification: config.skipSignatureVerification, streamPath: config.streaming?.enabled @@ -61,8 +71,8 @@ function resolveProvider(config: VoiceCallConfig): VoiceCallProvider { case "plivo": return new PlivoProvider( { - authId: config.plivo?.authId ?? process.env.PLIVO_AUTH_ID, - authToken: config.plivo?.authToken ?? process.env.PLIVO_AUTH_TOKEN, + authId: config.plivo?.authId, + authToken: config.plivo?.authToken, }, { publicUrl: config.publicUrl, @@ -85,7 +95,7 @@ export async function createVoiceCallRuntime(params: { ttsRuntime?: TelephonyTtsRuntime; logger?: Logger; }): Promise { - const { config, coreConfig, ttsRuntime, logger } = params; + const { config: rawConfig, coreConfig, ttsRuntime, logger } = params; const log = logger ?? { info: console.log, warn: console.warn, @@ -93,6 +103,8 @@ export async function createVoiceCallRuntime(params: { debug: console.debug, }; + const config = resolveVoiceCallConfig(rawConfig); + if (!config.enabled) { throw new Error( "Voice call disabled. Enable the plugin entry in config.", @@ -125,9 +137,8 @@ export async function createVoiceCallRuntime(params: { provider: config.tunnel.provider, port: config.serve.port, path: config.serve.path, - ngrokAuthToken: - config.tunnel.ngrokAuthToken ?? process.env.NGROK_AUTHTOKEN, - ngrokDomain: config.tunnel.ngrokDomain ?? process.env.NGROK_DOMAIN, + ngrokAuthToken: config.tunnel.ngrokAuthToken, + ngrokDomain: config.tunnel.ngrokDomain, }); publicUrl = tunnelResult?.publicUrl ?? null; } catch (err) { diff --git a/extensions/voice-call/src/types.ts b/extensions/voice-call/src/types.ts index 7f3928778..68cca11e6 100644 --- a/extensions/voice-call/src/types.ts +++ b/extensions/voice-call/src/types.ts @@ -180,6 +180,7 @@ export type WebhookContext = { url: string; method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; query?: Record; + remoteAddress?: string; }; export type ProviderWebhookParseResult = { diff --git a/extensions/voice-call/src/webhook-security.test.ts b/extensions/voice-call/src/webhook-security.test.ts index c31d7225a..3db2983ec 100644 --- a/extensions/voice-call/src/webhook-security.test.ts +++ b/extensions/voice-call/src/webhook-security.test.ts @@ -205,4 +205,56 @@ describe("verifyTwilioWebhook", () => { expect(result.ok).toBe(true); }); + + it("rejects invalid signatures even with ngrok free tier enabled", () => { + const authToken = "test-auth-token"; + const postBody = "CallSid=CS123&CallStatus=completed&From=%2B15550000000"; + + const result = verifyTwilioWebhook( + { + headers: { + host: "127.0.0.1:3334", + "x-forwarded-proto": "https", + "x-forwarded-host": "attacker.ngrok-free.app", + "x-twilio-signature": "invalid", + }, + rawBody: postBody, + url: "http://127.0.0.1:3334/voice/webhook", + method: "POST", + remoteAddress: "203.0.113.10", + }, + authToken, + { allowNgrokFreeTierLoopbackBypass: true }, + ); + + expect(result.ok).toBe(false); + expect(result.isNgrokFreeTier).toBe(true); + expect(result.reason).toMatch(/Invalid signature/); + }); + + it("allows invalid signatures for ngrok free tier only on loopback", () => { + const authToken = "test-auth-token"; + const postBody = "CallSid=CS123&CallStatus=completed&From=%2B15550000000"; + + const result = verifyTwilioWebhook( + { + headers: { + host: "127.0.0.1:3334", + "x-forwarded-proto": "https", + "x-forwarded-host": "local.ngrok-free.app", + "x-twilio-signature": "invalid", + }, + rawBody: postBody, + url: "http://127.0.0.1:3334/voice/webhook", + method: "POST", + remoteAddress: "127.0.0.1", + }, + authToken, + { allowNgrokFreeTierLoopbackBypass: true }, + ); + + expect(result.ok).toBe(true); + expect(result.isNgrokFreeTier).toBe(true); + expect(result.reason).toMatch(/compatibility mode/); + }); }); diff --git a/extensions/voice-call/src/webhook-security.ts b/extensions/voice-call/src/webhook-security.ts index 79bd96099..6c7d4d9ab 100644 --- a/extensions/voice-call/src/webhook-security.ts +++ b/extensions/voice-call/src/webhook-security.ts @@ -131,6 +131,13 @@ function getHeader( return value; } +function isLoopbackAddress(address?: string): boolean { + if (!address) return false; + if (address === "127.0.0.1" || address === "::1") return true; + if (address.startsWith("::ffff:127.")) return true; + return false; +} + /** * Result of Twilio webhook verification with detailed info. */ @@ -155,8 +162,8 @@ export function verifyTwilioWebhook( options?: { /** Override the public URL (e.g., from config) */ publicUrl?: string; - /** Allow ngrok free tier compatibility mode (less secure) */ - allowNgrokFreeTier?: boolean; + /** Allow ngrok free tier compatibility mode (loopback only, less secure) */ + allowNgrokFreeTierLoopbackBypass?: boolean; /** Skip verification entirely (only for development) */ skipVerification?: boolean; }, @@ -195,13 +202,17 @@ export function verifyTwilioWebhook( verificationUrl.includes(".ngrok-free.app") || verificationUrl.includes(".ngrok.io"); - if (isNgrokFreeTier && options?.allowNgrokFreeTier) { + if ( + isNgrokFreeTier && + options?.allowNgrokFreeTierLoopbackBypass && + isLoopbackAddress(ctx.remoteAddress) + ) { console.warn( - "[voice-call] Twilio signature validation failed (proceeding for ngrok free tier compatibility)", + "[voice-call] Twilio signature validation failed (ngrok free tier compatibility, loopback only)", ); return { ok: true, - reason: "ngrok free tier compatibility mode", + reason: "ngrok free tier compatibility mode (loopback only)", verificationUrl, isNgrokFreeTier: true, }; diff --git a/extensions/voice-call/src/webhook.ts b/extensions/voice-call/src/webhook.ts index 6ab4d0eed..09e96ffed 100644 --- a/extensions/voice-call/src/webhook.ts +++ b/extensions/voice-call/src/webhook.ts @@ -252,6 +252,7 @@ export class VoiceCallWebhookServer { url: `http://${req.headers.host}${req.url}`, method: "POST", query: Object.fromEntries(url.searchParams), + remoteAddress: req.socket.remoteAddress ?? undefined, }; // Verify signature diff --git a/extensions/whatsapp/package.json b/extensions/whatsapp/package.json index b7b57eb51..c8aef82ff 100644 --- a/extensions/whatsapp/package.json +++ b/extensions/whatsapp/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/whatsapp", - "version": "2026.1.25", + "name": "@moltbot/whatsapp", + "version": "2026.1.26", "type": "module", "description": "Clawdbot WhatsApp channel plugin", "clawdbot": { diff --git a/extensions/zalo/package.json b/extensions/zalo/package.json index 8f077a6b3..b011c9e89 100644 --- a/extensions/zalo/package.json +++ b/extensions/zalo/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/zalo", - "version": "2026.1.25", + "name": "@moltbot/zalo", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Zalo channel plugin", "clawdbot": { @@ -21,7 +21,7 @@ "quickstartAllowFrom": true }, "install": { - "npmSpec": "@clawdbot/zalo", + "npmSpec": "@moltbot/zalo", "localPath": "extensions/zalo", "defaultChoice": "npm" } diff --git a/extensions/zalouser/package.json b/extensions/zalouser/package.json index 0ab93d1ce..90077bafd 100644 --- a/extensions/zalouser/package.json +++ b/extensions/zalouser/package.json @@ -1,6 +1,6 @@ { - "name": "@clawdbot/zalouser", - "version": "2026.1.25", + "name": "@moltbot/zalouser", + "version": "2026.1.26", "type": "module", "description": "Clawdbot Zalo Personal Account plugin via zca-cli", "dependencies": { @@ -25,7 +25,7 @@ "quickstartAllowFrom": true }, "install": { - "npmSpec": "@clawdbot/zalouser", + "npmSpec": "@moltbot/zalouser", "localPath": "extensions/zalouser", "defaultChoice": "npm" } diff --git a/fly.private.toml b/fly.private.toml new file mode 100644 index 000000000..6edbc8005 --- /dev/null +++ b/fly.private.toml @@ -0,0 +1,39 @@ +# Clawdbot Fly.io PRIVATE deployment configuration +# Use this template for hardened deployments with no public IP exposure. +# +# This config is suitable when: +# - You only make outbound calls (no inbound webhooks needed) +# - You use ngrok/Tailscale tunnels for any webhook callbacks +# - You access the gateway via `fly proxy` or WireGuard, not public URL +# - You want the deployment hidden from internet scanners (Shodan, etc.) +# +# See https://fly.io/docs/reference/configuration/ + +app = "my-clawdbot" # change to your app name +primary_region = "iad" # change to your closest region + +[build] + dockerfile = "Dockerfile" + +[env] + NODE_ENV = "production" + CLAWDBOT_PREFER_PNPM = "1" + CLAWDBOT_STATE_DIR = "/data" + NODE_OPTIONS = "--max-old-space-size=1536" + +[processes] + app = "node dist/index.js gateway --allow-unconfigured --port 3000 --bind lan" + +# NOTE: No [http_service] block = no public ingress allocated. +# The gateway will only be accessible via: +# - fly proxy 3000:3000 -a +# - fly wireguard (then access via internal IPv6) +# - fly ssh console + +[[vm]] + size = "shared-cpu-2x" + memory = "2048mb" + +[mounts] + source = "clawdbot_data" + destination = "/data" diff --git a/package.json b/package.json index 0c63d5d69..6a88df982 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,17 @@ { - "name": "clawdbot", - "version": "2026.1.25", + "name": "moltbot", + "version": "2026.1.26", "description": "WhatsApp gateway CLI (Baileys web) with Pi RPC agent", "type": "module", "main": "dist/index.js", "exports": { ".": "./dist/index.js", "./plugin-sdk": "./dist/plugin-sdk/index.js", - "./plugin-sdk/*": "./dist/plugin-sdk/*" + "./plugin-sdk/*": "./dist/plugin-sdk/*", + "./cli-entry": "./dist/entry.js" }, "bin": { + "moltbot": "dist/entry.js", "clawdbot": "dist/entry.js" }, "files": [ @@ -82,7 +84,7 @@ "docs:bin": "node scripts/build-docs-list.mjs", "docs:dev": "cd docs && mint dev", "docs:build": "cd docs && pnpm dlx --reporter append-only mint broken-links", - "build": "tsc -p tsconfig.json && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/write-build-info.ts", + "build": "pnpm canvas:a2ui:bundle && tsc -p tsconfig.json && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/write-build-info.ts", "plugins:sync": "node --import tsx scripts/sync-plugin-versions.ts", "release:check": "node --import tsx scripts/release-check.ts", "ui:install": "node scripts/ui.js install", @@ -90,6 +92,7 @@ "ui:build": "node scripts/ui.js build", "start": "node scripts/run-node.mjs", "clawdbot": "node scripts/run-node.mjs", + "moltbot": "node scripts/run-node.mjs", "gateway:watch": "node scripts/watch-node.mjs gateway --force", "gateway:dev": "CLAWDBOT_SKIP_CHANNELS=1 node scripts/run-node.mjs --dev gateway", "gateway:dev:reset": "CLAWDBOT_SKIP_CHANNELS=1 node scripts/run-node.mjs --dev gateway --reset", @@ -237,6 +240,9 @@ "vitest": "^4.0.18", "wireit": "^0.14.12" }, + "overrides": { + "tar": "7.5.4" + }, "pnpm": { "minimumReleaseAge": 2880, "overrides": { diff --git a/packages/clawdbot/package.json b/packages/clawdbot/package.json new file mode 100644 index 000000000..dad75eda4 --- /dev/null +++ b/packages/clawdbot/package.json @@ -0,0 +1,16 @@ +{ + "name": "clawdbot", + "version": "2026.1.26", + "type": "module", + "description": "Compatibility shim that forwards to moltbot", + "exports": { + ".": "./index.js", + "./plugin-sdk": "./plugin-sdk/index.js" + }, + "bin": { + "clawdbot": "./bin/clawdbot.js" + }, + "dependencies": { + "moltbot": "workspace:*" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 14bef9f5c..d1c55dd8d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -335,12 +335,12 @@ importers: '@matrix-org/matrix-sdk-crypto-nodejs': specifier: ^0.4.0 version: 0.4.0 + '@vector-im/matrix-bot-sdk': + specifier: 0.8.0-element.3 + version: 0.8.0-element.3 markdown-it: specifier: 14.1.0 version: 14.1.0 - matrix-bot-sdk: - specifier: 0.8.0 - version: 0.8.0 music-metadata: specifier: ^11.10.6 version: 11.10.6 @@ -357,8 +357,8 @@ importers: extensions/memory-core: dependencies: clawdbot: - specifier: '>=2026.1.25' - version: link:../.. + specifier: '>=2026.1.24-3' + version: 2026.1.24-3(@types/express@5.0.6)(audio-decode@2.2.3)(devtools-protocol@0.0.1561482)(typescript@5.9.3) extensions/memory-lancedb: dependencies: @@ -424,6 +424,25 @@ importers: specifier: ^3.0.0 version: 3.0.0 + extensions/twitch: + dependencies: + '@twurple/api': + specifier: ^8.0.3 + version: 8.0.3(@twurple/auth@8.0.3) + '@twurple/auth': + specifier: ^8.0.3 + version: 8.0.3 + '@twurple/chat': + specifier: ^8.0.3 + version: 8.0.3(@twurple/auth@8.0.3) + zod: + specifier: ^4.3.5 + version: 4.3.6 + devDependencies: + clawdbot: + specifier: workspace:* + version: link:../.. + extensions/voice-call: dependencies: '@sinclair/typebox': @@ -810,6 +829,39 @@ packages: '@cloudflare/workers-types@4.20260120.0': resolution: {integrity: sha512-B8pueG+a5S+mdK3z8oKu1ShcxloZ7qWb68IEyLLaepvdryIbNC7JVPcY0bWsjS56UQVKc5fnyRge3yZIwc9bxw==} + '@d-fischer/cache-decorators@4.0.1': + resolution: {integrity: sha512-HNYLBLWs/t28GFZZeqdIBqq8f37mqDIFO6xNPof94VjpKvuP6ROqCZGafx88dk5zZUlBfViV9jD8iNNlXfc4CA==} + + '@d-fischer/connection@9.0.0': + resolution: {integrity: sha512-Mljp/EbaE+eYWfsFXUOk+RfpbHgrWGL/60JkAvjYixw6KREfi5r17XdUiXe54ByAQox6jwgdN2vebdmW1BT+nQ==} + + '@d-fischer/deprecate@2.0.2': + resolution: {integrity: sha512-wlw3HwEanJFJKctwLzhfOM6LKwR70FPfGZGoKOhWBKyOPXk+3a9Cc6S9zhm6tka7xKtpmfxVIReGUwPnMbIaZg==} + + '@d-fischer/detect-node@3.0.1': + resolution: {integrity: sha512-0Rf3XwTzuTh8+oPZW9SfxTIiL+26RRJ0BRPwj5oVjZFyFKmsj9RGfN2zuTRjOuA3FCK/jYm06HOhwNK+8Pfv8w==} + + '@d-fischer/escape-string-regexp@5.0.0': + resolution: {integrity: sha512-7eoxnxcto5eVPW5h1T+ePnVFukmI9f/ZR9nlBLh1t3kyzJDUNor2C+YW9H/Terw3YnbZSDgDYrpCJCHtOtAQHw==} + engines: {node: '>=10'} + + '@d-fischer/isomorphic-ws@7.0.2': + resolution: {integrity: sha512-xK+qIJUF0ne3dsjq5Y3BviQ4M+gx9dzkN+dPP7abBMje4YRfow+X9jBgeEoTe5e+Q6+8hI9R0b37Okkk8Vf0hQ==} + peerDependencies: + ws: ^8.2.0 + + '@d-fischer/logger@4.2.4': + resolution: {integrity: sha512-TFMZ/SVW8xyQtyJw9Rcuci4betSKy0qbQn2B5+1+72vVXeO8Qb1pYvuwF5qr0vDGundmSWq7W8r19nVPnXXSvA==} + + '@d-fischer/rate-limiter@1.1.0': + resolution: {integrity: sha512-O5HgACwApyCZhp4JTEBEtbv/W3eAwEkrARFvgWnEsDmXgCMWjIHwohWoHre5BW6IYXFSHBGsuZB/EvNL3942kQ==} + + '@d-fischer/shared-utils@3.6.4': + resolution: {integrity: sha512-BPkVLHfn2Lbyo/ENDBwtEB8JVQ+9OzkjJhUunLaxkw4k59YFlQxUUwlDBejVSFcpQT0t+D3CQlX+ySZnQj0wxw==} + + '@d-fischer/typed-event-emitter@3.3.3': + resolution: {integrity: sha512-OvSEOa8icfdWDqcRtjSEZtgJTFOFNgTjje7zaL0+nAtu2/kZtRCSK5wUMrI/aXtCH8o0Qz2vA8UqkhWUTARFQQ==} + '@discordjs/voice@0.19.0': resolution: {integrity: sha512-UyX6rGEXzVyPzb1yvjHtPfTlnLvB5jX/stAMdiytHhfoydX+98hfympdOwsnTktzr+IRvphxTbdErgYDJkEsvw==} engines: {node: '>=22.12.0'} @@ -2585,6 +2637,25 @@ packages: '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@twurple/api-call@8.0.3': + resolution: {integrity: sha512-/5DBTqFjpYB+qqOkkFzoTWE79a7+I8uLXmBIIIYjGoq/CIPxKcHnlemXlU8cQhTr87PVa3th8zJXGYiNkpRx8w==} + + '@twurple/api@8.0.3': + resolution: {integrity: sha512-vnqVi9YlNDbCqgpUUvTIq4sDitKCY0dkTw9zPluZvRNqUB1eCsuoaRNW96HQDhKtA9P4pRzwZ8xU7v/1KU2ytg==} + peerDependencies: + '@twurple/auth': 8.0.3 + + '@twurple/auth@8.0.3': + resolution: {integrity: sha512-Xlv+WNXmGQir4aBXYeRCqdno5XurA6jzYTIovSEHa7FZf3AMHMFqtzW7yqTCUn4iOahfUSA2TIIxmxFM0wis0g==} + + '@twurple/chat@8.0.3': + resolution: {integrity: sha512-rhm6xhWKp+4zYFimaEj5fPm6lw/yjrAOsGXXSvPDsEqFR+fc0cVXzmHmglTavkmEELRajFiqNBKZjg73JZWhTQ==} + peerDependencies: + '@twurple/auth': 8.0.3 + + '@twurple/common@8.0.3': + resolution: {integrity: sha512-JQ2lb5qSFT21Y9qMfIouAILb94ppedLHASq49Fe/AP8oq0k3IC9Q7tX2n6tiMzGWqn+n8MnONUpMSZ6FhulMXA==} + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -2597,6 +2668,9 @@ packages: '@types/bun@1.3.6': resolution: {integrity: sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA==} + '@types/caseless@0.12.5': + resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -2678,6 +2752,9 @@ packages: '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/request@2.48.13': + resolution: {integrity: sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==} + '@types/retry@0.12.0': resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} @@ -2696,6 +2773,9 @@ packages: '@types/serve-static@2.2.0': resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -2752,6 +2832,10 @@ packages: '@urbit/http-api@3.0.0': resolution: {integrity: sha512-EmyPbWHWXhfYQ/9wWFcLT53VvCn8ct9ljd6QEe+UBjNPEhUPOFBLpDsDp3iPLQgg8ykSU8JMMHxp95LHCorExA==} + '@vector-im/matrix-bot-sdk@0.8.0-element.3': + resolution: {integrity: sha512-2FFo/Kz2vTnOZDv59Q0s803LHf7KzuQ2EwOYYAtO0zUKJ8pV5CPsVC/IHyFb+Fsxl3R9XWFiX529yhslb4v9cQ==} + engines: {node: '>=22.0.0'} + '@vitest/browser-playwright@4.0.18': resolution: {integrity: sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g==} peerDependencies: @@ -3124,6 +3208,11 @@ packages: class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + clawdbot@2026.1.24-3: + resolution: {integrity: sha512-zt9BzhWXduq8ZZR4rfzQDurQWAgmijTTyPZCQGrn5ew6wCEwhxxEr2/NHG7IlCwcfRsKymsY4se9KMhoNz0JtQ==} + engines: {node: '>=22.12.0'} + hasBin: true + cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} @@ -3541,6 +3630,10 @@ packages: resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} engines: {node: '>= 0.12'} + form-data@2.5.5: + resolution: {integrity: sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==} + engines: {node: '>= 0.12'} + form-data@4.0.5: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} @@ -3775,6 +3868,9 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + ircv3@0.33.0: + resolution: {integrity: sha512-7rK1Aial3LBiFycE8w3MHiBBFb41/2GG2Ll/fR2IJj1vx0pLpn1s+78K+z/I4PZTqCCSp/Sb4QgKMh3NMhx0Kg==} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -3944,6 +4040,10 @@ packages: keyv@5.6.0: resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} + klona@2.0.6: + resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} + engines: {node: '>= 8'} + leac@0.6.0: resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} @@ -4158,10 +4258,6 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - matrix-bot-sdk@0.8.0: - resolution: {integrity: sha512-sCY5UvZfsZhJdCjSc8wZhGhIHOe5cSFSILxx9Zp5a/NEXtmQ6W/bIhefIk4zFAZXetFwXsgvKh1960k1hG5WDw==} - engines: {node: '>=22.0.0'} - mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} @@ -6383,6 +6479,54 @@ snapshots: '@cloudflare/workers-types@4.20260120.0': optional: true + '@d-fischer/cache-decorators@4.0.1': + dependencies: + '@d-fischer/shared-utils': 3.6.4 + tslib: 2.8.1 + + '@d-fischer/connection@9.0.0': + dependencies: + '@d-fischer/isomorphic-ws': 7.0.2(ws@8.19.0) + '@d-fischer/logger': 4.2.4 + '@d-fischer/shared-utils': 3.6.4 + '@d-fischer/typed-event-emitter': 3.3.3 + '@types/ws': 8.18.1 + tslib: 2.8.1 + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@d-fischer/deprecate@2.0.2': {} + + '@d-fischer/detect-node@3.0.1': {} + + '@d-fischer/escape-string-regexp@5.0.0': {} + + '@d-fischer/isomorphic-ws@7.0.2(ws@8.19.0)': + dependencies: + ws: 8.19.0 + + '@d-fischer/logger@4.2.4': + dependencies: + '@d-fischer/detect-node': 3.0.1 + '@d-fischer/shared-utils': 3.6.4 + tslib: 2.8.1 + + '@d-fischer/rate-limiter@1.1.0': + dependencies: + '@d-fischer/logger': 4.2.4 + '@d-fischer/shared-utils': 3.6.4 + tslib: 2.8.1 + + '@d-fischer/shared-utils@3.6.4': + dependencies: + tslib: 2.8.1 + + '@d-fischer/typed-event-emitter@3.3.3': + dependencies: + tslib: 2.8.1 + '@discordjs/voice@0.19.0': dependencies: '@types/ws': 8.18.1 @@ -8225,6 +8369,57 @@ snapshots: '@tokenizer/token@0.3.0': {} + '@twurple/api-call@8.0.3': + dependencies: + '@d-fischer/shared-utils': 3.6.4 + '@twurple/common': 8.0.3 + tslib: 2.8.1 + + '@twurple/api@8.0.3(@twurple/auth@8.0.3)': + dependencies: + '@d-fischer/cache-decorators': 4.0.1 + '@d-fischer/detect-node': 3.0.1 + '@d-fischer/logger': 4.2.4 + '@d-fischer/rate-limiter': 1.1.0 + '@d-fischer/shared-utils': 3.6.4 + '@d-fischer/typed-event-emitter': 3.3.3 + '@twurple/api-call': 8.0.3 + '@twurple/auth': 8.0.3 + '@twurple/common': 8.0.3 + retry: 0.13.1 + tslib: 2.8.1 + + '@twurple/auth@8.0.3': + dependencies: + '@d-fischer/logger': 4.2.4 + '@d-fischer/shared-utils': 3.6.4 + '@d-fischer/typed-event-emitter': 3.3.3 + '@twurple/api-call': 8.0.3 + '@twurple/common': 8.0.3 + tslib: 2.8.1 + + '@twurple/chat@8.0.3(@twurple/auth@8.0.3)': + dependencies: + '@d-fischer/cache-decorators': 4.0.1 + '@d-fischer/deprecate': 2.0.2 + '@d-fischer/logger': 4.2.4 + '@d-fischer/rate-limiter': 1.1.0 + '@d-fischer/shared-utils': 3.6.4 + '@d-fischer/typed-event-emitter': 3.3.3 + '@twurple/auth': 8.0.3 + '@twurple/common': 8.0.3 + ircv3: 0.33.0 + tslib: 2.8.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@twurple/common@8.0.3': + dependencies: + '@d-fischer/shared-utils': 3.6.4 + klona: 2.0.6 + tslib: 2.8.1 + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -8243,6 +8438,8 @@ snapshots: bun-types: 1.3.6 optional: true + '@types/caseless@0.12.5': {} + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -8335,6 +8532,13 @@ snapshots: '@types/range-parser@1.2.7': {} + '@types/request@2.48.13': + dependencies: + '@types/caseless': 0.12.5 + '@types/node': 25.0.10 + '@types/tough-cookie': 4.0.5 + form-data: 2.5.5 + '@types/retry@0.12.0': {} '@types/retry@0.12.5': {} @@ -8359,6 +8563,8 @@ snapshots: '@types/http-errors': 2.0.5 '@types/node': 25.0.10 + '@types/tough-cookie@4.0.5': {} + '@types/trusted-types@2.0.7': {} '@types/ws@8.18.1': @@ -8412,6 +8618,30 @@ snapshots: browser-or-node: 1.3.0 core-js: 3.48.0 + '@vector-im/matrix-bot-sdk@0.8.0-element.3': + dependencies: + '@matrix-org/matrix-sdk-crypto-nodejs': 0.4.0 + '@types/express': 4.17.25 + '@types/request': 2.48.13 + another-json: 0.2.0 + async-lock: 1.4.1 + chalk: 4.1.2 + express: 4.22.1 + glob-to-regexp: 0.4.1 + hash.js: 1.1.7 + html-to-text: 9.0.5 + htmlencode: 0.0.4 + lowdb: 1.0.0 + lru-cache: 10.4.3 + mkdirp: 3.0.1 + morgan: 1.10.1 + postgres: 3.4.8 + request: 2.88.2 + request-promise: 4.2.6(request@2.88.2) + sanitize-html: 2.17.0 + transitivePeerDependencies: + - supports-color + '@vitest/browser-playwright@4.0.18(playwright@1.58.0)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.18)': dependencies: '@vitest/browser': 4.0.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.18) @@ -8862,6 +9092,84 @@ snapshots: dependencies: clsx: 2.1.1 + clawdbot@2026.1.24-3(@types/express@5.0.6)(audio-decode@2.2.3)(devtools-protocol@0.0.1561482)(typescript@5.9.3): + dependencies: + '@agentclientprotocol/sdk': 0.13.1(zod@4.3.6) + '@aws-sdk/client-bedrock': 3.975.0 + '@buape/carbon': 0.14.0(hono@4.11.4) + '@clack/prompts': 0.11.0 + '@grammyjs/runner': 2.0.3(grammy@1.39.3) + '@grammyjs/transformer-throttler': 1.2.1(grammy@1.39.3) + '@homebridge/ciao': 1.3.4 + '@line/bot-sdk': 10.6.0 + '@lydell/node-pty': 1.2.0-beta.3 + '@mariozechner/pi-agent-core': 0.49.3(ws@8.19.0)(zod@4.3.6) + '@mariozechner/pi-ai': 0.49.3(ws@8.19.0)(zod@4.3.6) + '@mariozechner/pi-coding-agent': 0.49.3(ws@8.19.0)(zod@4.3.6) + '@mariozechner/pi-tui': 0.49.3 + '@mozilla/readability': 0.6.0 + '@sinclair/typebox': 0.34.47 + '@slack/bolt': 4.6.0(@types/express@5.0.6) + '@slack/web-api': 7.13.0 + '@whiskeysockets/baileys': 7.0.0-rc.9(audio-decode@2.2.3)(sharp@0.34.5) + ajv: 8.17.1 + body-parser: 2.2.2 + chalk: 5.6.2 + chokidar: 5.0.0 + chromium-bidi: 13.0.1(devtools-protocol@0.0.1561482) + cli-highlight: 2.1.11 + commander: 14.0.2 + croner: 9.1.0 + detect-libc: 2.1.2 + discord-api-types: 0.38.37 + dotenv: 17.2.3 + express: 5.2.1 + file-type: 21.3.0 + grammy: 1.39.3 + hono: 4.11.4 + jiti: 2.6.1 + json5: 2.2.3 + jszip: 3.10.1 + linkedom: 0.18.12 + long: 5.3.2 + markdown-it: 14.1.0 + node-edge-tts: 1.2.9 + osc-progress: 0.3.0 + pdfjs-dist: 5.4.530 + playwright-core: 1.58.0 + proper-lockfile: 4.1.2 + qrcode-terminal: 0.12.0 + sharp: 0.34.5 + sqlite-vec: 0.1.7-alpha.2 + tar: 7.5.4 + tslog: 4.10.2 + undici: 7.19.0 + ws: 8.19.0 + yaml: 2.8.2 + zod: 4.3.6 + optionalDependencies: + '@napi-rs/canvas': 0.1.88 + node-llama-cpp: 3.15.0(typescript@5.9.3) + transitivePeerDependencies: + - '@discordjs/opus' + - '@modelcontextprotocol/sdk' + - '@types/express' + - audio-decode + - aws-crt + - bufferutil + - canvas + - debug + - devtools-protocol + - encoding + - ffmpeg-static + - jimp + - link-preview-js + - node-opus + - opusscript + - supports-color + - typescript + - utf-8-validate + cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 @@ -9342,6 +9650,15 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 + form-data@2.5.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + safe-buffer: 5.2.1 + form-data@4.0.5: dependencies: asynckit: 0.4.0 @@ -9644,6 +9961,19 @@ snapshots: '@reflink/reflink': 0.1.19 optional: true + ircv3@0.33.0: + dependencies: + '@d-fischer/connection': 9.0.0 + '@d-fischer/escape-string-regexp': 5.0.0 + '@d-fischer/logger': 4.2.4 + '@d-fischer/shared-utils': 3.6.4 + '@d-fischer/typed-event-emitter': 3.3.3 + klona: 2.0.6 + tslib: 2.8.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -9814,6 +10144,8 @@ snapshots: dependencies: '@keyv/serialize': 1.1.1 + klona@2.0.6: {} + leac@0.6.0: {} lie@3.3.0: @@ -10006,29 +10338,6 @@ snapshots: math-intrinsics@1.1.0: {} - matrix-bot-sdk@0.8.0: - dependencies: - '@matrix-org/matrix-sdk-crypto-nodejs': 0.4.0 - '@types/express': 4.17.25 - another-json: 0.2.0 - async-lock: 1.4.1 - chalk: 4.1.2 - express: 4.22.1 - glob-to-regexp: 0.4.1 - hash.js: 1.1.7 - html-to-text: 9.0.5 - htmlencode: 0.0.4 - lowdb: 1.0.0 - lru-cache: 10.4.3 - mkdirp: 3.0.1 - morgan: 1.10.1 - postgres: 3.4.8 - request: 2.88.2 - request-promise: 4.2.6(request@2.88.2) - sanitize-html: 2.17.0 - transitivePeerDependencies: - - supports-color - mdurl@2.0.0: {} media-typer@0.3.0: {} diff --git a/scripts/bundle-a2ui.sh b/scripts/bundle-a2ui.sh index 7e3418fa5..a1cf7ff2b 100755 --- a/scripts/bundle-a2ui.sh +++ b/scripts/bundle-a2ui.sh @@ -1,8 +1,15 @@ #!/usr/bin/env bash set -euo pipefail +on_error() { + echo "A2UI bundling failed. Re-run with: pnpm canvas:a2ui:bundle" >&2 + echo "If this persists, verify pnpm deps and try again." >&2 +} +trap on_error ERR + ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" HASH_FILE="$ROOT_DIR/src/canvas-host/a2ui/.bundle.hash" +OUTPUT_FILE="$ROOT_DIR/src/canvas-host/a2ui/a2ui.bundle.js" INPUT_PATHS=( "$ROOT_DIR/package.json" @@ -33,7 +40,7 @@ compute_hash() { current_hash="$(compute_hash)" if [[ -f "$HASH_FILE" ]]; then previous_hash="$(cat "$HASH_FILE")" - if [[ "$previous_hash" == "$current_hash" ]]; then + if [[ "$previous_hash" == "$current_hash" && -f "$OUTPUT_FILE" ]]; then echo "A2UI bundle up to date; skipping." exit 0 fi diff --git a/scripts/canvas-a2ui-copy.ts b/scripts/canvas-a2ui-copy.ts index 3105dbfcf..b8a80675f 100644 --- a/scripts/canvas-a2ui-copy.ts +++ b/scripts/canvas-a2ui-copy.ts @@ -1,19 +1,44 @@ import fs from "node:fs/promises"; import path from "node:path"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); -const srcDir = path.join(repoRoot, "src", "canvas-host", "a2ui"); -const outDir = path.join(repoRoot, "dist", "canvas-host", "a2ui"); -async function main() { - await fs.stat(path.join(srcDir, "index.html")); - await fs.stat(path.join(srcDir, "a2ui.bundle.js")); +export function getA2uiPaths(env = process.env) { + const srcDir = + env.CLAWDBOT_A2UI_SRC_DIR ?? path.join(repoRoot, "src", "canvas-host", "a2ui"); + const outDir = + env.CLAWDBOT_A2UI_OUT_DIR ?? path.join(repoRoot, "dist", "canvas-host", "a2ui"); + return { srcDir, outDir }; +} + +export async function copyA2uiAssets({ + srcDir, + outDir, +}: { + srcDir: string; + outDir: string; +}) { + try { + await fs.stat(path.join(srcDir, "index.html")); + await fs.stat(path.join(srcDir, "a2ui.bundle.js")); + } catch (err) { + const message = + 'Missing A2UI bundle assets. Run "pnpm canvas:a2ui:bundle" and retry.'; + throw new Error(message, { cause: err }); + } await fs.mkdir(path.dirname(outDir), { recursive: true }); await fs.cp(srcDir, outDir, { recursive: true }); } -main().catch((err) => { - console.error(String(err)); - process.exit(1); -}); +async function main() { + const { srcDir, outDir } = getA2uiPaths(); + await copyA2uiAssets({ srcDir, outDir }); +} + +if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { + main().catch((err) => { + console.error(String(err)); + process.exit(1); + }); +} diff --git a/scripts/claude-auth-status.sh b/scripts/claude-auth-status.sh index cf10b197d..d0294d58d 100755 --- a/scripts/claude-auth-status.sh +++ b/scripts/claude-auth-status.sh @@ -54,7 +54,7 @@ calc_status_from_expires() { json_expires_for_claude_cli() { echo "$STATUS_JSON" | jq -r ' [.auth.oauth.profiles[] - | select(.provider == "anthropic" and .type == "oauth" and .source == "claude-cli") + | select(.provider == "anthropic" and (.type == "oauth" or .type == "token")) | .expiresAt // 0] | max // 0 ' 2>/dev/null || echo "0" diff --git a/skills/nano-banana-pro/SKILL.md b/skills/nano-banana-pro/SKILL.md index a36c21f64..469576ec7 100644 --- a/skills/nano-banana-pro/SKILL.md +++ b/skills/nano-banana-pro/SKILL.md @@ -14,9 +14,14 @@ Generate uv run {baseDir}/scripts/generate_image.py --prompt "your image description" --filename "output.png" --resolution 1K ``` -Edit +Edit (single image) ```bash -uv run {baseDir}/scripts/generate_image.py --prompt "edit instructions" --filename "output.png" --input-image "/path/in.png" --resolution 2K +uv run {baseDir}/scripts/generate_image.py --prompt "edit instructions" --filename "output.png" -i "/path/in.png" --resolution 2K +``` + +Multi-image composition (up to 14 images) +```bash +uv run {baseDir}/scripts/generate_image.py --prompt "combine these into one scene" --filename "output.png" -i img1.png -i img2.png -i img3.png ``` API key diff --git a/skills/nano-banana-pro/scripts/generate_image.py b/skills/nano-banana-pro/scripts/generate_image.py index 48dd9e9e5..32fc1fc32 100755 --- a/skills/nano-banana-pro/scripts/generate_image.py +++ b/skills/nano-banana-pro/scripts/generate_image.py @@ -11,6 +11,9 @@ Generate images using Google's Nano Banana Pro (Gemini 3 Pro Image) API. Usage: uv run generate_image.py --prompt "your image description" --filename "output.png" [--resolution 1K|2K|4K] [--api-key KEY] + +Multi-image editing (up to 14 images): + uv run generate_image.py --prompt "combine these images" --filename "output.png" -i img1.png -i img2.png -i img3.png """ import argparse @@ -42,7 +45,10 @@ def main(): ) parser.add_argument( "--input-image", "-i", - help="Optional input image path for editing/modification" + action="append", + dest="input_images", + metavar="IMAGE", + help="Input image path(s) for editing/composition. Can be specified multiple times (up to 14 images)." ) parser.add_argument( "--resolution", "-r", @@ -78,34 +84,43 @@ def main(): output_path = Path(args.filename) output_path.parent.mkdir(parents=True, exist_ok=True) - # Load input image if provided - input_image = None + # Load input images if provided (up to 14 supported by Nano Banana Pro) + input_images = [] output_resolution = args.resolution - if args.input_image: - try: - input_image = PILImage.open(args.input_image) - print(f"Loaded input image: {args.input_image}") - - # Auto-detect resolution if not explicitly set by user - if args.resolution == "1K": # Default value - # Map input image size to resolution - width, height = input_image.size - max_dim = max(width, height) - if max_dim >= 3000: - output_resolution = "4K" - elif max_dim >= 1500: - output_resolution = "2K" - else: - output_resolution = "1K" - print(f"Auto-detected resolution: {output_resolution} (from input {width}x{height})") - except Exception as e: - print(f"Error loading input image: {e}", file=sys.stderr) + if args.input_images: + if len(args.input_images) > 14: + print(f"Error: Too many input images ({len(args.input_images)}). Maximum is 14.", file=sys.stderr) sys.exit(1) - # Build contents (image first if editing, prompt only if generating) - if input_image: - contents = [input_image, args.prompt] - print(f"Editing image with resolution {output_resolution}...") + max_input_dim = 0 + for img_path in args.input_images: + try: + img = PILImage.open(img_path) + input_images.append(img) + print(f"Loaded input image: {img_path}") + + # Track largest dimension for auto-resolution + width, height = img.size + max_input_dim = max(max_input_dim, width, height) + except Exception as e: + print(f"Error loading input image '{img_path}': {e}", file=sys.stderr) + sys.exit(1) + + # Auto-detect resolution from largest input if not explicitly set + if args.resolution == "1K" and max_input_dim > 0: # Default value + if max_input_dim >= 3000: + output_resolution = "4K" + elif max_input_dim >= 1500: + output_resolution = "2K" + else: + output_resolution = "1K" + print(f"Auto-detected resolution: {output_resolution} (from max input dimension {max_input_dim})") + + # Build contents (images first if editing, prompt only if generating) + if input_images: + contents = [*input_images, args.prompt] + img_count = len(input_images) + print(f"Processing {img_count} image{'s' if img_count > 1 else ''} with resolution {output_resolution}...") else: contents = args.prompt print(f"Generating image with resolution {output_resolution}...") diff --git a/src/agents/auth-health.ts b/src/agents/auth-health.ts index 96e79dc66..15bf3a07f 100644 --- a/src/agents/auth-health.ts +++ b/src/agents/auth-health.ts @@ -2,12 +2,10 @@ import type { ClawdbotConfig } from "../config/config.js"; import { type AuthProfileCredential, type AuthProfileStore, - CLAUDE_CLI_PROFILE_ID, - CODEX_CLI_PROFILE_ID, resolveAuthProfileDisplayLabel, } from "./auth-profiles.js"; -export type AuthProfileSource = "claude-cli" | "codex-cli" | "store"; +export type AuthProfileSource = "store"; export type AuthProfileHealthStatus = "ok" | "expiring" | "expired" | "missing" | "static"; @@ -41,9 +39,7 @@ export type AuthHealthSummary = { export const DEFAULT_OAUTH_WARN_MS = 24 * 60 * 60 * 1000; -export function resolveAuthProfileSource(profileId: string): AuthProfileSource { - if (profileId === CLAUDE_CLI_PROFILE_ID) return "claude-cli"; - if (profileId === CODEX_CLI_PROFILE_ID) return "codex-cli"; +export function resolveAuthProfileSource(_profileId: string): AuthProfileSource { return "store"; } diff --git a/src/agents/auth-profiles.ensureauthprofilestore.test.ts b/src/agents/auth-profiles.ensureauthprofilestore.test.ts index 3eadb6c5b..db7d6f031 100644 --- a/src/agents/auth-profiles.ensureauthprofilestore.test.ts +++ b/src/agents/auth-profiles.ensureauthprofilestore.test.ts @@ -3,8 +3,7 @@ import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { ensureAuthProfileStore } from "./auth-profiles.js"; -import { AUTH_STORE_VERSION, CODEX_CLI_PROFILE_ID } from "./auth-profiles/constants.js"; -import { withTempHome } from "../../test/helpers/temp-home.js"; +import { AUTH_STORE_VERSION } from "./auth-profiles/constants.js"; describe("ensureAuthProfileStore", () => { it("migrates legacy auth.json and deletes it (PR #368)", () => { @@ -123,80 +122,4 @@ describe("ensureAuthProfileStore", () => { fs.rmSync(root, { recursive: true, force: true }); } }); - - it("drops codex-cli from merged store when a custom openai-codex profile matches", async () => { - await withTempHome(async (tempHome) => { - const root = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-auth-dedup-merge-")); - const previousAgentDir = process.env.CLAWDBOT_AGENT_DIR; - const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR; - try { - const mainDir = path.join(root, "main-agent"); - const agentDir = path.join(root, "agent-x"); - fs.mkdirSync(mainDir, { recursive: true }); - fs.mkdirSync(agentDir, { recursive: true }); - - process.env.CLAWDBOT_AGENT_DIR = mainDir; - process.env.PI_CODING_AGENT_DIR = mainDir; - process.env.HOME = tempHome; - - fs.writeFileSync( - path.join(mainDir, "auth-profiles.json"), - `${JSON.stringify( - { - version: AUTH_STORE_VERSION, - profiles: { - [CODEX_CLI_PROFILE_ID]: { - type: "oauth", - provider: "openai-codex", - access: "shared-access-token", - refresh: "shared-refresh-token", - expires: Date.now() + 3600000, - }, - }, - }, - null, - 2, - )}\n`, - "utf8", - ); - - fs.writeFileSync( - path.join(agentDir, "auth-profiles.json"), - `${JSON.stringify( - { - version: AUTH_STORE_VERSION, - profiles: { - "openai-codex:my-custom-profile": { - type: "oauth", - provider: "openai-codex", - access: "shared-access-token", - refresh: "shared-refresh-token", - expires: Date.now() + 3600000, - }, - }, - }, - null, - 2, - )}\n`, - "utf8", - ); - - const store = ensureAuthProfileStore(agentDir); - expect(store.profiles[CODEX_CLI_PROFILE_ID]).toBeUndefined(); - expect(store.profiles["openai-codex:my-custom-profile"]).toBeDefined(); - } finally { - if (previousAgentDir === undefined) { - delete process.env.CLAWDBOT_AGENT_DIR; - } else { - process.env.CLAWDBOT_AGENT_DIR = previousAgentDir; - } - if (previousPiAgentDir === undefined) { - delete process.env.PI_CODING_AGENT_DIR; - } else { - process.env.PI_CODING_AGENT_DIR = previousPiAgentDir; - } - fs.rmSync(root, { recursive: true, force: true }); - } - }); - }); }); diff --git a/src/agents/auth-profiles.external-cli-credential-sync.does-not-overwrite-api-keys-syncing-external.test.ts b/src/agents/auth-profiles.external-cli-credential-sync.does-not-overwrite-api-keys-syncing-external.test.ts deleted file mode 100644 index 1109d3452..000000000 --- a/src/agents/auth-profiles.external-cli-credential-sync.does-not-overwrite-api-keys-syncing-external.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { describe, expect, it } from "vitest"; -import { withTempHome } from "../../test/helpers/temp-home.js"; -import { CLAUDE_CLI_PROFILE_ID, ensureAuthProfileStore } from "./auth-profiles.js"; - -describe("external CLI credential sync", () => { - it("does not overwrite API keys when syncing external CLI creds", async () => { - const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-no-overwrite-")); - try { - await withTempHome( - async (tempHome) => { - // Create Claude Code CLI credentials - const claudeDir = path.join(tempHome, ".claude"); - fs.mkdirSync(claudeDir, { recursive: true }); - const claudeCreds = { - claudeAiOauth: { - accessToken: "cli-access", - refreshToken: "cli-refresh", - expiresAt: Date.now() + 30 * 60 * 1000, - }, - }; - fs.writeFileSync(path.join(claudeDir, ".credentials.json"), JSON.stringify(claudeCreds)); - - // Create auth-profiles.json with an API key - const authPath = path.join(agentDir, "auth-profiles.json"); - fs.writeFileSync( - authPath, - JSON.stringify({ - version: 1, - profiles: { - "anthropic:default": { - type: "api_key", - provider: "anthropic", - key: "sk-store", - }, - }, - }), - ); - - const store = ensureAuthProfileStore(agentDir); - - // Should keep the store's API key and still add the CLI profile. - expect((store.profiles["anthropic:default"] as { key: string }).key).toBe("sk-store"); - expect(store.profiles[CLAUDE_CLI_PROFILE_ID]).toBeDefined(); - }, - { prefix: "clawdbot-home-" }, - ); - } finally { - fs.rmSync(agentDir, { recursive: true, force: true }); - } - }); - it("prefers oauth over token even if token has later expiry (oauth enables auto-refresh)", async () => { - const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-cli-oauth-preferred-")); - try { - await withTempHome( - async (tempHome) => { - const claudeDir = path.join(tempHome, ".claude"); - fs.mkdirSync(claudeDir, { recursive: true }); - // CLI has OAuth credentials (with refresh token) expiring in 30 min - fs.writeFileSync( - path.join(claudeDir, ".credentials.json"), - JSON.stringify({ - claudeAiOauth: { - accessToken: "cli-oauth-access", - refreshToken: "cli-refresh", - expiresAt: Date.now() + 30 * 60 * 1000, - }, - }), - ); - - const authPath = path.join(agentDir, "auth-profiles.json"); - // Store has token credentials expiring in 60 min (later than CLI) - fs.writeFileSync( - authPath, - JSON.stringify({ - version: 1, - profiles: { - [CLAUDE_CLI_PROFILE_ID]: { - type: "token", - provider: "anthropic", - token: "store-token-access", - expires: Date.now() + 60 * 60 * 1000, - }, - }, - }), - ); - - const store = ensureAuthProfileStore(agentDir); - // OAuth should be preferred over token because it can auto-refresh - const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID]; - expect(cliProfile.type).toBe("oauth"); - expect((cliProfile as { access: string }).access).toBe("cli-oauth-access"); - }, - { prefix: "clawdbot-home-" }, - ); - } finally { - fs.rmSync(agentDir, { recursive: true, force: true }); - } - }); -}); diff --git a/src/agents/auth-profiles.external-cli-credential-sync.does-not-overwrite-fresher-store-oauth-older.test.ts b/src/agents/auth-profiles.external-cli-credential-sync.does-not-overwrite-fresher-store-oauth-older.test.ts deleted file mode 100644 index 3ca83a576..000000000 --- a/src/agents/auth-profiles.external-cli-credential-sync.does-not-overwrite-fresher-store-oauth-older.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { describe, expect, it } from "vitest"; -import { withTempHome } from "../../test/helpers/temp-home.js"; -import { CLAUDE_CLI_PROFILE_ID, ensureAuthProfileStore } from "./auth-profiles.js"; - -describe("external CLI credential sync", () => { - it("does not overwrite fresher store oauth with older CLI oauth", async () => { - const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-cli-oauth-no-downgrade-")); - try { - await withTempHome( - async (tempHome) => { - const claudeDir = path.join(tempHome, ".claude"); - fs.mkdirSync(claudeDir, { recursive: true }); - // CLI has OAuth credentials expiring in 30 min - fs.writeFileSync( - path.join(claudeDir, ".credentials.json"), - JSON.stringify({ - claudeAiOauth: { - accessToken: "cli-oauth-access", - refreshToken: "cli-refresh", - expiresAt: Date.now() + 30 * 60 * 1000, - }, - }), - ); - - const authPath = path.join(agentDir, "auth-profiles.json"); - // Store has OAuth credentials expiring in 60 min (later than CLI) - fs.writeFileSync( - authPath, - JSON.stringify({ - version: 1, - profiles: { - [CLAUDE_CLI_PROFILE_ID]: { - type: "oauth", - provider: "anthropic", - access: "store-oauth-access", - refresh: "store-refresh", - expires: Date.now() + 60 * 60 * 1000, - }, - }, - }), - ); - - const store = ensureAuthProfileStore(agentDir); - // Fresher store oauth should be kept - const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID]; - expect(cliProfile.type).toBe("oauth"); - expect((cliProfile as { access: string }).access).toBe("store-oauth-access"); - }, - { prefix: "clawdbot-home-" }, - ); - } finally { - fs.rmSync(agentDir, { recursive: true, force: true }); - } - }); - it("does not downgrade store oauth to token when CLI lacks refresh token", async () => { - const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-cli-no-downgrade-oauth-")); - try { - await withTempHome( - async (tempHome) => { - const claudeDir = path.join(tempHome, ".claude"); - fs.mkdirSync(claudeDir, { recursive: true }); - // CLI has token-only credentials (no refresh token) - fs.writeFileSync( - path.join(claudeDir, ".credentials.json"), - JSON.stringify({ - claudeAiOauth: { - accessToken: "cli-token-access", - expiresAt: Date.now() + 30 * 60 * 1000, - }, - }), - ); - - const authPath = path.join(agentDir, "auth-profiles.json"); - // Store already has OAuth credentials with refresh token - fs.writeFileSync( - authPath, - JSON.stringify({ - version: 1, - profiles: { - [CLAUDE_CLI_PROFILE_ID]: { - type: "oauth", - provider: "anthropic", - access: "store-oauth-access", - refresh: "store-refresh", - expires: Date.now() + 60 * 60 * 1000, - }, - }, - }), - ); - - const store = ensureAuthProfileStore(agentDir); - // Keep oauth to preserve auto-refresh capability - const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID]; - expect(cliProfile.type).toBe("oauth"); - expect((cliProfile as { access: string }).access).toBe("store-oauth-access"); - }, - { prefix: "clawdbot-home-" }, - ); - } finally { - fs.rmSync(agentDir, { recursive: true, force: true }); - } - }); -}); diff --git a/src/agents/auth-profiles.external-cli-credential-sync.skips-codex-sync-when-credentials-exist-in-another-profile.test.ts b/src/agents/auth-profiles.external-cli-credential-sync.skips-codex-sync-when-credentials-exist-in-another-profile.test.ts deleted file mode 100644 index 6fa6734d7..000000000 --- a/src/agents/auth-profiles.external-cli-credential-sync.skips-codex-sync-when-credentials-exist-in-another-profile.test.ts +++ /dev/null @@ -1,166 +0,0 @@ -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { describe, expect, it } from "vitest"; -import { withTempHome } from "../../test/helpers/temp-home.js"; -import { CODEX_CLI_PROFILE_ID, ensureAuthProfileStore } from "./auth-profiles.js"; - -describe("external CLI credential sync", () => { - it("skips codex-cli sync when credentials already exist in another openai-codex profile", async () => { - const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-codex-dedup-skip-")); - try { - await withTempHome( - async (tempHome) => { - const codexDir = path.join(tempHome, ".codex"); - fs.mkdirSync(codexDir, { recursive: true }); - const codexAuthPath = path.join(codexDir, "auth.json"); - fs.writeFileSync( - codexAuthPath, - JSON.stringify({ - tokens: { - access_token: "shared-access-token", - refresh_token: "shared-refresh-token", - }, - }), - ); - fs.utimesSync(codexAuthPath, new Date(), new Date()); - - const authPath = path.join(agentDir, "auth-profiles.json"); - fs.writeFileSync( - authPath, - JSON.stringify({ - version: 1, - profiles: { - "openai-codex:my-custom-profile": { - type: "oauth", - provider: "openai-codex", - access: "shared-access-token", - refresh: "shared-refresh-token", - expires: Date.now() + 3600000, - }, - }, - }), - ); - - const store = ensureAuthProfileStore(agentDir); - - expect(store.profiles[CODEX_CLI_PROFILE_ID]).toBeUndefined(); - expect(store.profiles["openai-codex:my-custom-profile"]).toBeDefined(); - }, - { prefix: "clawdbot-home-" }, - ); - } finally { - fs.rmSync(agentDir, { recursive: true, force: true }); - } - }); - - it("creates codex-cli profile when credentials differ from existing openai-codex profiles", async () => { - const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-codex-dedup-create-")); - try { - await withTempHome( - async (tempHome) => { - const codexDir = path.join(tempHome, ".codex"); - fs.mkdirSync(codexDir, { recursive: true }); - const codexAuthPath = path.join(codexDir, "auth.json"); - fs.writeFileSync( - codexAuthPath, - JSON.stringify({ - tokens: { - access_token: "unique-access-token", - refresh_token: "unique-refresh-token", - }, - }), - ); - fs.utimesSync(codexAuthPath, new Date(), new Date()); - - const authPath = path.join(agentDir, "auth-profiles.json"); - fs.writeFileSync( - authPath, - JSON.stringify({ - version: 1, - profiles: { - "openai-codex:my-custom-profile": { - type: "oauth", - provider: "openai-codex", - access: "different-access-token", - refresh: "different-refresh-token", - expires: Date.now() + 3600000, - }, - }, - }), - ); - - const store = ensureAuthProfileStore(agentDir); - - expect(store.profiles[CODEX_CLI_PROFILE_ID]).toBeDefined(); - expect((store.profiles[CODEX_CLI_PROFILE_ID] as { access: string }).access).toBe( - "unique-access-token", - ); - expect(store.profiles["openai-codex:my-custom-profile"]).toBeDefined(); - }, - { prefix: "clawdbot-home-" }, - ); - } finally { - fs.rmSync(agentDir, { recursive: true, force: true }); - } - }); - - it("removes codex-cli profile when it duplicates another openai-codex profile", async () => { - const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-codex-dedup-remove-")); - try { - await withTempHome( - async (tempHome) => { - const codexDir = path.join(tempHome, ".codex"); - fs.mkdirSync(codexDir, { recursive: true }); - const codexAuthPath = path.join(codexDir, "auth.json"); - fs.writeFileSync( - codexAuthPath, - JSON.stringify({ - tokens: { - access_token: "shared-access-token", - refresh_token: "shared-refresh-token", - }, - }), - ); - fs.utimesSync(codexAuthPath, new Date(), new Date()); - - const authPath = path.join(agentDir, "auth-profiles.json"); - fs.writeFileSync( - authPath, - JSON.stringify({ - version: 1, - profiles: { - [CODEX_CLI_PROFILE_ID]: { - type: "oauth", - provider: "openai-codex", - access: "shared-access-token", - refresh: "shared-refresh-token", - expires: Date.now() + 3600000, - }, - "openai-codex:my-custom-profile": { - type: "oauth", - provider: "openai-codex", - access: "shared-access-token", - refresh: "shared-refresh-token", - expires: Date.now() + 3600000, - }, - }, - }), - ); - - const store = ensureAuthProfileStore(agentDir); - - expect(store.profiles[CODEX_CLI_PROFILE_ID]).toBeUndefined(); - const saved = JSON.parse(fs.readFileSync(authPath, "utf8")) as { - profiles?: Record; - }; - expect(saved.profiles?.[CODEX_CLI_PROFILE_ID]).toBeUndefined(); - expect(saved.profiles?.["openai-codex:my-custom-profile"]).toBeDefined(); - }, - { prefix: "clawdbot-home-" }, - ); - } finally { - fs.rmSync(agentDir, { recursive: true, force: true }); - } - }); -}); diff --git a/src/agents/auth-profiles.external-cli-credential-sync.syncs-claude-cli-oauth-credentials-into-anthropic.test.ts b/src/agents/auth-profiles.external-cli-credential-sync.syncs-claude-cli-oauth-credentials-into-anthropic.test.ts deleted file mode 100644 index 1295552ba..000000000 --- a/src/agents/auth-profiles.external-cli-credential-sync.syncs-claude-cli-oauth-credentials-into-anthropic.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { describe, expect, it } from "vitest"; -import { withTempHome } from "../../test/helpers/temp-home.js"; -import { CLAUDE_CLI_PROFILE_ID, ensureAuthProfileStore } from "./auth-profiles.js"; - -describe("external CLI credential sync", () => { - it("syncs Claude Code CLI OAuth credentials into anthropic:claude-cli", async () => { - const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-cli-sync-")); - try { - // Create a temp home with Claude Code CLI credentials - await withTempHome( - async (tempHome) => { - // Create Claude Code CLI credentials with refreshToken (OAuth) - const claudeDir = path.join(tempHome, ".claude"); - fs.mkdirSync(claudeDir, { recursive: true }); - const claudeCreds = { - claudeAiOauth: { - accessToken: "fresh-access-token", - refreshToken: "fresh-refresh-token", - expiresAt: Date.now() + 60 * 60 * 1000, // 1 hour from now - }, - }; - fs.writeFileSync(path.join(claudeDir, ".credentials.json"), JSON.stringify(claudeCreds)); - - // Create empty auth-profiles.json - const authPath = path.join(agentDir, "auth-profiles.json"); - fs.writeFileSync( - authPath, - JSON.stringify({ - version: 1, - profiles: { - "anthropic:default": { - type: "api_key", - provider: "anthropic", - key: "sk-default", - }, - }, - }), - ); - - // Load the store - should sync from CLI as OAuth credential - const store = ensureAuthProfileStore(agentDir); - - expect(store.profiles["anthropic:default"]).toBeDefined(); - expect((store.profiles["anthropic:default"] as { key: string }).key).toBe("sk-default"); - expect(store.profiles[CLAUDE_CLI_PROFILE_ID]).toBeDefined(); - // Should be stored as OAuth credential (type: "oauth") for auto-refresh - const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID]; - expect(cliProfile.type).toBe("oauth"); - expect((cliProfile as { access: string }).access).toBe("fresh-access-token"); - expect((cliProfile as { refresh: string }).refresh).toBe("fresh-refresh-token"); - expect((cliProfile as { expires: number }).expires).toBeGreaterThan(Date.now()); - }, - { prefix: "clawdbot-home-" }, - ); - } finally { - fs.rmSync(agentDir, { recursive: true, force: true }); - } - }); - it("syncs Claude Code CLI credentials without refreshToken as token type", async () => { - const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-cli-token-sync-")); - try { - await withTempHome( - async (tempHome) => { - // Create Claude Code CLI credentials WITHOUT refreshToken (fallback to token type) - const claudeDir = path.join(tempHome, ".claude"); - fs.mkdirSync(claudeDir, { recursive: true }); - const claudeCreds = { - claudeAiOauth: { - accessToken: "access-only-token", - // No refreshToken - backward compatibility scenario - expiresAt: Date.now() + 60 * 60 * 1000, - }, - }; - fs.writeFileSync(path.join(claudeDir, ".credentials.json"), JSON.stringify(claudeCreds)); - - const authPath = path.join(agentDir, "auth-profiles.json"); - fs.writeFileSync(authPath, JSON.stringify({ version: 1, profiles: {} })); - - const store = ensureAuthProfileStore(agentDir); - - expect(store.profiles[CLAUDE_CLI_PROFILE_ID]).toBeDefined(); - // Should be stored as token type (no refresh capability) - const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID]; - expect(cliProfile.type).toBe("token"); - expect((cliProfile as { token: string }).token).toBe("access-only-token"); - }, - { prefix: "clawdbot-home-" }, - ); - } finally { - fs.rmSync(agentDir, { recursive: true, force: true }); - } - }); -}); diff --git a/src/agents/auth-profiles.external-cli-credential-sync.updates-codex-cli-profile-codex-cli-refresh.test.ts b/src/agents/auth-profiles.external-cli-credential-sync.updates-codex-cli-profile-codex-cli-refresh.test.ts deleted file mode 100644 index 16fe775ab..000000000 --- a/src/agents/auth-profiles.external-cli-credential-sync.updates-codex-cli-profile-codex-cli-refresh.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { describe, expect, it } from "vitest"; -import { withTempHome } from "../../test/helpers/temp-home.js"; -import { CODEX_CLI_PROFILE_ID, ensureAuthProfileStore } from "./auth-profiles.js"; - -describe("external CLI credential sync", () => { - it("updates codex-cli profile when Codex CLI refresh token changes", async () => { - const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-codex-refresh-sync-")); - try { - await withTempHome( - async (tempHome) => { - const codexDir = path.join(tempHome, ".codex"); - fs.mkdirSync(codexDir, { recursive: true }); - const codexAuthPath = path.join(codexDir, "auth.json"); - fs.writeFileSync( - codexAuthPath, - JSON.stringify({ - tokens: { - access_token: "same-access", - refresh_token: "new-refresh", - }, - }), - ); - fs.utimesSync(codexAuthPath, new Date(), new Date()); - - const authPath = path.join(agentDir, "auth-profiles.json"); - fs.writeFileSync( - authPath, - JSON.stringify({ - version: 1, - profiles: { - [CODEX_CLI_PROFILE_ID]: { - type: "oauth", - provider: "openai-codex", - access: "same-access", - refresh: "old-refresh", - expires: Date.now() - 1000, - }, - }, - }), - ); - - const store = ensureAuthProfileStore(agentDir); - expect((store.profiles[CODEX_CLI_PROFILE_ID] as { refresh: string }).refresh).toBe( - "new-refresh", - ); - }, - { prefix: "clawdbot-home-" }, - ); - } finally { - fs.rmSync(agentDir, { recursive: true, force: true }); - } - }); -}); diff --git a/src/agents/auth-profiles.external-cli-credential-sync.upgrades-token-oauth-claude-cli-gets-refreshtoken.test.ts b/src/agents/auth-profiles.external-cli-credential-sync.upgrades-token-oauth-claude-cli-gets-refreshtoken.test.ts deleted file mode 100644 index 2957215f6..000000000 --- a/src/agents/auth-profiles.external-cli-credential-sync.upgrades-token-oauth-claude-cli-gets-refreshtoken.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { describe, expect, it } from "vitest"; -import { withTempHome } from "../../test/helpers/temp-home.js"; -import { - CLAUDE_CLI_PROFILE_ID, - CODEX_CLI_PROFILE_ID, - ensureAuthProfileStore, -} from "./auth-profiles.js"; - -describe("external CLI credential sync", () => { - it("upgrades token to oauth when Claude Code CLI gets refreshToken", async () => { - const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-cli-upgrade-")); - try { - await withTempHome( - async (tempHome) => { - // Create Claude Code CLI credentials with refreshToken - const claudeDir = path.join(tempHome, ".claude"); - fs.mkdirSync(claudeDir, { recursive: true }); - fs.writeFileSync( - path.join(claudeDir, ".credentials.json"), - JSON.stringify({ - claudeAiOauth: { - accessToken: "new-oauth-access", - refreshToken: "new-refresh-token", - expiresAt: Date.now() + 60 * 60 * 1000, - }, - }), - ); - - // Create auth-profiles.json with existing token type credential - const authPath = path.join(agentDir, "auth-profiles.json"); - fs.writeFileSync( - authPath, - JSON.stringify({ - version: 1, - profiles: { - [CLAUDE_CLI_PROFILE_ID]: { - type: "token", - provider: "anthropic", - token: "old-token", - expires: Date.now() + 30 * 60 * 1000, - }, - }, - }), - ); - - const store = ensureAuthProfileStore(agentDir); - - // Should upgrade from token to oauth - const cliProfile = store.profiles[CLAUDE_CLI_PROFILE_ID]; - expect(cliProfile.type).toBe("oauth"); - expect((cliProfile as { access: string }).access).toBe("new-oauth-access"); - expect((cliProfile as { refresh: string }).refresh).toBe("new-refresh-token"); - }, - { prefix: "clawdbot-home-" }, - ); - } finally { - fs.rmSync(agentDir, { recursive: true, force: true }); - } - }); - it("syncs Codex CLI credentials into openai-codex:codex-cli", async () => { - const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-codex-sync-")); - try { - await withTempHome( - async (tempHome) => { - // Create Codex CLI credentials - const codexDir = path.join(tempHome, ".codex"); - fs.mkdirSync(codexDir, { recursive: true }); - const codexCreds = { - tokens: { - access_token: "codex-access-token", - refresh_token: "codex-refresh-token", - }, - }; - const codexAuthPath = path.join(codexDir, "auth.json"); - fs.writeFileSync(codexAuthPath, JSON.stringify(codexCreds)); - - // Create empty auth-profiles.json - const authPath = path.join(agentDir, "auth-profiles.json"); - fs.writeFileSync( - authPath, - JSON.stringify({ - version: 1, - profiles: {}, - }), - ); - - const store = ensureAuthProfileStore(agentDir); - - expect(store.profiles[CODEX_CLI_PROFILE_ID]).toBeDefined(); - expect((store.profiles[CODEX_CLI_PROFILE_ID] as { access: string }).access).toBe( - "codex-access-token", - ); - }, - { prefix: "clawdbot-home-" }, - ); - } finally { - fs.rmSync(agentDir, { recursive: true, force: true }); - } - }); -}); diff --git a/src/agents/auth-profiles/external-cli-sync.ts b/src/agents/auth-profiles/external-cli-sync.ts index 8a7d8270f..d1fa31f23 100644 --- a/src/agents/auth-profiles/external-cli-sync.ts +++ b/src/agents/auth-profiles/external-cli-sync.ts @@ -1,22 +1,11 @@ +import { readQwenCliCredentialsCached } from "../cli-credentials.js"; import { - readClaudeCliCredentialsCached, - readCodexCliCredentialsCached, - readQwenCliCredentialsCached, -} from "../cli-credentials.js"; -import { - CLAUDE_CLI_PROFILE_ID, - CODEX_CLI_PROFILE_ID, EXTERNAL_CLI_NEAR_EXPIRY_MS, EXTERNAL_CLI_SYNC_TTL_MS, QWEN_CLI_PROFILE_ID, log, } from "./constants.js"; -import type { - AuthProfileCredential, - AuthProfileStore, - OAuthCredential, - TokenCredential, -} from "./types.js"; +import type { AuthProfileCredential, AuthProfileStore, OAuthCredential } from "./types.js"; function shallowEqualOAuthCredentials(a: OAuthCredential | undefined, b: OAuthCredential): boolean { if (!a) return false; @@ -33,25 +22,10 @@ function shallowEqualOAuthCredentials(a: OAuthCredential | undefined, b: OAuthCr ); } -function shallowEqualTokenCredentials(a: TokenCredential | undefined, b: TokenCredential): boolean { - if (!a) return false; - if (a.type !== "token") return false; - return ( - a.provider === b.provider && - a.token === b.token && - a.expires === b.expires && - a.email === b.email - ); -} - function isExternalProfileFresh(cred: AuthProfileCredential | undefined, now: number): boolean { if (!cred) return false; if (cred.type !== "oauth" && cred.type !== "token") return false; - if ( - cred.provider !== "anthropic" && - cred.provider !== "openai-codex" && - cred.provider !== "qwen-portal" - ) { + if (cred.provider !== "qwen-portal") { return false; } if (typeof cred.expires !== "number") return true; @@ -59,163 +33,14 @@ function isExternalProfileFresh(cred: AuthProfileCredential | undefined, now: nu } /** - * Find any existing openai-codex profile (other than codex-cli) that has the same - * access and refresh tokens. This prevents creating a duplicate codex-cli profile - * when the user has already set up a custom profile with the same credentials. - */ -export function findDuplicateCodexProfile( - store: AuthProfileStore, - creds: OAuthCredential, -): string | undefined { - for (const [profileId, profile] of Object.entries(store.profiles)) { - if (profileId === CODEX_CLI_PROFILE_ID) continue; - if (profile.type !== "oauth") continue; - if (profile.provider !== "openai-codex") continue; - if (profile.access === creds.access && profile.refresh === creds.refresh) { - return profileId; - } - } - return undefined; -} - -/** - * Sync OAuth credentials from external CLI tools (Claude Code CLI, Codex CLI) into the store. - * This allows clawdbot to use the same credentials as these tools without requiring - * separate authentication, and keeps credentials in sync when CLI tools refresh tokens. + * Sync OAuth credentials from external CLI tools (Qwen Code CLI) into the store. * * Returns true if any credentials were updated. */ -export function syncExternalCliCredentials( - store: AuthProfileStore, - options?: { allowKeychainPrompt?: boolean }, -): boolean { +export function syncExternalCliCredentials(store: AuthProfileStore): boolean { let mutated = false; const now = Date.now(); - // Sync from Claude Code CLI (supports both OAuth and Token credentials) - const existingClaude = store.profiles[CLAUDE_CLI_PROFILE_ID]; - const shouldSyncClaude = - !existingClaude || - existingClaude.provider !== "anthropic" || - existingClaude.type === "token" || - !isExternalProfileFresh(existingClaude, now); - const claudeCreds = shouldSyncClaude - ? readClaudeCliCredentialsCached({ - allowKeychainPrompt: options?.allowKeychainPrompt, - ttlMs: EXTERNAL_CLI_SYNC_TTL_MS, - }) - : null; - if (claudeCreds) { - const existing = store.profiles[CLAUDE_CLI_PROFILE_ID]; - const claudeCredsExpires = claudeCreds.expires ?? 0; - - // Determine if we should update based on credential comparison - let shouldUpdate = false; - let isEqual = false; - - if (claudeCreds.type === "oauth") { - const existingOAuth = existing?.type === "oauth" ? existing : undefined; - isEqual = shallowEqualOAuthCredentials(existingOAuth, claudeCreds); - // Update if: no existing profile, type changed to oauth, expired, or CLI has newer token - shouldUpdate = - !existingOAuth || - existingOAuth.provider !== "anthropic" || - existingOAuth.expires <= now || - (claudeCredsExpires > now && claudeCredsExpires > existingOAuth.expires); - } else { - const existingToken = existing?.type === "token" ? existing : undefined; - isEqual = shallowEqualTokenCredentials(existingToken, claudeCreds); - // Update if: no existing profile, expired, or CLI has newer token - shouldUpdate = - !existingToken || - existingToken.provider !== "anthropic" || - (existingToken.expires ?? 0) <= now || - (claudeCredsExpires > now && claudeCredsExpires > (existingToken.expires ?? 0)); - } - - // Also update if credential type changed (token -> oauth upgrade) - if (existing && existing.type !== claudeCreds.type) { - // Prefer oauth over token (enables auto-refresh) - if (claudeCreds.type === "oauth") { - shouldUpdate = true; - isEqual = false; - } - } - - // Avoid downgrading from oauth to token-only credentials. - if (existing?.type === "oauth" && claudeCreds.type === "token") { - shouldUpdate = false; - } - - if (shouldUpdate && !isEqual) { - store.profiles[CLAUDE_CLI_PROFILE_ID] = claudeCreds; - mutated = true; - log.info("synced anthropic credentials from claude cli", { - profileId: CLAUDE_CLI_PROFILE_ID, - type: claudeCreds.type, - expires: - typeof claudeCreds.expires === "number" - ? new Date(claudeCreds.expires).toISOString() - : "unknown", - }); - } - } - - // Sync from Codex CLI - const existingCodex = store.profiles[CODEX_CLI_PROFILE_ID]; - const existingCodexOAuth = existingCodex?.type === "oauth" ? existingCodex : undefined; - const duplicateExistingId = existingCodexOAuth - ? findDuplicateCodexProfile(store, existingCodexOAuth) - : undefined; - if (duplicateExistingId) { - delete store.profiles[CODEX_CLI_PROFILE_ID]; - mutated = true; - log.info("removed codex-cli profile: credentials already exist in another profile", { - existingProfileId: duplicateExistingId, - removedProfileId: CODEX_CLI_PROFILE_ID, - }); - } - const shouldSyncCodex = - !existingCodex || - existingCodex.provider !== "openai-codex" || - !isExternalProfileFresh(existingCodex, now); - const codexCreds = - shouldSyncCodex || duplicateExistingId - ? readCodexCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS }) - : null; - if (codexCreds) { - const duplicateProfileId = findDuplicateCodexProfile(store, codexCreds); - if (duplicateProfileId) { - if (store.profiles[CODEX_CLI_PROFILE_ID]) { - delete store.profiles[CODEX_CLI_PROFILE_ID]; - mutated = true; - log.info("removed codex-cli profile: credentials already exist in another profile", { - existingProfileId: duplicateProfileId, - removedProfileId: CODEX_CLI_PROFILE_ID, - }); - } - } else { - const existing = store.profiles[CODEX_CLI_PROFILE_ID]; - const existingOAuth = existing?.type === "oauth" ? existing : undefined; - - // Codex creds don't carry expiry; use file mtime heuristic for freshness. - const shouldUpdate = - !existingOAuth || - existingOAuth.provider !== "openai-codex" || - existingOAuth.expires <= now || - codexCreds.expires > existingOAuth.expires; - - if (shouldUpdate && !shallowEqualOAuthCredentials(existingOAuth, codexCreds)) { - store.profiles[CODEX_CLI_PROFILE_ID] = codexCreds; - mutated = true; - log.info("synced openai-codex credentials from codex cli", { - profileId: CODEX_CLI_PROFILE_ID, - expires: new Date(codexCreds.expires).toISOString(), - }); - } - } - } - // Sync from Qwen Code CLI const existingQwen = store.profiles[QWEN_CLI_PROFILE_ID]; const shouldSyncQwen = diff --git a/src/agents/auth-profiles/oauth.fallback-to-main-agent.test.ts b/src/agents/auth-profiles/oauth.fallback-to-main-agent.test.ts new file mode 100644 index 000000000..d37d1a8c3 --- /dev/null +++ b/src/agents/auth-profiles/oauth.fallback-to-main-agent.test.ts @@ -0,0 +1,164 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import os from "node:os"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { resolveApiKeyForProfile } from "./oauth.js"; +import { ensureAuthProfileStore } from "./store.js"; +import type { AuthProfileStore } from "./types.js"; + +describe("resolveApiKeyForProfile fallback to main agent", () => { + const previousStateDir = process.env.CLAWDBOT_STATE_DIR; + const previousAgentDir = process.env.CLAWDBOT_AGENT_DIR; + const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR; + let tmpDir: string; + let mainAgentDir: string; + let secondaryAgentDir: string; + + beforeEach(async () => { + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "oauth-fallback-test-")); + mainAgentDir = path.join(tmpDir, "agents", "main", "agent"); + secondaryAgentDir = path.join(tmpDir, "agents", "kids", "agent"); + await fs.mkdir(mainAgentDir, { recursive: true }); + await fs.mkdir(secondaryAgentDir, { recursive: true }); + + // Set environment variables so resolveClawdbotAgentDir() returns mainAgentDir + process.env.CLAWDBOT_STATE_DIR = tmpDir; + process.env.CLAWDBOT_AGENT_DIR = mainAgentDir; + process.env.PI_CODING_AGENT_DIR = mainAgentDir; + }); + + afterEach(async () => { + vi.unstubAllGlobals(); + + // Restore original environment + if (previousStateDir === undefined) delete process.env.CLAWDBOT_STATE_DIR; + else process.env.CLAWDBOT_STATE_DIR = previousStateDir; + if (previousAgentDir === undefined) delete process.env.CLAWDBOT_AGENT_DIR; + else process.env.CLAWDBOT_AGENT_DIR = previousAgentDir; + if (previousPiAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR; + else process.env.PI_CODING_AGENT_DIR = previousPiAgentDir; + + await fs.rm(tmpDir, { recursive: true, force: true }); + }); + + it("falls back to main agent credentials when secondary agent token is expired and refresh fails", async () => { + const profileId = "anthropic:claude-cli"; + const now = Date.now(); + const expiredTime = now - 60 * 60 * 1000; // 1 hour ago + const freshTime = now + 60 * 60 * 1000; // 1 hour from now + + // Write expired credentials for secondary agent + const secondaryStore: AuthProfileStore = { + version: 1, + profiles: { + [profileId]: { + type: "oauth", + provider: "anthropic", + access: "expired-access-token", + refresh: "expired-refresh-token", + expires: expiredTime, + }, + }, + }; + await fs.writeFile( + path.join(secondaryAgentDir, "auth-profiles.json"), + JSON.stringify(secondaryStore), + ); + + // Write fresh credentials for main agent + const mainStore: AuthProfileStore = { + version: 1, + profiles: { + [profileId]: { + type: "oauth", + provider: "anthropic", + access: "fresh-access-token", + refresh: "fresh-refresh-token", + expires: freshTime, + }, + }, + }; + await fs.writeFile(path.join(mainAgentDir, "auth-profiles.json"), JSON.stringify(mainStore)); + + // Mock fetch to simulate OAuth refresh failure + const fetchSpy = vi.fn(async () => { + return new Response(JSON.stringify({ error: "invalid_grant" }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + }); + vi.stubGlobal("fetch", fetchSpy); + + // Load the secondary agent's store (will merge with main agent's store) + const loadedSecondaryStore = ensureAuthProfileStore(secondaryAgentDir); + + // Call resolveApiKeyForProfile with the secondary agent's expired credentials + // This should: + // 1. Try to refresh the expired token (fails due to mocked fetch) + // 2. Fall back to main agent's fresh credentials + // 3. Copy those credentials to the secondary agent + const result = await resolveApiKeyForProfile({ + store: loadedSecondaryStore, + profileId, + agentDir: secondaryAgentDir, + }); + + expect(result).not.toBeNull(); + expect(result?.apiKey).toBe("fresh-access-token"); + expect(result?.provider).toBe("anthropic"); + + // Verify the credentials were copied to the secondary agent + const updatedSecondaryStore = JSON.parse( + await fs.readFile(path.join(secondaryAgentDir, "auth-profiles.json"), "utf8"), + ) as AuthProfileStore; + expect(updatedSecondaryStore.profiles[profileId]).toMatchObject({ + access: "fresh-access-token", + expires: freshTime, + }); + }); + + it("throws error when both secondary and main agent credentials are expired", async () => { + const profileId = "anthropic:claude-cli"; + const now = Date.now(); + const expiredTime = now - 60 * 60 * 1000; // 1 hour ago + + // Write expired credentials for both agents + const expiredStore: AuthProfileStore = { + version: 1, + profiles: { + [profileId]: { + type: "oauth", + provider: "anthropic", + access: "expired-access-token", + refresh: "expired-refresh-token", + expires: expiredTime, + }, + }, + }; + await fs.writeFile( + path.join(secondaryAgentDir, "auth-profiles.json"), + JSON.stringify(expiredStore), + ); + await fs.writeFile(path.join(mainAgentDir, "auth-profiles.json"), JSON.stringify(expiredStore)); + + // Mock fetch to simulate OAuth refresh failure + const fetchSpy = vi.fn(async () => { + return new Response(JSON.stringify({ error: "invalid_grant" }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + }); + vi.stubGlobal("fetch", fetchSpy); + + const loadedSecondaryStore = ensureAuthProfileStore(secondaryAgentDir); + + // Should throw because both agents have expired credentials + await expect( + resolveApiKeyForProfile({ + store: loadedSecondaryStore, + profileId, + agentDir: secondaryAgentDir, + }), + ).rejects.toThrow(/OAuth token refresh failed/); + }); +}); diff --git a/src/agents/auth-profiles/oauth.ts b/src/agents/auth-profiles/oauth.ts index 8c59a3044..d7b3360de 100644 --- a/src/agents/auth-profiles/oauth.ts +++ b/src/agents/auth-profiles/oauth.ts @@ -4,8 +4,7 @@ import lockfile from "proper-lockfile"; import type { ClawdbotConfig } from "../../config/config.js"; import { refreshChutesTokens } from "../chutes-oauth.js"; import { refreshQwenPortalCredentials } from "../../providers/qwen-portal-oauth.js"; -import { writeClaudeCliCredentials } from "../cli-credentials.js"; -import { AUTH_STORE_LOCK_OPTIONS, CLAUDE_CLI_PROFILE_ID } from "./constants.js"; +import { AUTH_STORE_LOCK_OPTIONS, log } from "./constants.js"; import { formatAuthDoctorHint } from "./doctor.js"; import { ensureAuthStoreFile, resolveAuthStorePath } from "./paths.js"; import { suggestOAuthProfileIdForLegacyDefault } from "./repair.js"; @@ -72,12 +71,6 @@ async function refreshOAuthTokenWithLock(params: { }; saveAuthProfileStore(store, params.agentDir); - // Sync refreshed credentials back to Claude Code CLI if this is the claude-cli profile - // This ensures Claude Code continues to work after ClawdBot refreshes the token - if (params.profileId === CLAUDE_CLI_PROFILE_ID && cred.provider === "anthropic") { - writeClaudeCliCredentials(result.newCredentials); - } - return result; } finally { if (release) { @@ -203,6 +196,32 @@ export async function resolveApiKeyForProfile(params: { // keep original error } } + + // Fallback: if this is a secondary agent, try using the main agent's credentials + if (params.agentDir) { + try { + const mainStore = ensureAuthProfileStore(undefined); // main agent (no agentDir) + const mainCred = mainStore.profiles[profileId]; + if (mainCred?.type === "oauth" && Date.now() < mainCred.expires) { + // Main agent has fresh credentials - copy them to this agent and use them + refreshedStore.profiles[profileId] = { ...mainCred }; + saveAuthProfileStore(refreshedStore, params.agentDir); + log.info("inherited fresh OAuth credentials from main agent", { + profileId, + agentDir: params.agentDir, + expires: new Date(mainCred.expires).toISOString(), + }); + return { + apiKey: buildOAuthApiKey(mainCred.provider, mainCred), + provider: mainCred.provider, + email: mainCred.email, + }; + } + } catch { + // keep original error if main agent fallback also fails + } + } + const message = error instanceof Error ? error.message : String(error); const hint = formatAuthDoctorHint({ cfg, diff --git a/src/agents/auth-profiles/store.ts b/src/agents/auth-profiles/store.ts index 010f0e9b7..ae4a999b9 100644 --- a/src/agents/auth-profiles/store.ts +++ b/src/agents/auth-profiles/store.ts @@ -3,13 +3,8 @@ import type { OAuthCredentials } from "@mariozechner/pi-ai"; import lockfile from "proper-lockfile"; import { resolveOAuthPath } from "../../config/paths.js"; import { loadJsonFile, saveJsonFile } from "../../infra/json-file.js"; -import { - AUTH_STORE_LOCK_OPTIONS, - AUTH_STORE_VERSION, - CODEX_CLI_PROFILE_ID, - log, -} from "./constants.js"; -import { findDuplicateCodexProfile, syncExternalCliCredentials } from "./external-cli-sync.js"; +import { AUTH_STORE_LOCK_OPTIONS, AUTH_STORE_VERSION, log } from "./constants.js"; +import { syncExternalCliCredentials } from "./external-cli-sync.js"; import { ensureAuthStoreFile, resolveAuthStorePath, resolveLegacyAuthStorePath } from "./paths.js"; import type { AuthProfileCredential, AuthProfileStore, ProfileUsageStats } from "./types.js"; @@ -229,14 +224,14 @@ export function loadAuthProfileStore(): AuthProfileStore { function loadAuthProfileStoreForAgent( agentDir?: string, - options?: { allowKeychainPrompt?: boolean }, + _options?: { allowKeychainPrompt?: boolean }, ): AuthProfileStore { const authPath = resolveAuthStorePath(agentDir); const raw = loadJsonFile(authPath); const asStore = coerceAuthStore(raw); if (asStore) { // Sync from external CLI tools on every load - const synced = syncExternalCliCredentials(asStore, options); + const synced = syncExternalCliCredentials(asStore); if (synced) { saveJsonFile(authPath, asStore); } @@ -297,7 +292,7 @@ function loadAuthProfileStoreForAgent( } const mergedOAuth = mergeOAuthFileIntoStore(store); - const syncedCli = syncExternalCliCredentials(store, options); + const syncedCli = syncExternalCliCredentials(store); const shouldWrite = legacy !== null || mergedOAuth || syncedCli; if (shouldWrite) { saveJsonFile(authPath, store); @@ -337,15 +332,6 @@ export function ensureAuthProfileStore( const mainStore = loadAuthProfileStoreForAgent(undefined, options); const merged = mergeAuthProfileStores(mainStore, store); - // Keep per-agent view clean even if the main store has codex-cli. - const codexProfile = merged.profiles[CODEX_CLI_PROFILE_ID]; - if (codexProfile?.type === "oauth") { - const duplicateId = findDuplicateCodexProfile(merged, codexProfile); - if (duplicateId) { - delete merged.profiles[CODEX_CLI_PROFILE_ID]; - } - } - return merged; } diff --git a/src/agents/bash-tools.shared.ts b/src/agents/bash-tools.shared.ts index bd09a18ff..aa4e5d000 100644 --- a/src/agents/bash-tools.shared.ts +++ b/src/agents/bash-tools.shared.ts @@ -60,11 +60,18 @@ export function buildDockerExecArgs(params: { for (const [key, value] of Object.entries(params.env)) { args.push("-e", `${key}=${value}`); } + const hasCustomPath = typeof params.env.PATH === "string" && params.env.PATH.length > 0; + if (hasCustomPath) { + // Avoid interpolating PATH into the shell command; pass it via env instead. + args.push("-e", `CLAWDBOT_PREPEND_PATH=${params.env.PATH}`); + } // Login shell (-l) sources /etc/profile which resets PATH to a minimal set, // overriding both Docker ENV and -e PATH=... environment variables. // Prepend custom PATH after profile sourcing to ensure custom tools are accessible // while preserving system paths that /etc/profile may have added. - const pathExport = params.env.PATH ? `export PATH="${params.env.PATH}:$PATH"; ` : ""; + const pathExport = hasCustomPath + ? 'export PATH="${CLAWDBOT_PREPEND_PATH}:$PATH"; unset CLAWDBOT_PREPEND_PATH; ' + : ""; args.push(params.containerName, "sh", "-lc", `${pathExport}${params.command}`); return args; } diff --git a/src/agents/bash-tools.test.ts b/src/agents/bash-tools.test.ts index 500b95f30..e5a008ca2 100644 --- a/src/agents/bash-tools.test.ts +++ b/src/agents/bash-tools.test.ts @@ -318,9 +318,30 @@ describe("buildDockerExecArgs", () => { }); const commandArg = args[args.length - 1]; - expect(commandArg).toContain('export PATH="/custom/bin:/usr/local/bin:/usr/bin:$PATH"'); + expect(args).toContain("CLAWDBOT_PREPEND_PATH=/custom/bin:/usr/local/bin:/usr/bin"); + expect(commandArg).toContain('export PATH="${CLAWDBOT_PREPEND_PATH}:$PATH"'); expect(commandArg).toContain("echo hello"); - expect(commandArg).toBe('export PATH="/custom/bin:/usr/local/bin:/usr/bin:$PATH"; echo hello'); + expect(commandArg).toBe( + 'export PATH="${CLAWDBOT_PREPEND_PATH}:$PATH"; unset CLAWDBOT_PREPEND_PATH; echo hello', + ); + }); + + it("does not interpolate PATH into the shell command", () => { + const injectedPath = "$(touch /tmp/clawdbot-path-injection)"; + const args = buildDockerExecArgs({ + containerName: "test-container", + command: "echo hello", + env: { + PATH: injectedPath, + HOME: "/home/user", + }, + tty: false, + }); + + const commandArg = args[args.length - 1]; + expect(args).toContain(`CLAWDBOT_PREPEND_PATH=${injectedPath}`); + expect(commandArg).not.toContain(injectedPath); + expect(commandArg).toContain("CLAWDBOT_PREPEND_PATH"); }); it("does not add PATH export when PATH is not in env", () => { diff --git a/src/agents/clawdbot-gateway-tool.test.ts b/src/agents/clawdbot-gateway-tool.test.ts index b377a53ac..76d8ff8e4 100644 --- a/src/agents/clawdbot-gateway-tool.test.ts +++ b/src/agents/clawdbot-gateway-tool.test.ts @@ -20,8 +20,10 @@ describe("gateway tool", () => { vi.useFakeTimers(); const kill = vi.spyOn(process, "kill").mockImplementation(() => true); const previousStateDir = process.env.CLAWDBOT_STATE_DIR; + const previousProfile = process.env.CLAWDBOT_PROFILE; const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-test-")); process.env.CLAWDBOT_STATE_DIR = stateDir; + process.env.CLAWDBOT_PROFILE = "isolated"; try { const tool = createClawdbotTools({ @@ -47,7 +49,9 @@ describe("gateway tool", () => { payload?: { kind?: string; doctorHint?: string | null }; }; expect(parsed.payload?.kind).toBe("restart"); - expect(parsed.payload?.doctorHint).toBe("Run: clawdbot doctor --non-interactive"); + expect(parsed.payload?.doctorHint).toBe( + "Run: clawdbot --profile isolated doctor --non-interactive", + ); expect(kill).not.toHaveBeenCalled(); await vi.runAllTimersAsync(); @@ -60,6 +64,11 @@ describe("gateway tool", () => { } else { process.env.CLAWDBOT_STATE_DIR = previousStateDir; } + if (previousProfile === undefined) { + delete process.env.CLAWDBOT_PROFILE; + } else { + process.env.CLAWDBOT_PROFILE = previousProfile; + } } }); diff --git a/src/agents/clawdbot-tools.ts b/src/agents/clawdbot-tools.ts index b420cad6f..995925cfe 100644 --- a/src/agents/clawdbot-tools.ts +++ b/src/agents/clawdbot-tools.ts @@ -20,11 +20,8 @@ import { createWebFetchTool, createWebSearchTool } from "./tools/web-tools.js"; import { createTtsTool } from "./tools/tts-tool.js"; export function createClawdbotTools(options?: { - browserControlUrl?: string; + sandboxBrowserBridgeUrl?: string; allowHostBrowserControl?: boolean; - allowedControlUrls?: string[]; - allowedControlHosts?: string[]; - allowedControlPorts?: number[]; agentSessionKey?: string; agentChannel?: GatewayMessageChannel; agentAccountId?: string; @@ -75,11 +72,8 @@ export function createClawdbotTools(options?: { }); const tools: AnyAgentTool[] = [ createBrowserTool({ - defaultControlUrl: options?.browserControlUrl, + sandboxBridgeUrl: options?.sandboxBrowserBridgeUrl, allowHostControl: options?.allowHostBrowserControl, - allowedControlUrls: options?.allowedControlUrls, - allowedControlHosts: options?.allowedControlHosts, - allowedControlPorts: options?.allowedControlPorts, }), createCanvasTool(), createNodesTool({ diff --git a/src/agents/compaction.test.ts b/src/agents/compaction.test.ts index 1cfacda9a..32511a586 100644 --- a/src/agents/compaction.test.ts +++ b/src/agents/compaction.test.ts @@ -103,5 +103,47 @@ describe("pruneHistoryForContextShare", () => { expect(pruned.droppedChunks).toBe(0); expect(pruned.messages.length).toBe(messages.length); expect(pruned.keptTokens).toBe(estimateMessagesTokens(messages)); + expect(pruned.droppedMessagesList).toEqual([]); + }); + + it("returns droppedMessagesList containing dropped messages", () => { + const messages: AgentMessage[] = [ + makeMessage(1, 4000), + makeMessage(2, 4000), + makeMessage(3, 4000), + makeMessage(4, 4000), + ]; + const maxContextTokens = 2000; // budget is 1000 tokens (50%) + const pruned = pruneHistoryForContextShare({ + messages, + maxContextTokens, + maxHistoryShare: 0.5, + parts: 2, + }); + + expect(pruned.droppedChunks).toBeGreaterThan(0); + expect(pruned.droppedMessagesList.length).toBe(pruned.droppedMessages); + + // All messages accounted for: kept + dropped = original + const allIds = [ + ...pruned.droppedMessagesList.map((m) => m.timestamp), + ...pruned.messages.map((m) => m.timestamp), + ].sort((a, b) => a - b); + const originalIds = messages.map((m) => m.timestamp).sort((a, b) => a - b); + expect(allIds).toEqual(originalIds); + }); + + it("returns empty droppedMessagesList when no pruning needed", () => { + const messages: AgentMessage[] = [makeMessage(1, 100)]; + const pruned = pruneHistoryForContextShare({ + messages, + maxContextTokens: 100_000, + maxHistoryShare: 0.5, + parts: 2, + }); + + expect(pruned.droppedChunks).toBe(0); + expect(pruned.droppedMessagesList).toEqual([]); + expect(pruned.messages.length).toBe(1); }); }); diff --git a/src/agents/compaction.ts b/src/agents/compaction.ts index 2ab4566fd..a88447307 100644 --- a/src/agents/compaction.ts +++ b/src/agents/compaction.ts @@ -301,6 +301,7 @@ export function pruneHistoryForContextShare(params: { parts?: number; }): { messages: AgentMessage[]; + droppedMessagesList: AgentMessage[]; droppedChunks: number; droppedMessages: number; droppedTokens: number; @@ -310,6 +311,7 @@ export function pruneHistoryForContextShare(params: { const maxHistoryShare = params.maxHistoryShare ?? 0.5; const budgetTokens = Math.max(1, Math.floor(params.maxContextTokens * maxHistoryShare)); let keptMessages = params.messages; + const allDroppedMessages: AgentMessage[] = []; let droppedChunks = 0; let droppedMessages = 0; let droppedTokens = 0; @@ -323,11 +325,13 @@ export function pruneHistoryForContextShare(params: { droppedChunks += 1; droppedMessages += dropped.length; droppedTokens += estimateMessagesTokens(dropped); + allDroppedMessages.push(...dropped); keptMessages = rest.flat(); } return { messages: keptMessages, + droppedMessagesList: allDroppedMessages, droppedChunks, droppedMessages, droppedTokens, diff --git a/src/agents/memory-search.ts b/src/agents/memory-search.ts index c89bad422..9eb35f3ee 100644 --- a/src/agents/memory-search.ts +++ b/src/agents/memory-search.ts @@ -119,9 +119,16 @@ function mergeConfig( const provider = overrides?.provider ?? defaults?.provider ?? "auto"; const defaultRemote = defaults?.remote; const overrideRemote = overrides?.remote; - const hasRemote = Boolean(defaultRemote || overrideRemote); + const hasRemoteConfig = Boolean( + overrideRemote?.baseUrl || + overrideRemote?.apiKey || + overrideRemote?.headers || + defaultRemote?.baseUrl || + defaultRemote?.apiKey || + defaultRemote?.headers, + ); const includeRemote = - hasRemote || provider === "openai" || provider === "gemini" || provider === "auto"; + hasRemoteConfig || provider === "openai" || provider === "gemini" || provider === "auto"; const batch = { enabled: overrideRemote?.batch?.enabled ?? defaultRemote?.batch?.enabled ?? true, wait: overrideRemote?.batch?.wait ?? defaultRemote?.batch?.wait ?? true, diff --git a/src/agents/model-fallback.test.ts b/src/agents/model-fallback.test.ts index c3febd289..73008202d 100644 --- a/src/agents/model-fallback.test.ts +++ b/src/agents/model-fallback.test.ts @@ -1,6 +1,13 @@ +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; import { describe, expect, it, vi } from "vitest"; import type { ClawdbotConfig } from "../config/config.js"; +import type { AuthProfileStore } from "./auth-profiles.js"; +import { saveAuthProfileStore } from "./auth-profiles.js"; +import { AUTH_STORE_VERSION } from "./auth-profiles/constants.js"; import { runWithModelFallback } from "./model-fallback.js"; function makeCfg(overrides: Partial = {}): ClawdbotConfig { @@ -101,7 +108,7 @@ describe("runWithModelFallback", () => { const cfg = makeCfg(); const run = vi .fn() - .mockRejectedValueOnce(new Error('No credentials found for profile "anthropic:claude-cli".')) + .mockRejectedValueOnce(new Error('No credentials found for profile "anthropic:default".')) .mockResolvedValueOnce("ok"); const result = await runWithModelFallback({ @@ -117,6 +124,122 @@ describe("runWithModelFallback", () => { expect(run.mock.calls[1]?.[1]).toBe("claude-haiku-3-5"); }); + it("skips providers when all profiles are in cooldown", async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-auth-")); + const provider = `cooldown-test-${crypto.randomUUID()}`; + const profileId = `${provider}:default`; + + const store: AuthProfileStore = { + version: AUTH_STORE_VERSION, + profiles: { + [profileId]: { + type: "api_key", + provider, + key: "test-key", + }, + }, + usageStats: { + [profileId]: { + cooldownUntil: Date.now() + 60_000, + }, + }, + }; + + saveAuthProfileStore(store, tempDir); + + const cfg = makeCfg({ + agents: { + defaults: { + model: { + primary: `${provider}/m1`, + fallbacks: ["fallback/ok-model"], + }, + }, + }, + }); + const run = vi.fn().mockImplementation(async (providerId, modelId) => { + if (providerId === "fallback") return "ok"; + throw new Error(`unexpected provider: ${providerId}/${modelId}`); + }); + + try { + const result = await runWithModelFallback({ + cfg, + provider, + model: "m1", + agentDir: tempDir, + run, + }); + + expect(result.result).toBe("ok"); + expect(run.mock.calls).toEqual([["fallback", "ok-model"]]); + expect(result.attempts[0]?.reason).toBe("rate_limit"); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } + }); + + it("does not skip when any profile is available", async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-auth-")); + const provider = `cooldown-mixed-${crypto.randomUUID()}`; + const profileA = `${provider}:a`; + const profileB = `${provider}:b`; + + const store: AuthProfileStore = { + version: AUTH_STORE_VERSION, + profiles: { + [profileA]: { + type: "api_key", + provider, + key: "key-a", + }, + [profileB]: { + type: "api_key", + provider, + key: "key-b", + }, + }, + usageStats: { + [profileA]: { + cooldownUntil: Date.now() + 60_000, + }, + }, + }; + + saveAuthProfileStore(store, tempDir); + + const cfg = makeCfg({ + agents: { + defaults: { + model: { + primary: `${provider}/m1`, + fallbacks: ["fallback/ok-model"], + }, + }, + }, + }); + const run = vi.fn().mockImplementation(async (providerId) => { + if (providerId === provider) return "ok"; + return "unexpected"; + }); + + try { + const result = await runWithModelFallback({ + cfg, + provider, + model: "m1", + agentDir: tempDir, + run, + }); + + expect(result.result).toBe("ok"); + expect(run.mock.calls).toEqual([[provider, "m1"]]); + expect(result.attempts).toEqual([]); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } + }); + it("does not append configured primary when fallbacksOverride is set", async () => { const cfg = makeCfg({ agents: { diff --git a/src/agents/model-fallback.ts b/src/agents/model-fallback.ts index ea338ae60..074f7df59 100644 --- a/src/agents/model-fallback.ts +++ b/src/agents/model-fallback.ts @@ -14,6 +14,11 @@ import { resolveModelRefFromString, } from "./model-selection.js"; import type { FailoverReason } from "./pi-embedded-helpers.js"; +import { + ensureAuthProfileStore, + isProfileInCooldown, + resolveAuthProfileOrder, +} from "./auth-profiles.js"; type ModelCandidate = { provider: string; @@ -189,6 +194,7 @@ export async function runWithModelFallback(params: { cfg: ClawdbotConfig | undefined; provider: string; model: string; + agentDir?: string; /** Optional explicit fallbacks list; when provided (even empty), replaces agents.defaults.model.fallbacks. */ fallbacksOverride?: string[]; run: (provider: string, model: string) => Promise; @@ -211,11 +217,33 @@ export async function runWithModelFallback(params: { model: params.model, fallbacksOverride: params.fallbacksOverride, }); + const authStore = params.cfg + ? ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false }) + : null; const attempts: FallbackAttempt[] = []; let lastError: unknown; for (let i = 0; i < candidates.length; i += 1) { const candidate = candidates[i] as ModelCandidate; + if (authStore) { + const profileIds = resolveAuthProfileOrder({ + cfg: params.cfg, + store: authStore, + provider: candidate.provider, + }); + const isAnyProfileAvailable = profileIds.some((id) => !isProfileInCooldown(authStore, id)); + + if (profileIds.length > 0 && !isAnyProfileAvailable) { + // All profiles for this provider are in cooldown; skip without attempting + attempts.push({ + provider: candidate.provider, + model: candidate.model, + error: `Provider ${candidate.provider} is in cooldown (all profiles unavailable)`, + reason: "rate_limit", + }); + continue; + } + } try { const result = await params.run(candidate.provider, candidate.model); return { diff --git a/src/agents/pi-embedded-helpers.isautherrormessage.test.ts b/src/agents/pi-embedded-helpers.isautherrormessage.test.ts index 160054b11..2c8fd65d0 100644 --- a/src/agents/pi-embedded-helpers.isautherrormessage.test.ts +++ b/src/agents/pi-embedded-helpers.isautherrormessage.test.ts @@ -12,7 +12,7 @@ const _makeFile = (overrides: Partial): WorkspaceBootstr describe("isAuthErrorMessage", () => { it("matches credential validation errors", () => { const samples = [ - 'No credentials found for profile "anthropic:claude-cli".', + 'No credentials found for profile "anthropic:default".', "No API key found for profile openai.", ]; for (const sample of samples) { diff --git a/src/agents/pi-embedded-runner.buildembeddedsandboxinfo.test.ts b/src/agents/pi-embedded-runner.buildembeddedsandboxinfo.test.ts index 6602a8c20..325fef691 100644 --- a/src/agents/pi-embedded-runner.buildembeddedsandboxinfo.test.ts +++ b/src/agents/pi-embedded-runner.buildembeddedsandboxinfo.test.ts @@ -127,7 +127,7 @@ describe("buildEmbeddedSandboxInfo", () => { }, browserAllowHostControl: true, browser: { - controlUrl: "http://localhost:9222", + bridgeUrl: "http://localhost:9222", noVncUrl: "http://localhost:6080", containerName: "clawdbot-sbx-browser-test", }, @@ -138,7 +138,7 @@ describe("buildEmbeddedSandboxInfo", () => { workspaceDir: "/tmp/clawdbot-sandbox", workspaceAccess: "none", agentWorkspaceMount: undefined, - browserControlUrl: "http://localhost:9222", + browserBridgeUrl: "http://localhost:9222", browserNoVncUrl: "http://localhost:6080", hostBrowserAllowed: true, }); diff --git a/src/agents/pi-embedded-runner.test.ts b/src/agents/pi-embedded-runner.test.ts index 03192338b..ec01fdf63 100644 --- a/src/agents/pi-embedded-runner.test.ts +++ b/src/agents/pi-embedded-runner.test.ts @@ -3,6 +3,7 @@ import os from "node:os"; import path from "node:path"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import "./test-helpers/fast-coding-tools.js"; import type { ClawdbotConfig } from "../config/config.js"; import { ensureClawdbotModelsJson } from "./models-config.js"; diff --git a/src/agents/pi-embedded-runner/extensions.ts b/src/agents/pi-embedded-runner/extensions.ts index 73deae21d..bb592e930 100644 --- a/src/agents/pi-embedded-runner/extensions.ts +++ b/src/agents/pi-embedded-runner/extensions.ts @@ -7,6 +7,7 @@ import type { SessionManager } from "@mariozechner/pi-coding-agent"; import type { ClawdbotConfig } from "../../config/config.js"; import { resolveContextWindowInfo } from "../context-window-guard.js"; import { DEFAULT_CONTEXT_TOKENS } from "../defaults.js"; +import { setCompactionSafeguardRuntime } from "../pi-extensions/compaction-safeguard-runtime.js"; import { setContextPruningRuntime } from "../pi-extensions/context-pruning/runtime.js"; import { computeEffectiveSettings } from "../pi-extensions/context-pruning/settings.js"; import { makeToolPrunablePredicate } from "../pi-extensions/context-pruning/tools.js"; @@ -75,6 +76,10 @@ export function buildEmbeddedExtensionPaths(params: { }): string[] { const paths: string[] = []; if (resolveCompactionMode(params.cfg) === "safeguard") { + const compactionCfg = params.cfg?.agents?.defaults?.compaction; + setCompactionSafeguardRuntime(params.sessionManager, { + maxHistoryShare: compactionCfg?.maxHistoryShare, + }); paths.push(resolvePiExtensionPath("compaction-safeguard")); } const pruning = buildContextPruningExtension(params); diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index f1c487470..7b5193054 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -215,6 +215,10 @@ export async function runEmbeddedAttempt( groupChannel: params.groupChannel, groupSpace: params.groupSpace, spawnedBy: params.spawnedBy, + senderId: params.senderId, + senderName: params.senderName, + senderUsername: params.senderUsername, + senderE164: params.senderE164, sessionKey: params.sessionKey ?? params.sessionId, agentDir, workspaceDir: effectiveWorkspace, diff --git a/src/agents/pi-embedded-runner/run/params.ts b/src/agents/pi-embedded-runner/run/params.ts index b3b35cbdc..7ca51ab5f 100644 --- a/src/agents/pi-embedded-runner/run/params.ts +++ b/src/agents/pi-embedded-runner/run/params.ts @@ -35,6 +35,10 @@ export type RunEmbeddedPiAgentParams = { groupSpace?: string | null; /** Parent session key for subagent policy inheritance. */ spawnedBy?: string | null; + senderId?: string | null; + senderName?: string | null; + senderUsername?: string | null; + senderE164?: string | null; /** Current channel ID for auto-threading (Slack). */ currentChannelId?: string; /** Current thread timestamp for auto-threading (Slack). */ diff --git a/src/agents/pi-embedded-runner/run/types.ts b/src/agents/pi-embedded-runner/run/types.ts index 90bdfb721..0ce68d02a 100644 --- a/src/agents/pi-embedded-runner/run/types.ts +++ b/src/agents/pi-embedded-runner/run/types.ts @@ -31,6 +31,10 @@ export type EmbeddedRunAttemptParams = { groupSpace?: string | null; /** Parent session key for subagent policy inheritance. */ spawnedBy?: string | null; + senderId?: string | null; + senderName?: string | null; + senderUsername?: string | null; + senderE164?: string | null; currentChannelId?: string; currentThreadTs?: string; replyToMode?: "off" | "first" | "all"; diff --git a/src/agents/pi-embedded-runner/sandbox-info.ts b/src/agents/pi-embedded-runner/sandbox-info.ts index 1ffc76f0c..a72797c9c 100644 --- a/src/agents/pi-embedded-runner/sandbox-info.ts +++ b/src/agents/pi-embedded-runner/sandbox-info.ts @@ -13,12 +13,9 @@ export function buildEmbeddedSandboxInfo( workspaceDir: sandbox.workspaceDir, workspaceAccess: sandbox.workspaceAccess, agentWorkspaceMount: sandbox.workspaceAccess === "ro" ? "/agent" : undefined, - browserControlUrl: sandbox.browser?.controlUrl, + browserBridgeUrl: sandbox.browser?.bridgeUrl, browserNoVncUrl: sandbox.browser?.noVncUrl, hostBrowserAllowed: sandbox.browserAllowHostControl, - allowedControlUrls: sandbox.browserAllowedControlUrls, - allowedControlHosts: sandbox.browserAllowedControlHosts, - allowedControlPorts: sandbox.browserAllowedControlPorts, ...(elevatedAllowed ? { elevated: { diff --git a/src/agents/pi-embedded-runner/types.ts b/src/agents/pi-embedded-runner/types.ts index 6a1ee1128..4be395bce 100644 --- a/src/agents/pi-embedded-runner/types.ts +++ b/src/agents/pi-embedded-runner/types.ts @@ -69,12 +69,9 @@ export type EmbeddedSandboxInfo = { workspaceDir?: string; workspaceAccess?: "none" | "ro" | "rw"; agentWorkspaceMount?: string; - browserControlUrl?: string; + browserBridgeUrl?: string; browserNoVncUrl?: string; hostBrowserAllowed?: boolean; - allowedControlUrls?: string[]; - allowedControlHosts?: string[]; - allowedControlPorts?: number[]; elevated?: { allowed: boolean; defaultLevel: "on" | "off" | "ask" | "full"; diff --git a/src/agents/pi-embedded-utils.test.ts b/src/agents/pi-embedded-utils.test.ts index c765a4d3a..cca7f8cb4 100644 --- a/src/agents/pi-embedded-utils.test.ts +++ b/src/agents/pi-embedded-utils.test.ts @@ -1,6 +1,6 @@ import type { AssistantMessage } from "@mariozechner/pi-ai"; import { describe, expect, it } from "vitest"; -import { extractAssistantText } from "./pi-embedded-utils.js"; +import { extractAssistantText, formatReasoningMessage } from "./pi-embedded-utils.js"; describe("extractAssistantText", () => { it("strips Minimax tool invocation XML from text", () => { @@ -508,3 +508,41 @@ File contents here`, expect(result).toBe("StartMiddleEnd"); }); }); + +describe("formatReasoningMessage", () => { + it("returns empty string for empty input", () => { + expect(formatReasoningMessage("")).toBe(""); + }); + + it("returns empty string for whitespace-only input", () => { + expect(formatReasoningMessage(" \n \t ")).toBe(""); + }); + + it("wraps single line in italics", () => { + expect(formatReasoningMessage("Single line of reasoning")).toBe( + "Reasoning:\n_Single line of reasoning_", + ); + }); + + it("wraps each line separately for multiline text (Telegram fix)", () => { + expect(formatReasoningMessage("Line one\nLine two\nLine three")).toBe( + "Reasoning:\n_Line one_\n_Line two_\n_Line three_", + ); + }); + + it("preserves empty lines between reasoning text", () => { + expect(formatReasoningMessage("First block\n\nSecond block")).toBe( + "Reasoning:\n_First block_\n\n_Second block_", + ); + }); + + it("handles mixed empty and non-empty lines", () => { + expect(formatReasoningMessage("A\n\nB\nC")).toBe("Reasoning:\n_A_\n\n_B_\n_C_"); + }); + + it("trims leading/trailing whitespace", () => { + expect(formatReasoningMessage(" \n Reasoning here \n ")).toBe( + "Reasoning:\n_Reasoning here_", + ); + }); +}); diff --git a/src/agents/pi-embedded-utils.ts b/src/agents/pi-embedded-utils.ts index 89a9df805..969b0a316 100644 --- a/src/agents/pi-embedded-utils.ts +++ b/src/agents/pi-embedded-utils.ts @@ -211,7 +211,13 @@ export function formatReasoningMessage(text: string): string { if (!trimmed) return ""; // Show reasoning in italics (cursive) for markdown-friendly surfaces (Discord, etc.). // Keep the plain "Reasoning:" prefix so existing parsing/detection keeps working. - return `Reasoning:\n_${trimmed}_`; + // Note: Underscore markdown cannot span multiple lines on Telegram, so we wrap + // each non-empty line separately. + const italicLines = trimmed + .split("\n") + .map((line) => (line ? `_${line}_` : line)) + .join("\n"); + return `Reasoning:\n${italicLines}`; } type ThinkTaggedSplitBlock = diff --git a/src/agents/pi-extensions/compaction-safeguard-runtime.ts b/src/agents/pi-extensions/compaction-safeguard-runtime.ts new file mode 100644 index 000000000..f42cf7abe --- /dev/null +++ b/src/agents/pi-extensions/compaction-safeguard-runtime.ts @@ -0,0 +1,34 @@ +export type CompactionSafeguardRuntimeValue = { + maxHistoryShare?: number; +}; + +// Session-scoped runtime registry keyed by object identity. +// Follows the same WeakMap pattern as context-pruning/runtime.ts. +const REGISTRY = new WeakMap(); + +export function setCompactionSafeguardRuntime( + sessionManager: unknown, + value: CompactionSafeguardRuntimeValue | null, +): void { + if (!sessionManager || typeof sessionManager !== "object") { + return; + } + + const key = sessionManager as object; + if (value === null) { + REGISTRY.delete(key); + return; + } + + REGISTRY.set(key, value); +} + +export function getCompactionSafeguardRuntime( + sessionManager: unknown, +): CompactionSafeguardRuntimeValue | null { + if (!sessionManager || typeof sessionManager !== "object") { + return null; + } + + return REGISTRY.get(sessionManager as object) ?? null; +} diff --git a/src/agents/pi-extensions/compaction-safeguard.test.ts b/src/agents/pi-extensions/compaction-safeguard.test.ts index 275e10e9f..23ab1efda 100644 --- a/src/agents/pi-extensions/compaction-safeguard.test.ts +++ b/src/agents/pi-extensions/compaction-safeguard.test.ts @@ -1,6 +1,10 @@ import type { AgentMessage } from "@mariozechner/pi-agent-core"; import { describe, expect, it } from "vitest"; +import { + getCompactionSafeguardRuntime, + setCompactionSafeguardRuntime, +} from "./compaction-safeguard-runtime.js"; import { __testing } from "./compaction-safeguard.js"; const { @@ -208,3 +212,41 @@ describe("isOversizedForSummary", () => { expect(typeof isOversized).toBe("boolean"); }); }); + +describe("compaction-safeguard runtime registry", () => { + it("stores and retrieves config by session manager identity", () => { + const sm = {}; + setCompactionSafeguardRuntime(sm, { maxHistoryShare: 0.3 }); + const runtime = getCompactionSafeguardRuntime(sm); + expect(runtime).toEqual({ maxHistoryShare: 0.3 }); + }); + + it("returns null for unknown session manager", () => { + const sm = {}; + expect(getCompactionSafeguardRuntime(sm)).toBeNull(); + }); + + it("clears entry when value is null", () => { + const sm = {}; + setCompactionSafeguardRuntime(sm, { maxHistoryShare: 0.7 }); + expect(getCompactionSafeguardRuntime(sm)).not.toBeNull(); + setCompactionSafeguardRuntime(sm, null); + expect(getCompactionSafeguardRuntime(sm)).toBeNull(); + }); + + it("ignores non-object session managers", () => { + setCompactionSafeguardRuntime(null, { maxHistoryShare: 0.5 }); + expect(getCompactionSafeguardRuntime(null)).toBeNull(); + setCompactionSafeguardRuntime(undefined, { maxHistoryShare: 0.5 }); + expect(getCompactionSafeguardRuntime(undefined)).toBeNull(); + }); + + it("isolates different session managers", () => { + const sm1 = {}; + const sm2 = {}; + setCompactionSafeguardRuntime(sm1, { maxHistoryShare: 0.3 }); + setCompactionSafeguardRuntime(sm2, { maxHistoryShare: 0.8 }); + expect(getCompactionSafeguardRuntime(sm1)).toEqual({ maxHistoryShare: 0.3 }); + expect(getCompactionSafeguardRuntime(sm2)).toEqual({ maxHistoryShare: 0.8 }); + }); +}); diff --git a/src/agents/pi-extensions/compaction-safeguard.ts b/src/agents/pi-extensions/compaction-safeguard.ts index 82ad19f2a..b2fe39884 100644 --- a/src/agents/pi-extensions/compaction-safeguard.ts +++ b/src/agents/pi-extensions/compaction-safeguard.ts @@ -11,6 +11,7 @@ import { resolveContextWindowTokens, summarizeInStages, } from "../compaction.js"; +import { getCompactionSafeguardRuntime } from "./compaction-safeguard-runtime.js"; const FALLBACK_SUMMARY = "Summary unavailable due to context limits. Older messages were truncated."; const TURN_PREFIX_INSTRUCTIONS = @@ -174,21 +175,28 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void { const turnPrefixMessages = preparation.turnPrefixMessages ?? []; let messagesToSummarize = preparation.messagesToSummarize; + const runtime = getCompactionSafeguardRuntime(ctx.sessionManager); + const maxHistoryShare = runtime?.maxHistoryShare ?? 0.5; + const tokensBefore = typeof preparation.tokensBefore === "number" && Number.isFinite(preparation.tokensBefore) ? preparation.tokensBefore : undefined; + + let droppedSummary: string | undefined; + if (tokensBefore !== undefined) { const summarizableTokens = estimateMessagesTokens(messagesToSummarize) + estimateMessagesTokens(turnPrefixMessages); const newContentTokens = Math.max(0, Math.floor(tokensBefore - summarizableTokens)); - const maxHistoryTokens = Math.floor(contextWindowTokens * 0.5); + // Apply SAFETY_MARGIN so token underestimates don't trigger unnecessary pruning + const maxHistoryTokens = Math.floor(contextWindowTokens * maxHistoryShare * SAFETY_MARGIN); if (newContentTokens > maxHistoryTokens) { const pruned = pruneHistoryForContextShare({ messages: messagesToSummarize, maxContextTokens: contextWindowTokens, - maxHistoryShare: 0.5, + maxHistoryShare, parts: 2, }); if (pruned.droppedChunks > 0) { @@ -200,6 +208,37 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void { `(${pruned.droppedMessages} messages) to fit history budget.`, ); messagesToSummarize = pruned.messages; + + // Summarize dropped messages so context isn't lost + if (pruned.droppedMessagesList.length > 0) { + try { + const droppedChunkRatio = computeAdaptiveChunkRatio( + pruned.droppedMessagesList, + contextWindowTokens, + ); + const droppedMaxChunkTokens = Math.max( + 1, + Math.floor(contextWindowTokens * droppedChunkRatio), + ); + droppedSummary = await summarizeInStages({ + messages: pruned.droppedMessagesList, + model, + apiKey, + signal, + reserveTokens: Math.max(1, Math.floor(preparation.settings.reserveTokens)), + maxChunkTokens: droppedMaxChunkTokens, + contextWindow: contextWindowTokens, + customInstructions, + previousSummary: preparation.previousSummary, + }); + } catch (droppedError) { + console.warn( + `Compaction safeguard: failed to summarize dropped messages, continuing without: ${ + droppedError instanceof Error ? droppedError.message : String(droppedError) + }`, + ); + } + } } } } @@ -210,6 +249,10 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void { const maxChunkTokens = Math.max(1, Math.floor(contextWindowTokens * adaptiveRatio)); const reserveTokens = Math.max(1, Math.floor(preparation.settings.reserveTokens)); + // Feed dropped-messages summary as previousSummary so the main summarization + // incorporates context from pruned messages instead of losing it entirely. + const effectivePreviousSummary = droppedSummary ?? preparation.previousSummary; + const historySummary = await summarizeInStages({ messages: messagesToSummarize, model, @@ -219,7 +262,7 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void { maxChunkTokens, contextWindow: contextWindowTokens, customInstructions, - previousSummary: preparation.previousSummary, + previousSummary: effectivePreviousSummary, }); let summary = historySummary; diff --git a/src/agents/pi-tools-agent-config.test.ts b/src/agents/pi-tools-agent-config.test.ts index bec7680a5..804786150 100644 --- a/src/agents/pi-tools-agent-config.test.ts +++ b/src/agents/pi-tools-agent-config.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; +import "./test-helpers/fast-coding-tools.js"; import type { ClawdbotConfig } from "../config/config.js"; import { createClawdbotCodingTools } from "./pi-tools.js"; import type { SandboxDockerConfig } from "./sandbox.js"; @@ -270,6 +271,75 @@ describe("Agent-specific tool filtering", () => { expect(defaultNames).not.toContain("exec"); }); + it("should apply per-sender tool policies for group tools", () => { + const cfg: ClawdbotConfig = { + channels: { + whatsapp: { + groups: { + "*": { + tools: { allow: ["read"] }, + toolsBySender: { + alice: { allow: ["read", "exec"] }, + }, + }, + }, + }, + }, + }; + + const aliceTools = createClawdbotCodingTools({ + config: cfg, + sessionKey: "agent:main:whatsapp:group:family", + senderId: "alice", + workspaceDir: "/tmp/test-group-sender", + agentDir: "/tmp/agent-group-sender", + }); + const aliceNames = aliceTools.map((t) => t.name); + expect(aliceNames).toContain("read"); + expect(aliceNames).toContain("exec"); + + const bobTools = createClawdbotCodingTools({ + config: cfg, + sessionKey: "agent:main:whatsapp:group:family", + senderId: "bob", + workspaceDir: "/tmp/test-group-sender-bob", + agentDir: "/tmp/agent-group-sender", + }); + const bobNames = bobTools.map((t) => t.name); + expect(bobNames).toContain("read"); + expect(bobNames).not.toContain("exec"); + }); + + it("should not let default sender policy override group tools", () => { + const cfg: ClawdbotConfig = { + channels: { + whatsapp: { + groups: { + "*": { + toolsBySender: { + admin: { allow: ["read", "exec"] }, + }, + }, + locked: { + tools: { allow: ["read"] }, + }, + }, + }, + }, + }; + + const adminTools = createClawdbotCodingTools({ + config: cfg, + sessionKey: "agent:main:whatsapp:group:locked", + senderId: "admin", + workspaceDir: "/tmp/test-group-default-override", + agentDir: "/tmp/agent-group-default-override", + }); + const adminNames = adminTools.map((t) => t.name); + expect(adminNames).toContain("read"); + expect(adminNames).not.toContain("exec"); + }); + it("should resolve telegram group tool policy for topic session keys", () => { const cfg: ClawdbotConfig = { channels: { diff --git a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping.test.ts b/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping.test.ts index 221222338..329f58c72 100644 --- a/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping.test.ts +++ b/src/agents/pi-tools.create-clawdbot-coding-tools.adds-claude-style-aliases-schemas-without-dropping.test.ts @@ -96,7 +96,6 @@ describe("createClawdbotCodingTools", () => { }; expect(parameters.properties?.action).toBeDefined(); expect(parameters.properties?.target).toBeDefined(); - expect(parameters.properties?.controlUrl).toBeDefined(); expect(parameters.properties?.targetUrl).toBeDefined(); expect(parameters.properties?.request).toBeDefined(); expect(parameters.required ?? []).toContain("action"); diff --git a/src/agents/pi-tools.policy.ts b/src/agents/pi-tools.policy.ts index 98585ca9d..4445775d3 100644 --- a/src/agents/pi-tools.policy.ts +++ b/src/agents/pi-tools.policy.ts @@ -96,13 +96,28 @@ export function filterToolsByPolicy(tools: AnyAgentTool[], policy?: SandboxToolP type ToolPolicyConfig = { allow?: string[]; + alsoAllow?: string[]; deny?: string[]; profile?: string; }; +function unionAllow(base?: string[], extra?: string[]) { + if (!Array.isArray(extra) || extra.length === 0) return base; + // If the user is using alsoAllow without an allowlist, treat it as additive on top of + // an implicit allow-all policy. + if (!Array.isArray(base) || base.length === 0) { + return Array.from(new Set(["*", ...extra])); + } + return Array.from(new Set([...base, ...extra])); +} + function pickToolPolicy(config?: ToolPolicyConfig): SandboxToolPolicy | undefined { if (!config) return undefined; - const allow = Array.isArray(config.allow) ? config.allow : undefined; + const allow = Array.isArray(config.allow) + ? unionAllow(config.allow, config.alsoAllow) + : Array.isArray(config.alsoAllow) && config.alsoAllow.length > 0 + ? unionAllow(undefined, config.alsoAllow) + : undefined; const deny = Array.isArray(config.deny) ? config.deny : undefined; if (!allow && !deny) return undefined; return { allow, deny }; @@ -195,6 +210,17 @@ export function resolveEffectiveToolPolicy(params: { agentProviderPolicy: pickToolPolicy(agentProviderPolicy), profile, providerProfile: agentProviderPolicy?.profile ?? providerPolicy?.profile, + // alsoAllow is applied at the profile stage (to avoid being filtered out early). + profileAlsoAllow: Array.isArray(agentTools?.alsoAllow) + ? agentTools?.alsoAllow + : Array.isArray(globalTools?.alsoAllow) + ? globalTools?.alsoAllow + : undefined, + providerProfileAlsoAllow: Array.isArray(agentProviderPolicy?.alsoAllow) + ? agentProviderPolicy?.alsoAllow + : Array.isArray(providerPolicy?.alsoAllow) + ? providerPolicy?.alsoAllow + : undefined, }; } @@ -207,6 +233,10 @@ export function resolveGroupToolPolicy(params: { groupChannel?: string | null; groupSpace?: string | null; accountId?: string | null; + senderId?: string | null; + senderName?: string | null; + senderUsername?: string | null; + senderE164?: string | null; }): SandboxToolPolicy | undefined { if (!params.config) return undefined; const sessionContext = resolveGroupContextFromSessionKey(params.sessionKey); @@ -229,12 +259,20 @@ export function resolveGroupToolPolicy(params: { groupChannel: params.groupChannel, groupSpace: params.groupSpace, accountId: params.accountId, + senderId: params.senderId, + senderName: params.senderName, + senderUsername: params.senderUsername, + senderE164: params.senderE164, }) ?? resolveChannelGroupToolsPolicy({ cfg: params.config, channel, groupId, accountId: params.accountId, + senderId: params.senderId, + senderName: params.senderName, + senderUsername: params.senderUsername, + senderE164: params.senderE164, }); return pickToolPolicy(toolsConfig); } diff --git a/src/agents/pi-tools.safe-bins.test.ts b/src/agents/pi-tools.safe-bins.test.ts new file mode 100644 index 000000000..43202bbb5 --- /dev/null +++ b/src/agents/pi-tools.safe-bins.test.ts @@ -0,0 +1,78 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { describe, expect, it, vi } from "vitest"; +import type { ClawdbotConfig } from "../config/config.js"; +import type { ExecApprovalsResolved } from "../infra/exec-approvals.js"; +import { createClawdbotCodingTools } from "./pi-tools.js"; + +vi.mock("../infra/exec-approvals.js", async (importOriginal) => { + const mod = await importOriginal(); + const approvals: ExecApprovalsResolved = { + path: "/tmp/exec-approvals.json", + socketPath: "/tmp/exec-approvals.sock", + token: "token", + defaults: { + security: "allowlist", + ask: "off", + askFallback: "deny", + autoAllowSkills: false, + }, + agent: { + security: "allowlist", + ask: "off", + askFallback: "deny", + autoAllowSkills: false, + }, + allowlist: [], + file: { + version: 1, + socket: { path: "/tmp/exec-approvals.sock", token: "token" }, + defaults: { + security: "allowlist", + ask: "off", + askFallback: "deny", + autoAllowSkills: false, + }, + agents: {}, + }, + }; + return { ...mod, resolveExecApprovals: () => approvals }; +}); + +describe("createClawdbotCodingTools safeBins", () => { + it("threads tools.exec.safeBins into exec allowlist checks", async () => { + if (process.platform === "win32") return; + + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-safe-bins-")); + const cfg: ClawdbotConfig = { + tools: { + exec: { + host: "gateway", + security: "allowlist", + ask: "off", + safeBins: ["echo"], + }, + }, + }; + + const tools = createClawdbotCodingTools({ + config: cfg, + sessionKey: "agent:main:main", + workspaceDir: tmpDir, + agentDir: path.join(tmpDir, "agent"), + }); + const execTool = tools.find((tool) => tool.name === "exec"); + expect(execTool).toBeDefined(); + + const marker = `safe-bins-${Date.now()}`; + const result = await execTool!.execute("call1", { + command: `echo ${marker}`, + workdir: tmpDir, + }); + const text = result.content.find((content) => content.type === "text")?.text ?? ""; + + expect(result.details.status).toBe("completed"); + expect(text).toContain(marker); + }); +}); diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index bd745da03..3d2f46ff1 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -86,6 +86,7 @@ function resolveExecConfig(cfg: ClawdbotConfig | undefined) { ask: globalExec?.ask, node: globalExec?.node, pathPrepend: globalExec?.pathPrepend, + safeBins: globalExec?.safeBins, backgroundMs: globalExec?.backgroundMs, timeoutSec: globalExec?.timeoutSec, approvalRunningNoticeMs: globalExec?.approvalRunningNoticeMs, @@ -139,6 +140,10 @@ export function createClawdbotCodingTools(options?: { groupSpace?: string | null; /** Parent session key for subagent group policy inheritance. */ spawnedBy?: string | null; + senderId?: string | null; + senderName?: string | null; + senderUsername?: string | null; + senderE164?: string | null; /** Reply-to mode for Slack auto-threading. */ replyToMode?: "off" | "first" | "all"; /** Mutable ref to track if a reply was sent (for "first" mode). */ @@ -156,6 +161,8 @@ export function createClawdbotCodingTools(options?: { agentProviderPolicy, profile, providerProfile, + profileAlsoAllow, + providerProfileAlsoAllow, } = resolveEffectiveToolPolicy({ config: options?.config, sessionKey: options?.sessionKey, @@ -171,17 +178,32 @@ export function createClawdbotCodingTools(options?: { groupChannel: options?.groupChannel, groupSpace: options?.groupSpace, accountId: options?.agentAccountId, + senderId: options?.senderId, + senderName: options?.senderName, + senderUsername: options?.senderUsername, + senderE164: options?.senderE164, }); const profilePolicy = resolveToolProfilePolicy(profile); const providerProfilePolicy = resolveToolProfilePolicy(providerProfile); + + const mergeAlsoAllow = (policy: typeof profilePolicy, alsoAllow?: string[]) => { + if (!policy?.allow || !Array.isArray(alsoAllow) || alsoAllow.length === 0) return policy; + return { ...policy, allow: Array.from(new Set([...policy.allow, ...alsoAllow])) }; + }; + + const profilePolicyWithAlsoAllow = mergeAlsoAllow(profilePolicy, profileAlsoAllow); + const providerProfilePolicyWithAlsoAllow = mergeAlsoAllow( + providerProfilePolicy, + providerProfileAlsoAllow, + ); const scopeKey = options?.exec?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined); const subagentPolicy = isSubagentSessionKey(options?.sessionKey) && options?.sessionKey ? resolveSubagentToolPolicy(options.config) : undefined; const allowBackground = isToolAllowedByPolicies("process", [ - profilePolicy, - providerProfilePolicy, + profilePolicyWithAlsoAllow, + providerProfilePolicyWithAlsoAllow, globalPolicy, globalProviderPolicy, agentPolicy, @@ -235,6 +257,7 @@ export function createClawdbotCodingTools(options?: { ask: options?.exec?.ask ?? execConfig.ask, node: options?.exec?.node ?? execConfig.node, pathPrepend: options?.exec?.pathPrepend ?? execConfig.pathPrepend, + safeBins: options?.exec?.safeBins ?? execConfig.safeBins, agentId, cwd: options?.workspaceDir, allowBackground, @@ -279,11 +302,8 @@ export function createClawdbotCodingTools(options?: { // Channel docking: include channel-defined agent tools (login, etc.). ...listChannelAgentTools({ cfg: options?.config }), ...createClawdbotTools({ - browserControlUrl: sandbox?.browser?.controlUrl, + sandboxBrowserBridgeUrl: sandbox?.browser?.bridgeUrl, allowHostBrowserControl: sandbox ? sandbox.browserAllowHostControl : true, - allowedControlUrls: sandbox?.browserAllowedControlUrls, - allowedControlHosts: sandbox?.browserAllowedControlHosts, - allowedControlPorts: sandbox?.browserAllowedControlPorts, agentSessionKey: options?.sessionKey, agentChannel: resolveGatewayMessageChannel(options?.messageProvider), agentAccountId: options?.agentAccountId, @@ -331,18 +351,18 @@ export function createClawdbotCodingTools(options?: { if (resolved.unknownAllowlist.length > 0) { const entries = resolved.unknownAllowlist.join(", "); const suffix = resolved.strippedAllowlist - ? "Ignoring allowlist so core tools remain available." + ? "Ignoring allowlist so core tools remain available. Use tools.alsoAllow for additive plugin tool enablement." : "These entries won't match any tool unless the plugin is enabled."; logWarn(`tools: ${label} allowlist contains unknown entries (${entries}). ${suffix}`); } return expandPolicyWithPluginGroups(resolved.policy, pluginGroups); }; const profilePolicyExpanded = resolvePolicy( - profilePolicy, + profilePolicyWithAlsoAllow, profile ? `tools.profile (${profile})` : "tools.profile", ); const providerProfileExpanded = resolvePolicy( - providerProfilePolicy, + providerProfilePolicyWithAlsoAllow, providerProfile ? `tools.byProvider.profile (${providerProfile})` : "tools.byProvider.profile", ); const globalPolicyExpanded = resolvePolicy(globalPolicy, "tools.allow"); diff --git a/src/agents/sandbox/browser.ts b/src/agents/sandbox/browser.ts index dcfde8975..4951d9ce5 100644 --- a/src/agents/sandbox/browser.ts +++ b/src/agents/sandbox/browser.ts @@ -1,6 +1,9 @@ import { startBrowserBridgeServer, stopBrowserBridgeServer } from "../../browser/bridge-server.js"; import { type ResolvedBrowserConfig, resolveProfile } from "../../browser/config.js"; -import { DEFAULT_CLAWD_BROWSER_COLOR } from "../../browser/constants.js"; +import { + DEFAULT_BROWSER_EVALUATE_ENABLED, + DEFAULT_CLAWD_BROWSER_COLOR, +} from "../../browser/constants.js"; import { BROWSER_BRIDGES } from "./browser-bridges.js"; import { DEFAULT_SANDBOX_BROWSER_IMAGE, SANDBOX_AGENT_WORKSPACE_MOUNT } from "./constants.js"; import { @@ -39,14 +42,12 @@ function buildSandboxBrowserResolvedConfig(params: { controlPort: number; cdpPort: number; headless: boolean; + evaluateEnabled: boolean; }): ResolvedBrowserConfig { - const controlHost = "127.0.0.1"; - const controlUrl = `http://${controlHost}:${params.controlPort}`; const cdpHost = "127.0.0.1"; return { enabled: true, - controlUrl, - controlHost, + evaluateEnabled: params.evaluateEnabled, controlPort: params.controlPort, cdpProtocol: "http", cdpHost, @@ -80,6 +81,7 @@ export async function ensureSandboxBrowser(params: { workspaceDir: string; agentWorkspaceDir: string; cfg: SandboxConfig; + evaluateEnabled?: boolean; }): Promise { if (!params.cfg.browser.enabled) return null; if (!isToolAllowed(params.cfg.tools, "browser")) return null; @@ -174,6 +176,7 @@ export async function ensureSandboxBrowser(params: { controlPort: 0, cdpPort: mappedCdp, headless: params.cfg.browser.headless, + evaluateEnabled: params.evaluateEnabled ?? DEFAULT_BROWSER_EVALUATE_ENABLED, }), onEnsureAttachTarget, }); @@ -204,7 +207,7 @@ export async function ensureSandboxBrowser(params: { : undefined; return { - controlUrl: resolvedBridge.baseUrl, + bridgeUrl: resolvedBridge.baseUrl, noVncUrl, containerName, }; diff --git a/src/agents/sandbox/config.ts b/src/agents/sandbox/config.ts index a633a0fd9..cabf907bc 100644 --- a/src/agents/sandbox/config.ts +++ b/src/agents/sandbox/config.ts @@ -86,11 +86,6 @@ export function resolveSandboxBrowserConfig(params: { }): SandboxBrowserConfig { const agentBrowser = params.scope === "shared" ? undefined : params.agentBrowser; const globalBrowser = params.globalBrowser; - const allowedControlUrls = agentBrowser?.allowedControlUrls ?? globalBrowser?.allowedControlUrls; - const allowedControlHosts = - agentBrowser?.allowedControlHosts ?? globalBrowser?.allowedControlHosts; - const allowedControlPorts = - agentBrowser?.allowedControlPorts ?? globalBrowser?.allowedControlPorts; return { enabled: agentBrowser?.enabled ?? globalBrowser?.enabled ?? false, image: agentBrowser?.image ?? globalBrowser?.image ?? DEFAULT_SANDBOX_BROWSER_IMAGE, @@ -105,18 +100,6 @@ export function resolveSandboxBrowserConfig(params: { headless: agentBrowser?.headless ?? globalBrowser?.headless ?? false, enableNoVnc: agentBrowser?.enableNoVnc ?? globalBrowser?.enableNoVnc ?? true, allowHostControl: agentBrowser?.allowHostControl ?? globalBrowser?.allowHostControl ?? false, - allowedControlUrls: - Array.isArray(allowedControlUrls) && allowedControlUrls.length > 0 - ? allowedControlUrls - : undefined, - allowedControlHosts: - Array.isArray(allowedControlHosts) && allowedControlHosts.length > 0 - ? allowedControlHosts - : undefined, - allowedControlPorts: - Array.isArray(allowedControlPorts) && allowedControlPorts.length > 0 - ? allowedControlPorts - : undefined, autoStart: agentBrowser?.autoStart ?? globalBrowser?.autoStart ?? true, autoStartTimeoutMs: agentBrowser?.autoStartTimeoutMs ?? diff --git a/src/agents/sandbox/context.ts b/src/agents/sandbox/context.ts index ff6372e69..c983ca6df 100644 --- a/src/agents/sandbox/context.ts +++ b/src/agents/sandbox/context.ts @@ -3,6 +3,7 @@ import fs from "node:fs/promises"; import type { ClawdbotConfig } from "../../config/config.js"; import { defaultRuntime } from "../../runtime.js"; import { resolveUserPath } from "../../utils.js"; +import { DEFAULT_BROWSER_EVALUATE_ENABLED } from "../../browser/constants.js"; import { syncSkillsToWorkspace } from "../skills.js"; import { DEFAULT_AGENT_WORKSPACE_DIR } from "../workspace.js"; import { ensureSandboxBrowser } from "./browser.js"; @@ -69,11 +70,14 @@ export async function resolveSandboxContext(params: { cfg, }); + const evaluateEnabled = + params.config?.browser?.evaluateEnabled ?? DEFAULT_BROWSER_EVALUATE_ENABLED; const browser = await ensureSandboxBrowser({ scopeKey, workspaceDir, agentWorkspaceDir, cfg, + evaluateEnabled, }); return { @@ -87,9 +91,6 @@ export async function resolveSandboxContext(params: { docker: cfg.docker, tools: cfg.tools, browserAllowHostControl: cfg.browser.allowHostControl, - browserAllowedControlUrls: cfg.browser.allowedControlUrls, - browserAllowedControlHosts: cfg.browser.allowedControlHosts, - browserAllowedControlPorts: cfg.browser.allowedControlPorts, browser: browser ?? undefined, }; } diff --git a/src/agents/sandbox/types.ts b/src/agents/sandbox/types.ts index 03b7713ce..f27dfd715 100644 --- a/src/agents/sandbox/types.ts +++ b/src/agents/sandbox/types.ts @@ -37,9 +37,6 @@ export type SandboxBrowserConfig = { headless: boolean; enableNoVnc: boolean; allowHostControl: boolean; - allowedControlUrls?: string[]; - allowedControlHosts?: string[]; - allowedControlPorts?: number[]; autoStart: boolean; autoStartTimeoutMs: number; }; @@ -63,7 +60,7 @@ export type SandboxConfig = { }; export type SandboxBrowserContext = { - controlUrl: string; + bridgeUrl: string; noVncUrl?: string; containerName: string; }; @@ -79,9 +76,6 @@ export type SandboxContext = { docker: SandboxDockerConfig; tools: SandboxToolPolicy; browserAllowHostControl: boolean; - browserAllowedControlUrls?: string[]; - browserAllowedControlHosts?: string[]; - browserAllowedControlPorts?: number[]; browser?: SandboxBrowserContext; }; diff --git a/src/agents/session-write-lock.test.ts b/src/agents/session-write-lock.test.ts index 8f93bface..524945e19 100644 --- a/src/agents/session-write-lock.test.ts +++ b/src/agents/session-write-lock.test.ts @@ -3,7 +3,7 @@ import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; -import { acquireSessionWriteLock } from "./session-write-lock.js"; +import { __testing, acquireSessionWriteLock } from "./session-write-lock.js"; describe("acquireSessionWriteLock", () => { it("reuses locks across symlinked session paths", async () => { @@ -31,4 +31,132 @@ describe("acquireSessionWriteLock", () => { await fs.rm(root, { recursive: true, force: true }); } }); + + it("keeps the lock file until the last release", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-lock-")); + try { + const sessionFile = path.join(root, "sessions.json"); + const lockPath = `${sessionFile}.lock`; + + const lockA = await acquireSessionWriteLock({ sessionFile, timeoutMs: 500 }); + const lockB = await acquireSessionWriteLock({ sessionFile, timeoutMs: 500 }); + + await expect(fs.access(lockPath)).resolves.toBeUndefined(); + await lockA.release(); + await expect(fs.access(lockPath)).resolves.toBeUndefined(); + await lockB.release(); + await expect(fs.access(lockPath)).rejects.toThrow(); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } + }); + + it("reclaims stale lock files", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-lock-")); + try { + const sessionFile = path.join(root, "sessions.json"); + const lockPath = `${sessionFile}.lock`; + await fs.writeFile( + lockPath, + JSON.stringify({ pid: 123456, createdAt: new Date(Date.now() - 60_000).toISOString() }), + "utf8", + ); + + const lock = await acquireSessionWriteLock({ sessionFile, timeoutMs: 500, staleMs: 10 }); + const raw = await fs.readFile(lockPath, "utf8"); + const payload = JSON.parse(raw) as { pid: number }; + + expect(payload.pid).toBe(process.pid); + await lock.release(); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } + }); + + it("removes held locks on termination signals", async () => { + const signals = ["SIGINT", "SIGTERM", "SIGQUIT", "SIGABRT"] as const; + for (const signal of signals) { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-lock-cleanup-")); + try { + const sessionFile = path.join(root, "sessions.json"); + const lockPath = `${sessionFile}.lock`; + await acquireSessionWriteLock({ sessionFile, timeoutMs: 500 }); + const keepAlive = () => {}; + if (signal === "SIGINT") { + process.on(signal, keepAlive); + } + + __testing.handleTerminationSignal(signal); + + await expect(fs.stat(lockPath)).rejects.toThrow(); + if (signal === "SIGINT") { + process.off(signal, keepAlive); + } + } finally { + await fs.rm(root, { recursive: true, force: true }); + } + } + }); + + it("registers cleanup for SIGQUIT and SIGABRT", () => { + expect(__testing.cleanupSignals).toContain("SIGQUIT"); + expect(__testing.cleanupSignals).toContain("SIGABRT"); + }); + it("cleans up locks on SIGINT without removing other handlers", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-lock-")); + const originalKill = process.kill.bind(process) as typeof process.kill; + const killCalls: Array = []; + let otherHandlerCalled = false; + + process.kill = ((pid: number, signal?: NodeJS.Signals) => { + killCalls.push(signal); + return true; + }) as typeof process.kill; + + const otherHandler = () => { + otherHandlerCalled = true; + }; + + process.on("SIGINT", otherHandler); + + try { + const sessionFile = path.join(root, "sessions.json"); + const lockPath = `${sessionFile}.lock`; + await acquireSessionWriteLock({ sessionFile, timeoutMs: 500 }); + + process.emit("SIGINT"); + + await expect(fs.access(lockPath)).rejects.toThrow(); + expect(otherHandlerCalled).toBe(true); + expect(killCalls).toEqual([]); + } finally { + process.off("SIGINT", otherHandler); + process.kill = originalKill; + await fs.rm(root, { recursive: true, force: true }); + } + }); + + it("cleans up locks on exit", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-lock-")); + try { + const sessionFile = path.join(root, "sessions.json"); + const lockPath = `${sessionFile}.lock`; + await acquireSessionWriteLock({ sessionFile, timeoutMs: 500 }); + + process.emit("exit", 0); + + await expect(fs.access(lockPath)).rejects.toThrow(); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } + }); + it("keeps other signal listeners registered", () => { + const keepAlive = () => {}; + process.on("SIGINT", keepAlive); + + __testing.handleTerminationSignal("SIGINT"); + + expect(process.listeners("SIGINT")).toContain(keepAlive); + process.off("SIGINT", keepAlive); + }); }); diff --git a/src/agents/session-write-lock.ts b/src/agents/session-write-lock.ts index 54e61d965..832d368a6 100644 --- a/src/agents/session-write-lock.ts +++ b/src/agents/session-write-lock.ts @@ -1,3 +1,4 @@ +import fsSync from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; @@ -13,6 +14,9 @@ type HeldLock = { }; const HELD_LOCKS = new Map(); +const CLEANUP_SIGNALS = ["SIGINT", "SIGTERM", "SIGQUIT", "SIGABRT"] as const; +type CleanupSignal = (typeof CLEANUP_SIGNALS)[number]; +const cleanupHandlers = new Map void>(); function isAlive(pid: number): boolean { if (!Number.isFinite(pid) || pid <= 0) return false; @@ -24,6 +28,65 @@ function isAlive(pid: number): boolean { } } +/** + * Synchronously release all held locks. + * Used during process exit when async operations aren't reliable. + */ +function releaseAllLocksSync(): void { + for (const [sessionFile, held] of HELD_LOCKS) { + try { + if (typeof held.handle.fd === "number") { + fsSync.closeSync(held.handle.fd); + } + } catch { + // Ignore errors during cleanup - best effort + } + try { + fsSync.rmSync(held.lockPath, { force: true }); + } catch { + // Ignore errors during cleanup - best effort + } + HELD_LOCKS.delete(sessionFile); + } +} + +let cleanupRegistered = false; + +function handleTerminationSignal(signal: CleanupSignal): void { + releaseAllLocksSync(); + const shouldReraise = process.listenerCount(signal) === 1; + if (shouldReraise) { + const handler = cleanupHandlers.get(signal); + if (handler) process.off(signal, handler); + try { + process.kill(process.pid, signal); + } catch { + // Ignore errors during shutdown + } + } +} + +function registerCleanupHandlers(): void { + if (cleanupRegistered) return; + cleanupRegistered = true; + + // Cleanup on normal exit and process.exit() calls + process.on("exit", () => { + releaseAllLocksSync(); + }); + + // Handle termination signals + for (const signal of CLEANUP_SIGNALS) { + try { + const handler = () => handleTerminationSignal(signal); + cleanupHandlers.set(signal, handler); + process.on(signal, handler); + } catch { + // Ignore unsupported signals on this platform. + } + } +} + async function readLockPayload(lockPath: string): Promise { try { const raw = await fs.readFile(lockPath, "utf8"); @@ -43,6 +106,7 @@ export async function acquireSessionWriteLock(params: { }): Promise<{ release: () => Promise; }> { + registerCleanupHandlers(); const timeoutMs = params.timeoutMs ?? 10_000; const staleMs = params.staleMs ?? 30 * 60 * 1000; const sessionFile = path.resolve(params.sessionFile); @@ -116,3 +180,9 @@ export async function acquireSessionWriteLock(params: { const owner = payload?.pid ? `pid=${payload.pid}` : "unknown"; throw new Error(`session file locked (timeout ${timeoutMs}ms): ${owner} ${lockPath}`); } + +export const __testing = { + cleanupSignals: [...CLEANUP_SIGNALS], + handleTerminationSignal, + releaseAllLocksSync, +}; diff --git a/src/agents/skills/config.ts b/src/agents/skills/config.ts index a0dab4125..71b3130a4 100644 --- a/src/agents/skills/config.ts +++ b/src/agents/skills/config.ts @@ -6,6 +6,7 @@ import type { SkillEligibilityContext, SkillEntry } from "./types.js"; const DEFAULT_CONFIG_VALUES: Record = { "browser.enabled": true, + "browser.evaluateEnabled": true, }; function isTruthy(value: unknown): boolean { diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index 41ec9a7d5..1d1a6a5eb 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -165,12 +165,9 @@ export function buildAgentSystemPrompt(params: { workspaceDir?: string; workspaceAccess?: "none" | "ro" | "rw"; agentWorkspaceMount?: string; - browserControlUrl?: string; + browserBridgeUrl?: string; browserNoVncUrl?: string; hostBrowserAllowed?: boolean; - allowedControlUrls?: string[]; - allowedControlHosts?: string[]; - allowedControlPorts?: number[]; elevated?: { allowed: boolean; defaultLevel: "on" | "off" | "ask" | "full"; @@ -419,9 +416,7 @@ export function buildAgentSystemPrompt(params: { : "" }` : "", - params.sandboxInfo.browserControlUrl - ? `Sandbox browser control URL: ${params.sandboxInfo.browserControlUrl}` - : "", + params.sandboxInfo.browserBridgeUrl ? "Sandbox browser: enabled." : "", params.sandboxInfo.browserNoVncUrl ? `Sandbox browser observer (noVNC): ${params.sandboxInfo.browserNoVncUrl}` : "", @@ -430,15 +425,6 @@ export function buildAgentSystemPrompt(params: { : params.sandboxInfo.hostBrowserAllowed === false ? "Host browser control: blocked." : "", - params.sandboxInfo.allowedControlUrls?.length - ? `Browser control URL allowlist: ${params.sandboxInfo.allowedControlUrls.join(", ")}` - : "", - params.sandboxInfo.allowedControlHosts?.length - ? `Browser control host allowlist: ${params.sandboxInfo.allowedControlHosts.join(", ")}` - : "", - params.sandboxInfo.allowedControlPorts?.length - ? `Browser control port allowlist: ${params.sandboxInfo.allowedControlPorts.join(", ")}` - : "", params.sandboxInfo.elevated?.allowed ? "Elevated exec is available for this session." : "", diff --git a/src/agents/tool-policy.ts b/src/agents/tool-policy.ts index ac2b1a91c..85152069e 100644 --- a/src/agents/tool-policy.ts +++ b/src/agents/tool-policy.ts @@ -209,6 +209,12 @@ export function stripPluginOnlyAllowlist( if (!isCoreEntry && !isPluginEntry) unknownAllowlist.push(entry); } const strippedAllowlist = !hasCoreEntry; + // When an allowlist contains only plugin tools, we strip it to avoid accidentally + // disabling core tools. Users who want additive behavior should prefer `tools.alsoAllow`. + if (strippedAllowlist) { + // Note: logging happens in the caller (pi-tools/tools-invoke) after this function returns. + // We keep this note here for future maintainers. + } return { policy: strippedAllowlist ? { ...policy, allow: undefined } : policy, unknownAllowlist: Array.from(new Set(unknownAllowlist)), diff --git a/src/agents/tools/browser-tool.schema.ts b/src/agents/tools/browser-tool.schema.ts index 5861f7de4..30cf3cc0f 100644 --- a/src/agents/tools/browser-tool.schema.ts +++ b/src/agents/tools/browser-tool.schema.ts @@ -35,7 +35,7 @@ const BROWSER_TOOL_ACTIONS = [ "act", ] as const; -const BROWSER_TARGETS = ["sandbox", "host", "custom", "node"] as const; +const BROWSER_TARGETS = ["sandbox", "host", "node"] as const; const BROWSER_SNAPSHOT_FORMATS = ["aria", "ai"] as const; const BROWSER_SNAPSHOT_MODES = ["efficient"] as const; @@ -86,7 +86,6 @@ export const BrowserToolSchema = Type.Object({ target: optionalStringEnum(BROWSER_TARGETS), node: Type.Optional(Type.String()), profile: Type.Optional(Type.String()), - controlUrl: Type.Optional(Type.String()), targetUrl: Type.Optional(Type.String()), targetId: Type.Optional(Type.String()), limit: Type.Optional(Type.Number()), diff --git a/src/agents/tools/browser-tool.test.ts b/src/agents/tools/browser-tool.test.ts index e50082d66..7248a7a2f 100644 --- a/src/agents/tools/browser-tool.test.ts +++ b/src/agents/tools/browser-tool.test.ts @@ -28,23 +28,7 @@ vi.mock("../../browser/client.js", () => browserClientMocks); const browserConfigMocks = vi.hoisted(() => ({ resolveBrowserConfig: vi.fn(() => ({ enabled: true, - controlUrl: "http://127.0.0.1:18791", - controlHost: "127.0.0.1", controlPort: 18791, - cdpProtocol: "http", - cdpHost: "127.0.0.1", - cdpIsLoopback: true, - color: "#FF0000", - headless: true, - noSandbox: false, - attachOnly: false, - defaultProfile: "clawd", - profiles: { - clawd: { - cdpPort: 18792, - color: "#FF0000", - }, - }, })), })); vi.mock("../../browser/config.js", () => browserConfigMocks); @@ -99,7 +83,7 @@ describe("browser tool snapshot maxChars", () => { await tool.execute?.(null, { action: "snapshot", snapshotFormat: "ai" }); expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith( - "http://127.0.0.1:18791", + undefined, expect.objectContaining({ format: "ai", maxChars: DEFAULT_AI_SNAPSHOT_MAX_CHARS, @@ -117,7 +101,7 @@ describe("browser tool snapshot maxChars", () => { }); expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith( - "http://127.0.0.1:18791", + undefined, expect.objectContaining({ maxChars: override, }), @@ -141,7 +125,7 @@ describe("browser tool snapshot maxChars", () => { const tool = createBrowserTool(); await tool.execute?.(null, { action: "profiles" }); - expect(browserClientMocks.browserProfiles).toHaveBeenCalledWith("http://127.0.0.1:18791"); + expect(browserClientMocks.browserProfiles).toHaveBeenCalledWith(undefined); }); it("passes refs mode through to browser snapshot", async () => { @@ -149,7 +133,7 @@ describe("browser tool snapshot maxChars", () => { await tool.execute?.(null, { action: "snapshot", snapshotFormat: "ai", refs: "aria" }); expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith( - "http://127.0.0.1:18791", + undefined, expect.objectContaining({ format: "ai", refs: "aria", @@ -165,7 +149,7 @@ describe("browser tool snapshot maxChars", () => { await tool.execute?.(null, { action: "snapshot", snapshotFormat: "ai" }); expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith( - "http://127.0.0.1:18791", + undefined, expect.objectContaining({ mode: "efficient", }), @@ -185,11 +169,11 @@ describe("browser tool snapshot maxChars", () => { }); it("defaults to host when using profile=chrome (even in sandboxed sessions)", async () => { - const tool = createBrowserTool({ defaultControlUrl: "http://127.0.0.1:9999" }); + const tool = createBrowserTool({ sandboxBridgeUrl: "http://127.0.0.1:9999" }); await tool.execute?.(null, { action: "snapshot", profile: "chrome", snapshotFormat: "ai" }); expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith( - "http://127.0.0.1:18791", + undefined, expect.objectContaining({ profile: "chrome", }), @@ -220,7 +204,7 @@ describe("browser tool snapshot maxChars", () => { expect(browserClientMocks.browserStatus).not.toHaveBeenCalled(); }); - it("keeps sandbox control url when node proxy is available", async () => { + it("keeps sandbox bridge url when node proxy is available", async () => { nodesUtilsMocks.listNodes.mockResolvedValue([ { nodeId: "node-1", @@ -230,7 +214,7 @@ describe("browser tool snapshot maxChars", () => { commands: ["browser.proxy"], }, ]); - const tool = createBrowserTool({ defaultControlUrl: "http://127.0.0.1:9999" }); + const tool = createBrowserTool({ sandboxBridgeUrl: "http://127.0.0.1:9999" }); await tool.execute?.(null, { action: "status" }); expect(browserClientMocks.browserStatus).toHaveBeenCalledWith( @@ -254,7 +238,7 @@ describe("browser tool snapshot maxChars", () => { await tool.execute?.(null, { action: "status", profile: "chrome" }); expect(browserClientMocks.browserStatus).toHaveBeenCalledWith( - "http://127.0.0.1:18791", + undefined, expect.objectContaining({ profile: "chrome" }), ); expect(gatewayMocks.callGatewayTool).not.toHaveBeenCalled(); diff --git a/src/agents/tools/browser-tool.ts b/src/agents/tools/browser-tool.ts index 998059b54..1f063bdea 100644 --- a/src/agents/tools/browser-tool.ts +++ b/src/agents/tools/browser-tool.ts @@ -55,9 +55,8 @@ function isBrowserNode(node: NodeListNode) { async function resolveBrowserNodeTarget(params: { requestedNode?: string; - target?: "sandbox" | "host" | "custom" | "node"; - controlUrl?: string; - defaultControlUrl?: string; + target?: "sandbox" | "host" | "node"; + sandboxBridgeUrl?: string; }): Promise { const cfg = loadConfig(); const policy = cfg.gateway?.nodes?.browser; @@ -68,10 +67,9 @@ async function resolveBrowserNodeTarget(params: { } return null; } - if (params.defaultControlUrl?.trim() && params.target !== "node" && !params.requestedNode) { + if (params.sandboxBridgeUrl?.trim() && params.target !== "node" && !params.requestedNode) { return null; } - if (params.controlUrl?.trim()) return null; if (params.target && params.target !== "node") return null; if (mode === "manual" && params.target !== "node" && !params.requestedNode) { return null; @@ -187,70 +185,22 @@ function applyProxyPaths(result: unknown, mapping: Map) { } function resolveBrowserBaseUrl(params: { - target?: "sandbox" | "host" | "custom"; - controlUrl?: string; - defaultControlUrl?: string; + target?: "sandbox" | "host"; + sandboxBridgeUrl?: string; allowHostControl?: boolean; - allowedControlUrls?: string[]; - allowedControlHosts?: string[]; - allowedControlPorts?: number[]; -}) { +}): string | undefined { const cfg = loadConfig(); - const resolved = resolveBrowserConfig(cfg.browser); - const normalizedControlUrl = params.controlUrl?.trim() ?? ""; - const normalizedDefault = params.defaultControlUrl?.trim() ?? ""; - const target = - params.target ?? (normalizedControlUrl ? "custom" : normalizedDefault ? "sandbox" : "host"); - - const assertAllowedControlUrl = (url: string) => { - const allowedUrls = params.allowedControlUrls?.map((entry) => entry.trim().replace(/\/$/, "")); - const allowedHosts = params.allowedControlHosts?.map((entry) => entry.trim().toLowerCase()); - const allowedPorts = params.allowedControlPorts; - if (!allowedUrls?.length && !allowedHosts?.length && !allowedPorts?.length) { - return; - } - let parsed: URL; - try { - parsed = new URL(url); - } catch { - throw new Error(`Invalid browser controlUrl: ${url}`); - } - const normalizedUrl = parsed.toString().replace(/\/$/, ""); - if (allowedUrls?.length && !allowedUrls.includes(normalizedUrl)) { - throw new Error("Browser controlUrl is not in the allowed URL list."); - } - if (allowedHosts?.length && !allowedHosts.includes(parsed.hostname)) { - throw new Error("Browser controlUrl hostname is not in the allowed host list."); - } - if (allowedPorts?.length) { - const port = - parsed.port?.trim() !== "" ? Number(parsed.port) : parsed.protocol === "https:" ? 443 : 80; - if (!Number.isFinite(port) || !allowedPorts.includes(port)) { - throw new Error("Browser controlUrl port is not in the allowed port list."); - } - } - }; - - if (target !== "custom" && params.target && normalizedControlUrl) { - throw new Error('controlUrl is only supported with target="custom".'); - } - - if (target === "custom") { - if (!normalizedControlUrl) { - throw new Error("Custom browser target requires controlUrl."); - } - const normalized = normalizedControlUrl.replace(/\/$/, ""); - assertAllowedControlUrl(normalized); - return normalized; - } + const resolved = resolveBrowserConfig(cfg.browser, cfg); + const normalizedSandbox = params.sandboxBridgeUrl?.trim() ?? ""; + const target = params.target ?? (normalizedSandbox ? "sandbox" : "host"); if (target === "sandbox") { - if (!normalizedDefault) { + if (!normalizedSandbox) { throw new Error( 'Sandbox browser is unavailable. Enable agents.defaults.sandbox.browser.enabled or use target="host" if allowed.', ); } - return normalizedDefault.replace(/\/$/, ""); + return normalizedSandbox.replace(/\/$/, ""); } if (params.allowHostControl === false) { @@ -261,27 +211,16 @@ function resolveBrowserBaseUrl(params: { "Browser control is disabled. Set browser.enabled=true in ~/.clawdbot/clawdbot.json.", ); } - const normalized = resolved.controlUrl.replace(/\/$/, ""); - assertAllowedControlUrl(normalized); - return normalized; + return undefined; } export function createBrowserTool(opts?: { - defaultControlUrl?: string; + sandboxBridgeUrl?: string; allowHostControl?: boolean; - allowedControlUrls?: string[]; - allowedControlHosts?: string[]; - allowedControlPorts?: number[]; }): AnyAgentTool { - const targetDefault = opts?.defaultControlUrl ? "sandbox" : "host"; + const targetDefault = opts?.sandboxBridgeUrl ? "sandbox" : "host"; const hostHint = opts?.allowHostControl === false ? "Host target blocked by policy." : "Host target allowed."; - const allowlistHint = - opts?.allowedControlUrls?.length || - opts?.allowedControlHosts?.length || - opts?.allowedControlPorts?.length - ? "Custom targets are restricted by sandbox allowlists." - : "Custom targets are unrestricted."; return { label: "Browser", name: "browser", @@ -294,33 +233,22 @@ export function createBrowserTool(opts?: { "When using refs from snapshot (e.g. e12), keep the same tab: prefer passing targetId from the snapshot response into subsequent actions (act/click/type/etc).", 'For stable, self-resolving refs across calls, use snapshot with refs="aria" (Playwright aria-ref ids). Default refs="role" are role+name-based.', "Use snapshot+act for UI automation. Avoid act:wait by default; use only in exceptional cases when no reliable UI state exists.", - `target selects browser location (sandbox|host|custom|node). Default: ${targetDefault}.`, - "controlUrl implies target=custom (remote control server).", + `target selects browser location (sandbox|host|node). Default: ${targetDefault}.`, hostHint, - allowlistHint, ].join(" "), parameters: BrowserToolSchema, execute: async (_toolCallId, args) => { const params = args as Record; const action = readStringParam(params, "action", { required: true }); - const controlUrl = readStringParam(params, "controlUrl"); const profile = readStringParam(params, "profile"); const requestedNode = readStringParam(params, "node"); - let target = readStringParam(params, "target") as - | "sandbox" - | "host" - | "custom" - | "node" - | undefined; + let target = readStringParam(params, "target") as "sandbox" | "host" | "node" | undefined; - if (controlUrl?.trim() && (target === "node" || requestedNode)) { - throw new Error('controlUrl is not supported with target="node".'); - } - if (target === "custom" && requestedNode) { - throw new Error('node is not supported with target="custom".'); + if (requestedNode && target && target !== "node") { + throw new Error('node is only supported with target="node".'); } - if (!target && !controlUrl?.trim() && !requestedNode && profile === "chrome") { + if (!target && !requestedNode && profile === "chrome") { // Chrome extension relay takeover is a host Chrome feature; prefer host unless explicitly targeting a node. target = "host"; } @@ -328,21 +256,16 @@ export function createBrowserTool(opts?: { const nodeTarget = await resolveBrowserNodeTarget({ requestedNode: requestedNode ?? undefined, target, - controlUrl, - defaultControlUrl: opts?.defaultControlUrl, + sandboxBridgeUrl: opts?.sandboxBridgeUrl, }); const resolvedTarget = target === "node" ? undefined : target; const baseUrl = nodeTarget - ? "" + ? undefined : resolveBrowserBaseUrl({ target: resolvedTarget, - controlUrl, - defaultControlUrl: opts?.defaultControlUrl, + sandboxBridgeUrl: opts?.sandboxBridgeUrl, allowHostControl: opts?.allowHostControl, - allowedControlUrls: opts?.allowedControlUrls, - allowedControlHosts: opts?.allowedControlHosts, - allowedControlPorts: opts?.allowedControlPorts, }); const proxyRequest = nodeTarget diff --git a/src/agents/tools/discord-actions-guild.ts b/src/agents/tools/discord-actions-guild.ts index 0994829bd..26e21c82e 100644 --- a/src/agents/tools/discord-actions-guild.ts +++ b/src/agents/tools/discord-actions-guild.ts @@ -1,5 +1,6 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core"; import type { DiscordActionConfig } from "../../config/config.js"; +import { getPresence } from "../../discord/monitor/presence-cache.js"; import { addRoleDiscord, createChannelDiscord, @@ -54,7 +55,10 @@ export async function handleDiscordGuildAction( const member = accountId ? await fetchMemberInfoDiscord(guildId, userId, { accountId }) : await fetchMemberInfoDiscord(guildId, userId); - return jsonResult({ ok: true, member }); + const presence = getPresence(accountId, userId); + const activities = presence?.activities ?? undefined; + const status = presence?.status ?? undefined; + return jsonResult({ ok: true, member, ...(presence ? { status, activities } : {}) }); } case "roleInfo": { if (!isActionEnabled("roleInfo")) { diff --git a/src/agents/tools/message-tool.ts b/src/agents/tools/message-tool.ts index eae4356db..73969cb54 100644 --- a/src/agents/tools/message-tool.ts +++ b/src/agents/tools/message-tool.ts @@ -59,6 +59,7 @@ function buildSendSchema(options: { includeButtons: boolean; includeCards: boole replyTo: Type.Optional(Type.String()), threadId: Type.Optional(Type.String()), asVoice: Type.Optional(Type.Boolean()), + silent: Type.Optional(Type.Boolean()), bestEffort: Type.Optional(Type.Boolean()), gifPlayback: Type.Optional(Type.Boolean()), buttons: Type.Optional( diff --git a/src/agents/tools/telegram-actions.test.ts b/src/agents/tools/telegram-actions.test.ts index 5c0629e38..db276849b 100644 --- a/src/agents/tools/telegram-actions.test.ts +++ b/src/agents/tools/telegram-actions.test.ts @@ -8,12 +8,17 @@ const sendMessageTelegram = vi.fn(async () => ({ messageId: "789", chatId: "123", })); +const sendStickerTelegram = vi.fn(async () => ({ + messageId: "456", + chatId: "123", +})); const deleteMessageTelegram = vi.fn(async () => ({ ok: true })); const originalToken = process.env.TELEGRAM_BOT_TOKEN; vi.mock("../../telegram/send.js", () => ({ reactMessageTelegram: (...args: unknown[]) => reactMessageTelegram(...args), sendMessageTelegram: (...args: unknown[]) => sendMessageTelegram(...args), + sendStickerTelegram: (...args: unknown[]) => sendStickerTelegram(...args), deleteMessageTelegram: (...args: unknown[]) => deleteMessageTelegram(...args), })); @@ -21,6 +26,7 @@ describe("handleTelegramAction", () => { beforeEach(() => { reactMessageTelegram.mockClear(); sendMessageTelegram.mockClear(); + sendStickerTelegram.mockClear(); deleteMessageTelegram.mockClear(); process.env.TELEGRAM_BOT_TOKEN = "tok"; }); @@ -96,6 +102,40 @@ describe("handleTelegramAction", () => { ); }); + it("rejects sticker actions when disabled by default", async () => { + const cfg = { channels: { telegram: { botToken: "tok" } } } as ClawdbotConfig; + await expect( + handleTelegramAction( + { + action: "sendSticker", + to: "123", + fileId: "sticker", + }, + cfg, + ), + ).rejects.toThrow(/sticker actions are disabled/i); + expect(sendStickerTelegram).not.toHaveBeenCalled(); + }); + + it("sends stickers when enabled", async () => { + const cfg = { + channels: { telegram: { botToken: "tok", actions: { sticker: true } } }, + } as ClawdbotConfig; + await handleTelegramAction( + { + action: "sendSticker", + to: "123", + fileId: "sticker", + }, + cfg, + ); + expect(sendStickerTelegram).toHaveBeenCalledWith( + "123", + "sticker", + expect.objectContaining({ token: "tok" }), + ); + }); + it("removes reactions when remove flag set", async () => { const cfg = { channels: { telegram: { botToken: "tok", reactionLevel: "extensive" } }, diff --git a/src/agents/tools/telegram-actions.ts b/src/agents/tools/telegram-actions.ts index 5385dd10f..d2a4e4b93 100644 --- a/src/agents/tools/telegram-actions.ts +++ b/src/agents/tools/telegram-actions.ts @@ -3,9 +3,12 @@ import type { ClawdbotConfig } from "../../config/config.js"; import { resolveTelegramReactionLevel } from "../../telegram/reaction-level.js"; import { deleteMessageTelegram, + editMessageTelegram, reactMessageTelegram, sendMessageTelegram, + sendStickerTelegram, } from "../../telegram/send.js"; +import { getCacheStats, searchStickers } from "../../telegram/sticker-cache.js"; import { resolveTelegramToken } from "../../telegram/token.js"; import { resolveTelegramInlineButtonsScope, @@ -176,6 +179,7 @@ export async function handleTelegramAction( replyToMessageId: replyToMessageId ?? undefined, messageThreadId: messageThreadId ?? undefined, asVoice: typeof params.asVoice === "boolean" ? params.asVoice : undefined, + silent: typeof params.silent === "boolean" ? params.silent : undefined, }); return jsonResult({ ok: true, @@ -208,5 +212,109 @@ export async function handleTelegramAction( return jsonResult({ ok: true, deleted: true }); } + if (action === "editMessage") { + if (!isActionEnabled("editMessage")) { + throw new Error("Telegram editMessage is disabled."); + } + const chatId = readStringOrNumberParam(params, "chatId", { + required: true, + }); + const messageId = readNumberParam(params, "messageId", { + required: true, + integer: true, + }); + const content = readStringParam(params, "content", { + required: true, + allowEmpty: false, + }); + const buttons = readTelegramButtons(params); + if (buttons) { + const inlineButtonsScope = resolveTelegramInlineButtonsScope({ + cfg, + accountId: accountId ?? undefined, + }); + if (inlineButtonsScope === "off") { + throw new Error( + 'Telegram inline buttons are disabled. Set channels.telegram.capabilities.inlineButtons to "dm", "group", "all", or "allowlist".', + ); + } + } + const token = resolveTelegramToken(cfg, { accountId }).token; + if (!token) { + throw new Error( + "Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or channels.telegram.botToken.", + ); + } + const result = await editMessageTelegram(chatId ?? "", messageId ?? 0, content, { + token, + accountId: accountId ?? undefined, + buttons, + }); + return jsonResult({ + ok: true, + messageId: result.messageId, + chatId: result.chatId, + }); + } + + if (action === "sendSticker") { + if (!isActionEnabled("sticker", false)) { + throw new Error( + "Telegram sticker actions are disabled. Set channels.telegram.actions.sticker to true.", + ); + } + const to = readStringParam(params, "to", { required: true }); + const fileId = readStringParam(params, "fileId", { required: true }); + const replyToMessageId = readNumberParam(params, "replyToMessageId", { + integer: true, + }); + const messageThreadId = readNumberParam(params, "messageThreadId", { + integer: true, + }); + const token = resolveTelegramToken(cfg, { accountId }).token; + if (!token) { + throw new Error( + "Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or channels.telegram.botToken.", + ); + } + const result = await sendStickerTelegram(to, fileId, { + token, + accountId: accountId ?? undefined, + replyToMessageId: replyToMessageId ?? undefined, + messageThreadId: messageThreadId ?? undefined, + }); + return jsonResult({ + ok: true, + messageId: result.messageId, + chatId: result.chatId, + }); + } + + if (action === "searchSticker") { + if (!isActionEnabled("sticker", false)) { + throw new Error( + "Telegram sticker actions are disabled. Set channels.telegram.actions.sticker to true.", + ); + } + const query = readStringParam(params, "query", { required: true }); + const limit = readNumberParam(params, "limit", { integer: true }) ?? 5; + const results = searchStickers(query, limit); + return jsonResult({ + ok: true, + count: results.length, + stickers: results.map((s) => ({ + fileId: s.fileId, + emoji: s.emoji, + description: s.description, + setName: s.setName, + })), + }); + } + + if (action === "stickerCacheStats") { + const stats = getCacheStats(); + return jsonResult({ ok: true, ...stats }); + } + throw new Error(`Unsupported Telegram action: ${action}`); } diff --git a/src/agents/tools/web-fetch.ts b/src/agents/tools/web-fetch.ts index c8bcaa609..9f1e565dd 100644 --- a/src/agents/tools/web-fetch.ts +++ b/src/agents/tools/web-fetch.ts @@ -1,7 +1,13 @@ import { Type } from "@sinclair/typebox"; import type { ClawdbotConfig } from "../../config/config.js"; -import { assertPublicHostname, SsrFBlockedError } from "../../infra/net/ssrf.js"; +import { + closeDispatcher, + createPinnedDispatcher, + resolvePinnedHostname, + SsrFBlockedError, +} from "../../infra/net/ssrf.js"; +import type { Dispatcher } from "undici"; import { stringEnum } from "../schema/typebox.js"; import type { AnyAgentTool } from "./common.js"; import { jsonResult, readNumberParam, readStringParam } from "./common.js"; @@ -167,7 +173,7 @@ async function fetchWithRedirects(params: { maxRedirects: number; timeoutSeconds: number; userAgent: string; -}): Promise<{ response: Response; finalUrl: string }> { +}): Promise<{ response: Response; finalUrl: string; dispatcher: Dispatcher }> { const signal = withTimeout(undefined, params.timeoutSeconds * 1000); const visited = new Set(); let currentUrl = params.url; @@ -184,39 +190,50 @@ async function fetchWithRedirects(params: { throw new Error("Invalid URL: must be http or https"); } - await assertPublicHostname(parsedUrl.hostname); - - const res = await fetch(parsedUrl.toString(), { - method: "GET", - headers: { - Accept: "*/*", - "User-Agent": params.userAgent, - "Accept-Language": "en-US,en;q=0.9", - }, - signal, - redirect: "manual", - }); + const pinned = await resolvePinnedHostname(parsedUrl.hostname); + const dispatcher = createPinnedDispatcher(pinned); + let res: Response; + try { + res = await fetch(parsedUrl.toString(), { + method: "GET", + headers: { + Accept: "*/*", + "User-Agent": params.userAgent, + "Accept-Language": "en-US,en;q=0.9", + }, + signal, + redirect: "manual", + dispatcher, + } as RequestInit); + } catch (err) { + await closeDispatcher(dispatcher); + throw err; + } if (isRedirectStatus(res.status)) { const location = res.headers.get("location"); if (!location) { + await closeDispatcher(dispatcher); throw new Error(`Redirect missing location header (${res.status})`); } redirectCount += 1; if (redirectCount > params.maxRedirects) { + await closeDispatcher(dispatcher); throw new Error(`Too many redirects (limit: ${params.maxRedirects})`); } const nextUrl = new URL(location, parsedUrl).toString(); if (visited.has(nextUrl)) { + await closeDispatcher(dispatcher); throw new Error("Redirect loop detected"); } visited.add(nextUrl); void res.body?.cancel(); + await closeDispatcher(dispatcher); currentUrl = nextUrl; continue; } - return { response: res, finalUrl: currentUrl }; + return { response: res, finalUrl: currentUrl, dispatcher }; } } @@ -348,6 +365,7 @@ async function runWebFetch(params: { const start = Date.now(); let res: Response; + let dispatcher: Dispatcher | null = null; let finalUrl = params.url; try { const result = await fetchWithRedirects({ @@ -358,6 +376,7 @@ async function runWebFetch(params: { }); res = result.response; finalUrl = result.finalUrl; + dispatcher = result.dispatcher; } catch (error) { if (error instanceof SsrFBlockedError) { throw error; @@ -396,108 +415,112 @@ async function runWebFetch(params: { throw error; } - if (!res.ok) { - if (params.firecrawlEnabled && params.firecrawlApiKey) { - const firecrawl = await fetchFirecrawlContent({ - url: params.url, - extractMode: params.extractMode, - apiKey: params.firecrawlApiKey, - baseUrl: params.firecrawlBaseUrl, - onlyMainContent: params.firecrawlOnlyMainContent, - maxAgeMs: params.firecrawlMaxAgeMs, - proxy: params.firecrawlProxy, - storeInCache: params.firecrawlStoreInCache, - timeoutSeconds: params.firecrawlTimeoutSeconds, - }); - const truncated = truncateText(firecrawl.text, params.maxChars); - const payload = { - url: params.url, - finalUrl: firecrawl.finalUrl || finalUrl, - status: firecrawl.status ?? res.status, - contentType: "text/markdown", - title: firecrawl.title, - extractMode: params.extractMode, - extractor: "firecrawl", - truncated: truncated.truncated, - length: truncated.text.length, - fetchedAt: new Date().toISOString(), - tookMs: Date.now() - start, - text: truncated.text, - warning: firecrawl.warning, - }; - writeCache(FETCH_CACHE, cacheKey, payload, params.cacheTtlMs); - return payload; - } - const rawDetail = await readResponseText(res); - const detail = formatWebFetchErrorDetail({ - detail: rawDetail, - contentType: res.headers.get("content-type"), - maxChars: DEFAULT_ERROR_MAX_CHARS, - }); - throw new Error(`Web fetch failed (${res.status}): ${detail || res.statusText}`); - } - - const contentType = res.headers.get("content-type") ?? "application/octet-stream"; - const body = await readResponseText(res); - - let title: string | undefined; - let extractor = "raw"; - let text = body; - if (contentType.includes("text/html")) { - if (params.readabilityEnabled) { - const readable = await extractReadableContent({ - html: body, - url: finalUrl, - extractMode: params.extractMode, - }); - if (readable?.text) { - text = readable.text; - title = readable.title; - extractor = "readability"; - } else { - const firecrawl = await tryFirecrawlFallback({ ...params, url: finalUrl }); - if (firecrawl) { - text = firecrawl.text; - title = firecrawl.title; - extractor = "firecrawl"; - } else { - throw new Error( - "Web fetch extraction failed: Readability and Firecrawl returned no content.", - ); - } + try { + if (!res.ok) { + if (params.firecrawlEnabled && params.firecrawlApiKey) { + const firecrawl = await fetchFirecrawlContent({ + url: params.url, + extractMode: params.extractMode, + apiKey: params.firecrawlApiKey, + baseUrl: params.firecrawlBaseUrl, + onlyMainContent: params.firecrawlOnlyMainContent, + maxAgeMs: params.firecrawlMaxAgeMs, + proxy: params.firecrawlProxy, + storeInCache: params.firecrawlStoreInCache, + timeoutSeconds: params.firecrawlTimeoutSeconds, + }); + const truncated = truncateText(firecrawl.text, params.maxChars); + const payload = { + url: params.url, + finalUrl: firecrawl.finalUrl || finalUrl, + status: firecrawl.status ?? res.status, + contentType: "text/markdown", + title: firecrawl.title, + extractMode: params.extractMode, + extractor: "firecrawl", + truncated: truncated.truncated, + length: truncated.text.length, + fetchedAt: new Date().toISOString(), + tookMs: Date.now() - start, + text: truncated.text, + warning: firecrawl.warning, + }; + writeCache(FETCH_CACHE, cacheKey, payload, params.cacheTtlMs); + return payload; } - } else { - throw new Error( - "Web fetch extraction failed: Readability disabled and Firecrawl unavailable.", - ); + const rawDetail = await readResponseText(res); + const detail = formatWebFetchErrorDetail({ + detail: rawDetail, + contentType: res.headers.get("content-type"), + maxChars: DEFAULT_ERROR_MAX_CHARS, + }); + throw new Error(`Web fetch failed (${res.status}): ${detail || res.statusText}`); } - } else if (contentType.includes("application/json")) { - try { - text = JSON.stringify(JSON.parse(body), null, 2); - extractor = "json"; - } catch { - text = body; - extractor = "raw"; - } - } - const truncated = truncateText(text, params.maxChars); - const payload = { - url: params.url, - finalUrl, - status: res.status, - contentType, - title, - extractMode: params.extractMode, - extractor, - truncated: truncated.truncated, - length: truncated.text.length, - fetchedAt: new Date().toISOString(), - tookMs: Date.now() - start, - text: truncated.text, - }; - writeCache(FETCH_CACHE, cacheKey, payload, params.cacheTtlMs); - return payload; + const contentType = res.headers.get("content-type") ?? "application/octet-stream"; + const body = await readResponseText(res); + + let title: string | undefined; + let extractor = "raw"; + let text = body; + if (contentType.includes("text/html")) { + if (params.readabilityEnabled) { + const readable = await extractReadableContent({ + html: body, + url: finalUrl, + extractMode: params.extractMode, + }); + if (readable?.text) { + text = readable.text; + title = readable.title; + extractor = "readability"; + } else { + const firecrawl = await tryFirecrawlFallback({ ...params, url: finalUrl }); + if (firecrawl) { + text = firecrawl.text; + title = firecrawl.title; + extractor = "firecrawl"; + } else { + throw new Error( + "Web fetch extraction failed: Readability and Firecrawl returned no content.", + ); + } + } + } else { + throw new Error( + "Web fetch extraction failed: Readability disabled and Firecrawl unavailable.", + ); + } + } else if (contentType.includes("application/json")) { + try { + text = JSON.stringify(JSON.parse(body), null, 2); + extractor = "json"; + } catch { + text = body; + extractor = "raw"; + } + } + + const truncated = truncateText(text, params.maxChars); + const payload = { + url: params.url, + finalUrl, + status: res.status, + contentType, + title, + extractMode: params.extractMode, + extractor, + truncated: truncated.truncated, + length: truncated.text.length, + fetchedAt: new Date().toISOString(), + tookMs: Date.now() - start, + text: truncated.text, + }; + writeCache(FETCH_CACHE, cacheKey, payload, params.cacheTtlMs); + return payload; + } finally { + await closeDispatcher(dispatcher); + } } async function tryFirecrawlFallback(params: { diff --git a/src/agents/workspace.test.ts b/src/agents/workspace.test.ts index 8c4f5a0de..ff589a193 100644 --- a/src/agents/workspace.test.ts +++ b/src/agents/workspace.test.ts @@ -1,152 +1,49 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; import { describe, expect, it } from "vitest"; -import { runCommandWithTimeout } from "../process/exec.js"; -import type { WorkspaceBootstrapFile } from "./workspace.js"; + import { - DEFAULT_AGENTS_FILENAME, - DEFAULT_BOOTSTRAP_FILENAME, - DEFAULT_HEARTBEAT_FILENAME, - DEFAULT_IDENTITY_FILENAME, - DEFAULT_SOUL_FILENAME, - DEFAULT_TOOLS_FILENAME, - DEFAULT_USER_FILENAME, - ensureAgentWorkspace, - filterBootstrapFilesForSession, + DEFAULT_MEMORY_ALT_FILENAME, + DEFAULT_MEMORY_FILENAME, + loadWorkspaceBootstrapFiles, } from "./workspace.js"; +import { makeTempWorkspace, writeWorkspaceFile } from "../test-helpers/workspace.js"; -describe("ensureAgentWorkspace", () => { - it("creates directory and bootstrap files when missing", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-ws-")); - const nested = path.join(dir, "nested"); - const result = await ensureAgentWorkspace({ - dir: nested, - ensureBootstrapFiles: true, - }); - expect(result.dir).toBe(path.resolve(nested)); - expect(result.agentsPath).toBe(path.join(path.resolve(nested), "AGENTS.md")); - expect(result.agentsPath).toBeDefined(); - if (!result.agentsPath) throw new Error("agentsPath missing"); - const content = await fs.readFile(result.agentsPath, "utf-8"); - expect(content).toContain("# AGENTS.md"); +describe("loadWorkspaceBootstrapFiles", () => { + it("includes MEMORY.md when present", async () => { + const tempDir = await makeTempWorkspace("clawdbot-workspace-"); + await writeWorkspaceFile({ dir: tempDir, name: "MEMORY.md", content: "memory" }); - const identity = path.join(path.resolve(nested), "IDENTITY.md"); - const user = path.join(path.resolve(nested), "USER.md"); - const heartbeat = path.join(path.resolve(nested), "HEARTBEAT.md"); - const bootstrap = path.join(path.resolve(nested), "BOOTSTRAP.md"); - await expect(fs.stat(identity)).resolves.toBeDefined(); - await expect(fs.stat(user)).resolves.toBeDefined(); - await expect(fs.stat(heartbeat)).resolves.toBeDefined(); - await expect(fs.stat(bootstrap)).resolves.toBeDefined(); + const files = await loadWorkspaceBootstrapFiles(tempDir); + const memoryEntries = files.filter((file) => + [DEFAULT_MEMORY_FILENAME, DEFAULT_MEMORY_ALT_FILENAME].includes(file.name), + ); + + expect(memoryEntries).toHaveLength(1); + expect(memoryEntries[0]?.missing).toBe(false); + expect(memoryEntries[0]?.content).toBe("memory"); }); - it("initializes a git repo for brand-new workspaces when git is available", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-ws-")); - const nested = path.join(dir, "nested"); - const gitAvailable = await runCommandWithTimeout(["git", "--version"], { timeoutMs: 2_000 }) - .then((res) => res.code === 0) - .catch(() => false); - if (!gitAvailable) return; + it("includes memory.md when MEMORY.md is absent", async () => { + const tempDir = await makeTempWorkspace("clawdbot-workspace-"); + await writeWorkspaceFile({ dir: tempDir, name: "memory.md", content: "alt" }); - await ensureAgentWorkspace({ - dir: nested, - ensureBootstrapFiles: true, - }); + const files = await loadWorkspaceBootstrapFiles(tempDir); + const memoryEntries = files.filter((file) => + [DEFAULT_MEMORY_FILENAME, DEFAULT_MEMORY_ALT_FILENAME].includes(file.name), + ); - await expect(fs.stat(path.join(nested, ".git"))).resolves.toBeDefined(); + expect(memoryEntries).toHaveLength(1); + expect(memoryEntries[0]?.missing).toBe(false); + expect(memoryEntries[0]?.content).toBe("alt"); }); - it("does not initialize git when workspace already exists", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-ws-")); - await fs.writeFile(path.join(dir, "AGENTS.md"), "custom", "utf-8"); + it("omits memory entries when no memory files exist", async () => { + const tempDir = await makeTempWorkspace("clawdbot-workspace-"); - await ensureAgentWorkspace({ - dir, - ensureBootstrapFiles: true, - }); + const files = await loadWorkspaceBootstrapFiles(tempDir); + const memoryEntries = files.filter((file) => + [DEFAULT_MEMORY_FILENAME, DEFAULT_MEMORY_ALT_FILENAME].includes(file.name), + ); - await expect(fs.stat(path.join(dir, ".git"))).rejects.toBeDefined(); - }); - - it("does not overwrite existing AGENTS.md", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-ws-")); - const agentsPath = path.join(dir, "AGENTS.md"); - await fs.writeFile(agentsPath, "custom", "utf-8"); - await ensureAgentWorkspace({ dir, ensureBootstrapFiles: true }); - expect(await fs.readFile(agentsPath, "utf-8")).toBe("custom"); - }); - - it("does not recreate BOOTSTRAP.md once workspace exists", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-ws-")); - const agentsPath = path.join(dir, "AGENTS.md"); - const bootstrapPath = path.join(dir, "BOOTSTRAP.md"); - - await fs.writeFile(agentsPath, "custom", "utf-8"); - await fs.rm(bootstrapPath, { force: true }); - - await ensureAgentWorkspace({ dir, ensureBootstrapFiles: true }); - - await expect(fs.stat(bootstrapPath)).rejects.toBeDefined(); - }); -}); - -describe("filterBootstrapFilesForSession", () => { - const files: WorkspaceBootstrapFile[] = [ - { - name: DEFAULT_AGENTS_FILENAME, - path: "/tmp/AGENTS.md", - content: "agents", - missing: false, - }, - { - name: DEFAULT_SOUL_FILENAME, - path: "/tmp/SOUL.md", - content: "soul", - missing: false, - }, - { - name: DEFAULT_TOOLS_FILENAME, - path: "/tmp/TOOLS.md", - content: "tools", - missing: false, - }, - { - name: DEFAULT_IDENTITY_FILENAME, - path: "/tmp/IDENTITY.md", - content: "identity", - missing: false, - }, - { - name: DEFAULT_USER_FILENAME, - path: "/tmp/USER.md", - content: "user", - missing: false, - }, - { - name: DEFAULT_HEARTBEAT_FILENAME, - path: "/tmp/HEARTBEAT.md", - content: "heartbeat", - missing: false, - }, - { - name: DEFAULT_BOOTSTRAP_FILENAME, - path: "/tmp/BOOTSTRAP.md", - content: "bootstrap", - missing: false, - }, - ]; - - it("keeps full bootstrap set for non-subagent sessions", () => { - const result = filterBootstrapFilesForSession(files, "agent:main:session:abc"); - expect(result.map((file) => file.name)).toEqual(files.map((file) => file.name)); - }); - - it("limits bootstrap files for subagent sessions", () => { - const result = filterBootstrapFilesForSession(files, "agent:main:subagent:abc"); - expect(result.map((file) => file.name)).toEqual([ - DEFAULT_AGENTS_FILENAME, - DEFAULT_TOOLS_FILENAME, - ]); + expect(memoryEntries).toHaveLength(0); }); }); diff --git a/src/agents/workspace.ts b/src/agents/workspace.ts index 6732069a9..0cef8e5f0 100644 --- a/src/agents/workspace.ts +++ b/src/agents/workspace.ts @@ -26,6 +26,8 @@ export const DEFAULT_IDENTITY_FILENAME = "IDENTITY.md"; export const DEFAULT_USER_FILENAME = "USER.md"; export const DEFAULT_HEARTBEAT_FILENAME = "HEARTBEAT.md"; export const DEFAULT_BOOTSTRAP_FILENAME = "BOOTSTRAP.md"; +export const DEFAULT_MEMORY_FILENAME = "MEMORY.md"; +export const DEFAULT_MEMORY_ALT_FILENAME = "memory.md"; const TEMPLATE_DIR = path.resolve( path.dirname(fileURLToPath(import.meta.url)), @@ -61,7 +63,9 @@ export type WorkspaceBootstrapFileName = | typeof DEFAULT_IDENTITY_FILENAME | typeof DEFAULT_USER_FILENAME | typeof DEFAULT_HEARTBEAT_FILENAME - | typeof DEFAULT_BOOTSTRAP_FILENAME; + | typeof DEFAULT_BOOTSTRAP_FILENAME + | typeof DEFAULT_MEMORY_FILENAME + | typeof DEFAULT_MEMORY_ALT_FILENAME; export type WorkspaceBootstrapFile = { name: WorkspaceBootstrapFileName; @@ -184,6 +188,39 @@ export async function ensureAgentWorkspace(params?: { }; } +async function resolveMemoryBootstrapEntries( + resolvedDir: string, +): Promise> { + const candidates: WorkspaceBootstrapFileName[] = [ + DEFAULT_MEMORY_FILENAME, + DEFAULT_MEMORY_ALT_FILENAME, + ]; + const entries: Array<{ name: WorkspaceBootstrapFileName; filePath: string }> = []; + for (const name of candidates) { + const filePath = path.join(resolvedDir, name); + try { + await fs.access(filePath); + entries.push({ name, filePath }); + } catch { + // optional + } + } + if (entries.length <= 1) return entries; + + const seen = new Set(); + const deduped: Array<{ name: WorkspaceBootstrapFileName; filePath: string }> = []; + for (const entry of entries) { + let key = entry.filePath; + try { + key = await fs.realpath(entry.filePath); + } catch {} + if (seen.has(key)) continue; + seen.add(key); + deduped.push(entry); + } + return deduped; +} + export async function loadWorkspaceBootstrapFiles(dir: string): Promise { const resolvedDir = resolveUserPath(dir); @@ -221,6 +258,8 @@ export async function loadWorkspaceBootstrapFiles(dir: string): Promise { const menu = resolveCommandArgMenu({ command, args: undefined, cfg: {} as never }); expect(menu?.arg.name).toBe("mode"); - expect(menu?.choices).toEqual(["off", "tokens", "full", "cost"]); + expect(menu?.choices).toEqual([ + { label: "off", value: "off" }, + { label: "tokens", value: "tokens" }, + { label: "full", value: "full" }, + { label: "cost", value: "cost" }, + ]); }); it("does not show menus when arg already provided", () => { @@ -284,7 +289,10 @@ describe("commands registry args", () => { const menu = resolveCommandArgMenu({ command, args: undefined, cfg: {} as never }); expect(menu?.arg.name).toBe("level"); - expect(menu?.choices).toEqual(["low", "high"]); + expect(menu?.choices).toEqual([ + { label: "low", value: "low" }, + { label: "high", value: "high" }, + ]); expect(seen?.commandKey).toBe("think"); expect(seen?.argName).toBe("level"); expect(seen?.provider).toBeTruthy(); diff --git a/src/auto-reply/commands-registry.ts b/src/auto-reply/commands-registry.ts index 5bca565f0..f772ac7fc 100644 --- a/src/auto-reply/commands-registry.ts +++ b/src/auto-reply/commands-registry.ts @@ -255,33 +255,41 @@ function resolveDefaultCommandContext(cfg?: ClawdbotConfig): { }; } +export type ResolvedCommandArgChoice = { value: string; label: string }; + export function resolveCommandArgChoices(params: { command: ChatCommandDefinition; arg: CommandArgDefinition; cfg?: ClawdbotConfig; provider?: string; model?: string; -}): string[] { +}): ResolvedCommandArgChoice[] { const { command, arg, cfg } = params; if (!arg.choices) return []; const provided = arg.choices; - if (Array.isArray(provided)) return provided; - const defaults = resolveDefaultCommandContext(cfg); - const context: CommandArgChoiceContext = { - cfg, - provider: params.provider ?? defaults.provider, - model: params.model ?? defaults.model, - command, - arg, - }; - return provided(context); + const raw = Array.isArray(provided) + ? provided + : (() => { + const defaults = resolveDefaultCommandContext(cfg); + const context: CommandArgChoiceContext = { + cfg, + provider: params.provider ?? defaults.provider, + model: params.model ?? defaults.model, + command, + arg, + }; + return provided(context); + })(); + return raw.map((choice) => + typeof choice === "string" ? { value: choice, label: choice } : choice, + ); } export function resolveCommandArgMenu(params: { command: ChatCommandDefinition; args?: CommandArgs; cfg?: ClawdbotConfig; -}): { arg: CommandArgDefinition; choices: string[]; title?: string } | null { +}): { arg: CommandArgDefinition; choices: ResolvedCommandArgChoice[]; title?: string } | null { const { command, args, cfg } = params; if (!command.args || !command.argsMenu) return null; if (command.argsParsing === "none") return null; diff --git a/src/auto-reply/commands-registry.types.ts b/src/auto-reply/commands-registry.types.ts index c19c9d9a7..6b9371604 100644 --- a/src/auto-reply/commands-registry.types.ts +++ b/src/auto-reply/commands-registry.types.ts @@ -2,6 +2,15 @@ import type { ClawdbotConfig } from "../config/types.js"; export type CommandScope = "text" | "native" | "both"; +export type CommandCategory = + | "session" + | "options" + | "status" + | "management" + | "media" + | "tools" + | "docks"; + export type CommandArgType = "string" | "number" | "boolean"; export type CommandArgChoiceContext = { @@ -12,14 +21,16 @@ export type CommandArgChoiceContext = { arg: CommandArgDefinition; }; -export type CommandArgChoicesProvider = (context: CommandArgChoiceContext) => string[]; +export type CommandArgChoice = string | { value: string; label: string }; + +export type CommandArgChoicesProvider = (context: CommandArgChoiceContext) => CommandArgChoice[]; export type CommandArgDefinition = { name: string; description: string; type: CommandArgType; required?: boolean; - choices?: string[] | CommandArgChoicesProvider; + choices?: CommandArgChoice[] | CommandArgChoicesProvider; captureRemaining?: boolean; }; @@ -49,6 +60,7 @@ export type ChatCommandDefinition = { formatArgs?: (values: CommandArgValues) => string | undefined; argsMenu?: CommandArgMenuSpec | "auto"; scope: CommandScope; + category?: CommandCategory; }; export type NativeCommandSpec = { diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index 939fa92f0..5aff68639 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -89,6 +89,7 @@ export async function runAgentTurnWithFallback(params: { registerAgentRunContext(runId, { sessionKey: params.sessionKey, verboseLevel: params.resolvedVerboseLevel, + isHeartbeat: params.isHeartbeat, }); } let runResult: Awaited>; @@ -137,6 +138,7 @@ export async function runAgentTurnWithFallback(params: { cfg: params.followupRun.run.config, provider: params.followupRun.run.provider, model: params.followupRun.run.model, + agentDir: params.followupRun.run.agentDir, fallbacksOverride: resolveAgentModelFallbacksOverride( params.followupRun.run.config, resolveAgentIdFromSessionKey(params.followupRun.run.sessionKey), @@ -230,6 +232,10 @@ export async function runAgentTurnWithFallback(params: { groupChannel: params.sessionCtx.GroupChannel?.trim() ?? params.sessionCtx.GroupSubject?.trim(), groupSpace: params.sessionCtx.GroupSpace?.trim() ?? undefined, + senderId: params.sessionCtx.SenderId?.trim() || undefined, + senderName: params.sessionCtx.SenderName?.trim() || undefined, + senderUsername: params.sessionCtx.SenderUsername?.trim() || undefined, + senderE164: params.sessionCtx.SenderE164?.trim() || undefined, // Provider threading context for tool auto-injection ...buildThreadingToolContext({ sessionCtx: params.sessionCtx, diff --git a/src/auto-reply/reply/agent-runner-memory.ts b/src/auto-reply/reply/agent-runner-memory.ts index a7d590750..ae6d244a8 100644 --- a/src/auto-reply/reply/agent-runner-memory.ts +++ b/src/auto-reply/reply/agent-runner-memory.ts @@ -92,6 +92,7 @@ export async function runMemoryFlushIfNeeded(params: { cfg: params.followupRun.run.config, provider: params.followupRun.run.provider, model: params.followupRun.run.model, + agentDir: params.followupRun.run.agentDir, fallbacksOverride: resolveAgentModelFallbacksOverride( params.followupRun.run.config, resolveAgentIdFromSessionKey(params.followupRun.run.sessionKey), @@ -114,6 +115,10 @@ export async function runMemoryFlushIfNeeded(params: { config: params.followupRun.run.config, hasRepliedRef: params.opts?.hasRepliedRef, }), + senderId: params.sessionCtx.SenderId?.trim() || undefined, + senderName: params.sessionCtx.SenderName?.trim() || undefined, + senderUsername: params.sessionCtx.SenderUsername?.trim() || undefined, + senderE164: params.sessionCtx.SenderE164?.trim() || undefined, sessionFile: params.followupRun.run.sessionFile, workspaceDir: params.followupRun.run.workspaceDir, agentDir: params.followupRun.run.agentDir, diff --git a/src/auto-reply/reply/commands-info.test.ts b/src/auto-reply/reply/commands-info.test.ts new file mode 100644 index 000000000..9751c39cc --- /dev/null +++ b/src/auto-reply/reply/commands-info.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from "vitest"; +import { buildCommandsPaginationKeyboard } from "./commands-info.js"; + +describe("buildCommandsPaginationKeyboard", () => { + it("adds agent id to callback data when provided", () => { + const keyboard = buildCommandsPaginationKeyboard(2, 3, "agent-main"); + expect(keyboard[0]).toEqual([ + { text: "◀ Prev", callback_data: "commands_page_1:agent-main" }, + { text: "2/3", callback_data: "commands_page_noop:agent-main" }, + { text: "Next ▶", callback_data: "commands_page_3:agent-main" }, + ]); + }); +}); diff --git a/src/auto-reply/reply/commands-info.ts b/src/auto-reply/reply/commands-info.ts index 1a525150c..e7d8a8f6f 100644 --- a/src/auto-reply/reply/commands-info.ts +++ b/src/auto-reply/reply/commands-info.ts @@ -1,6 +1,10 @@ import { logVerbose } from "../../globals.js"; -import { listSkillCommandsForWorkspace } from "../skill-commands.js"; -import { buildCommandsMessage, buildHelpMessage } from "../status.js"; +import { listSkillCommandsForAgents } from "../skill-commands.js"; +import { + buildCommandsMessage, + buildCommandsMessagePaginated, + buildHelpMessage, +} from "../status.js"; import { buildStatusReply } from "./commands-status.js"; import { buildContextReply } from "./commands-context-report.js"; import type { CommandHandler } from "./commands-types.js"; @@ -31,16 +35,78 @@ export const handleCommandsListCommand: CommandHandler = async (params, allowTex } const skillCommands = params.skillCommands ?? - listSkillCommandsForWorkspace({ - workspaceDir: params.workspaceDir, + listSkillCommandsForAgents({ cfg: params.cfg, + agentIds: params.agentId ? [params.agentId] : undefined, }); + const surface = params.ctx.Surface; + + if (surface === "telegram") { + const result = buildCommandsMessagePaginated(params.cfg, skillCommands, { + page: 1, + surface, + }); + + if (result.totalPages > 1) { + return { + shouldContinue: false, + reply: { + text: result.text, + channelData: { + telegram: { + buttons: buildCommandsPaginationKeyboard( + result.currentPage, + result.totalPages, + params.agentId, + ), + }, + }, + }, + }; + } + + return { + shouldContinue: false, + reply: { text: result.text }, + }; + } + return { shouldContinue: false, - reply: { text: buildCommandsMessage(params.cfg, skillCommands) }, + reply: { text: buildCommandsMessage(params.cfg, skillCommands, { surface }) }, }; }; +export function buildCommandsPaginationKeyboard( + currentPage: number, + totalPages: number, + agentId?: string, +): Array> { + const buttons: Array<{ text: string; callback_data: string }> = []; + const suffix = agentId ? `:${agentId}` : ""; + + if (currentPage > 1) { + buttons.push({ + text: "◀ Prev", + callback_data: `commands_page_${currentPage - 1}${suffix}`, + }); + } + + buttons.push({ + text: `${currentPage}/${totalPages}`, + callback_data: `commands_page_noop${suffix}`, + }); + + if (currentPage < totalPages) { + buttons.push({ + text: "Next ▶", + callback_data: `commands_page_${currentPage + 1}${suffix}`, + }); + } + + return [buttons]; +} + export const handleStatusCommand: CommandHandler = async (params, allowTextCommands) => { if (!allowTextCommands) return null; const statusRequested = diff --git a/src/auto-reply/reply/commands-tts.ts b/src/auto-reply/reply/commands-tts.ts index 5c65fb94c..04b60a4e9 100644 --- a/src/auto-reply/reply/commands-tts.ts +++ b/src/auto-reply/reply/commands-tts.ts @@ -6,20 +6,18 @@ import { getTtsMaxLength, getTtsProvider, isSummarizationEnabled, + isTtsEnabled, isTtsProviderConfigured, - normalizeTtsAutoMode, - resolveTtsAutoMode, resolveTtsApiKey, resolveTtsConfig, resolveTtsPrefsPath, - resolveTtsProviderOrder, setLastTtsAttempt, setSummarizationEnabled, + setTtsEnabled, setTtsMaxLength, setTtsProvider, textToSpeech, } from "../../tts/tts.js"; -import { updateSessionStore } from "../../config/sessions.js"; type ParsedTtsCommand = { action: string; @@ -40,14 +38,27 @@ function ttsUsage(): ReplyPayload { // Keep usage in one place so help/validation stays consistent. return { text: - "⚙️ Usage: /tts [value]" + - "\nExamples:\n" + - "/tts always\n" + - "/tts provider openai\n" + - "/tts provider edge\n" + - "/tts limit 2000\n" + - "/tts summary off\n" + - "/tts audio Hello from Clawdbot", + `🔊 **TTS (Text-to-Speech) Help**\n\n` + + `**Commands:**\n` + + `• /tts on — Enable automatic TTS for replies\n` + + `• /tts off — Disable TTS\n` + + `• /tts status — Show current settings\n` + + `• /tts provider [name] — View/change provider\n` + + `• /tts limit [number] — View/change text limit\n` + + `• /tts summary [on|off] — View/change auto-summary\n` + + `• /tts audio — Generate audio from text\n\n` + + `**Providers:**\n` + + `• edge — Free, fast (default)\n` + + `• openai — High quality (requires API key)\n` + + `• elevenlabs — Premium voices (requires API key)\n\n` + + `**Text Limit (default: 1500, max: 4096):**\n` + + `When text exceeds the limit:\n` + + `• Summary ON: AI summarizes, then generates audio\n` + + `• Summary OFF: Truncates text, then generates audio\n\n` + + `**Examples:**\n` + + `/tts provider edge\n` + + `/tts limit 2000\n` + + `/tts audio Hello, this is a test!`, }; } @@ -72,35 +83,27 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand return { shouldContinue: false, reply: ttsUsage() }; } - const requestedAuto = normalizeTtsAutoMode( - action === "on" ? "always" : action === "off" ? "off" : action, - ); - if (requestedAuto) { - const entry = params.sessionEntry; - const sessionKey = params.sessionKey; - const store = params.sessionStore; - if (entry && store && sessionKey) { - entry.ttsAuto = requestedAuto; - entry.updatedAt = Date.now(); - store[sessionKey] = entry; - if (params.storePath) { - await updateSessionStore(params.storePath, (store) => { - store[sessionKey] = entry; - }); - } - } - const label = requestedAuto === "always" ? "enabled (always)" : requestedAuto; - return { - shouldContinue: false, - reply: { - text: requestedAuto === "off" ? "🔇 TTS disabled." : `🔊 TTS ${label}.`, - }, - }; + if (action === "on") { + setTtsEnabled(prefsPath, true); + return { shouldContinue: false, reply: { text: "🔊 TTS enabled." } }; + } + + if (action === "off") { + setTtsEnabled(prefsPath, false); + return { shouldContinue: false, reply: { text: "🔇 TTS disabled." } }; } if (action === "audio") { if (!args.trim()) { - return { shouldContinue: false, reply: ttsUsage() }; + return { + shouldContinue: false, + reply: { + text: + `🎤 Generate audio from text.\n\n` + + `Usage: /tts audio \n` + + `Example: /tts audio Hello, this is a test!`, + }, + }; } const start = Date.now(); @@ -146,9 +149,6 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand if (action === "provider") { const currentProvider = getTtsProvider(config, prefsPath); if (!args.trim()) { - const fallback = resolveTtsProviderOrder(currentProvider) - .slice(1) - .filter((provider) => isTtsProviderConfigured(config, provider)); const hasOpenAI = Boolean(resolveTtsApiKey(config, "openai")); const hasElevenLabs = Boolean(resolveTtsApiKey(config, "elevenlabs")); const hasEdge = isTtsProviderConfigured(config, "edge"); @@ -158,7 +158,6 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand text: `🎙️ TTS provider\n` + `Primary: ${currentProvider}\n` + - `Fallbacks: ${fallback.join(", ") || "none"}\n` + `OpenAI key: ${hasOpenAI ? "✅" : "❌"}\n` + `ElevenLabs key: ${hasElevenLabs ? "✅" : "❌"}\n` + `Edge enabled: ${hasEdge ? "✅" : "❌"}\n` + @@ -173,18 +172,9 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand } setTtsProvider(prefsPath, requested); - const fallback = resolveTtsProviderOrder(requested) - .slice(1) - .filter((provider) => isTtsProviderConfigured(config, provider)); return { shouldContinue: false, - reply: { - text: - `✅ TTS provider set to ${requested} (fallbacks: ${fallback.join(", ") || "none"}).` + - (requested === "edge" - ? "\nEnable Edge TTS in config: messages.tts.edge.enabled = true." - : ""), - }, + reply: { text: `✅ TTS provider set to ${requested}.` }, }; } @@ -193,12 +183,22 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand const currentLimit = getTtsMaxLength(prefsPath); return { shouldContinue: false, - reply: { text: `📏 TTS limit: ${currentLimit} characters.` }, + reply: { + text: + `📏 TTS limit: ${currentLimit} characters.\n\n` + + `Text longer than this triggers summary (if enabled).\n` + + `Range: 100-4096 chars (Telegram max).\n\n` + + `To change: /tts limit \n` + + `Example: /tts limit 2000`, + }, }; } const next = Number.parseInt(args.trim(), 10); - if (!Number.isFinite(next) || next < 100 || next > 10_000) { - return { shouldContinue: false, reply: ttsUsage() }; + if (!Number.isFinite(next) || next < 100 || next > 4096) { + return { + shouldContinue: false, + reply: { text: "❌ Limit must be between 100 and 4096 characters." }, + }; } setTtsMaxLength(prefsPath, next); return { @@ -210,9 +210,17 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand if (action === "summary") { if (!args.trim()) { const enabled = isSummarizationEnabled(prefsPath); + const maxLen = getTtsMaxLength(prefsPath); return { shouldContinue: false, - reply: { text: `📝 TTS auto-summary: ${enabled ? "on" : "off"}.` }, + reply: { + text: + `📝 TTS auto-summary: ${enabled ? "on" : "off"}.\n\n` + + `When text exceeds ${maxLen} chars:\n` + + `• ON: summarizes text, then generates audio\n` + + `• OFF: truncates text, then generates audio\n\n` + + `To change: /tts summary on | off`, + }, }; } const requested = args.trim().toLowerCase(); @@ -229,27 +237,16 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand } if (action === "status") { - const sessionAuto = params.sessionEntry?.ttsAuto; - const autoMode = resolveTtsAutoMode({ config, prefsPath, sessionAuto }); - const enabled = autoMode !== "off"; + const enabled = isTtsEnabled(config, prefsPath); const provider = getTtsProvider(config, prefsPath); const hasKey = isTtsProviderConfigured(config, provider); - const providerStatus = - provider === "edge" - ? hasKey - ? "✅ enabled" - : "❌ disabled" - : hasKey - ? "✅ key" - : "❌ no key"; const maxLength = getTtsMaxLength(prefsPath); const summarize = isSummarizationEnabled(prefsPath); const last = getLastTtsAttempt(); - const autoLabel = sessionAuto ? `${autoMode} (session)` : autoMode; const lines = [ "📊 TTS status", - `Auto: ${enabled ? autoLabel : "off"}`, - `Provider: ${provider} (${providerStatus})`, + `State: ${enabled ? "✅ enabled" : "❌ disabled"}`, + `Provider: ${provider} (${hasKey ? "✅ configured" : "❌ not configured"})`, `Text limit: ${maxLength} chars`, `Auto-summary: ${summarize ? "on" : "off"}`, ]; diff --git a/src/auto-reply/reply/commands.test.ts b/src/auto-reply/reply/commands.test.ts index 7078c15dc..fd8236c95 100644 --- a/src/auto-reply/reply/commands.test.ts +++ b/src/auto-reply/reply/commands.test.ts @@ -420,3 +420,17 @@ describe("handleCommands subagents", () => { expect(result.reply?.text).toContain("Status: done"); }); }); + +describe("handleCommands /tts", () => { + it("returns status for bare /tts on text command surfaces", async () => { + const cfg = { + commands: { text: true }, + channels: { whatsapp: { allowFrom: ["*"] } }, + messages: { tts: { prefsPath: path.join(testWorkspaceDir, "tts.json") } }, + } as ClawdbotConfig; + const params = buildParams("/tts", cfg); + const result = await handleCommands(params); + expect(result.shouldContinue).toBe(false); + expect(result.reply?.text).toContain("TTS status"); + }); +}); diff --git a/src/auto-reply/reply/dispatch-from-config.ts b/src/auto-reply/reply/dispatch-from-config.ts index f946c05f9..1dcd770bc 100644 --- a/src/auto-reply/reply/dispatch-from-config.ts +++ b/src/auto-reply/reply/dispatch-from-config.ts @@ -16,7 +16,7 @@ import { formatAbortReplyText, tryFastAbortFromMessage } from "./abort.js"; import { shouldSkipDuplicateInbound } from "./inbound-dedupe.js"; import type { ReplyDispatcher, ReplyDispatchKind } from "./reply-dispatcher.js"; import { isRoutableChannel, routeReply } from "./route-reply.js"; -import { maybeApplyTtsToPayload, normalizeTtsAutoMode } from "../../tts/tts.js"; +import { maybeApplyTtsToPayload, normalizeTtsAutoMode, resolveTtsConfig } from "../../tts/tts.js"; const AUDIO_PLACEHOLDER_RE = /^(\s*\([^)]*\))?$/i; const AUDIO_HEADER_RE = /^\[Audio\b/i; @@ -266,12 +266,26 @@ export async function dispatchReplyFromConfig(params: { return { queuedFinal, counts }; } + // Track accumulated block text for TTS generation after streaming completes. + // When block streaming succeeds, there's no final reply, so we need to generate + // TTS audio separately from the accumulated block content. + let accumulatedBlockText = ""; + let blockCount = 0; + const replyResult = await (params.replyResolver ?? getReplyFromConfig)( ctx, { ...params.replyOptions, onBlockReply: (payload: ReplyPayload, context) => { const run = async () => { + // Accumulate block text for TTS generation after streaming + if (payload.text) { + if (accumulatedBlockText.length > 0) { + accumulatedBlockText += "\n"; + } + accumulatedBlockText += payload.text; + blockCount++; + } const ttsPayload = await maybeApplyTtsToPayload({ payload, cfg, @@ -327,6 +341,62 @@ export async function dispatchReplyFromConfig(params: { queuedFinal = dispatcher.sendFinalReply(ttsReply) || queuedFinal; } } + + const ttsMode = resolveTtsConfig(cfg).mode ?? "final"; + // Generate TTS-only reply after block streaming completes (when there's no final reply). + // This handles the case where block streaming succeeds and drops final payloads, + // but we still want TTS audio to be generated from the accumulated block content. + if ( + ttsMode === "final" && + replies.length === 0 && + blockCount > 0 && + accumulatedBlockText.trim() + ) { + try { + const ttsSyntheticReply = await maybeApplyTtsToPayload({ + payload: { text: accumulatedBlockText }, + cfg, + channel: ttsChannel, + kind: "final", + inboundAudio, + ttsAuto: sessionTtsAuto, + }); + // Only send if TTS was actually applied (mediaUrl exists) + if (ttsSyntheticReply.mediaUrl) { + // Send TTS-only payload (no text, just audio) so it doesn't duplicate the block content + const ttsOnlyPayload: ReplyPayload = { + mediaUrl: ttsSyntheticReply.mediaUrl, + audioAsVoice: ttsSyntheticReply.audioAsVoice, + }; + if (shouldRouteToOriginating && originatingChannel && originatingTo) { + const result = await routeReply({ + payload: ttsOnlyPayload, + channel: originatingChannel, + to: originatingTo, + sessionKey: ctx.SessionKey, + accountId: ctx.AccountId, + threadId: ctx.MessageThreadId, + cfg, + }); + queuedFinal = result.ok || queuedFinal; + if (result.ok) routedFinalCount += 1; + if (!result.ok) { + logVerbose( + `dispatch-from-config: route-reply (tts-only) failed: ${result.error ?? "unknown error"}`, + ); + } + } else { + const didQueue = dispatcher.sendFinalReply(ttsOnlyPayload); + queuedFinal = didQueue || queuedFinal; + } + } + } catch (err) { + logVerbose( + `dispatch-from-config: accumulated block TTS failed: ${err instanceof Error ? err.message : String(err)}`, + ); + } + } + await dispatcher.waitForIdle(); const counts = dispatcher.getQueuedCounts(); diff --git a/src/auto-reply/reply/followup-runner.ts b/src/auto-reply/reply/followup-runner.ts index febbc6e6a..77edf66e5 100644 --- a/src/auto-reply/reply/followup-runner.ts +++ b/src/auto-reply/reply/followup-runner.ts @@ -129,6 +129,7 @@ export function createFollowupRunner(params: { cfg: queued.run.config, provider: queued.run.provider, model: queued.run.model, + agentDir: queued.run.agentDir, fallbacksOverride: resolveAgentModelFallbacksOverride( queued.run.config, resolveAgentIdFromSessionKey(queued.run.sessionKey), @@ -146,6 +147,10 @@ export function createFollowupRunner(params: { groupId: queued.run.groupId, groupChannel: queued.run.groupChannel, groupSpace: queued.run.groupSpace, + senderId: queued.run.senderId, + senderName: queued.run.senderName, + senderUsername: queued.run.senderUsername, + senderE164: queued.run.senderE164, sessionFile: queued.run.sessionFile, workspaceDir: queued.run.workspaceDir, config: queued.run.config, diff --git a/src/auto-reply/reply/get-reply-run.ts b/src/auto-reply/reply/get-reply-run.ts index 40802d2b7..895309aa7 100644 --- a/src/auto-reply/reply/get-reply-run.ts +++ b/src/auto-reply/reply/get-reply-run.ts @@ -370,6 +370,10 @@ export async function runPreparedReply( groupId: resolveGroupSessionKey(sessionCtx)?.id ?? undefined, groupChannel: sessionCtx.GroupChannel?.trim() ?? sessionCtx.GroupSubject?.trim(), groupSpace: sessionCtx.GroupSpace?.trim() ?? undefined, + senderId: sessionCtx.SenderId?.trim() || undefined, + senderName: sessionCtx.SenderName?.trim() || undefined, + senderUsername: sessionCtx.SenderUsername?.trim() || undefined, + senderE164: sessionCtx.SenderE164?.trim() || undefined, sessionFile, workspaceDir, config: cfg, diff --git a/src/auto-reply/reply/history.ts b/src/auto-reply/reply/history.ts index bc59b4f2e..45ad44d5a 100644 --- a/src/auto-reply/reply/history.ts +++ b/src/auto-reply/reply/history.ts @@ -3,6 +3,26 @@ import { CURRENT_MESSAGE_MARKER } from "./mentions.js"; export const HISTORY_CONTEXT_MARKER = "[Chat messages since your last reply - for context]"; export const DEFAULT_GROUP_HISTORY_LIMIT = 50; +/** Maximum number of group history keys to retain (LRU eviction when exceeded). */ +export const MAX_HISTORY_KEYS = 1000; + +/** + * Evict oldest keys from a history map when it exceeds MAX_HISTORY_KEYS. + * Uses Map's insertion order for LRU-like behavior. + */ +export function evictOldHistoryKeys( + historyMap: Map, + maxKeys: number = MAX_HISTORY_KEYS, +): void { + if (historyMap.size <= maxKeys) return; + const keysToDelete = historyMap.size - maxKeys; + const iterator = historyMap.keys(); + for (let i = 0; i < keysToDelete; i++) { + const key = iterator.next().value; + if (key !== undefined) historyMap.delete(key); + } +} + export type HistoryEntry = { sender: string; body: string; @@ -34,7 +54,13 @@ export function appendHistoryEntry(params: { const history = historyMap.get(historyKey) ?? []; history.push(entry); while (history.length > params.limit) history.shift(); + if (historyMap.has(historyKey)) { + // Refresh insertion order so eviction keeps recently used histories. + historyMap.delete(historyKey); + } historyMap.set(historyKey, history); + // Evict oldest keys if map exceeds max size to prevent unbounded memory growth + evictOldHistoryKeys(historyMap); return history; } diff --git a/src/auto-reply/reply/queue/types.ts b/src/auto-reply/reply/queue/types.ts index 332e9bae1..31dbd9e7b 100644 --- a/src/auto-reply/reply/queue/types.ts +++ b/src/auto-reply/reply/queue/types.ts @@ -51,6 +51,10 @@ export type FollowupRun = { groupId?: string; groupChannel?: string; groupSpace?: string; + senderId?: string; + senderName?: string; + senderUsername?: string; + senderE164?: string; sessionFile: string; workspaceDir: string; config: ClawdbotConfig; diff --git a/src/auto-reply/reply/session-updates.ts b/src/auto-reply/reply/session-updates.ts index 970a714d0..0fea27708 100644 --- a/src/auto-reply/reply/session-updates.ts +++ b/src/auto-reply/reply/session-updates.ts @@ -21,7 +21,11 @@ export async function prependSystemEvents(params: { if (!trimmed) return null; const lower = trimmed.toLowerCase(); if (lower.includes("reason periodic")) return null; - if (lower.includes("heartbeat")) return null; + // Filter out the actual heartbeat prompt, but not cron jobs that mention "heartbeat" + // The heartbeat prompt starts with "Read HEARTBEAT.md" - cron payloads won't match this + if (lower.startsWith("read heartbeat.md")) return null; + // Also filter heartbeat poll/wake noise + if (lower.includes("heartbeat poll") || lower.includes("heartbeat wake")) return null; if (trimmed.startsWith("Node:")) { return trimmed.replace(/ · last input [^·]+/i, "").trim(); } diff --git a/src/auto-reply/status.test.ts b/src/auto-reply/status.test.ts index 31b6b92ec..465352538 100644 --- a/src/auto-reply/status.test.ts +++ b/src/auto-reply/status.test.ts @@ -4,7 +4,20 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { normalizeTestText } from "../../test/helpers/normalize-text.js"; import { withTempHome } from "../../test/helpers/temp-home.js"; import type { ClawdbotConfig } from "../config/config.js"; -import { buildCommandsMessage, buildHelpMessage, buildStatusMessage } from "./status.js"; +import { + buildCommandsMessage, + buildCommandsMessagePaginated, + buildHelpMessage, + buildStatusMessage, +} from "./status.js"; + +const { listPluginCommands } = vi.hoisted(() => ({ + listPluginCommands: vi.fn(() => []), +})); + +vi.mock("../plugins/commands.js", () => ({ + listPluginCommands, +})); afterEach(() => { vi.restoreAllMocks(); @@ -400,10 +413,12 @@ describe("buildCommandsMessage", () => { const text = buildCommandsMessage({ commands: { config: false, debug: false }, } as ClawdbotConfig); + expect(text).toContain("ℹ️ Slash commands"); + expect(text).toContain("Status"); expect(text).toContain("/commands - List all slash commands."); expect(text).toContain("/skill - Run a skill by name."); - expect(text).toContain("/think (aliases: /thinking, /t) - Set thinking level."); - expect(text).toContain("/compact (text-only) - Compact the session context."); + expect(text).toContain("/think (/thinking, /t) - Set thinking level."); + expect(text).toContain("/compact [text] - Compact the session context."); expect(text).not.toContain("/config"); expect(text).not.toContain("/debug"); }); @@ -430,8 +445,39 @@ describe("buildHelpMessage", () => { const text = buildHelpMessage({ commands: { config: false, debug: false }, } as ClawdbotConfig); - expect(text).toContain("Skills: /skill [input]"); + expect(text).toContain("Skills"); + expect(text).toContain("/skill [input]"); expect(text).not.toContain("/config"); expect(text).not.toContain("/debug"); }); }); + +describe("buildCommandsMessagePaginated", () => { + it("formats telegram output with pages", () => { + const result = buildCommandsMessagePaginated( + { + commands: { config: false, debug: false }, + } as ClawdbotConfig, + undefined, + { surface: "telegram", page: 1 }, + ); + expect(result.text).toContain("ℹ️ Commands (1/"); + expect(result.text).toContain("Session"); + expect(result.text).toContain("/stop - Stop the current run."); + }); + + it("includes plugin commands in the paginated list", () => { + listPluginCommands.mockReturnValue([ + { name: "plugin_cmd", description: "Plugin command", pluginId: "demo-plugin" }, + ]); + const result = buildCommandsMessagePaginated( + { + commands: { config: false, debug: false }, + } as ClawdbotConfig, + undefined, + { surface: "telegram", page: 99 }, + ); + expect(result.text).toContain("Plugins"); + expect(result.text).toContain("/plugin_cmd (demo-plugin) - Plugin command"); + }); +}); diff --git a/src/auto-reply/status.ts b/src/auto-reply/status.ts index 733205c8c..7344b7502 100644 --- a/src/auto-reply/status.ts +++ b/src/auto-reply/status.ts @@ -29,9 +29,14 @@ import { resolveModelCostConfig, } from "../utils/usage-format.js"; import { VERSION } from "../version.js"; -import { listChatCommands, listChatCommandsForConfig } from "./commands-registry.js"; +import { + listChatCommands, + listChatCommandsForConfig, + type ChatCommandDefinition, +} from "./commands-registry.js"; import { listPluginCommands } from "../plugins/commands.js"; import type { SkillCommandSpec } from "../agents/skills.js"; +import type { CommandCategory } from "./commands-registry.types.js"; import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "./thinking.js"; import type { MediaUnderstandingDecision } from "../media-understanding/types.js"; @@ -427,61 +432,203 @@ export function buildStatusMessage(args: StatusArgs): string { .join("\n"); } +const CATEGORY_LABELS: Record = { + session: "Session", + options: "Options", + status: "Status", + management: "Management", + media: "Media", + tools: "Tools", + docks: "Docks", +}; + +const CATEGORY_ORDER: CommandCategory[] = [ + "session", + "options", + "status", + "management", + "media", + "tools", + "docks", +]; + +function groupCommandsByCategory( + commands: ChatCommandDefinition[], +): Map { + const grouped = new Map(); + for (const category of CATEGORY_ORDER) { + grouped.set(category, []); + } + for (const command of commands) { + const category = command.category ?? "tools"; + const list = grouped.get(category) ?? []; + list.push(command); + grouped.set(category, list); + } + return grouped; +} + export function buildHelpMessage(cfg?: ClawdbotConfig): string { - const options = [ - "/think ", - "/verbose on|full|off", - "/reasoning on|off", - "/elevated on|off|ask|full", - "/model ", - "/usage off|tokens|full", - ]; - if (cfg?.commands?.config === true) options.push("/config show"); - if (cfg?.commands?.debug === true) options.push("/debug show"); - return [ - "ℹ️ Help", - "Shortcuts: /new reset | /compact [instructions] | /restart relink (if enabled)", - `Options: ${options.join(" | ")}`, - "Skills: /skill [input]", - "More: /commands for all slash commands", - ].join("\n"); + const lines = ["ℹ️ Help", ""]; + + lines.push("Session"); + lines.push(" /new | /reset | /compact [instructions] | /stop"); + lines.push(""); + + const optionParts = ["/think ", "/model ", "/verbose on|off"]; + if (cfg?.commands?.config === true) optionParts.push("/config"); + if (cfg?.commands?.debug === true) optionParts.push("/debug"); + lines.push("Options"); + lines.push(` ${optionParts.join(" | ")}`); + lines.push(""); + + lines.push("Status"); + lines.push(" /status | /whoami | /context"); + lines.push(""); + + lines.push("Skills"); + lines.push(" /skill [input]"); + + lines.push(""); + lines.push("More: /commands for full list"); + + return lines.join("\n"); +} + +const COMMANDS_PER_PAGE = 8; + +export type CommandsMessageOptions = { + page?: number; + surface?: string; +}; + +export type CommandsMessageResult = { + text: string; + totalPages: number; + currentPage: number; + hasNext: boolean; + hasPrev: boolean; +}; + +function formatCommandEntry(command: ChatCommandDefinition): string { + const primary = command.nativeName + ? `/${command.nativeName}` + : command.textAliases[0]?.trim() || `/${command.key}`; + const seen = new Set(); + const aliases = command.textAliases + .map((alias) => alias.trim()) + .filter(Boolean) + .filter((alias) => alias.toLowerCase() !== primary.toLowerCase()) + .filter((alias) => { + const key = alias.toLowerCase(); + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + const aliasLabel = aliases.length ? ` (${aliases.join(", ")})` : ""; + const scopeLabel = command.scope === "text" ? " [text]" : ""; + return `${primary}${aliasLabel}${scopeLabel} - ${command.description}`; +} + +type CommandsListItem = { + label: string; + text: string; +}; + +function buildCommandItems( + commands: ChatCommandDefinition[], + pluginCommands: ReturnType, +): CommandsListItem[] { + const grouped = groupCommandsByCategory(commands); + const items: CommandsListItem[] = []; + + for (const category of CATEGORY_ORDER) { + const categoryCommands = grouped.get(category) ?? []; + if (categoryCommands.length === 0) continue; + const label = CATEGORY_LABELS[category]; + for (const command of categoryCommands) { + items.push({ label, text: formatCommandEntry(command) }); + } + } + + for (const command of pluginCommands) { + const pluginLabel = command.pluginId ? ` (${command.pluginId})` : ""; + items.push({ + label: "Plugins", + text: `/${command.name}${pluginLabel} - ${command.description}`, + }); + } + + return items; +} + +function formatCommandList(items: CommandsListItem[]): string { + const lines: string[] = []; + let currentLabel: string | null = null; + + for (const item of items) { + if (item.label !== currentLabel) { + if (lines.length > 0) lines.push(""); + lines.push(item.label); + currentLabel = item.label; + } + lines.push(` ${item.text}`); + } + + return lines.join("\n"); } export function buildCommandsMessage( cfg?: ClawdbotConfig, skillCommands?: SkillCommandSpec[], + options?: CommandsMessageOptions, ): string { - const lines = ["ℹ️ Slash commands"]; + const result = buildCommandsMessagePaginated(cfg, skillCommands, options); + return result.text; +} + +export function buildCommandsMessagePaginated( + cfg?: ClawdbotConfig, + skillCommands?: SkillCommandSpec[], + options?: CommandsMessageOptions, +): CommandsMessageResult { + const page = Math.max(1, options?.page ?? 1); + const surface = options?.surface?.toLowerCase(); + const isTelegram = surface === "telegram"; + const commands = cfg ? listChatCommandsForConfig(cfg, { skillCommands }) : listChatCommands({ skillCommands }); - for (const command of commands) { - const primary = command.nativeName - ? `/${command.nativeName}` - : command.textAliases[0]?.trim() || `/${command.key}`; - const seen = new Set(); - const aliases = command.textAliases - .map((alias) => alias.trim()) - .filter(Boolean) - .filter((alias) => alias.toLowerCase() !== primary.toLowerCase()) - .filter((alias) => { - const key = alias.toLowerCase(); - if (seen.has(key)) return false; - seen.add(key); - return true; - }); - const aliasLabel = aliases.length ? ` (aliases: ${aliases.join(", ")})` : ""; - const scopeLabel = command.scope === "text" ? " (text-only)" : ""; - lines.push(`${primary}${aliasLabel}${scopeLabel} - ${command.description}`); - } const pluginCommands = listPluginCommands(); - if (pluginCommands.length > 0) { - lines.push(""); - lines.push("Plugin commands:"); - for (const command of pluginCommands) { - const pluginLabel = command.pluginId ? ` (plugin: ${command.pluginId})` : ""; - lines.push(`/${command.name}${pluginLabel} - ${command.description}`); - } + const items = buildCommandItems(commands, pluginCommands); + + if (!isTelegram) { + const lines = ["ℹ️ Slash commands", ""]; + lines.push(formatCommandList(items)); + return { + text: lines.join("\n").trim(), + totalPages: 1, + currentPage: 1, + hasNext: false, + hasPrev: false, + }; } - return lines.join("\n"); + + const totalCommands = items.length; + const totalPages = Math.max(1, Math.ceil(totalCommands / COMMANDS_PER_PAGE)); + const currentPage = Math.min(page, totalPages); + const startIndex = (currentPage - 1) * COMMANDS_PER_PAGE; + const endIndex = startIndex + COMMANDS_PER_PAGE; + const pageItems = items.slice(startIndex, endIndex); + + const lines = [`ℹ️ Commands (${currentPage}/${totalPages})`, ""]; + lines.push(formatCommandList(pageItems)); + + return { + text: lines.join("\n").trim(), + totalPages, + currentPage, + hasNext: currentPage < totalPages, + hasPrev: currentPage > 1, + }; } diff --git a/src/auto-reply/templating.ts b/src/auto-reply/templating.ts index dd424ee71..79692a50d 100644 --- a/src/auto-reply/templating.ts +++ b/src/auto-reply/templating.ts @@ -1,4 +1,5 @@ import type { ChannelId } from "../channels/plugins/types.js"; +import type { StickerMetadata } from "../telegram/bot/types.js"; import type { InternalMessageChannel } from "../utils/message-channel.js"; import type { CommandArgs } from "./commands-registry.types.js"; import type { @@ -64,6 +65,8 @@ export type MsgContext = { MediaPaths?: string[]; MediaUrls?: string[]; MediaTypes?: string[]; + /** Telegram sticker metadata (emoji, set name, file IDs, cached description). */ + Sticker?: StickerMetadata; OutputDir?: string; OutputBase?: string; /** Remote host for SCP when media lives on a different machine (e.g., clawdbot@192.168.64.3). */ diff --git a/src/browser/bridge-server.ts b/src/browser/bridge-server.ts index 6bfe190a5..eaab062cc 100644 --- a/src/browser/bridge-server.ts +++ b/src/browser/bridge-server.ts @@ -4,6 +4,7 @@ import express from "express"; import type { ResolvedBrowserConfig } from "./config.js"; import { registerBrowserRoutes } from "./routes/index.js"; +import type { BrowserRouteRegistrar } from "./routes/types.js"; import { type BrowserServerState, createBrowserRouteContext, @@ -50,7 +51,7 @@ export async function startBrowserBridgeServer(params: { getState: () => state, onEnsureAttachTarget: params.onEnsureAttachTarget, }); - registerBrowserRoutes(app, ctx); + registerBrowserRoutes(app as unknown as BrowserRouteRegistrar, ctx); const server = await new Promise((resolve, reject) => { const s = app.listen(port, host, () => resolve(s)); @@ -61,11 +62,9 @@ export async function startBrowserBridgeServer(params: { const resolvedPort = address?.port ?? port; state.server = server; state.port = resolvedPort; - state.resolved.controlHost = host; state.resolved.controlPort = resolvedPort; - state.resolved.controlUrl = `http://${host}:${resolvedPort}`; - const baseUrl = state.resolved.controlUrl; + const baseUrl = `http://${host}:${resolvedPort}`; return { server, port: resolvedPort, baseUrl, state }; } diff --git a/src/browser/client-actions-core.ts b/src/browser/client-actions-core.ts index 667600ef3..96688f701 100644 --- a/src/browser/client-actions-core.ts +++ b/src/browser/client-actions-core.ts @@ -9,6 +9,12 @@ function buildProfileQuery(profile?: string): string { return profile ? `?profile=${encodeURIComponent(profile)}` : ""; } +function withBaseUrl(baseUrl: string | undefined, path: string): string { + const trimmed = baseUrl?.trim(); + if (!trimmed) return path; + return `${trimmed.replace(/\/$/, "")}${path}`; +} + export type BrowserFormField = { ref: string; type: string; @@ -92,11 +98,15 @@ export type BrowserDownloadPayload = { }; export async function browserNavigate( - baseUrl: string, - opts: { url: string; targetId?: string; profile?: string }, + baseUrl: string | undefined, + opts: { + url: string; + targetId?: string; + profile?: string; + }, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson(`${baseUrl}/navigate${q}`, { + return await fetchBrowserJson(withBaseUrl(baseUrl, `/navigate${q}`), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url: opts.url, targetId: opts.targetId }), @@ -105,7 +115,7 @@ export async function browserNavigate( } export async function browserArmDialog( - baseUrl: string, + baseUrl: string | undefined, opts: { accept: boolean; promptText?: string; @@ -115,7 +125,7 @@ export async function browserArmDialog( }, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson(`${baseUrl}/hooks/dialog${q}`, { + return await fetchBrowserJson(withBaseUrl(baseUrl, `/hooks/dialog${q}`), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -129,7 +139,7 @@ export async function browserArmDialog( } export async function browserArmFileChooser( - baseUrl: string, + baseUrl: string | undefined, opts: { paths: string[]; ref?: string; @@ -141,7 +151,7 @@ export async function browserArmFileChooser( }, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson(`${baseUrl}/hooks/file-chooser${q}`, { + return await fetchBrowserJson(withBaseUrl(baseUrl, `/hooks/file-chooser${q}`), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -157,7 +167,7 @@ export async function browserArmFileChooser( } export async function browserWaitForDownload( - baseUrl: string, + baseUrl: string | undefined, opts: { path?: string; targetId?: string; @@ -170,7 +180,7 @@ export async function browserWaitForDownload( ok: true; targetId: string; download: BrowserDownloadPayload; - }>(`${baseUrl}/wait/download${q}`, { + }>(withBaseUrl(baseUrl, `/wait/download${q}`), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -183,7 +193,7 @@ export async function browserWaitForDownload( } export async function browserDownload( - baseUrl: string, + baseUrl: string | undefined, opts: { ref: string; path: string; @@ -197,7 +207,7 @@ export async function browserDownload( ok: true; targetId: string; download: BrowserDownloadPayload; - }>(`${baseUrl}/download${q}`, { + }>(withBaseUrl(baseUrl, `/download${q}`), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -211,12 +221,12 @@ export async function browserDownload( } export async function browserAct( - baseUrl: string, + baseUrl: string | undefined, req: BrowserActRequest, opts?: { profile?: string }, ): Promise { const q = buildProfileQuery(opts?.profile); - return await fetchBrowserJson(`${baseUrl}/act${q}`, { + return await fetchBrowserJson(withBaseUrl(baseUrl, `/act${q}`), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(req), @@ -225,7 +235,7 @@ export async function browserAct( } export async function browserScreenshotAction( - baseUrl: string, + baseUrl: string | undefined, opts: { targetId?: string; fullPage?: boolean; @@ -236,7 +246,7 @@ export async function browserScreenshotAction( }, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson(`${baseUrl}/screenshot${q}`, { + return await fetchBrowserJson(withBaseUrl(baseUrl, `/screenshot${q}`), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ diff --git a/src/browser/client-actions-observe.ts b/src/browser/client-actions-observe.ts index 4002d48c5..4b042d9b0 100644 --- a/src/browser/client-actions-observe.ts +++ b/src/browser/client-actions-observe.ts @@ -10,8 +10,14 @@ function buildProfileQuery(profile?: string): string { return profile ? `?profile=${encodeURIComponent(profile)}` : ""; } +function withBaseUrl(baseUrl: string | undefined, path: string): string { + const trimmed = baseUrl?.trim(); + if (!trimmed) return path; + return `${trimmed.replace(/\/$/, "")}${path}`; +} + export async function browserConsoleMessages( - baseUrl: string, + baseUrl: string | undefined, opts: { level?: string; targetId?: string; profile?: string } = {}, ): Promise<{ ok: true; messages: BrowserConsoleMessage[]; targetId: string }> { const q = new URLSearchParams(); @@ -23,15 +29,15 @@ export async function browserConsoleMessages( ok: true; messages: BrowserConsoleMessage[]; targetId: string; - }>(`${baseUrl}/console${suffix}`, { timeoutMs: 20000 }); + }>(withBaseUrl(baseUrl, `/console${suffix}`), { timeoutMs: 20000 }); } export async function browserPdfSave( - baseUrl: string, + baseUrl: string | undefined, opts: { targetId?: string; profile?: string } = {}, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson(`${baseUrl}/pdf${q}`, { + return await fetchBrowserJson(withBaseUrl(baseUrl, `/pdf${q}`), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ targetId: opts.targetId }), @@ -40,7 +46,7 @@ export async function browserPdfSave( } export async function browserPageErrors( - baseUrl: string, + baseUrl: string | undefined, opts: { targetId?: string; clear?: boolean; profile?: string } = {}, ): Promise<{ ok: true; targetId: string; errors: BrowserPageError[] }> { const q = new URLSearchParams(); @@ -52,11 +58,11 @@ export async function browserPageErrors( ok: true; targetId: string; errors: BrowserPageError[]; - }>(`${baseUrl}/errors${suffix}`, { timeoutMs: 20000 }); + }>(withBaseUrl(baseUrl, `/errors${suffix}`), { timeoutMs: 20000 }); } export async function browserRequests( - baseUrl: string, + baseUrl: string | undefined, opts: { targetId?: string; filter?: string; @@ -74,11 +80,11 @@ export async function browserRequests( ok: true; targetId: string; requests: BrowserNetworkRequest[]; - }>(`${baseUrl}/requests${suffix}`, { timeoutMs: 20000 }); + }>(withBaseUrl(baseUrl, `/requests${suffix}`), { timeoutMs: 20000 }); } export async function browserTraceStart( - baseUrl: string, + baseUrl: string | undefined, opts: { targetId?: string; screenshots?: boolean; @@ -88,7 +94,7 @@ export async function browserTraceStart( } = {}, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson(`${baseUrl}/trace/start${q}`, { + return await fetchBrowserJson(withBaseUrl(baseUrl, `/trace/start${q}`), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -102,11 +108,11 @@ export async function browserTraceStart( } export async function browserTraceStop( - baseUrl: string, + baseUrl: string | undefined, opts: { targetId?: string; path?: string; profile?: string } = {}, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson(`${baseUrl}/trace/stop${q}`, { + return await fetchBrowserJson(withBaseUrl(baseUrl, `/trace/stop${q}`), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ targetId: opts.targetId, path: opts.path }), @@ -115,11 +121,11 @@ export async function browserTraceStop( } export async function browserHighlight( - baseUrl: string, + baseUrl: string | undefined, opts: { ref: string; targetId?: string; profile?: string }, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson(`${baseUrl}/highlight${q}`, { + return await fetchBrowserJson(withBaseUrl(baseUrl, `/highlight${q}`), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ targetId: opts.targetId, ref: opts.ref }), @@ -128,7 +134,7 @@ export async function browserHighlight( } export async function browserResponseBody( - baseUrl: string, + baseUrl: string | undefined, opts: { url: string; targetId?: string; @@ -158,7 +164,7 @@ export async function browserResponseBody( body: string; truncated?: boolean; }; - }>(`${baseUrl}/response/body${q}`, { + }>(withBaseUrl(baseUrl, `/response/body${q}`), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ diff --git a/src/browser/client-actions-state.ts b/src/browser/client-actions-state.ts index 192cad997..751634a9e 100644 --- a/src/browser/client-actions-state.ts +++ b/src/browser/client-actions-state.ts @@ -5,8 +5,14 @@ function buildProfileQuery(profile?: string): string { return profile ? `?profile=${encodeURIComponent(profile)}` : ""; } +function withBaseUrl(baseUrl: string | undefined, path: string): string { + const trimmed = baseUrl?.trim(); + if (!trimmed) return path; + return `${trimmed.replace(/\/$/, "")}${path}`; +} + export async function browserCookies( - baseUrl: string, + baseUrl: string | undefined, opts: { targetId?: string; profile?: string } = {}, ): Promise<{ ok: true; targetId: string; cookies: unknown[] }> { const q = new URLSearchParams(); @@ -17,11 +23,11 @@ export async function browserCookies( ok: true; targetId: string; cookies: unknown[]; - }>(`${baseUrl}/cookies${suffix}`, { timeoutMs: 20000 }); + }>(withBaseUrl(baseUrl, `/cookies${suffix}`), { timeoutMs: 20000 }); } export async function browserCookiesSet( - baseUrl: string, + baseUrl: string | undefined, opts: { cookie: Record; targetId?: string; @@ -29,7 +35,7 @@ export async function browserCookiesSet( }, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson(`${baseUrl}/cookies/set${q}`, { + return await fetchBrowserJson(withBaseUrl(baseUrl, `/cookies/set${q}`), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ targetId: opts.targetId, cookie: opts.cookie }), @@ -38,11 +44,11 @@ export async function browserCookiesSet( } export async function browserCookiesClear( - baseUrl: string, + baseUrl: string | undefined, opts: { targetId?: string; profile?: string } = {}, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson(`${baseUrl}/cookies/clear${q}`, { + return await fetchBrowserJson(withBaseUrl(baseUrl, `/cookies/clear${q}`), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ targetId: opts.targetId }), @@ -51,7 +57,7 @@ export async function browserCookiesClear( } export async function browserStorageGet( - baseUrl: string, + baseUrl: string | undefined, opts: { kind: "local" | "session"; key?: string; @@ -68,11 +74,11 @@ export async function browserStorageGet( ok: true; targetId: string; values: Record; - }>(`${baseUrl}/storage/${opts.kind}${suffix}`, { timeoutMs: 20000 }); + }>(withBaseUrl(baseUrl, `/storage/${opts.kind}${suffix}`), { timeoutMs: 20000 }); } export async function browserStorageSet( - baseUrl: string, + baseUrl: string | undefined, opts: { kind: "local" | "session"; key: string; @@ -82,25 +88,28 @@ export async function browserStorageSet( }, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson(`${baseUrl}/storage/${opts.kind}/set${q}`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - targetId: opts.targetId, - key: opts.key, - value: opts.value, - }), - timeoutMs: 20000, - }); + return await fetchBrowserJson( + withBaseUrl(baseUrl, `/storage/${opts.kind}/set${q}`), + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + targetId: opts.targetId, + key: opts.key, + value: opts.value, + }), + timeoutMs: 20000, + }, + ); } export async function browserStorageClear( - baseUrl: string, + baseUrl: string | undefined, opts: { kind: "local" | "session"; targetId?: string; profile?: string }, ): Promise { const q = buildProfileQuery(opts.profile); return await fetchBrowserJson( - `${baseUrl}/storage/${opts.kind}/clear${q}`, + withBaseUrl(baseUrl, `/storage/${opts.kind}/clear${q}`), { method: "POST", headers: { "Content-Type": "application/json" }, @@ -111,11 +120,11 @@ export async function browserStorageClear( } export async function browserSetOffline( - baseUrl: string, + baseUrl: string | undefined, opts: { offline: boolean; targetId?: string; profile?: string }, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson(`${baseUrl}/set/offline${q}`, { + return await fetchBrowserJson(withBaseUrl(baseUrl, `/set/offline${q}`), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ targetId: opts.targetId, offline: opts.offline }), @@ -124,7 +133,7 @@ export async function browserSetOffline( } export async function browserSetHeaders( - baseUrl: string, + baseUrl: string | undefined, opts: { headers: Record; targetId?: string; @@ -132,7 +141,7 @@ export async function browserSetHeaders( }, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson(`${baseUrl}/set/headers${q}`, { + return await fetchBrowserJson(withBaseUrl(baseUrl, `/set/headers${q}`), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ targetId: opts.targetId, headers: opts.headers }), @@ -141,7 +150,7 @@ export async function browserSetHeaders( } export async function browserSetHttpCredentials( - baseUrl: string, + baseUrl: string | undefined, opts: { username?: string; password?: string; @@ -151,21 +160,24 @@ export async function browserSetHttpCredentials( } = {}, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson(`${baseUrl}/set/credentials${q}`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - targetId: opts.targetId, - username: opts.username, - password: opts.password, - clear: opts.clear, - }), - timeoutMs: 20000, - }); + return await fetchBrowserJson( + withBaseUrl(baseUrl, `/set/credentials${q}`), + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + targetId: opts.targetId, + username: opts.username, + password: opts.password, + clear: opts.clear, + }), + timeoutMs: 20000, + }, + ); } export async function browserSetGeolocation( - baseUrl: string, + baseUrl: string | undefined, opts: { latitude?: number; longitude?: number; @@ -177,23 +189,26 @@ export async function browserSetGeolocation( } = {}, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson(`${baseUrl}/set/geolocation${q}`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - targetId: opts.targetId, - latitude: opts.latitude, - longitude: opts.longitude, - accuracy: opts.accuracy, - origin: opts.origin, - clear: opts.clear, - }), - timeoutMs: 20000, - }); + return await fetchBrowserJson( + withBaseUrl(baseUrl, `/set/geolocation${q}`), + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + targetId: opts.targetId, + latitude: opts.latitude, + longitude: opts.longitude, + accuracy: opts.accuracy, + origin: opts.origin, + clear: opts.clear, + }), + timeoutMs: 20000, + }, + ); } export async function browserSetMedia( - baseUrl: string, + baseUrl: string | undefined, opts: { colorScheme: "dark" | "light" | "no-preference" | "none"; targetId?: string; @@ -201,7 +216,7 @@ export async function browserSetMedia( }, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson(`${baseUrl}/set/media${q}`, { + return await fetchBrowserJson(withBaseUrl(baseUrl, `/set/media${q}`), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -213,11 +228,11 @@ export async function browserSetMedia( } export async function browserSetTimezone( - baseUrl: string, + baseUrl: string | undefined, opts: { timezoneId: string; targetId?: string; profile?: string }, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson(`${baseUrl}/set/timezone${q}`, { + return await fetchBrowserJson(withBaseUrl(baseUrl, `/set/timezone${q}`), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -229,11 +244,11 @@ export async function browserSetTimezone( } export async function browserSetLocale( - baseUrl: string, + baseUrl: string | undefined, opts: { locale: string; targetId?: string; profile?: string }, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson(`${baseUrl}/set/locale${q}`, { + return await fetchBrowserJson(withBaseUrl(baseUrl, `/set/locale${q}`), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ targetId: opts.targetId, locale: opts.locale }), @@ -242,11 +257,11 @@ export async function browserSetLocale( } export async function browserSetDevice( - baseUrl: string, + baseUrl: string | undefined, opts: { name: string; targetId?: string; profile?: string }, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson(`${baseUrl}/set/device${q}`, { + return await fetchBrowserJson(withBaseUrl(baseUrl, `/set/device${q}`), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ targetId: opts.targetId, name: opts.name }), @@ -255,11 +270,11 @@ export async function browserSetDevice( } export async function browserClearPermissions( - baseUrl: string, + baseUrl: string | undefined, opts: { targetId?: string; profile?: string } = {}, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson(`${baseUrl}/set/geolocation${q}`, { + return await fetchBrowserJson(withBaseUrl(baseUrl, `/set/geolocation${q}`), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ targetId: opts.targetId, clear: true }), diff --git a/src/browser/client-fetch.ts b/src/browser/client-fetch.ts index e815f45c4..63b71d733 100644 --- a/src/browser/client-fetch.ts +++ b/src/browser/client-fetch.ts @@ -1,57 +1,51 @@ -import { extractErrorCode, formatErrorMessage } from "../infra/errors.js"; -import { loadConfig } from "../config/config.js"; import { formatCliCommand } from "../cli/command-format.js"; -import { resolveBrowserConfig } from "./config.js"; +import { + createBrowserControlContext, + startBrowserControlServiceFromConfig, +} from "./control-service.js"; +import { createBrowserRouteDispatcher } from "./routes/dispatcher.js"; -let cachedConfigToken: string | null | undefined = undefined; - -function getBrowserControlToken(): string | null { - const env = process.env.CLAWDBOT_BROWSER_CONTROL_TOKEN?.trim(); - if (env) return env; - - if (cachedConfigToken !== undefined) return cachedConfigToken; - try { - const cfg = loadConfig(); - const resolved = resolveBrowserConfig(cfg.browser); - const token = resolved.controlToken?.trim() || ""; - cachedConfigToken = token ? token : null; - } catch { - cachedConfigToken = null; - } - return cachedConfigToken; -} - -function unwrapCause(err: unknown): unknown { - if (!err || typeof err !== "object") return null; - const cause = (err as { cause?: unknown }).cause; - return cause ?? null; +function isAbsoluteHttp(url: string): boolean { + return /^https?:\/\//i.test(url.trim()); } function enhanceBrowserFetchError(url: string, err: unknown, timeoutMs: number): Error { - const cause = unwrapCause(err); - const code = extractErrorCode(cause) ?? extractErrorCode(err) ?? ""; - - const hint = `Start (or restart) the Clawdbot gateway (Clawdbot.app menubar, or \`${formatCliCommand("clawdbot gateway")}\`) and try again.`; - - if (code === "ECONNREFUSED") { + const hint = isAbsoluteHttp(url) + ? "If this is a sandboxed session, ensure the sandbox browser is running and try again." + : `Start (or restart) the Clawdbot gateway (Clawdbot.app menubar, or \`${formatCliCommand("clawdbot gateway")}\`) and try again.`; + const msg = String(err); + const msgLower = msg.toLowerCase(); + const looksLikeTimeout = + msgLower.includes("timed out") || + msgLower.includes("timeout") || + msgLower.includes("aborted") || + msgLower.includes("abort") || + msgLower.includes("aborterror"); + if (looksLikeTimeout) { return new Error( - `Can't reach the clawd browser control server at ${url} (connection refused). ${hint}`, - ); - } - if (code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT") { - return new Error( - `Can't reach the clawd browser control server at ${url} (timed out after ${timeoutMs}ms). ${hint}`, + `Can't reach the clawd browser control service (timed out after ${timeoutMs}ms). ${hint}`, ); } + return new Error(`Can't reach the clawd browser control service. ${hint} (${msg})`); +} - const msg = formatErrorMessage(err); - if (msg.toLowerCase().includes("abort")) { - return new Error( - `Can't reach the clawd browser control server at ${url} (timed out after ${timeoutMs}ms). ${hint}`, - ); +async function fetchHttpJson( + url: string, + init: RequestInit & { timeoutMs?: number }, +): Promise { + const timeoutMs = init.timeoutMs ?? 5000; + const ctrl = new AbortController(); + const t = setTimeout(() => ctrl.abort(), timeoutMs); + try { + const res = await fetch(url, { ...init, signal: ctrl.signal }); + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(text || `HTTP ${res.status}`); + } + return (await res.json()) as T; + } finally { + clearTimeout(t); } - - return new Error(`Can't reach the clawd browser control server at ${url}. ${hint} (${msg})`); } export async function fetchBrowserJson( @@ -59,32 +53,58 @@ export async function fetchBrowserJson( init?: RequestInit & { timeoutMs?: number }, ): Promise { const timeoutMs = init?.timeoutMs ?? 5000; - const ctrl = new AbortController(); - const t = setTimeout(() => ctrl.abort(), timeoutMs); - let res: Response; try { - const token = getBrowserControlToken(); - const mergedHeaders = (() => { - if (!token) return init?.headers; - const h = new Headers(init?.headers ?? {}); - if (!h.has("Authorization")) { - h.set("Authorization", `Bearer ${token}`); + if (isAbsoluteHttp(url)) { + return await fetchHttpJson(url, { ...init, timeoutMs }); + } + const started = await startBrowserControlServiceFromConfig(); + if (!started) { + throw new Error("browser control disabled"); + } + const dispatcher = createBrowserRouteDispatcher(createBrowserControlContext()); + const parsed = new URL(url, "http://localhost"); + const query: Record = {}; + for (const [key, value] of parsed.searchParams.entries()) { + query[key] = value; + } + let body = init?.body; + if (typeof body === "string") { + try { + body = JSON.parse(body); + } catch { + // keep as string } - return h; - })(); - res = await fetch(url, { - ...init, - ...(mergedHeaders ? { headers: mergedHeaders } : {}), - signal: ctrl.signal, - } as RequestInit); + } + const dispatchPromise = dispatcher.dispatch({ + method: + init?.method?.toUpperCase() === "DELETE" + ? "DELETE" + : init?.method?.toUpperCase() === "POST" + ? "POST" + : "GET", + path: parsed.pathname, + query, + body, + }); + + const result = await (timeoutMs + ? Promise.race([ + dispatchPromise, + new Promise((_, reject) => + setTimeout(() => reject(new Error("timed out")), timeoutMs), + ), + ]) + : dispatchPromise); + + if (result.status >= 400) { + const message = + result.body && typeof result.body === "object" && "error" in result.body + ? String((result.body as { error?: unknown }).error) + : `HTTP ${result.status}`; + throw new Error(message); + } + return result.body as T; } catch (err) { throw enhanceBrowserFetchError(url, err, timeoutMs); - } finally { - clearTimeout(t); } - if (!res.ok) { - const text = await res.text().catch(() => ""); - throw new Error(text ? `${res.status}: ${text}` : `HTTP ${res.status}`); - } - return (await res.json()) as T; } diff --git a/src/browser/client.test.ts b/src/browser/client.test.ts index 7721828f8..848c53d18 100644 --- a/src/browser/client.test.ts +++ b/src/browser/client.test.ts @@ -16,7 +16,7 @@ describe("browser client", () => { vi.unstubAllGlobals(); }); - it("wraps connection failures with a gateway hint", async () => { + it("wraps connection failures with a sandbox hint", async () => { const refused = Object.assign(new Error("connect ECONNREFUSED 127.0.0.1"), { code: "ECONNREFUSED", }); @@ -26,7 +26,7 @@ describe("browser client", () => { vi.stubGlobal("fetch", vi.fn().mockRejectedValue(fetchFailed)); - await expect(browserStatus("http://127.0.0.1:18791")).rejects.toThrow(/Start .*gateway/i); + await expect(browserStatus("http://127.0.0.1:18791")).rejects.toThrow(/sandboxed session/i); }); it("adds useful timeout messaging for abort-like failures", async () => { @@ -34,41 +34,6 @@ describe("browser client", () => { await expect(browserStatus("http://127.0.0.1:18791")).rejects.toThrow(/timed out/i); }); - it("adds Authorization when CLAWDBOT_BROWSER_CONTROL_TOKEN is set", async () => { - const prev = process.env.CLAWDBOT_BROWSER_CONTROL_TOKEN; - process.env.CLAWDBOT_BROWSER_CONTROL_TOKEN = "t1"; - - const calls: Array<{ url: string; init?: RequestInit }> = []; - vi.stubGlobal( - "fetch", - vi.fn(async (url: string, init?: RequestInit) => { - calls.push({ url, init }); - return { - ok: true, - json: async () => ({ - enabled: true, - controlUrl: "http://127.0.0.1:18791", - running: false, - pid: null, - cdpPort: 18792, - chosenBrowser: null, - userDataDir: null, - color: "#FF0000", - headless: true, - attachOnly: false, - }), - } as unknown as Response; - }), - ); - - await browserStatus("http://127.0.0.1:18791"); - const init = calls[0]?.init; - const auth = new Headers(init?.headers ?? {}).get("Authorization"); - expect(auth).toBe("Bearer t1"); - - process.env.CLAWDBOT_BROWSER_CONTROL_TOKEN = prev; - }); - it("surfaces non-2xx responses with body text", async () => { vi.stubGlobal( "fetch", @@ -81,7 +46,7 @@ describe("browser client", () => { await expect( browserSnapshot("http://127.0.0.1:18791", { format: "aria", limit: 1 }), - ).rejects.toThrow(/409: conflict/i); + ).rejects.toThrow(/conflict/i); }); it("adds labels + efficient mode query params to snapshots", async () => { @@ -255,7 +220,6 @@ describe("browser client", () => { ok: true, json: async () => ({ enabled: true, - controlUrl: "http://127.0.0.1:18791", running: true, pid: 1, cdpPort: 18792, diff --git a/src/browser/client.ts b/src/browser/client.ts index eb98e8138..fa8cf2d69 100644 --- a/src/browser/client.ts +++ b/src/browser/client.ts @@ -1,10 +1,7 @@ -import { loadConfig } from "../config/config.js"; import { fetchBrowserJson } from "./client-fetch.js"; -import { resolveBrowserConfig } from "./config.js"; export type BrowserStatus = { enabled: boolean; - controlUrl: string; profile?: string; running: boolean; cdpReady?: boolean; @@ -89,59 +86,64 @@ export type SnapshotResult = imageType?: "png" | "jpeg"; }; -export function resolveBrowserControlUrl(overrideUrl?: string) { - const cfg = loadConfig(); - const resolved = resolveBrowserConfig(cfg.browser); - const url = overrideUrl?.trim() ? overrideUrl.trim() : resolved.controlUrl; - return url.replace(/\/$/, ""); -} - function buildProfileQuery(profile?: string): string { return profile ? `?profile=${encodeURIComponent(profile)}` : ""; } +function withBaseUrl(baseUrl: string | undefined, path: string): string { + const trimmed = baseUrl?.trim(); + if (!trimmed) return path; + return `${trimmed.replace(/\/$/, "")}${path}`; +} + export async function browserStatus( - baseUrl: string, + baseUrl?: string, opts?: { profile?: string }, ): Promise { const q = buildProfileQuery(opts?.profile); - return await fetchBrowserJson(`${baseUrl}/${q}`, { + return await fetchBrowserJson(withBaseUrl(baseUrl, `/${q}`), { timeoutMs: 1500, }); } -export async function browserProfiles(baseUrl: string): Promise { - const res = await fetchBrowserJson<{ profiles: ProfileStatus[] }>(`${baseUrl}/profiles`, { - timeoutMs: 3000, - }); +export async function browserProfiles(baseUrl?: string): Promise { + const res = await fetchBrowserJson<{ profiles: ProfileStatus[] }>( + withBaseUrl(baseUrl, `/profiles`), + { + timeoutMs: 3000, + }, + ); return res.profiles ?? []; } -export async function browserStart(baseUrl: string, opts?: { profile?: string }): Promise { +export async function browserStart(baseUrl?: string, opts?: { profile?: string }): Promise { const q = buildProfileQuery(opts?.profile); - await fetchBrowserJson(`${baseUrl}/start${q}`, { + await fetchBrowserJson(withBaseUrl(baseUrl, `/start${q}`), { method: "POST", timeoutMs: 15000, }); } -export async function browserStop(baseUrl: string, opts?: { profile?: string }): Promise { +export async function browserStop(baseUrl?: string, opts?: { profile?: string }): Promise { const q = buildProfileQuery(opts?.profile); - await fetchBrowserJson(`${baseUrl}/stop${q}`, { + await fetchBrowserJson(withBaseUrl(baseUrl, `/stop${q}`), { method: "POST", timeoutMs: 15000, }); } export async function browserResetProfile( - baseUrl: string, + baseUrl?: string, opts?: { profile?: string }, ): Promise { const q = buildProfileQuery(opts?.profile); - return await fetchBrowserJson(`${baseUrl}/reset-profile${q}`, { - method: "POST", - timeoutMs: 20000, - }); + return await fetchBrowserJson( + withBaseUrl(baseUrl, `/reset-profile${q}`), + { + method: "POST", + timeoutMs: 20000, + }, + ); } export type BrowserCreateProfileResult = { @@ -154,7 +156,7 @@ export type BrowserCreateProfileResult = { }; export async function browserCreateProfile( - baseUrl: string, + baseUrl: string | undefined, opts: { name: string; color?: string; @@ -162,17 +164,20 @@ export async function browserCreateProfile( driver?: "clawd" | "extension"; }, ): Promise { - return await fetchBrowserJson(`${baseUrl}/profiles/create`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - name: opts.name, - color: opts.color, - cdpUrl: opts.cdpUrl, - driver: opts.driver, - }), - timeoutMs: 10000, - }); + return await fetchBrowserJson( + withBaseUrl(baseUrl, `/profiles/create`), + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: opts.name, + color: opts.color, + cdpUrl: opts.cdpUrl, + driver: opts.driver, + }), + timeoutMs: 10000, + }, + ); } export type BrowserDeleteProfileResult = { @@ -182,11 +187,11 @@ export type BrowserDeleteProfileResult = { }; export async function browserDeleteProfile( - baseUrl: string, + baseUrl: string | undefined, profile: string, ): Promise { return await fetchBrowserJson( - `${baseUrl}/profiles/${encodeURIComponent(profile)}`, + withBaseUrl(baseUrl, `/profiles/${encodeURIComponent(profile)}`), { method: "DELETE", timeoutMs: 20000, @@ -195,24 +200,24 @@ export async function browserDeleteProfile( } export async function browserTabs( - baseUrl: string, + baseUrl?: string, opts?: { profile?: string }, ): Promise { const q = buildProfileQuery(opts?.profile); const res = await fetchBrowserJson<{ running: boolean; tabs: BrowserTab[] }>( - `${baseUrl}/tabs${q}`, + withBaseUrl(baseUrl, `/tabs${q}`), { timeoutMs: 3000 }, ); return res.tabs ?? []; } export async function browserOpenTab( - baseUrl: string, + baseUrl: string | undefined, url: string, opts?: { profile?: string }, ): Promise { const q = buildProfileQuery(opts?.profile); - return await fetchBrowserJson(`${baseUrl}/tabs/open${q}`, { + return await fetchBrowserJson(withBaseUrl(baseUrl, `/tabs/open${q}`), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url }), @@ -221,12 +226,12 @@ export async function browserOpenTab( } export async function browserFocusTab( - baseUrl: string, + baseUrl: string | undefined, targetId: string, opts?: { profile?: string }, ): Promise { const q = buildProfileQuery(opts?.profile); - await fetchBrowserJson(`${baseUrl}/tabs/focus${q}`, { + await fetchBrowserJson(withBaseUrl(baseUrl, `/tabs/focus${q}`), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ targetId }), @@ -235,19 +240,19 @@ export async function browserFocusTab( } export async function browserCloseTab( - baseUrl: string, + baseUrl: string | undefined, targetId: string, opts?: { profile?: string }, ): Promise { const q = buildProfileQuery(opts?.profile); - await fetchBrowserJson(`${baseUrl}/tabs/${encodeURIComponent(targetId)}${q}`, { + await fetchBrowserJson(withBaseUrl(baseUrl, `/tabs/${encodeURIComponent(targetId)}${q}`), { method: "DELETE", timeoutMs: 5000, }); } export async function browserTabAction( - baseUrl: string, + baseUrl: string | undefined, opts: { action: "list" | "new" | "close" | "select"; index?: number; @@ -255,7 +260,7 @@ export async function browserTabAction( }, ): Promise { const q = buildProfileQuery(opts.profile); - return await fetchBrowserJson(`${baseUrl}/tabs/action${q}`, { + return await fetchBrowserJson(withBaseUrl(baseUrl, `/tabs/action${q}`), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -267,7 +272,7 @@ export async function browserTabAction( } export async function browserSnapshot( - baseUrl: string, + baseUrl: string | undefined, opts: { format: "aria" | "ai"; targetId?: string; @@ -301,7 +306,7 @@ export async function browserSnapshot( if (opts.labels === true) q.set("labels", "1"); if (opts.mode) q.set("mode", opts.mode); if (opts.profile) q.set("profile", opts.profile); - return await fetchBrowserJson(`${baseUrl}/snapshot?${q.toString()}`, { + return await fetchBrowserJson(withBaseUrl(baseUrl, `/snapshot?${q.toString()}`), { timeoutMs: 20000, }); } diff --git a/src/browser/config.test.ts b/src/browser/config.test.ts index 0304896dc..136846059 100644 --- a/src/browser/config.test.ts +++ b/src/browser/config.test.ts @@ -2,13 +2,14 @@ import { describe, expect, it } from "vitest"; import { resolveBrowserConfig, resolveProfile, shouldStartLocalBrowserServer } from "./config.js"; describe("browser config", () => { - it("defaults to enabled with loopback control url and lobster-orange color", () => { + it("defaults to enabled with loopback defaults and lobster-orange color", () => { const resolved = resolveBrowserConfig(undefined); expect(resolved.enabled).toBe(true); expect(resolved.controlPort).toBe(18791); - expect(resolved.controlHost).toBe("127.0.0.1"); expect(resolved.color).toBe("#FF4500"); expect(shouldStartLocalBrowserServer(resolved)).toBe(true); + expect(resolved.cdpHost).toBe("127.0.0.1"); + expect(resolved.cdpProtocol).toBe("http"); const profile = resolveProfile(resolved, resolved.defaultProfile); expect(profile?.name).toBe("chrome"); expect(profile?.driver).toBe("extension"); @@ -46,9 +47,31 @@ describe("browser config", () => { } }); + it("derives default ports from gateway.port when env is unset", () => { + const prev = process.env.CLAWDBOT_GATEWAY_PORT; + delete process.env.CLAWDBOT_GATEWAY_PORT; + try { + const resolved = resolveBrowserConfig(undefined, { gateway: { port: 19011 } }); + expect(resolved.controlPort).toBe(19013); + const chrome = resolveProfile(resolved, "chrome"); + expect(chrome?.driver).toBe("extension"); + expect(chrome?.cdpPort).toBe(19014); + expect(chrome?.cdpUrl).toBe("http://127.0.0.1:19014"); + + const clawd = resolveProfile(resolved, "clawd"); + expect(clawd?.cdpPort).toBe(19022); + expect(clawd?.cdpUrl).toBe("http://127.0.0.1:19022"); + } finally { + if (prev === undefined) { + delete process.env.CLAWDBOT_GATEWAY_PORT; + } else { + process.env.CLAWDBOT_GATEWAY_PORT = prev; + } + } + }); + it("normalizes hex colors", () => { const resolved = resolveBrowserConfig({ - controlUrl: "http://localhost:18791", color: "ff4500", }); expect(resolved.color).toBe("#FF4500"); @@ -56,7 +79,6 @@ describe("browser config", () => { it("supports custom remote CDP timeouts", () => { const resolved = resolveBrowserConfig({ - controlUrl: "http://127.0.0.1:18791", remoteCdpTimeoutMs: 2200, remoteCdpHandshakeTimeoutMs: 5000, }); @@ -66,31 +88,21 @@ describe("browser config", () => { it("falls back to default color for invalid hex", () => { const resolved = resolveBrowserConfig({ - controlUrl: "http://localhost:18791", color: "#GGGGGG", }); expect(resolved.color).toBe("#FF4500"); }); - it("treats non-loopback control urls as remote", () => { + it("treats non-loopback cdpUrl as remote", () => { const resolved = resolveBrowserConfig({ - controlUrl: "http://example.com:18791", + cdpUrl: "http://example.com:9222", }); - expect(shouldStartLocalBrowserServer(resolved)).toBe(false); - }); - - it("derives CDP host/protocol from control url when cdpUrl is unset", () => { - const resolved = resolveBrowserConfig({ - controlUrl: "http://127.0.0.1:19000", - }); - expect(resolved.controlPort).toBe(19000); - expect(resolved.cdpHost).toBe("127.0.0.1"); - expect(resolved.cdpProtocol).toBe("http"); + const profile = resolveProfile(resolved, "clawd"); + expect(profile?.cdpIsLoopback).toBe(false); }); it("supports explicit CDP URLs for the default profile", () => { const resolved = resolveBrowserConfig({ - controlUrl: "http://127.0.0.1:18791", cdpUrl: "http://example.com:9222", }); const profile = resolveProfile(resolved, "clawd"); @@ -101,7 +113,6 @@ describe("browser config", () => { it("uses profile cdpUrl when provided", () => { const resolved = resolveBrowserConfig({ - controlUrl: "http://127.0.0.1:18791", profiles: { remote: { cdpUrl: "http://10.0.0.42:9222", color: "#0066CC" }, }, @@ -115,7 +126,6 @@ describe("browser config", () => { it("uses base protocol for profiles with only cdpPort", () => { const resolved = resolveBrowserConfig({ - controlUrl: "http://127.0.0.1:18791", cdpUrl: "https://example.com:9443", profiles: { work: { cdpPort: 18801, color: "#0066CC" }, @@ -127,14 +137,11 @@ describe("browser config", () => { }); it("rejects unsupported protocols", () => { - expect(() => resolveBrowserConfig({ controlUrl: "ws://127.0.0.1:18791" })).toThrow( - /must be http/i, - ); + expect(() => resolveBrowserConfig({ cdpUrl: "ws://127.0.0.1:18791" })).toThrow(/must be http/i); }); it("does not add the built-in chrome extension profile if the derived relay port is already used", () => { const resolved = resolveBrowserConfig({ - controlUrl: "http://127.0.0.1:18791", profiles: { clawd: { cdpPort: 18792, color: "#FF4500" }, }, diff --git a/src/browser/config.ts b/src/browser/config.ts index e8e1bb128..02ab0e0a1 100644 --- a/src/browser/config.ts +++ b/src/browser/config.ts @@ -1,12 +1,14 @@ -import type { BrowserConfig, BrowserProfileConfig } from "../config/config.js"; +import type { BrowserConfig, BrowserProfileConfig, ClawdbotConfig } from "../config/config.js"; import { deriveDefaultBrowserCdpPortRange, deriveDefaultBrowserControlPort, + DEFAULT_BROWSER_CONTROL_PORT, } from "../config/port-defaults.js"; +import { resolveGatewayPort } from "../config/paths.js"; import { DEFAULT_CLAWD_BROWSER_COLOR, - DEFAULT_CLAWD_BROWSER_CONTROL_URL, DEFAULT_CLAWD_BROWSER_ENABLED, + DEFAULT_BROWSER_EVALUATE_ENABLED, DEFAULT_BROWSER_DEFAULT_PROFILE_NAME, DEFAULT_CLAWD_BROWSER_PROFILE_NAME, } from "./constants.js"; @@ -14,10 +16,8 @@ import { CDP_PORT_RANGE_START, getUsedPorts } from "./profiles.js"; export type ResolvedBrowserConfig = { enabled: boolean; - controlUrl: string; - controlHost: string; + evaluateEnabled: boolean; controlPort: number; - controlToken?: string; cdpProtocol: "http" | "https"; cdpHost: string; cdpIsLoopback: boolean; @@ -137,24 +137,14 @@ function ensureDefaultChromeExtensionProfile( }; return result; } -export function resolveBrowserConfig(cfg: BrowserConfig | undefined): ResolvedBrowserConfig { +export function resolveBrowserConfig( + cfg: BrowserConfig | undefined, + rootConfig?: ClawdbotConfig, +): ResolvedBrowserConfig { const enabled = cfg?.enabled ?? DEFAULT_CLAWD_BROWSER_ENABLED; - const envControlUrl = process.env.CLAWDBOT_BROWSER_CONTROL_URL?.trim(); - const controlToken = cfg?.controlToken?.trim() || undefined; - const derivedControlPort = (() => { - const raw = process.env.CLAWDBOT_GATEWAY_PORT?.trim(); - if (!raw) return null; - const gatewayPort = Number.parseInt(raw, 10); - if (!Number.isFinite(gatewayPort) || gatewayPort <= 0) return null; - return deriveDefaultBrowserControlPort(gatewayPort); - })(); - const derivedControlUrl = derivedControlPort ? `http://127.0.0.1:${derivedControlPort}` : null; - - const controlInfo = parseHttpUrl( - cfg?.controlUrl ?? envControlUrl ?? derivedControlUrl ?? DEFAULT_CLAWD_BROWSER_CONTROL_URL, - "browser.controlUrl", - ); - const controlPort = controlInfo.port; + const evaluateEnabled = cfg?.evaluateEnabled ?? DEFAULT_BROWSER_EVALUATE_ENABLED; + const gatewayPort = resolveGatewayPort(rootConfig); + const controlPort = deriveDefaultBrowserControlPort(gatewayPort ?? DEFAULT_BROWSER_CONTROL_PORT); const defaultColor = normalizeHexColor(cfg?.color); const remoteCdpTimeoutMs = normalizeTimeoutMs(cfg?.remoteCdpTimeoutMs, 1500); const remoteCdpHandshakeTimeoutMs = normalizeTimeoutMs( @@ -178,11 +168,10 @@ export function resolveBrowserConfig(cfg: BrowserConfig | undefined): ResolvedBr const derivedPort = controlPort + 1; if (derivedPort > 65535) { throw new Error( - `browser.controlUrl port (${controlPort}) is too high; cannot derive CDP port (${derivedPort})`, + `Derived CDP port (${derivedPort}) is too high; check gateway port configuration.`, ); } - const derived = new URL(controlInfo.normalized); - derived.port = String(derivedPort); + const derived = new URL(`http://127.0.0.1:${derivedPort}`); cdpInfo = { parsed: derived, port: derivedPort, @@ -211,10 +200,8 @@ export function resolveBrowserConfig(cfg: BrowserConfig | undefined): ResolvedBr return { enabled, - controlUrl: controlInfo.normalized, - controlHost: controlInfo.parsed.hostname, + evaluateEnabled, controlPort, - ...(controlToken ? { controlToken } : {}), cdpProtocol, cdpHost: cdpInfo.parsed.hostname, cdpIsLoopback: isLoopbackHost(cdpInfo.parsed.hostname), @@ -269,6 +256,6 @@ export function resolveProfile( }; } -export function shouldStartLocalBrowserServer(resolved: ResolvedBrowserConfig) { - return isLoopbackHost(resolved.controlHost); +export function shouldStartLocalBrowserServer(_resolved: ResolvedBrowserConfig) { + return true; } diff --git a/src/browser/constants.ts b/src/browser/constants.ts index 3e8cb60d2..e06a7dff8 100644 --- a/src/browser/constants.ts +++ b/src/browser/constants.ts @@ -1,5 +1,5 @@ export const DEFAULT_CLAWD_BROWSER_ENABLED = true; -export const DEFAULT_CLAWD_BROWSER_CONTROL_URL = "http://127.0.0.1:18791"; +export const DEFAULT_BROWSER_EVALUATE_ENABLED = true; export const DEFAULT_CLAWD_BROWSER_COLOR = "#FF4500"; export const DEFAULT_CLAWD_BROWSER_PROFILE_NAME = "clawd"; export const DEFAULT_BROWSER_DEFAULT_PROFILE_NAME = "chrome"; diff --git a/src/browser/control-service.ts b/src/browser/control-service.ts new file mode 100644 index 000000000..6c8f3cef2 --- /dev/null +++ b/src/browser/control-service.ts @@ -0,0 +1,80 @@ +import { loadConfig } from "../config/config.js"; +import { createSubsystemLogger } from "../logging/subsystem.js"; +import { resolveBrowserConfig, resolveProfile } from "./config.js"; +import { ensureChromeExtensionRelayServer } from "./extension-relay.js"; +import { type BrowserServerState, createBrowserRouteContext } from "./server-context.js"; + +let state: BrowserServerState | null = null; +const log = createSubsystemLogger("browser"); +const logService = log.child("service"); + +export function getBrowserControlState(): BrowserServerState | null { + return state; +} + +export function createBrowserControlContext() { + return createBrowserRouteContext({ + getState: () => state, + }); +} + +export async function startBrowserControlServiceFromConfig(): Promise { + if (state) return state; + + const cfg = loadConfig(); + const resolved = resolveBrowserConfig(cfg.browser, cfg); + if (!resolved.enabled) return null; + + state = { + server: null, + port: resolved.controlPort, + resolved, + profiles: new Map(), + }; + + // If any profile uses the Chrome extension relay, start the local relay server eagerly + // so the extension can connect before the first browser action. + for (const name of Object.keys(resolved.profiles)) { + const profile = resolveProfile(resolved, name); + if (!profile || profile.driver !== "extension") continue; + await ensureChromeExtensionRelayServer({ cdpUrl: profile.cdpUrl }).catch((err) => { + logService.warn(`Chrome extension relay init failed for profile "${name}": ${String(err)}`); + }); + } + + logService.info( + `Browser control service ready (profiles=${Object.keys(resolved.profiles).length})`, + ); + return state; +} + +export async function stopBrowserControlService(): Promise { + const current = state; + if (!current) return; + + const ctx = createBrowserRouteContext({ + getState: () => state, + }); + + try { + for (const name of Object.keys(current.resolved.profiles)) { + try { + await ctx.forProfile(name).stopRunningBrowser(); + } catch { + // ignore + } + } + } catch (err) { + logService.warn(`clawd browser stop failed: ${String(err)}`); + } + + state = null; + + // Optional: Playwright is not always available (e.g. embedded gateway builds). + try { + const mod = await import("./pw-ai.js"); + await mod.closePlaywrightBrowserConnection(); + } catch { + // ignore + } +} diff --git a/src/browser/profiles-service.test.ts b/src/browser/profiles-service.test.ts index 4092cf655..1daeec2de 100644 --- a/src/browser/profiles-service.test.ts +++ b/src/browser/profiles-service.test.ts @@ -49,9 +49,7 @@ function createCtx(resolved: BrowserServerState["resolved"]) { describe("BrowserProfilesService", () => { it("allocates next local port for new profiles", async () => { - const resolved = resolveBrowserConfig({ - controlUrl: "http://127.0.0.1:18791", - }); + const resolved = resolveBrowserConfig({}); const { ctx, state } = createCtx(resolved); vi.mocked(loadConfig).mockReturnValue({ browser: { profiles: {} } }); @@ -66,9 +64,7 @@ describe("BrowserProfilesService", () => { }); it("accepts per-profile cdpUrl for remote Chrome", async () => { - const resolved = resolveBrowserConfig({ - controlUrl: "http://127.0.0.1:18791", - }); + const resolved = resolveBrowserConfig({}); const { ctx } = createCtx(resolved); vi.mocked(loadConfig).mockReturnValue({ browser: { profiles: {} } }); @@ -97,7 +93,6 @@ describe("BrowserProfilesService", () => { it("deletes remote profiles without stopping or removing local data", async () => { const resolved = resolveBrowserConfig({ - controlUrl: "http://127.0.0.1:18791", profiles: { remote: { cdpUrl: "http://10.0.0.42:9222", color: "#0066CC" }, }, @@ -124,7 +119,6 @@ describe("BrowserProfilesService", () => { it("deletes local profiles and moves data to Trash", async () => { const resolved = resolveBrowserConfig({ - controlUrl: "http://127.0.0.1:18791", profiles: { work: { cdpPort: 18801, color: "#0066CC" }, }, diff --git a/src/browser/routes/agent.act.ts b/src/browser/routes/agent.act.ts index ea681bd25..ee1242a4a 100644 --- a/src/browser/routes/agent.act.ts +++ b/src/browser/routes/agent.act.ts @@ -1,5 +1,3 @@ -import type express from "express"; - import type { BrowserFormField } from "../client-actions-core.js"; import type { BrowserRouteContext } from "../server-context.js"; import { @@ -16,8 +14,12 @@ import { SELECTOR_UNSUPPORTED_MESSAGE, } from "./agent.shared.js"; import { jsonError, toBoolean, toNumber, toStringArray, toStringOrEmpty } from "./utils.js"; +import type { BrowserRouteRegistrar } from "./types.js"; -export function registerBrowserAgentActRoutes(app: express.Express, ctx: BrowserRouteContext) { +export function registerBrowserAgentActRoutes( + app: BrowserRouteRegistrar, + ctx: BrowserRouteContext, +) { app.post("/act", async (req, res) => { const profileCtx = resolveProfileContext(req, res, ctx); if (!profileCtx) return; @@ -37,6 +39,7 @@ export function registerBrowserAgentActRoutes(app: express.Express, ctx: Browser const cdpUrl = profileCtx.profile.cdpUrl; const pw = await requirePwAi(res, `act:${kind}`); if (!pw) return; + const evaluateEnabled = ctx.state().resolved.evaluateEnabled; switch (kind) { case "click": { @@ -208,6 +211,16 @@ export function registerBrowserAgentActRoutes(app: express.Express, ctx: Browser : undefined; const fn = toStringOrEmpty(body.fn) || undefined; const timeoutMs = toNumber(body.timeoutMs) ?? undefined; + if (fn && !evaluateEnabled) { + return jsonError( + res, + 403, + [ + "wait --fn is disabled by config (browser.evaluateEnabled=false).", + "Docs: /gateway/configuration#browser-clawd-managed-browser", + ].join("\n"), + ); + } if ( timeMs === undefined && !text && @@ -238,6 +251,16 @@ export function registerBrowserAgentActRoutes(app: express.Express, ctx: Browser return res.json({ ok: true, targetId: tab.targetId }); } case "evaluate": { + if (!evaluateEnabled) { + return jsonError( + res, + 403, + [ + "act:evaluate is disabled by config (browser.evaluateEnabled=false).", + "Docs: /gateway/configuration#browser-clawd-managed-browser", + ].join("\n"), + ); + } const fn = toStringOrEmpty(body.fn); if (!fn) return jsonError(res, 400, "fn is required"); const ref = toStringOrEmpty(body.ref) || undefined; diff --git a/src/browser/routes/agent.debug.ts b/src/browser/routes/agent.debug.ts index c71bd7b47..2533474f7 100644 --- a/src/browser/routes/agent.debug.ts +++ b/src/browser/routes/agent.debug.ts @@ -2,13 +2,15 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; -import type express from "express"; - import type { BrowserRouteContext } from "../server-context.js"; import { handleRouteError, readBody, requirePwAi, resolveProfileContext } from "./agent.shared.js"; import { toBoolean, toStringOrEmpty } from "./utils.js"; +import type { BrowserRouteRegistrar } from "./types.js"; -export function registerBrowserAgentDebugRoutes(app: express.Express, ctx: BrowserRouteContext) { +export function registerBrowserAgentDebugRoutes( + app: BrowserRouteRegistrar, + ctx: BrowserRouteContext, +) { app.get("/console", async (req, res) => { const profileCtx = resolveProfileContext(req, res, ctx); if (!profileCtx) return; diff --git a/src/browser/routes/agent.shared.ts b/src/browser/routes/agent.shared.ts index da8c608e3..08b010149 100644 --- a/src/browser/routes/agent.shared.ts +++ b/src/browser/routes/agent.shared.ts @@ -1,9 +1,8 @@ -import type express from "express"; - import type { BrowserRouteContext, ProfileContext } from "../server-context.js"; import type { PwAiModule } from "../pw-ai-module.js"; import { getPwAiModule as getPwAiModuleBase } from "../pw-ai-module.js"; import { getProfileContext, jsonError } from "./utils.js"; +import type { BrowserRequest, BrowserResponse } from "./types.js"; export const SELECTOR_UNSUPPORTED_MESSAGE = [ "Error: 'selector' is not supported. Use 'ref' from snapshot instead.", @@ -15,21 +14,21 @@ export const SELECTOR_UNSUPPORTED_MESSAGE = [ "This is more reliable for modern SPAs.", ].join("\n"); -export function readBody(req: express.Request): Record { +export function readBody(req: BrowserRequest): Record { const body = req.body as Record | undefined; if (!body || typeof body !== "object" || Array.isArray(body)) return {}; return body; } -export function handleRouteError(ctx: BrowserRouteContext, res: express.Response, err: unknown) { +export function handleRouteError(ctx: BrowserRouteContext, res: BrowserResponse, err: unknown) { const mapped = ctx.mapTabError(err); if (mapped) return jsonError(res, mapped.status, mapped.message); jsonError(res, 500, String(err)); } export function resolveProfileContext( - req: express.Request, - res: express.Response, + req: BrowserRequest, + res: BrowserResponse, ctx: BrowserRouteContext, ): ProfileContext | null { const profileCtx = getProfileContext(req, ctx); @@ -45,7 +44,7 @@ export async function getPwAiModule(): Promise { } export async function requirePwAi( - res: express.Response, + res: BrowserResponse, feature: string, ): Promise { const mod = await getPwAiModule(); diff --git a/src/browser/routes/agent.snapshot.ts b/src/browser/routes/agent.snapshot.ts index fdeb7f69e..41ade72ff 100644 --- a/src/browser/routes/agent.snapshot.ts +++ b/src/browser/routes/agent.snapshot.ts @@ -1,7 +1,5 @@ import path from "node:path"; -import type express from "express"; - import { ensureMediaDir, saveMediaBuffer } from "../../media/store.js"; import { captureScreenshot, snapshotAria } from "../cdp.js"; import { @@ -23,8 +21,12 @@ import { resolveProfileContext, } from "./agent.shared.js"; import { jsonError, toBoolean, toNumber, toStringOrEmpty } from "./utils.js"; +import type { BrowserRouteRegistrar } from "./types.js"; -export function registerBrowserAgentSnapshotRoutes(app: express.Express, ctx: BrowserRouteContext) { +export function registerBrowserAgentSnapshotRoutes( + app: BrowserRouteRegistrar, + ctx: BrowserRouteContext, +) { app.post("/navigate", async (req, res) => { const profileCtx = resolveProfileContext(req, res, ctx); if (!profileCtx) return; diff --git a/src/browser/routes/agent.storage.ts b/src/browser/routes/agent.storage.ts index 1f5c137b6..2f1905c22 100644 --- a/src/browser/routes/agent.storage.ts +++ b/src/browser/routes/agent.storage.ts @@ -1,10 +1,12 @@ -import type express from "express"; - import type { BrowserRouteContext } from "../server-context.js"; import { handleRouteError, readBody, requirePwAi, resolveProfileContext } from "./agent.shared.js"; import { jsonError, toBoolean, toNumber, toStringOrEmpty } from "./utils.js"; +import type { BrowserRouteRegistrar } from "./types.js"; -export function registerBrowserAgentStorageRoutes(app: express.Express, ctx: BrowserRouteContext) { +export function registerBrowserAgentStorageRoutes( + app: BrowserRouteRegistrar, + ctx: BrowserRouteContext, +) { app.get("/cookies", async (req, res) => { const profileCtx = resolveProfileContext(req, res, ctx); if (!profileCtx) return; diff --git a/src/browser/routes/agent.ts b/src/browser/routes/agent.ts index b2cf4950c..dc5e65433 100644 --- a/src/browser/routes/agent.ts +++ b/src/browser/routes/agent.ts @@ -1,12 +1,11 @@ -import type express from "express"; - import type { BrowserRouteContext } from "../server-context.js"; import { registerBrowserAgentActRoutes } from "./agent.act.js"; import { registerBrowserAgentDebugRoutes } from "./agent.debug.js"; import { registerBrowserAgentSnapshotRoutes } from "./agent.snapshot.js"; import { registerBrowserAgentStorageRoutes } from "./agent.storage.js"; +import type { BrowserRouteRegistrar } from "./types.js"; -export function registerBrowserAgentRoutes(app: express.Express, ctx: BrowserRouteContext) { +export function registerBrowserAgentRoutes(app: BrowserRouteRegistrar, ctx: BrowserRouteContext) { registerBrowserAgentSnapshotRoutes(app, ctx); registerBrowserAgentActRoutes(app, ctx); registerBrowserAgentDebugRoutes(app, ctx); diff --git a/src/browser/routes/basic.ts b/src/browser/routes/basic.ts index 4b1fcab5c..32be15704 100644 --- a/src/browser/routes/basic.ts +++ b/src/browser/routes/basic.ts @@ -1,11 +1,10 @@ -import type express from "express"; - import { resolveBrowserExecutableForPlatform } from "../chrome.executables.js"; import { createBrowserProfilesService } from "../profiles-service.js"; import type { BrowserRouteContext } from "../server-context.js"; import { getProfileContext, jsonError, toStringOrEmpty } from "./utils.js"; +import type { BrowserRouteRegistrar } from "./types.js"; -export function registerBrowserBasicRoutes(app: express.Express, ctx: BrowserRouteContext) { +export function registerBrowserBasicRoutes(app: BrowserRouteRegistrar, ctx: BrowserRouteContext) { // List all profiles with their status app.get("/profiles", async (_req, res) => { try { @@ -53,7 +52,6 @@ export function registerBrowserBasicRoutes(app: express.Express, ctx: BrowserRou res.json({ enabled: current.resolved.enabled, - controlUrl: current.resolved.controlUrl, profile: profileCtx.profile.name, running: cdpReady, cdpReady, diff --git a/src/browser/routes/dispatcher.ts b/src/browser/routes/dispatcher.ts new file mode 100644 index 000000000..d5a26598d --- /dev/null +++ b/src/browser/routes/dispatcher.ts @@ -0,0 +1,122 @@ +import type { BrowserRouteContext } from "../server-context.js"; +import { registerBrowserRoutes } from "./index.js"; +import type { BrowserRequest, BrowserResponse, BrowserRouteRegistrar } from "./types.js"; + +type BrowserDispatchRequest = { + method: "GET" | "POST" | "DELETE"; + path: string; + query?: Record; + body?: unknown; +}; + +type BrowserDispatchResponse = { + status: number; + body: unknown; +}; + +type RouteEntry = { + method: BrowserDispatchRequest["method"]; + path: string; + regex: RegExp; + paramNames: string[]; + handler: (req: BrowserRequest, res: BrowserResponse) => void | Promise; +}; + +function escapeRegex(value: string) { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function compileRoute(path: string): { regex: RegExp; paramNames: string[] } { + const paramNames: string[] = []; + const parts = path.split("/").map((part) => { + if (part.startsWith(":")) { + const name = part.slice(1); + paramNames.push(name); + return "([^/]+)"; + } + return escapeRegex(part); + }); + return { regex: new RegExp(`^${parts.join("/")}$`), paramNames }; +} + +function createRegistry() { + const routes: RouteEntry[] = []; + const register = + (method: RouteEntry["method"]) => (path: string, handler: RouteEntry["handler"]) => { + const { regex, paramNames } = compileRoute(path); + routes.push({ method, path, regex, paramNames, handler }); + }; + const router: BrowserRouteRegistrar = { + get: register("GET"), + post: register("POST"), + delete: register("DELETE"), + }; + return { routes, router }; +} + +function normalizePath(path: string) { + if (!path) return "/"; + return path.startsWith("/") ? path : `/${path}`; +} + +export function createBrowserRouteDispatcher(ctx: BrowserRouteContext) { + const registry = createRegistry(); + registerBrowserRoutes(registry.router, ctx); + + return { + dispatch: async (req: BrowserDispatchRequest): Promise => { + const method = req.method; + const path = normalizePath(req.path); + const query = req.query ?? {}; + const body = req.body; + + const match = registry.routes.find((route) => { + if (route.method !== method) return false; + return route.regex.test(path); + }); + if (!match) { + return { status: 404, body: { error: "Not Found" } }; + } + + const exec = match.regex.exec(path); + const params: Record = {}; + if (exec) { + for (const [idx, name] of match.paramNames.entries()) { + const value = exec[idx + 1]; + if (typeof value === "string") { + params[name] = decodeURIComponent(value); + } + } + } + + let status = 200; + let payload: unknown = undefined; + const res: BrowserResponse = { + status(code) { + status = code; + return res; + }, + json(bodyValue) { + payload = bodyValue; + }, + }; + + try { + await match.handler( + { + params, + query, + body, + }, + res, + ); + } catch (err) { + return { status: 500, body: { error: String(err) } }; + } + + return { status, body: payload }; + }, + }; +} + +export type { BrowserDispatchRequest, BrowserDispatchResponse }; diff --git a/src/browser/routes/index.ts b/src/browser/routes/index.ts index df435ffb8..3c20ef1c6 100644 --- a/src/browser/routes/index.ts +++ b/src/browser/routes/index.ts @@ -1,11 +1,10 @@ -import type express from "express"; - import type { BrowserRouteContext } from "../server-context.js"; import { registerBrowserAgentRoutes } from "./agent.js"; import { registerBrowserBasicRoutes } from "./basic.js"; import { registerBrowserTabRoutes } from "./tabs.js"; +import type { BrowserRouteRegistrar } from "./types.js"; -export function registerBrowserRoutes(app: express.Express, ctx: BrowserRouteContext) { +export function registerBrowserRoutes(app: BrowserRouteRegistrar, ctx: BrowserRouteContext) { registerBrowserBasicRoutes(app, ctx); registerBrowserTabRoutes(app, ctx); registerBrowserAgentRoutes(app, ctx); diff --git a/src/browser/routes/tabs.ts b/src/browser/routes/tabs.ts index 44596be85..e510dd521 100644 --- a/src/browser/routes/tabs.ts +++ b/src/browser/routes/tabs.ts @@ -1,9 +1,8 @@ -import type express from "express"; - import type { BrowserRouteContext } from "../server-context.js"; import { getProfileContext, jsonError, toNumber, toStringOrEmpty } from "./utils.js"; +import type { BrowserRouteRegistrar } from "./types.js"; -export function registerBrowserTabRoutes(app: express.Express, ctx: BrowserRouteContext) { +export function registerBrowserTabRoutes(app: BrowserRouteRegistrar, ctx: BrowserRouteContext) { app.get("/tabs", async (req, res) => { const profileCtx = getProfileContext(req, ctx); if ("error" in profileCtx) return jsonError(res, profileCtx.status, profileCtx.error); diff --git a/src/browser/routes/types.ts b/src/browser/routes/types.ts new file mode 100644 index 000000000..76e6051c9 --- /dev/null +++ b/src/browser/routes/types.ts @@ -0,0 +1,21 @@ +export type BrowserRequest = { + params: Record; + query: Record; + body?: unknown; +}; + +export type BrowserResponse = { + status: (code: number) => BrowserResponse; + json: (body: unknown) => void; +}; + +export type BrowserRouteHandler = ( + req: BrowserRequest, + res: BrowserResponse, +) => void | Promise; + +export type BrowserRouteRegistrar = { + get: (path: string, handler: BrowserRouteHandler) => void; + post: (path: string, handler: BrowserRouteHandler) => void; + delete: (path: string, handler: BrowserRouteHandler) => void; +}; diff --git a/src/browser/routes/utils.ts b/src/browser/routes/utils.ts index 1c6e37fc5..ad510da4b 100644 --- a/src/browser/routes/utils.ts +++ b/src/browser/routes/utils.ts @@ -1,14 +1,13 @@ -import type express from "express"; - import type { BrowserRouteContext, ProfileContext } from "../server-context.js"; import { parseBooleanValue } from "../../utils/boolean.js"; +import type { BrowserRequest, BrowserResponse } from "./types.js"; /** * Extract profile name from query string or body and get profile context. * Query string takes precedence over body for consistency with GET routes. */ export function getProfileContext( - req: express.Request, + req: BrowserRequest, ctx: BrowserRouteContext, ): ProfileContext | { error: string; status: number } { let profileName: string | undefined; @@ -33,7 +32,7 @@ export function getProfileContext( } } -export function jsonError(res: express.Response, status: number, message: string) { +export function jsonError(res: BrowserResponse, status: number, message: string) { res.status(status).json({ error: message }); } diff --git a/src/browser/server-context.ensure-tab-available.prefers-last-target.test.ts b/src/browser/server-context.ensure-tab-available.prefers-last-target.test.ts index 4863cc1fe..5d9555387 100644 --- a/src/browser/server-context.ensure-tab-available.prefers-last-target.test.ts +++ b/src/browser/server-context.ensure-tab-available.prefers-last-target.test.ts @@ -62,8 +62,6 @@ describe("browser server-context ensureTabAvailable", () => { port: 0, resolved: { enabled: true, - controlUrl: "http://127.0.0.1:18791", - controlHost: "127.0.0.1", controlPort: 18791, cdpProtocol: "http", cdpHost: "127.0.0.1", @@ -121,8 +119,6 @@ describe("browser server-context ensureTabAvailable", () => { port: 0, resolved: { enabled: true, - controlUrl: "http://127.0.0.1:18791", - controlHost: "127.0.0.1", controlPort: 18791, cdpProtocol: "http", cdpHost: "127.0.0.1", @@ -170,8 +166,6 @@ describe("browser server-context ensureTabAvailable", () => { port: 0, resolved: { enabled: true, - controlUrl: "http://127.0.0.1:18791", - controlHost: "127.0.0.1", controlPort: 18791, cdpProtocol: "http", cdpHost: "127.0.0.1", diff --git a/src/browser/server-context.remote-tab-ops.test.ts b/src/browser/server-context.remote-tab-ops.test.ts index 379394497..b3cdb38f9 100644 --- a/src/browser/server-context.remote-tab-ops.test.ts +++ b/src/browser/server-context.remote-tab-ops.test.ts @@ -21,8 +21,6 @@ function makeState( port: 0, resolved: { enabled: true, - controlUrl: "http://127.0.0.1:18791", - controlHost: "127.0.0.1", controlPort: 18791, cdpProtocol: profile === "remote" ? "https" : "http", cdpHost: profile === "remote" ? "browserless.example" : "127.0.0.1", diff --git a/src/browser/server-context.types.ts b/src/browser/server-context.types.ts index 6532010e9..7fa6d273a 100644 --- a/src/browser/server-context.types.ts +++ b/src/browser/server-context.types.ts @@ -17,7 +17,7 @@ export type ProfileRuntimeState = { }; export type BrowserServerState = { - server: Server; + server?: Server | null; port: number; resolved: ResolvedBrowserConfig; profiles: Map; diff --git a/src/browser/server.agent-contract-form-layout-act-commands.test.ts b/src/browser/server.agent-contract-form-layout-act-commands.test.ts index cbff6dca5..9276a5280 100644 --- a/src/browser/server.agent-contract-form-layout-act-commands.test.ts +++ b/src/browser/server.agent-contract-form-layout-act-commands.test.ts @@ -7,7 +7,9 @@ let testPort = 0; let cdpBaseUrl = ""; let reachable = false; let cfgAttachOnly = false; +let cfgEvaluateEnabled = true; let createTargetId: string | null = null; +let prevGatewayPort: string | undefined; const cdpMocks = vi.hoisted(() => ({ createTargetViaCdp: vi.fn(async () => { @@ -88,7 +90,7 @@ vi.mock("../config/config.js", async (importOriginal) => { loadConfig: () => ({ browser: { enabled: true, - controlUrl: `http://127.0.0.1:${testPort}`, + evaluateEnabled: cfgEvaluateEnabled, color: "#FF4500", attachOnly: cfgAttachOnly, headless: true, @@ -185,6 +187,7 @@ describe("browser control server", () => { beforeEach(async () => { reachable = false; cfgAttachOnly = false; + cfgEvaluateEnabled = true; createTargetId = null; cdpMocks.createTargetViaCdp.mockImplementation(async () => { @@ -197,6 +200,8 @@ describe("browser control server", () => { testPort = await getFreePort(); cdpBaseUrl = `http://127.0.0.1:${testPort + 1}`; + prevGatewayPort = process.env.CLAWDBOT_GATEWAY_PORT; + process.env.CLAWDBOT_GATEWAY_PORT = String(testPort - 2); // Minimal CDP JSON endpoints used by the server. let putNewCalls = 0; @@ -248,6 +253,11 @@ describe("browser control server", () => { afterEach(async () => { vi.unstubAllGlobals(); vi.restoreAllMocks(); + if (prevGatewayPort === undefined) { + delete process.env.CLAWDBOT_GATEWAY_PORT; + } else { + process.env.CLAWDBOT_GATEWAY_PORT = prevGatewayPort; + } const { stopBrowserControlServer } = await import("./server.js"); await stopBrowserControlServer(); }); @@ -342,6 +352,30 @@ describe("browser control server", () => { slowTimeoutMs, ); + it( + "blocks act:evaluate when browser.evaluateEnabled=false", + async () => { + cfgEvaluateEnabled = false; + const base = await startServerAndBase(); + + const waitRes = (await postJson(`${base}/act`, { + kind: "wait", + fn: "() => window.ready === true", + })) as { error?: string }; + expect(waitRes.error).toContain("browser.evaluateEnabled=false"); + expect(pwMocks.waitForViaPlaywright).not.toHaveBeenCalled(); + + const res = (await postJson(`${base}/act`, { + kind: "evaluate", + fn: "() => 1", + })) as { error?: string }; + + expect(res.error).toContain("browser.evaluateEnabled=false"); + expect(pwMocks.evaluateViaPlaywright).not.toHaveBeenCalled(); + }, + slowTimeoutMs, + ); + it("agent contract: hooks + response + downloads + screenshot", async () => { const base = await startServerAndBase(); diff --git a/src/browser/server.agent-contract-snapshot-endpoints.test.ts b/src/browser/server.agent-contract-snapshot-endpoints.test.ts index 65a86facb..494d64657 100644 --- a/src/browser/server.agent-contract-snapshot-endpoints.test.ts +++ b/src/browser/server.agent-contract-snapshot-endpoints.test.ts @@ -9,6 +9,7 @@ let cdpBaseUrl = ""; let reachable = false; let cfgAttachOnly = false; let createTargetId: string | null = null; +let prevGatewayPort: string | undefined; const cdpMocks = vi.hoisted(() => ({ createTargetViaCdp: vi.fn(async () => { @@ -89,7 +90,6 @@ vi.mock("../config/config.js", async (importOriginal) => { loadConfig: () => ({ browser: { enabled: true, - controlUrl: `http://127.0.0.1:${testPort}`, color: "#FF4500", attachOnly: cfgAttachOnly, headless: true, @@ -198,6 +198,8 @@ describe("browser control server", () => { testPort = await getFreePort(); cdpBaseUrl = `http://127.0.0.1:${testPort + 1}`; + prevGatewayPort = process.env.CLAWDBOT_GATEWAY_PORT; + process.env.CLAWDBOT_GATEWAY_PORT = String(testPort - 2); // Minimal CDP JSON endpoints used by the server. let putNewCalls = 0; @@ -249,6 +251,11 @@ describe("browser control server", () => { afterEach(async () => { vi.unstubAllGlobals(); vi.restoreAllMocks(); + if (prevGatewayPort === undefined) { + delete process.env.CLAWDBOT_GATEWAY_PORT; + } else { + process.env.CLAWDBOT_GATEWAY_PORT = prevGatewayPort; + } const { stopBrowserControlServer } = await import("./server.js"); await stopBrowserControlServer(); }); diff --git a/src/browser/server.covers-additional-endpoint-branches.test.ts b/src/browser/server.covers-additional-endpoint-branches.test.ts index bc16040cf..3710d8ed6 100644 --- a/src/browser/server.covers-additional-endpoint-branches.test.ts +++ b/src/browser/server.covers-additional-endpoint-branches.test.ts @@ -8,6 +8,7 @@ let _cdpBaseUrl = ""; let reachable = false; let cfgAttachOnly = false; let createTargetId: string | null = null; +let prevGatewayPort: string | undefined; const cdpMocks = vi.hoisted(() => ({ createTargetViaCdp: vi.fn(async () => { @@ -88,7 +89,6 @@ vi.mock("../config/config.js", async (importOriginal) => { loadConfig: () => ({ browser: { enabled: true, - controlUrl: `http://127.0.0.1:${testPort}`, color: "#FF4500", attachOnly: cfgAttachOnly, headless: true, @@ -197,6 +197,8 @@ describe("browser control server", () => { testPort = await getFreePort(); _cdpBaseUrl = `http://127.0.0.1:${testPort + 1}`; + prevGatewayPort = process.env.CLAWDBOT_GATEWAY_PORT; + process.env.CLAWDBOT_GATEWAY_PORT = String(testPort - 2); // Minimal CDP JSON endpoints used by the server. let putNewCalls = 0; @@ -248,6 +250,11 @@ describe("browser control server", () => { afterEach(async () => { vi.unstubAllGlobals(); vi.restoreAllMocks(); + if (prevGatewayPort === undefined) { + delete process.env.CLAWDBOT_GATEWAY_PORT; + } else { + process.env.CLAWDBOT_GATEWAY_PORT = prevGatewayPort; + } const { stopBrowserControlServer } = await import("./server.js"); await stopBrowserControlServer(); }); @@ -301,6 +308,11 @@ describe("backward compatibility (profile parameter)", () => { testPort = await getFreePort(); _cdpBaseUrl = `http://127.0.0.1:${testPort + 1}`; + prevGatewayPort = process.env.CLAWDBOT_GATEWAY_PORT; + process.env.CLAWDBOT_GATEWAY_PORT = String(testPort - 2); + + prevGatewayPort = process.env.CLAWDBOT_GATEWAY_PORT; + process.env.CLAWDBOT_GATEWAY_PORT = String(testPort - 2); vi.stubGlobal( "fetch", @@ -337,6 +349,11 @@ describe("backward compatibility (profile parameter)", () => { afterEach(async () => { vi.unstubAllGlobals(); vi.restoreAllMocks(); + if (prevGatewayPort === undefined) { + delete process.env.CLAWDBOT_GATEWAY_PORT; + } else { + process.env.CLAWDBOT_GATEWAY_PORT = prevGatewayPort; + } const { stopBrowserControlServer } = await import("./server.js"); await stopBrowserControlServer(); }); diff --git a/src/browser/server.post-tabs-open-profile-unknown-returns-404.test.ts b/src/browser/server.post-tabs-open-profile-unknown-returns-404.test.ts index 4908a536c..3b55339b5 100644 --- a/src/browser/server.post-tabs-open-profile-unknown-returns-404.test.ts +++ b/src/browser/server.post-tabs-open-profile-unknown-returns-404.test.ts @@ -8,6 +8,7 @@ let _cdpBaseUrl = ""; let reachable = false; let cfgAttachOnly = false; let createTargetId: string | null = null; +let prevGatewayPort: string | undefined; const cdpMocks = vi.hoisted(() => ({ createTargetViaCdp: vi.fn(async () => { @@ -88,7 +89,6 @@ vi.mock("../config/config.js", async (importOriginal) => { loadConfig: () => ({ browser: { enabled: true, - controlUrl: `http://127.0.0.1:${testPort}`, color: "#FF4500", attachOnly: cfgAttachOnly, headless: true, @@ -197,6 +197,8 @@ describe("browser control server", () => { testPort = await getFreePort(); _cdpBaseUrl = `http://127.0.0.1:${testPort + 1}`; + prevGatewayPort = process.env.CLAWDBOT_GATEWAY_PORT; + process.env.CLAWDBOT_GATEWAY_PORT = String(testPort - 2); // Minimal CDP JSON endpoints used by the server. let putNewCalls = 0; @@ -248,6 +250,11 @@ describe("browser control server", () => { afterEach(async () => { vi.unstubAllGlobals(); vi.restoreAllMocks(); + if (prevGatewayPort === undefined) { + delete process.env.CLAWDBOT_GATEWAY_PORT; + } else { + process.env.CLAWDBOT_GATEWAY_PORT = prevGatewayPort; + } const { stopBrowserControlServer } = await import("./server.js"); await stopBrowserControlServer(); }); @@ -278,6 +285,11 @@ describe("profile CRUD endpoints", () => { testPort = await getFreePort(); _cdpBaseUrl = `http://127.0.0.1:${testPort + 1}`; + prevGatewayPort = process.env.CLAWDBOT_GATEWAY_PORT; + process.env.CLAWDBOT_GATEWAY_PORT = String(testPort - 2); + + prevGatewayPort = process.env.CLAWDBOT_GATEWAY_PORT; + process.env.CLAWDBOT_GATEWAY_PORT = String(testPort - 2); vi.stubGlobal( "fetch", @@ -292,6 +304,11 @@ describe("profile CRUD endpoints", () => { afterEach(async () => { vi.unstubAllGlobals(); vi.restoreAllMocks(); + if (prevGatewayPort === undefined) { + delete process.env.CLAWDBOT_GATEWAY_PORT; + } else { + process.env.CLAWDBOT_GATEWAY_PORT = prevGatewayPort; + } const { stopBrowserControlServer } = await import("./server.js"); await stopBrowserControlServer(); }); diff --git a/src/browser/server.serves-status-starts-browser-requested.test.ts b/src/browser/server.serves-status-starts-browser-requested.test.ts index 8cfcca40e..41ea6ab47 100644 --- a/src/browser/server.serves-status-starts-browser-requested.test.ts +++ b/src/browser/server.serves-status-starts-browser-requested.test.ts @@ -8,6 +8,7 @@ let _cdpBaseUrl = ""; let reachable = false; let cfgAttachOnly = false; let createTargetId: string | null = null; +let prevGatewayPort: string | undefined; const cdpMocks = vi.hoisted(() => ({ createTargetViaCdp: vi.fn(async () => { @@ -88,7 +89,6 @@ vi.mock("../config/config.js", async (importOriginal) => { loadConfig: () => ({ browser: { enabled: true, - controlUrl: `http://127.0.0.1:${testPort}`, color: "#FF4500", attachOnly: cfgAttachOnly, headless: true, @@ -197,6 +197,8 @@ describe("browser control server", () => { testPort = await getFreePort(); _cdpBaseUrl = `http://127.0.0.1:${testPort + 1}`; + prevGatewayPort = process.env.CLAWDBOT_GATEWAY_PORT; + process.env.CLAWDBOT_GATEWAY_PORT = String(testPort - 2); // Minimal CDP JSON endpoints used by the server. let putNewCalls = 0; @@ -248,6 +250,11 @@ describe("browser control server", () => { afterEach(async () => { vi.unstubAllGlobals(); vi.restoreAllMocks(); + if (prevGatewayPort === undefined) { + delete process.env.CLAWDBOT_GATEWAY_PORT; + } else { + process.env.CLAWDBOT_GATEWAY_PORT = prevGatewayPort; + } const { stopBrowserControlServer } = await import("./server.js"); await stopBrowserControlServer(); }); diff --git a/src/browser/server.skips-default-maxchars-explicitly-set-zero.test.ts b/src/browser/server.skips-default-maxchars-explicitly-set-zero.test.ts index d50313413..ed4d4f9f5 100644 --- a/src/browser/server.skips-default-maxchars-explicitly-set-zero.test.ts +++ b/src/browser/server.skips-default-maxchars-explicitly-set-zero.test.ts @@ -8,6 +8,7 @@ let cdpBaseUrl = ""; let reachable = false; let cfgAttachOnly = false; let createTargetId: string | null = null; +let prevGatewayPort: string | undefined; const cdpMocks = vi.hoisted(() => ({ createTargetViaCdp: vi.fn(async () => { @@ -88,7 +89,6 @@ vi.mock("../config/config.js", async (importOriginal) => { loadConfig: () => ({ browser: { enabled: true, - controlUrl: `http://127.0.0.1:${testPort}`, color: "#FF4500", attachOnly: cfgAttachOnly, headless: true, @@ -197,6 +197,8 @@ describe("browser control server", () => { testPort = await getFreePort(); cdpBaseUrl = `http://127.0.0.1:${testPort + 1}`; + prevGatewayPort = process.env.CLAWDBOT_GATEWAY_PORT; + process.env.CLAWDBOT_GATEWAY_PORT = String(testPort - 2); // Minimal CDP JSON endpoints used by the server. let putNewCalls = 0; @@ -248,6 +250,11 @@ describe("browser control server", () => { afterEach(async () => { vi.unstubAllGlobals(); vi.restoreAllMocks(); + if (prevGatewayPort === undefined) { + delete process.env.CLAWDBOT_GATEWAY_PORT; + } else { + process.env.CLAWDBOT_GATEWAY_PORT = prevGatewayPort; + } const { stopBrowserControlServer } = await import("./server.js"); await stopBrowserControlServer(); }); @@ -394,8 +401,6 @@ describe("browser control server", () => { const bridge = await startBrowserBridgeServer({ resolved: { enabled: true, - controlUrl: "http://127.0.0.1:0", - controlHost: "127.0.0.1", controlPort: 0, cdpProtocol: "http", cdpHost: "127.0.0.1", diff --git a/src/browser/server.ts b/src/browser/server.ts index 9fb9a5f81..8eda99508 100644 --- a/src/browser/server.ts +++ b/src/browser/server.ts @@ -3,9 +3,10 @@ import express from "express"; import { loadConfig } from "../config/config.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; -import { resolveBrowserConfig, resolveProfile, shouldStartLocalBrowserServer } from "./config.js"; +import { resolveBrowserConfig, resolveProfile } from "./config.js"; import { ensureChromeExtensionRelayServer } from "./extension-relay.js"; import { registerBrowserRoutes } from "./routes/index.js"; +import type { BrowserRouteRegistrar } from "./routes/types.js"; import { type BrowserServerState, createBrowserRouteContext } from "./server-context.js"; let state: BrowserServerState | null = null; @@ -16,23 +17,16 @@ export async function startBrowserControlServerFromConfig(): Promise state, }); - registerBrowserRoutes(app, ctx); + registerBrowserRoutes(app as unknown as BrowserRouteRegistrar, ctx); const port = resolved.controlPort; const server = await new Promise((resolve, reject) => { @@ -89,9 +83,11 @@ export async function stopBrowserControlServer(): Promise { logServer.warn(`clawd browser stop failed: ${String(err)}`); } - await new Promise((resolve) => { - current.server.close(() => resolve()); - }); + if (current.server) { + await new Promise((resolve) => { + current.server?.close(() => resolve()); + }); + } state = null; // Optional: Playwright is not always available (e.g. embedded gateway builds). diff --git a/src/canvas-host/a2ui/.bundle.hash b/src/canvas-host/a2ui/.bundle.hash index a4155f132..19a232f5c 100644 --- a/src/canvas-host/a2ui/.bundle.hash +++ b/src/canvas-host/a2ui/.bundle.hash @@ -1 +1 @@ -a99455ba0c4d0aad0a110bf25440c208b798198d5524b269f0f2d3f984262ae4 +2567ca5bbc065b922d96717a488d5db3120b5b033c5d0508682d1aa8fbba470a diff --git a/src/canvas-host/a2ui/a2ui.bundle.js b/src/canvas-host/a2ui/a2ui.bundle.js deleted file mode 100644 index c29280acd..000000000 --- a/src/canvas-host/a2ui/a2ui.bundle.js +++ /dev/null @@ -1,17768 +0,0 @@ -var __defProp$1 = Object.defineProperty; -var __exportAll = (all, symbols) => { - let target = {}; - for (var name in all) { - __defProp$1(target, name, { - get: all[name], - enumerable: true - }); - } - if (symbols) { - __defProp$1(target, Symbol.toStringTag, { value: "Module" }); - } - return target; -}; - -/** -* @license -* Copyright 2019 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -const t$6 = globalThis, e$13 = t$6.ShadowRoot && (void 0 === t$6.ShadyCSS || t$6.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, s$8 = Symbol(), o$14 = new WeakMap(); -var n$12 = class { - constructor(t$7, e$14, o$15) { - if (this._$cssResult$ = !0, o$15 !== s$8) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead."); - this.cssText = t$7, this.t = e$14; - } - get styleSheet() { - let t$7 = this.o; - const s$9 = this.t; - if (e$13 && void 0 === t$7) { - const e$14 = void 0 !== s$9 && 1 === s$9.length; - e$14 && (t$7 = o$14.get(s$9)), void 0 === t$7 && ((this.o = t$7 = new CSSStyleSheet()).replaceSync(this.cssText), e$14 && o$14.set(s$9, t$7)); - } - return t$7; - } - toString() { - return this.cssText; - } -}; -const r$11 = (t$7) => new n$12("string" == typeof t$7 ? t$7 : t$7 + "", void 0, s$8), i$9 = (t$7, ...e$14) => { - const o$15 = 1 === t$7.length ? t$7[0] : e$14.reduce((e$15, s$9, o$16) => e$15 + ((t$8) => { - if (!0 === t$8._$cssResult$) return t$8.cssText; - if ("number" == typeof t$8) return t$8; - throw Error("Value passed to 'css' function must be a 'css' function result: " + t$8 + ". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security."); - })(s$9) + t$7[o$16 + 1], t$7[0]); - return new n$12(o$15, t$7, s$8); -}, S$1 = (s$9, o$15) => { - if (e$13) s$9.adoptedStyleSheets = o$15.map((t$7) => t$7 instanceof CSSStyleSheet ? t$7 : t$7.styleSheet); - else for (const e$14 of o$15) { - const o$16 = document.createElement("style"), n$13 = t$6.litNonce; - void 0 !== n$13 && o$16.setAttribute("nonce", n$13), o$16.textContent = e$14.cssText, s$9.appendChild(o$16); - } -}, c$6 = e$13 ? (t$7) => t$7 : (t$7) => t$7 instanceof CSSStyleSheet ? ((t$8) => { - let e$14 = ""; - for (const s$9 of t$8.cssRules) e$14 += s$9.cssText; - return r$11(e$14); -})(t$7) : t$7; - -/** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ const { is: i$8, defineProperty: e$12, getOwnPropertyDescriptor: h$6, getOwnPropertyNames: r$10, getOwnPropertySymbols: o$13, getPrototypeOf: n$11 } = Object, a$1 = globalThis, c$5 = a$1.trustedTypes, l$4 = c$5 ? c$5.emptyScript : "", p$2 = a$1.reactiveElementPolyfillSupport, d$2 = (t$7, s$9) => t$7, u$3 = { - toAttribute(t$7, s$9) { - switch (s$9) { - case Boolean: - t$7 = t$7 ? l$4 : null; - break; - case Object: - case Array: t$7 = null == t$7 ? t$7 : JSON.stringify(t$7); - } - return t$7; - }, - fromAttribute(t$7, s$9) { - let i$10 = t$7; - switch (s$9) { - case Boolean: - i$10 = null !== t$7; - break; - case Number: - i$10 = null === t$7 ? null : Number(t$7); - break; - case Object: - case Array: try { - i$10 = JSON.parse(t$7); - } catch (t$8) { - i$10 = null; - } - } - return i$10; - } -}, f$3 = (t$7, s$9) => !i$8(t$7, s$9), b$1 = { - attribute: !0, - type: String, - converter: u$3, - reflect: !1, - useDefault: !1, - hasChanged: f$3 -}; -Symbol.metadata ??= Symbol("metadata"), a$1.litPropertyMetadata ??= new WeakMap(); -var y$1 = class extends HTMLElement { - static addInitializer(t$7) { - this._$Ei(), (this.l ??= []).push(t$7); - } - static get observedAttributes() { - return this.finalize(), this._$Eh && [...this._$Eh.keys()]; - } - static createProperty(t$7, s$9 = b$1) { - if (s$9.state && (s$9.attribute = !1), this._$Ei(), this.prototype.hasOwnProperty(t$7) && ((s$9 = Object.create(s$9)).wrapped = !0), this.elementProperties.set(t$7, s$9), !s$9.noAccessor) { - const i$10 = Symbol(), h$7 = this.getPropertyDescriptor(t$7, i$10, s$9); - void 0 !== h$7 && e$12(this.prototype, t$7, h$7); - } - } - static getPropertyDescriptor(t$7, s$9, i$10) { - const { get: e$14, set: r$12 } = h$6(this.prototype, t$7) ?? { - get() { - return this[s$9]; - }, - set(t$8) { - this[s$9] = t$8; - } - }; - return { - get: e$14, - set(s$10) { - const h$7 = e$14?.call(this); - r$12?.call(this, s$10), this.requestUpdate(t$7, h$7, i$10); - }, - configurable: !0, - enumerable: !0 - }; - } - static getPropertyOptions(t$7) { - return this.elementProperties.get(t$7) ?? b$1; - } - static _$Ei() { - if (this.hasOwnProperty(d$2("elementProperties"))) return; - const t$7 = n$11(this); - t$7.finalize(), void 0 !== t$7.l && (this.l = [...t$7.l]), this.elementProperties = new Map(t$7.elementProperties); - } - static finalize() { - if (this.hasOwnProperty(d$2("finalized"))) return; - if (this.finalized = !0, this._$Ei(), this.hasOwnProperty(d$2("properties"))) { - const t$8 = this.properties, s$9 = [...r$10(t$8), ...o$13(t$8)]; - for (const i$10 of s$9) this.createProperty(i$10, t$8[i$10]); - } - const t$7 = this[Symbol.metadata]; - if (null !== t$7) { - const s$9 = litPropertyMetadata.get(t$7); - if (void 0 !== s$9) for (const [t$8, i$10] of s$9) this.elementProperties.set(t$8, i$10); - } - this._$Eh = new Map(); - for (const [t$8, s$9] of this.elementProperties) { - const i$10 = this._$Eu(t$8, s$9); - void 0 !== i$10 && this._$Eh.set(i$10, t$8); - } - this.elementStyles = this.finalizeStyles(this.styles); - } - static finalizeStyles(s$9) { - const i$10 = []; - if (Array.isArray(s$9)) { - const e$14 = new Set(s$9.flat(1 / 0).reverse()); - for (const s$10 of e$14) i$10.unshift(c$6(s$10)); - } else void 0 !== s$9 && i$10.push(c$6(s$9)); - return i$10; - } - static _$Eu(t$7, s$9) { - const i$10 = s$9.attribute; - return !1 === i$10 ? void 0 : "string" == typeof i$10 ? i$10 : "string" == typeof t$7 ? t$7.toLowerCase() : void 0; - } - constructor() { - super(), this._$Ep = void 0, this.isUpdatePending = !1, this.hasUpdated = !1, this._$Em = null, this._$Ev(); - } - _$Ev() { - this._$ES = new Promise((t$7) => this.enableUpdating = t$7), this._$AL = new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t$7) => t$7(this)); - } - addController(t$7) { - (this._$EO ??= new Set()).add(t$7), void 0 !== this.renderRoot && this.isConnected && t$7.hostConnected?.(); - } - removeController(t$7) { - this._$EO?.delete(t$7); - } - _$E_() { - const t$7 = new Map(), s$9 = this.constructor.elementProperties; - for (const i$10 of s$9.keys()) this.hasOwnProperty(i$10) && (t$7.set(i$10, this[i$10]), delete this[i$10]); - t$7.size > 0 && (this._$Ep = t$7); - } - createRenderRoot() { - const t$7 = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions); - return S$1(t$7, this.constructor.elementStyles), t$7; - } - connectedCallback() { - this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(!0), this._$EO?.forEach((t$7) => t$7.hostConnected?.()); - } - enableUpdating(t$7) {} - disconnectedCallback() { - this._$EO?.forEach((t$7) => t$7.hostDisconnected?.()); - } - attributeChangedCallback(t$7, s$9, i$10) { - this._$AK(t$7, i$10); - } - _$ET(t$7, s$9) { - const i$10 = this.constructor.elementProperties.get(t$7), e$14 = this.constructor._$Eu(t$7, i$10); - if (void 0 !== e$14 && !0 === i$10.reflect) { - const h$7 = (void 0 !== i$10.converter?.toAttribute ? i$10.converter : u$3).toAttribute(s$9, i$10.type); - this._$Em = t$7, null == h$7 ? this.removeAttribute(e$14) : this.setAttribute(e$14, h$7), this._$Em = null; - } - } - _$AK(t$7, s$9) { - const i$10 = this.constructor, e$14 = i$10._$Eh.get(t$7); - if (void 0 !== e$14 && this._$Em !== e$14) { - const t$8 = i$10.getPropertyOptions(e$14), h$7 = "function" == typeof t$8.converter ? { fromAttribute: t$8.converter } : void 0 !== t$8.converter?.fromAttribute ? t$8.converter : u$3; - this._$Em = e$14; - const r$12 = h$7.fromAttribute(s$9, t$8.type); - this[e$14] = r$12 ?? this._$Ej?.get(e$14) ?? r$12, this._$Em = null; - } - } - requestUpdate(t$7, s$9, i$10, e$14 = !1, h$7) { - if (void 0 !== t$7) { - const r$12 = this.constructor; - if (!1 === e$14 && (h$7 = this[t$7]), i$10 ??= r$12.getPropertyOptions(t$7), !((i$10.hasChanged ?? f$3)(h$7, s$9) || i$10.useDefault && i$10.reflect && h$7 === this._$Ej?.get(t$7) && !this.hasAttribute(r$12._$Eu(t$7, i$10)))) return; - this.C(t$7, s$9, i$10); - } - !1 === this.isUpdatePending && (this._$ES = this._$EP()); - } - C(t$7, s$9, { useDefault: i$10, reflect: e$14, wrapped: h$7 }, r$12) { - i$10 && !(this._$Ej ??= new Map()).has(t$7) && (this._$Ej.set(t$7, r$12 ?? s$9 ?? this[t$7]), !0 !== h$7 || void 0 !== r$12) || (this._$AL.has(t$7) || (this.hasUpdated || i$10 || (s$9 = void 0), this._$AL.set(t$7, s$9)), !0 === e$14 && this._$Em !== t$7 && (this._$Eq ??= new Set()).add(t$7)); - } - async _$EP() { - this.isUpdatePending = !0; - try { - await this._$ES; - } catch (t$8) { - Promise.reject(t$8); - } - const t$7 = this.scheduleUpdate(); - return null != t$7 && await t$7, !this.isUpdatePending; - } - scheduleUpdate() { - return this.performUpdate(); - } - performUpdate() { - if (!this.isUpdatePending) return; - if (!this.hasUpdated) { - if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) { - for (const [t$9, s$10] of this._$Ep) this[t$9] = s$10; - this._$Ep = void 0; - } - const t$8 = this.constructor.elementProperties; - if (t$8.size > 0) for (const [s$10, i$10] of t$8) { - const { wrapped: t$9 } = i$10, e$14 = this[s$10]; - !0 !== t$9 || this._$AL.has(s$10) || void 0 === e$14 || this.C(s$10, void 0, i$10, e$14); - } - } - let t$7 = !1; - const s$9 = this._$AL; - try { - t$7 = this.shouldUpdate(s$9), t$7 ? (this.willUpdate(s$9), this._$EO?.forEach((t$8) => t$8.hostUpdate?.()), this.update(s$9)) : this._$EM(); - } catch (s$10) { - throw t$7 = !1, this._$EM(), s$10; - } - t$7 && this._$AE(s$9); - } - willUpdate(t$7) {} - _$AE(t$7) { - this._$EO?.forEach((t$8) => t$8.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = !0, this.firstUpdated(t$7)), this.updated(t$7); - } - _$EM() { - this._$AL = new Map(), this.isUpdatePending = !1; - } - get updateComplete() { - return this.getUpdateComplete(); - } - getUpdateComplete() { - return this._$ES; - } - shouldUpdate(t$7) { - return !0; - } - update(t$7) { - this._$Eq &&= this._$Eq.forEach((t$8) => this._$ET(t$8, this[t$8])), this._$EM(); - } - updated(t$7) {} - firstUpdated(t$7) {} -}; -y$1.elementStyles = [], y$1.shadowRootOptions = { mode: "open" }, y$1[d$2("elementProperties")] = new Map(), y$1[d$2("finalized")] = new Map(), p$2?.({ ReactiveElement: y$1 }), (a$1.reactiveElementVersions ??= []).push("2.1.2"); - -/** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -const t$5 = globalThis, i$7 = (t$7) => t$7, s$7 = t$5.trustedTypes, e$11 = s$7 ? s$7.createPolicy("lit-html", { createHTML: (t$7) => t$7 }) : void 0, h$5 = "$lit$", o$12 = `lit$${Math.random().toFixed(9).slice(2)}$`, n$10 = "?" + o$12, r$9 = `<${n$10}>`, l$3 = document, c$4 = () => l$3.createComment(""), a = (t$7) => null === t$7 || "object" != typeof t$7 && "function" != typeof t$7, u$2 = Array.isArray, d$1 = (t$7) => u$2(t$7) || "function" == typeof t$7?.[Symbol.iterator], f$2 = "[ \n\f\r]", v$1 = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, _ = /-->/g, m$2 = />/g, p$1 = RegExp(`>|${f$2}(?:([^\\s"'>=/]+)(${f$2}*=${f$2}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`, "g"), g = /'/g, $ = /"/g, y = /^(?:script|style|textarea|title)$/i, x = (t$7) => (i$10, ...s$9) => ({ - _$litType$: t$7, - strings: i$10, - values: s$9 -}), b = x(1), w = x(2), T = x(3), E = Symbol.for("lit-noChange"), A = Symbol.for("lit-nothing"), C = new WeakMap(), P = l$3.createTreeWalker(l$3, 129); -function V(t$7, i$10) { - if (!u$2(t$7) || !t$7.hasOwnProperty("raw")) throw Error("invalid template strings array"); - return void 0 !== e$11 ? e$11.createHTML(i$10) : i$10; -} -const N = (t$7, i$10) => { - const s$9 = t$7.length - 1, e$14 = []; - let n$13, l$5 = 2 === i$10 ? "" : 3 === i$10 ? "" : "", c$7 = v$1; - for (let i$11 = 0; i$11 < s$9; i$11++) { - const s$10 = t$7[i$11]; - let a, u$4, d$3 = -1, f$4 = 0; - for (; f$4 < s$10.length && (c$7.lastIndex = f$4, u$4 = c$7.exec(s$10), null !== u$4);) f$4 = c$7.lastIndex, c$7 === v$1 ? "!--" === u$4[1] ? c$7 = _ : void 0 !== u$4[1] ? c$7 = m$2 : void 0 !== u$4[2] ? (y.test(u$4[2]) && (n$13 = RegExp("" === u$4[0] ? (c$7 = n$13 ?? v$1, d$3 = -1) : void 0 === u$4[1] ? d$3 = -2 : (d$3 = c$7.lastIndex - u$4[2].length, a = u$4[1], c$7 = void 0 === u$4[3] ? p$1 : "\"" === u$4[3] ? $ : g) : c$7 === $ || c$7 === g ? c$7 = p$1 : c$7 === _ || c$7 === m$2 ? c$7 = v$1 : (c$7 = p$1, n$13 = void 0); - const x = c$7 === p$1 && t$7[i$11 + 1].startsWith("/>") ? " " : ""; - l$5 += c$7 === v$1 ? s$10 + r$9 : d$3 >= 0 ? (e$14.push(a), s$10.slice(0, d$3) + h$5 + s$10.slice(d$3) + o$12 + x) : s$10 + o$12 + (-2 === d$3 ? i$11 : x); - } - return [V(t$7, l$5 + (t$7[s$9] || "") + (2 === i$10 ? "" : 3 === i$10 ? "" : "")), e$14]; -}; -var S = class S { - constructor({ strings: t$7, _$litType$: i$10 }, e$14) { - let r$12; - this.parts = []; - let l$5 = 0, a = 0; - const u$4 = t$7.length - 1, d$3 = this.parts, [f$4, v$2] = N(t$7, i$10); - if (this.el = S.createElement(f$4, e$14), P.currentNode = this.el.content, 2 === i$10 || 3 === i$10) { - const t$8 = this.el.content.firstChild; - t$8.replaceWith(...t$8.childNodes); - } - for (; null !== (r$12 = P.nextNode()) && d$3.length < u$4;) { - if (1 === r$12.nodeType) { - if (r$12.hasAttributes()) for (const t$8 of r$12.getAttributeNames()) if (t$8.endsWith(h$5)) { - const i$11 = v$2[a++], s$9 = r$12.getAttribute(t$8).split(o$12), e$15 = /([.?@])?(.*)/.exec(i$11); - d$3.push({ - type: 1, - index: l$5, - name: e$15[2], - strings: s$9, - ctor: "." === e$15[1] ? I : "?" === e$15[1] ? L : "@" === e$15[1] ? z : H - }), r$12.removeAttribute(t$8); - } else t$8.startsWith(o$12) && (d$3.push({ - type: 6, - index: l$5 - }), r$12.removeAttribute(t$8)); - if (y.test(r$12.tagName)) { - const t$8 = r$12.textContent.split(o$12), i$11 = t$8.length - 1; - if (i$11 > 0) { - r$12.textContent = s$7 ? s$7.emptyScript : ""; - for (let s$9 = 0; s$9 < i$11; s$9++) r$12.append(t$8[s$9], c$4()), P.nextNode(), d$3.push({ - type: 2, - index: ++l$5 - }); - r$12.append(t$8[i$11], c$4()); - } - } - } else if (8 === r$12.nodeType) if (r$12.data === n$10) d$3.push({ - type: 2, - index: l$5 - }); - else { - let t$8 = -1; - for (; -1 !== (t$8 = r$12.data.indexOf(o$12, t$8 + 1));) d$3.push({ - type: 7, - index: l$5 - }), t$8 += o$12.length - 1; - } - l$5++; - } - } - static createElement(t$7, i$10) { - const s$9 = l$3.createElement("template"); - return s$9.innerHTML = t$7, s$9; - } -}; -function M$1(t$7, i$10, s$9 = t$7, e$14) { - if (i$10 === E) return i$10; - let h$7 = void 0 !== e$14 ? s$9._$Co?.[e$14] : s$9._$Cl; - const o$15 = a(i$10) ? void 0 : i$10._$litDirective$; - return h$7?.constructor !== o$15 && (h$7?._$AO?.(!1), void 0 === o$15 ? h$7 = void 0 : (h$7 = new o$15(t$7), h$7._$AT(t$7, s$9, e$14)), void 0 !== e$14 ? (s$9._$Co ??= [])[e$14] = h$7 : s$9._$Cl = h$7), void 0 !== h$7 && (i$10 = M$1(t$7, h$7._$AS(t$7, i$10.values), h$7, e$14)), i$10; -} -var R = class { - constructor(t$7, i$10) { - this._$AV = [], this._$AN = void 0, this._$AD = t$7, this._$AM = i$10; - } - get parentNode() { - return this._$AM.parentNode; - } - get _$AU() { - return this._$AM._$AU; - } - u(t$7) { - const { el: { content: i$10 }, parts: s$9 } = this._$AD, e$14 = (t$7?.creationScope ?? l$3).importNode(i$10, !0); - P.currentNode = e$14; - let h$7 = P.nextNode(), o$15 = 0, n$13 = 0, r$12 = s$9[0]; - for (; void 0 !== r$12;) { - if (o$15 === r$12.index) { - let i$11; - 2 === r$12.type ? i$11 = new k(h$7, h$7.nextSibling, this, t$7) : 1 === r$12.type ? i$11 = new r$12.ctor(h$7, r$12.name, r$12.strings, this, t$7) : 6 === r$12.type && (i$11 = new Z(h$7, this, t$7)), this._$AV.push(i$11), r$12 = s$9[++n$13]; - } - o$15 !== r$12?.index && (h$7 = P.nextNode(), o$15++); - } - return P.currentNode = l$3, e$14; - } - p(t$7) { - let i$10 = 0; - for (const s$9 of this._$AV) void 0 !== s$9 && (void 0 !== s$9.strings ? (s$9._$AI(t$7, s$9, i$10), i$10 += s$9.strings.length - 2) : s$9._$AI(t$7[i$10])), i$10++; - } -}; -var k = class k { - get _$AU() { - return this._$AM?._$AU ?? this._$Cv; - } - constructor(t$7, i$10, s$9, e$14) { - this.type = 2, this._$AH = A, this._$AN = void 0, this._$AA = t$7, this._$AB = i$10, this._$AM = s$9, this.options = e$14, this._$Cv = e$14?.isConnected ?? !0; - } - get parentNode() { - let t$7 = this._$AA.parentNode; - const i$10 = this._$AM; - return void 0 !== i$10 && 11 === t$7?.nodeType && (t$7 = i$10.parentNode), t$7; - } - get startNode() { - return this._$AA; - } - get endNode() { - return this._$AB; - } - _$AI(t$7, i$10 = this) { - t$7 = M$1(this, t$7, i$10), a(t$7) ? t$7 === A || null == t$7 || "" === t$7 ? (this._$AH !== A && this._$AR(), this._$AH = A) : t$7 !== this._$AH && t$7 !== E && this._(t$7) : void 0 !== t$7._$litType$ ? this.$(t$7) : void 0 !== t$7.nodeType ? this.T(t$7) : d$1(t$7) ? this.k(t$7) : this._(t$7); - } - O(t$7) { - return this._$AA.parentNode.insertBefore(t$7, this._$AB); - } - T(t$7) { - this._$AH !== t$7 && (this._$AR(), this._$AH = this.O(t$7)); - } - _(t$7) { - this._$AH !== A && a(this._$AH) ? this._$AA.nextSibling.data = t$7 : this.T(l$3.createTextNode(t$7)), this._$AH = t$7; - } - $(t$7) { - const { values: i$10, _$litType$: s$9 } = t$7, e$14 = "number" == typeof s$9 ? this._$AC(t$7) : (void 0 === s$9.el && (s$9.el = S.createElement(V(s$9.h, s$9.h[0]), this.options)), s$9); - if (this._$AH?._$AD === e$14) this._$AH.p(i$10); - else { - const t$8 = new R(e$14, this), s$10 = t$8.u(this.options); - t$8.p(i$10), this.T(s$10), this._$AH = t$8; - } - } - _$AC(t$7) { - let i$10 = C.get(t$7.strings); - return void 0 === i$10 && C.set(t$7.strings, i$10 = new S(t$7)), i$10; - } - k(t$7) { - u$2(this._$AH) || (this._$AH = [], this._$AR()); - const i$10 = this._$AH; - let s$9, e$14 = 0; - for (const h$7 of t$7) e$14 === i$10.length ? i$10.push(s$9 = new k(this.O(c$4()), this.O(c$4()), this, this.options)) : s$9 = i$10[e$14], s$9._$AI(h$7), e$14++; - e$14 < i$10.length && (this._$AR(s$9 && s$9._$AB.nextSibling, e$14), i$10.length = e$14); - } - _$AR(t$7 = this._$AA.nextSibling, s$9) { - for (this._$AP?.(!1, !0, s$9); t$7 !== this._$AB;) { - const s$10 = i$7(t$7).nextSibling; - i$7(t$7).remove(), t$7 = s$10; - } - } - setConnected(t$7) { - void 0 === this._$AM && (this._$Cv = t$7, this._$AP?.(t$7)); - } -}; -var H = class { - get tagName() { - return this.element.tagName; - } - get _$AU() { - return this._$AM._$AU; - } - constructor(t$7, i$10, s$9, e$14, h$7) { - this.type = 1, this._$AH = A, this._$AN = void 0, this.element = t$7, this.name = i$10, this._$AM = e$14, this.options = h$7, s$9.length > 2 || "" !== s$9[0] || "" !== s$9[1] ? (this._$AH = Array(s$9.length - 1).fill(new String()), this.strings = s$9) : this._$AH = A; - } - _$AI(t$7, i$10 = this, s$9, e$14) { - const h$7 = this.strings; - let o$15 = !1; - if (void 0 === h$7) t$7 = M$1(this, t$7, i$10, 0), o$15 = !a(t$7) || t$7 !== this._$AH && t$7 !== E, o$15 && (this._$AH = t$7); - else { - const e$15 = t$7; - let n$13, r$12; - for (t$7 = h$7[0], n$13 = 0; n$13 < h$7.length - 1; n$13++) r$12 = M$1(this, e$15[s$9 + n$13], i$10, n$13), r$12 === E && (r$12 = this._$AH[n$13]), o$15 ||= !a(r$12) || r$12 !== this._$AH[n$13], r$12 === A ? t$7 = A : t$7 !== A && (t$7 += (r$12 ?? "") + h$7[n$13 + 1]), this._$AH[n$13] = r$12; - } - o$15 && !e$14 && this.j(t$7); - } - j(t$7) { - t$7 === A ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t$7 ?? ""); - } -}; -var I = class extends H { - constructor() { - super(...arguments), this.type = 3; - } - j(t$7) { - this.element[this.name] = t$7 === A ? void 0 : t$7; - } -}; -var L = class extends H { - constructor() { - super(...arguments), this.type = 4; - } - j(t$7) { - this.element.toggleAttribute(this.name, !!t$7 && t$7 !== A); - } -}; -var z = class extends H { - constructor(t$7, i$10, s$9, e$14, h$7) { - super(t$7, i$10, s$9, e$14, h$7), this.type = 5; - } - _$AI(t$7, i$10 = this) { - if ((t$7 = M$1(this, t$7, i$10, 0) ?? A) === E) return; - const s$9 = this._$AH, e$14 = t$7 === A && s$9 !== A || t$7.capture !== s$9.capture || t$7.once !== s$9.once || t$7.passive !== s$9.passive, h$7 = t$7 !== A && (s$9 === A || e$14); - e$14 && this.element.removeEventListener(this.name, this, s$9), h$7 && this.element.addEventListener(this.name, this, t$7), this._$AH = t$7; - } - handleEvent(t$7) { - "function" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t$7) : this._$AH.handleEvent(t$7); - } -}; -var Z = class { - constructor(t$7, i$10, s$9) { - this.element = t$7, this.type = 6, this._$AN = void 0, this._$AM = i$10, this.options = s$9; - } - get _$AU() { - return this._$AM._$AU; - } - _$AI(t$7) { - M$1(this, t$7); - } -}; -const j$1 = { - M: h$5, - P: o$12, - A: n$10, - C: 1, - L: N, - R, - D: d$1, - V: M$1, - I: k, - H, - N: L, - U: z, - B: I, - F: Z -}, B = t$5.litHtmlPolyfillSupport; -B?.(S, k), (t$5.litHtmlVersions ??= []).push("3.3.2"); -const D = (t$7, i$10, s$9) => { - const e$14 = s$9?.renderBefore ?? i$10; - let h$7 = e$14._$litPart$; - if (void 0 === h$7) { - const t$8 = s$9?.renderBefore ?? null; - e$14._$litPart$ = h$7 = new k(i$10.insertBefore(c$4(), t$8), t$8, void 0, s$9 ?? {}); - } - return h$7._$AI(t$7), h$7; -}; - -/** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ const s$6 = globalThis; -var i$6 = class extends y$1 { - constructor() { - super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0; - } - createRenderRoot() { - const t$7 = super.createRenderRoot(); - return this.renderOptions.renderBefore ??= t$7.firstChild, t$7; - } - update(t$7) { - const r$12 = this.render(); - this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t$7), this._$Do = D(r$12, this.renderRoot, this.renderOptions); - } - connectedCallback() { - super.connectedCallback(), this._$Do?.setConnected(!0); - } - disconnectedCallback() { - super.disconnectedCallback(), this._$Do?.setConnected(!1); - } - render() { - return E; - } -}; -i$6._$litElement$ = !0, i$6["finalized"] = !0, s$6.litElementHydrateSupport?.({ LitElement: i$6 }); -const o$11 = s$6.litElementPolyfillSupport; -o$11?.({ LitElement: i$6 }); -const n$9 = { - _$AK: (t$7, e$14, r$12) => { - t$7._$AK(e$14, r$12); - }, - _$AL: (t$7) => t$7._$AL -}; -(s$6.litElementVersions ??= []).push("4.2.2"); - -/** -* @license -* Copyright 2022 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -const o$10 = !1; - -/** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -const t$4 = { - ATTRIBUTE: 1, - CHILD: 2, - PROPERTY: 3, - BOOLEAN_ATTRIBUTE: 4, - EVENT: 5, - ELEMENT: 6 -}, e$10 = (t$7) => (...e$14) => ({ - _$litDirective$: t$7, - values: e$14 -}); -var i$5 = class { - constructor(t$7) {} - get _$AU() { - return this._$AM._$AU; - } - _$AT(t$7, e$14, i$10) { - this._$Ct = t$7, this._$AM = e$14, this._$Ci = i$10; - } - _$AS(t$7, e$14) { - return this.update(t$7, e$14); - } - update(t$7, e$14) { - return this.render(...e$14); - } -}; - -/** -* @license -* Copyright 2020 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ const { I: t$3 } = j$1, i$4 = (o$15) => o$15, n$8 = (o$15) => null === o$15 || "object" != typeof o$15 && "function" != typeof o$15, e$9 = { - HTML: 1, - SVG: 2, - MATHML: 3 -}, l$2 = (o$15, t$7) => void 0 === t$7 ? void 0 !== o$15?._$litType$ : o$15?._$litType$ === t$7, d = (o$15) => null != o$15?._$litType$?.h, c$3 = (o$15) => void 0 !== o$15?._$litDirective$, f$1 = (o$15) => o$15?._$litDirective$, r$8 = (o$15) => void 0 === o$15.strings, s$5 = () => document.createComment(""), v = (o$15, n$13, e$14) => { - const l$5 = o$15._$AA.parentNode, d = void 0 === n$13 ? o$15._$AB : n$13._$AA; - if (void 0 === e$14) { - const i$10 = l$5.insertBefore(s$5(), d), n$14 = l$5.insertBefore(s$5(), d); - e$14 = new t$3(i$10, n$14, o$15, o$15.options); - } else { - const t$7 = e$14._$AB.nextSibling, n$14 = e$14._$AM, c$7 = n$14 !== o$15; - if (c$7) { - let t$8; - e$14._$AQ?.(o$15), e$14._$AM = o$15, void 0 !== e$14._$AP && (t$8 = o$15._$AU) !== n$14._$AU && e$14._$AP(t$8); - } - if (t$7 !== d || c$7) { - let o$16 = e$14._$AA; - for (; o$16 !== t$7;) { - const t$8 = i$4(o$16).nextSibling; - i$4(l$5).insertBefore(o$16, d), o$16 = t$8; - } - } - } - return e$14; -}, u$1 = (o$15, t$7, i$10 = o$15) => (o$15._$AI(t$7, i$10), o$15), m$1 = {}, p = (o$15, t$7 = m$1) => o$15._$AH = t$7, M = (o$15) => o$15._$AH, h$4 = (o$15) => { - o$15._$AR(), o$15._$AA.remove(); -}, j = (o$15) => { - o$15._$AR(); -}; - -/** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -const u = (e$14, s$9, t$7) => { - const r$12 = new Map(); - for (let l$5 = s$9; l$5 <= t$7; l$5++) r$12.set(e$14[l$5], l$5); - return r$12; -}, c$2 = e$10(class extends i$5 { - constructor(e$14) { - if (super(e$14), e$14.type !== t$4.CHILD) throw Error("repeat() can only be used in text expressions"); - } - dt(e$14, s$9, t$7) { - let r$12; - void 0 === t$7 ? t$7 = s$9 : void 0 !== s$9 && (r$12 = s$9); - const l$5 = [], o$15 = []; - let i$10 = 0; - for (const s$10 of e$14) l$5[i$10] = r$12 ? r$12(s$10, i$10) : i$10, o$15[i$10] = t$7(s$10, i$10), i$10++; - return { - values: o$15, - keys: l$5 - }; - } - render(e$14, s$9, t$7) { - return this.dt(e$14, s$9, t$7).values; - } - update(s$9, [t$7, r$12, c$7]) { - const d$3 = M(s$9), { values: p$3, keys: a$2 } = this.dt(t$7, r$12, c$7); - if (!Array.isArray(d$3)) return this.ut = a$2, p$3; - const h$7 = this.ut ??= [], v$2 = []; - let m$3, y$2, x$1 = 0, j$2 = d$3.length - 1, k$1 = 0, w$1 = p$3.length - 1; - for (; x$1 <= j$2 && k$1 <= w$1;) if (null === d$3[x$1]) x$1++; - else if (null === d$3[j$2]) j$2--; - else if (h$7[x$1] === a$2[k$1]) v$2[k$1] = u$1(d$3[x$1], p$3[k$1]), x$1++, k$1++; - else if (h$7[j$2] === a$2[w$1]) v$2[w$1] = u$1(d$3[j$2], p$3[w$1]), j$2--, w$1--; - else if (h$7[x$1] === a$2[w$1]) v$2[w$1] = u$1(d$3[x$1], p$3[w$1]), v(s$9, v$2[w$1 + 1], d$3[x$1]), x$1++, w$1--; - else if (h$7[j$2] === a$2[k$1]) v$2[k$1] = u$1(d$3[j$2], p$3[k$1]), v(s$9, d$3[x$1], d$3[j$2]), j$2--, k$1++; - else if (void 0 === m$3 && (m$3 = u(a$2, k$1, w$1), y$2 = u(h$7, x$1, j$2)), m$3.has(h$7[x$1])) if (m$3.has(h$7[j$2])) { - const e$14 = y$2.get(a$2[k$1]), t$8 = void 0 !== e$14 ? d$3[e$14] : null; - if (null === t$8) { - const e$15 = v(s$9, d$3[x$1]); - u$1(e$15, p$3[k$1]), v$2[k$1] = e$15; - } else v$2[k$1] = u$1(t$8, p$3[k$1]), v(s$9, d$3[x$1], t$8), d$3[e$14] = null; - k$1++; - } else h$4(d$3[j$2]), j$2--; - else h$4(d$3[x$1]), x$1++; - for (; k$1 <= w$1;) { - const e$14 = v(s$9, v$2[w$1 + 1]); - u$1(e$14, p$3[k$1]), v$2[k$1++] = e$14; - } - for (; x$1 <= j$2;) { - const e$14 = d$3[x$1++]; - null !== e$14 && h$4(e$14); - } - return this.ut = a$2, p(s$9, v$2), E; - } -}); - -/** -* @license -* Copyright 2021 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -var s$4 = class extends Event { - constructor(s$9, t$7, e$14, o$15) { - super("context-request", { - bubbles: !0, - composed: !0 - }), this.context = s$9, this.contextTarget = t$7, this.callback = e$14, this.subscribe = o$15 ?? !1; - } -}; - -/** -* @license -* Copyright 2021 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -function n$7(n$13) { - return n$13; -} - -/** -* @license -* Copyright 2021 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ var s$3 = class { - constructor(t$7, s$9, i$10, h$7) { - if (this.subscribe = !1, this.provided = !1, this.value = void 0, this.t = (t$8, s$10) => { - this.unsubscribe && (this.unsubscribe !== s$10 && (this.provided = !1, this.unsubscribe()), this.subscribe || this.unsubscribe()), this.value = t$8, this.host.requestUpdate(), this.provided && !this.subscribe || (this.provided = !0, this.callback && this.callback(t$8, s$10)), this.unsubscribe = s$10; - }, this.host = t$7, void 0 !== s$9.context) { - const t$8 = s$9; - this.context = t$8.context, this.callback = t$8.callback, this.subscribe = t$8.subscribe ?? !1; - } else this.context = s$9, this.callback = i$10, this.subscribe = h$7 ?? !1; - this.host.addController(this); - } - hostConnected() { - this.dispatchRequest(); - } - hostDisconnected() { - this.unsubscribe && (this.unsubscribe(), this.unsubscribe = void 0); - } - dispatchRequest() { - this.host.dispatchEvent(new s$4(this.context, this.host, this.t, this.subscribe)); - } -}; - -/** -* @license -* Copyright 2021 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -var s$2 = class { - get value() { - return this.o; - } - set value(s$9) { - this.setValue(s$9); - } - setValue(s$9, t$7 = !1) { - const i$10 = t$7 || !Object.is(s$9, this.o); - this.o = s$9, i$10 && this.updateObservers(); - } - constructor(s$9) { - this.subscriptions = new Map(), this.updateObservers = () => { - for (const [s$10, { disposer: t$7 }] of this.subscriptions) s$10(this.o, t$7); - }, void 0 !== s$9 && (this.value = s$9); - } - addCallback(s$9, t$7, i$10) { - if (!i$10) return void s$9(this.value); - this.subscriptions.has(s$9) || this.subscriptions.set(s$9, { - disposer: () => { - this.subscriptions.delete(s$9); - }, - consumerHost: t$7 - }); - const { disposer: h$7 } = this.subscriptions.get(s$9); - s$9(this.value, h$7); - } - clearCallbacks() { - this.subscriptions.clear(); - } -}; - -/** -* @license -* Copyright 2021 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ var e$8 = class extends Event { - constructor(t$7, s$9) { - super("context-provider", { - bubbles: !0, - composed: !0 - }), this.context = t$7, this.contextTarget = s$9; - } -}; -var i$3 = class extends s$2 { - constructor(s$9, e$14, i$10) { - super(void 0 !== e$14.context ? e$14.initialValue : i$10), this.onContextRequest = (t$7) => { - if (t$7.context !== this.context) return; - const s$10 = t$7.contextTarget ?? t$7.composedPath()[0]; - s$10 !== this.host && (t$7.stopPropagation(), this.addCallback(t$7.callback, s$10, t$7.subscribe)); - }, this.onProviderRequest = (s$10) => { - if (s$10.context !== this.context) return; - if ((s$10.contextTarget ?? s$10.composedPath()[0]) === this.host) return; - const e$15 = new Set(); - for (const [s$11, { consumerHost: i$11 }] of this.subscriptions) e$15.has(s$11) || (e$15.add(s$11), i$11.dispatchEvent(new s$4(this.context, i$11, s$11, !0))); - s$10.stopPropagation(); - }, this.host = s$9, void 0 !== e$14.context ? this.context = e$14.context : this.context = e$14, this.attachListeners(), this.host.addController?.(this); - } - attachListeners() { - this.host.addEventListener("context-request", this.onContextRequest), this.host.addEventListener("context-provider", this.onProviderRequest); - } - hostConnected() { - this.host.dispatchEvent(new e$8(this.context, this.host)); - } -}; - -/** -* @license -* Copyright 2021 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ var t$2 = class { - constructor() { - this.pendingContextRequests = new Map(), this.onContextProvider = (t$7) => { - const s$9 = this.pendingContextRequests.get(t$7.context); - if (void 0 === s$9) return; - this.pendingContextRequests.delete(t$7.context); - const { requests: o$15 } = s$9; - for (const { elementRef: s$10, callbackRef: n$13 } of o$15) { - const o$16 = s$10.deref(), c$7 = n$13.deref(); - void 0 === o$16 || void 0 === c$7 || o$16.dispatchEvent(new s$4(t$7.context, o$16, c$7, !0)); - } - }, this.onContextRequest = (e$14) => { - if (!0 !== e$14.subscribe) return; - const t$7 = e$14.contextTarget ?? e$14.composedPath()[0], s$9 = e$14.callback; - let o$15 = this.pendingContextRequests.get(e$14.context); - void 0 === o$15 && this.pendingContextRequests.set(e$14.context, o$15 = { - callbacks: new WeakMap(), - requests: [] - }); - let n$13 = o$15.callbacks.get(t$7); - void 0 === n$13 && o$15.callbacks.set(t$7, n$13 = new WeakSet()), n$13.has(s$9) || (n$13.add(s$9), o$15.requests.push({ - elementRef: new WeakRef(t$7), - callbackRef: new WeakRef(s$9) - })); - }; - } - attach(e$14) { - e$14.addEventListener("context-request", this.onContextRequest), e$14.addEventListener("context-provider", this.onContextProvider); - } - detach(e$14) { - e$14.removeEventListener("context-request", this.onContextRequest), e$14.removeEventListener("context-provider", this.onContextProvider); - } -}; - -/** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ function e$7({ context: e$14 }) { - return (n$13, i$10) => { - const r$12 = new WeakMap(); - if ("object" == typeof i$10) return { - get() { - return n$13.get.call(this); - }, - set(t$7) { - return r$12.get(this).setValue(t$7), n$13.set.call(this, t$7); - }, - init(n$14) { - return r$12.set(this, new i$3(this, { - context: e$14, - initialValue: n$14 - })), n$14; - } - }; - { - n$13.constructor.addInitializer(((n$14) => { - r$12.set(n$14, new i$3(n$14, { context: e$14 })); - })); - const o$15 = Object.getOwnPropertyDescriptor(n$13, i$10); - let s$9; - if (void 0 === o$15) { - const t$7 = new WeakMap(); - s$9 = { - get() { - return t$7.get(this); - }, - set(e$15) { - r$12.get(this).setValue(e$15), t$7.set(this, e$15); - }, - configurable: !0, - enumerable: !0 - }; - } else { - const t$7 = o$15.set; - s$9 = { - ...o$15, - set(e$15) { - r$12.get(this).setValue(e$15), t$7?.call(this, e$15); - } - }; - } - return void Object.defineProperty(n$13, i$10, s$9); - } - }; -} - -/** -* @license -* Copyright 2022 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ function c$1({ context: c$7, subscribe: e$14 }) { - return (o$15, n$13) => { - "object" == typeof n$13 ? n$13.addInitializer((function() { - new s$3(this, { - context: c$7, - callback: (t$7) => { - o$15.set.call(this, t$7); - }, - subscribe: e$14 - }); - })) : o$15.constructor.addInitializer(((o$16) => { - new s$3(o$16, { - context: c$7, - callback: (t$7) => { - o$16[n$13] = t$7; - }, - subscribe: e$14 - }); - })); - }; -} - -const eventInit = { - bubbles: true, - cancelable: true, - composed: true -}; -var StateEvent = class StateEvent extends CustomEvent { - static { - this.eventName = "a2uiaction"; - } - constructor(payload) { - super(StateEvent.eventName, { - detail: payload, - ...eventInit - }); - this.payload = payload; - } -}; - -const opacityBehavior = ` - &:not([disabled]) { - cursor: pointer; - opacity: var(--opacity, 0); - transition: opacity var(--speed, 0.2s) cubic-bezier(0, 0, 0.3, 1); - - &:hover, - &:focus { - opacity: 1; - } - }`; -const behavior = ` - ${new Array(21).fill(0).map((_$1, idx) => { - return `.behavior-ho-${idx * 5} { - --opacity: ${idx / 20}; - ${opacityBehavior} - }`; -}).join("\n")} - - .behavior-o-s { - overflow: scroll; - } - - .behavior-o-a { - overflow: auto; - } - - .behavior-o-h { - overflow: hidden; - } - - .behavior-sw-n { - scrollbar-width: none; - } -`; - -const grid = 4; - -const border = ` - ${new Array(25).fill(0).map((_$1, idx) => { - return ` - .border-bw-${idx} { border-width: ${idx}px; } - .border-btw-${idx} { border-top-width: ${idx}px; } - .border-bbw-${idx} { border-bottom-width: ${idx}px; } - .border-blw-${idx} { border-left-width: ${idx}px; } - .border-brw-${idx} { border-right-width: ${idx}px; } - - .border-ow-${idx} { outline-width: ${idx}px; } - .border-br-${idx} { border-radius: ${idx * grid}px; overflow: hidden;}`; -}).join("\n")} - - .border-br-50pc { - border-radius: 50%; - } - - .border-bs-s { - border-style: solid; - } -`; - -const shades = [ - 0, - 5, - 10, - 15, - 20, - 25, - 30, - 35, - 40, - 50, - 60, - 70, - 80, - 90, - 95, - 98, - 99, - 100 -]; - -function merge(...classes) { - const styles = {}; - for (const clazz of classes) { - for (const [key, val] of Object.entries(clazz)) { - const prefix = key.split("-").with(-1, "").join("-"); - const existingKeys = Object.keys(styles).filter((key$1) => key$1.startsWith(prefix)); - for (const existingKey of existingKeys) { - delete styles[existingKey]; - } - styles[key] = val; - } - } - return styles; -} -function appendToAll(target, exclusions, ...classes) { - const updatedTarget = structuredClone(target); - for (const clazz of classes) { - for (const key of Object.keys(clazz)) { - const prefix = key.split("-").with(-1, "").join("-"); - for (const [tagName, classesToAdd] of Object.entries(updatedTarget)) { - if (exclusions.includes(tagName)) { - continue; - } - let found = false; - for (let t$7 = 0; t$7 < classesToAdd.length; t$7++) { - if (classesToAdd[t$7].startsWith(prefix)) { - found = true; - classesToAdd[t$7] = key; - } - } - if (!found) { - classesToAdd.push(key); - } - } - } - } - return updatedTarget; -} -function createThemeStyles(palettes) { - const styles = {}; - for (const palette of Object.values(palettes)) { - for (const [key, val] of Object.entries(palette)) { - const prop = toProp(key); - styles[prop] = val; - } - } - return styles; -} -function toProp(key) { - if (key.startsWith("nv")) { - return `--nv-${key.slice(2)}`; - } - return `--${key[0]}-${key.slice(1)}`; -} - -const color = (src) => ` - ${src.map((key) => { - const inverseKey = getInverseKey(key); - return `.color-bc-${key} { border-color: light-dark(var(${toProp(key)}), var(${toProp(inverseKey)})); }`; -}).join("\n")} - - ${src.map((key) => { - const inverseKey = getInverseKey(key); - const vals = [`.color-bgc-${key} { background-color: light-dark(var(${toProp(key)}), var(${toProp(inverseKey)})); }`, `.color-bbgc-${key}::backdrop { background-color: light-dark(var(${toProp(key)}), var(${toProp(inverseKey)})); }`]; - for (let o$15 = .1; o$15 < 1; o$15 += .1) { - vals.push(`.color-bbgc-${key}_${(o$15 * 100).toFixed(0)}::backdrop { - background-color: light-dark(oklch(from var(${toProp(key)}) l c h / calc(alpha * ${o$15.toFixed(1)})), oklch(from var(${toProp(inverseKey)}) l c h / calc(alpha * ${o$15.toFixed(1)})) ); - } - `); - } - return vals.join("\n"); -}).join("\n")} - - ${src.map((key) => { - const inverseKey = getInverseKey(key); - return `.color-c-${key} { color: light-dark(var(${toProp(key)}), var(${toProp(inverseKey)})); }`; -}).join("\n")} - `; -const getInverseKey = (key) => { - const match = key.match(/^([a-z]+)(\d+)$/); - if (!match) return key; - const [, prefix, shadeStr] = match; - const shade = parseInt(shadeStr, 10); - const target = 100 - shade; - const inverseShade = shades.reduce((prev, curr) => Math.abs(curr - target) < Math.abs(prev - target) ? curr : prev); - return `${prefix}${inverseShade}`; -}; -const keyFactory = (prefix) => { - return shades.map((v$2) => `${prefix}${v$2}`); -}; -const colors = [ - color(keyFactory("p")), - color(keyFactory("s")), - color(keyFactory("t")), - color(keyFactory("n")), - color(keyFactory("nv")), - color(keyFactory("e")), - ` - .color-bgc-transparent { - background-color: transparent; - } - - :host { - color-scheme: var(--color-scheme); - } - ` -]; - -/** -* CSS classes for Google Symbols. -* -* Usage: -* -* ```html -* pen_spark -* ``` -*/ -const icons = ` - .g-icon { - font-family: "Material Symbols Outlined", "Google Symbols"; - font-weight: normal; - font-style: normal; - font-display: optional; - font-size: 20px; - width: 1em; - height: 1em; - user-select: none; - line-height: 1; - letter-spacing: normal; - text-transform: none; - display: inline-block; - white-space: nowrap; - word-wrap: normal; - direction: ltr; - -webkit-font-feature-settings: "liga"; - -webkit-font-smoothing: antialiased; - overflow: hidden; - - font-variation-settings: "FILL" 0, "wght" 300, "GRAD" 0, "opsz" 48, - "ROND" 100; - - &.filled { - font-variation-settings: "FILL" 1, "wght" 300, "GRAD" 0, "opsz" 48, - "ROND" 100; - } - - &.filled-heavy { - font-variation-settings: "FILL" 1, "wght" 700, "GRAD" 0, "opsz" 48, - "ROND" 100; - } - } -`; - -const layout = ` - :host { - ${new Array(16).fill(0).map((_$1, idx) => { - return `--g-${idx + 1}: ${(idx + 1) * grid}px;`; -}).join("\n")} - } - - ${new Array(49).fill(0).map((_$1, index) => { - const idx = index - 24; - const lbl = idx < 0 ? `n${Math.abs(idx)}` : idx.toString(); - return ` - .layout-p-${lbl} { --padding: ${idx * grid}px; padding: var(--padding); } - .layout-pt-${lbl} { padding-top: ${idx * grid}px; } - .layout-pr-${lbl} { padding-right: ${idx * grid}px; } - .layout-pb-${lbl} { padding-bottom: ${idx * grid}px; } - .layout-pl-${lbl} { padding-left: ${idx * grid}px; } - - .layout-m-${lbl} { --margin: ${idx * grid}px; margin: var(--margin); } - .layout-mt-${lbl} { margin-top: ${idx * grid}px; } - .layout-mr-${lbl} { margin-right: ${idx * grid}px; } - .layout-mb-${lbl} { margin-bottom: ${idx * grid}px; } - .layout-ml-${lbl} { margin-left: ${idx * grid}px; } - - .layout-t-${lbl} { top: ${idx * grid}px; } - .layout-r-${lbl} { right: ${idx * grid}px; } - .layout-b-${lbl} { bottom: ${idx * grid}px; } - .layout-l-${lbl} { left: ${idx * grid}px; }`; -}).join("\n")} - - ${new Array(25).fill(0).map((_$1, idx) => { - return ` - .layout-g-${idx} { gap: ${idx * grid}px; }`; -}).join("\n")} - - ${new Array(8).fill(0).map((_$1, idx) => { - return ` - .layout-grd-col${idx + 1} { grid-template-columns: ${"1fr ".repeat(idx + 1).trim()}; }`; -}).join("\n")} - - .layout-pos-a { - position: absolute; - } - - .layout-pos-rel { - position: relative; - } - - .layout-dsp-none { - display: none; - } - - .layout-dsp-block { - display: block; - } - - .layout-dsp-grid { - display: grid; - } - - .layout-dsp-iflex { - display: inline-flex; - } - - .layout-dsp-flexvert { - display: flex; - flex-direction: column; - } - - .layout-dsp-flexhor { - display: flex; - flex-direction: row; - } - - .layout-fw-w { - flex-wrap: wrap; - } - - .layout-al-fs { - align-items: start; - } - - .layout-al-fe { - align-items: end; - } - - .layout-al-c { - align-items: center; - } - - .layout-as-n { - align-self: normal; - } - - .layout-js-c { - justify-self: center; - } - - .layout-sp-c { - justify-content: center; - } - - .layout-sp-ev { - justify-content: space-evenly; - } - - .layout-sp-bt { - justify-content: space-between; - } - - .layout-sp-s { - justify-content: start; - } - - .layout-sp-e { - justify-content: end; - } - - .layout-ji-e { - justify-items: end; - } - - .layout-r-none { - resize: none; - } - - .layout-fs-c { - field-sizing: content; - } - - .layout-fs-n { - field-sizing: none; - } - - .layout-flx-0 { - flex: 0 0 auto; - } - - .layout-flx-1 { - flex: 1 0 auto; - } - - .layout-c-s { - contain: strict; - } - - /** Widths **/ - - ${new Array(10).fill(0).map((_$1, idx) => { - const weight = (idx + 1) * 10; - return `.layout-w-${weight} { width: ${weight}%; max-width: ${weight}%; }`; -}).join("\n")} - - ${new Array(16).fill(0).map((_$1, idx) => { - const weight = idx * grid; - return `.layout-wp-${idx} { width: ${weight}px; }`; -}).join("\n")} - - /** Heights **/ - - ${new Array(10).fill(0).map((_$1, idx) => { - const height = (idx + 1) * 10; - return `.layout-h-${height} { height: ${height}%; }`; -}).join("\n")} - - ${new Array(16).fill(0).map((_$1, idx) => { - const height = idx * grid; - return `.layout-hp-${idx} { height: ${height}px; }`; -}).join("\n")} - - .layout-el-cv { - & img, - & video { - width: 100%; - height: 100%; - object-fit: cover; - margin: 0; - } - } - - .layout-ar-sq { - aspect-ratio: 1 / 1; - } - - .layout-ex-fb { - margin: calc(var(--padding) * -1) 0 0 calc(var(--padding) * -1); - width: calc(100% + var(--padding) * 2); - height: calc(100% + var(--padding) * 2); - } -`; - -const opacity = ` - ${new Array(21).fill(0).map((_$1, idx) => { - return `.opacity-el-${idx * 5} { opacity: ${idx / 20}; }`; -}).join("\n")} -`; - -const type$1 = ` - :host { - --default-font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - --default-font-family-mono: "Courier New", Courier, monospace; - } - - .typography-f-s { - font-family: var(--font-family, var(--default-font-family)); - font-optical-sizing: auto; - font-variation-settings: "slnt" 0, "wdth" 100, "GRAD" 0; - } - - .typography-f-sf { - font-family: var(--font-family-flex, var(--default-font-family)); - font-optical-sizing: auto; - } - - .typography-f-c { - font-family: var(--font-family-mono, var(--default-font-family)); - font-optical-sizing: auto; - font-variation-settings: "slnt" 0, "wdth" 100, "GRAD" 0; - } - - .typography-v-r { - font-variation-settings: "slnt" 0, "wdth" 100, "GRAD" 0, "ROND" 100; - } - - .typography-ta-s { - text-align: start; - } - - .typography-ta-c { - text-align: center; - } - - .typography-fs-n { - font-style: normal; - } - - .typography-fs-i { - font-style: italic; - } - - .typography-sz-ls { - font-size: 11px; - line-height: 16px; - } - - .typography-sz-lm { - font-size: 12px; - line-height: 16px; - } - - .typography-sz-ll { - font-size: 14px; - line-height: 20px; - } - - .typography-sz-bs { - font-size: 12px; - line-height: 16px; - } - - .typography-sz-bm { - font-size: 14px; - line-height: 20px; - } - - .typography-sz-bl { - font-size: 16px; - line-height: 24px; - } - - .typography-sz-ts { - font-size: 14px; - line-height: 20px; - } - - .typography-sz-tm { - font-size: 16px; - line-height: 24px; - } - - .typography-sz-tl { - font-size: 22px; - line-height: 28px; - } - - .typography-sz-hs { - font-size: 24px; - line-height: 32px; - } - - .typography-sz-hm { - font-size: 28px; - line-height: 36px; - } - - .typography-sz-hl { - font-size: 32px; - line-height: 40px; - } - - .typography-sz-ds { - font-size: 36px; - line-height: 44px; - } - - .typography-sz-dm { - font-size: 45px; - line-height: 52px; - } - - .typography-sz-dl { - font-size: 57px; - line-height: 64px; - } - - .typography-ws-p { - white-space: pre-line; - } - - .typography-ws-nw { - white-space: nowrap; - } - - .typography-td-none { - text-decoration: none; - } - - /** Weights **/ - - ${new Array(9).fill(0).map((_$1, idx) => { - const weight = (idx + 1) * 100; - return `.typography-w-${weight} { font-weight: ${weight}; }`; -}).join("\n")} -`; - -const structuralStyles$1 = [ - behavior, - border, - colors, - icons, - layout, - opacity, - type$1 -].flat(Infinity).join("\n"); - -var guards_exports = /* @__PURE__ */ __exportAll({ - isComponentArrayReference: () => isComponentArrayReference, - isObject: () => isObject$1, - isPath: () => isPath, - isResolvedAudioPlayer: () => isResolvedAudioPlayer, - isResolvedButton: () => isResolvedButton, - isResolvedCard: () => isResolvedCard, - isResolvedCheckbox: () => isResolvedCheckbox, - isResolvedColumn: () => isResolvedColumn, - isResolvedDateTimeInput: () => isResolvedDateTimeInput, - isResolvedDivider: () => isResolvedDivider, - isResolvedIcon: () => isResolvedIcon, - isResolvedImage: () => isResolvedImage, - isResolvedList: () => isResolvedList, - isResolvedModal: () => isResolvedModal, - isResolvedMultipleChoice: () => isResolvedMultipleChoice, - isResolvedRow: () => isResolvedRow, - isResolvedSlider: () => isResolvedSlider, - isResolvedTabs: () => isResolvedTabs, - isResolvedText: () => isResolvedText, - isResolvedTextField: () => isResolvedTextField, - isResolvedVideo: () => isResolvedVideo, - isValueMap: () => isValueMap -}); -function isValueMap(value) { - return isObject$1(value) && "key" in value; -} -function isPath(key, value) { - return key === "path" && typeof value === "string"; -} -function isObject$1(value) { - return typeof value === "object" && value !== null && !Array.isArray(value); -} -function isComponentArrayReference(value) { - if (!isObject$1(value)) return false; - return "explicitList" in value || "template" in value; -} -function isStringValue(value) { - return isObject$1(value) && ("path" in value || "literal" in value && typeof value.literal === "string" || "literalString" in value); -} -function isNumberValue(value) { - return isObject$1(value) && ("path" in value || "literal" in value && typeof value.literal === "number" || "literalNumber" in value); -} -function isBooleanValue(value) { - return isObject$1(value) && ("path" in value || "literal" in value && typeof value.literal === "boolean" || "literalBoolean" in value); -} -function isAnyComponentNode(value) { - if (!isObject$1(value)) return false; - const hasBaseKeys = "id" in value && "type" in value && "properties" in value; - if (!hasBaseKeys) return false; - return true; -} -function isResolvedAudioPlayer(props) { - return isObject$1(props) && "url" in props && isStringValue(props.url); -} -function isResolvedButton(props) { - return isObject$1(props) && "child" in props && isAnyComponentNode(props.child) && "action" in props; -} -function isResolvedCard(props) { - if (!isObject$1(props)) return false; - if (!("child" in props)) { - if (!("children" in props)) { - return false; - } else { - return Array.isArray(props.children) && props.children.every(isAnyComponentNode); - } - } - return isAnyComponentNode(props.child); -} -function isResolvedCheckbox(props) { - return isObject$1(props) && "label" in props && isStringValue(props.label) && "value" in props && isBooleanValue(props.value); -} -function isResolvedColumn(props) { - return isObject$1(props) && "children" in props && Array.isArray(props.children) && props.children.every(isAnyComponentNode); -} -function isResolvedDateTimeInput(props) { - return isObject$1(props) && "value" in props && isStringValue(props.value); -} -function isResolvedDivider(props) { - return isObject$1(props); -} -function isResolvedImage(props) { - return isObject$1(props) && "url" in props && isStringValue(props.url); -} -function isResolvedIcon(props) { - return isObject$1(props) && "name" in props && isStringValue(props.name); -} -function isResolvedList(props) { - return isObject$1(props) && "children" in props && Array.isArray(props.children) && props.children.every(isAnyComponentNode); -} -function isResolvedModal(props) { - return isObject$1(props) && "entryPointChild" in props && isAnyComponentNode(props.entryPointChild) && "contentChild" in props && isAnyComponentNode(props.contentChild); -} -function isResolvedMultipleChoice(props) { - return isObject$1(props) && "selections" in props; -} -function isResolvedRow(props) { - return isObject$1(props) && "children" in props && Array.isArray(props.children) && props.children.every(isAnyComponentNode); -} -function isResolvedSlider(props) { - return isObject$1(props) && "value" in props && isNumberValue(props.value); -} -function isResolvedTabItem(item) { - return isObject$1(item) && "title" in item && isStringValue(item.title) && "child" in item && isAnyComponentNode(item.child); -} -function isResolvedTabs(props) { - return isObject$1(props) && "tabItems" in props && Array.isArray(props.tabItems) && props.tabItems.every(isResolvedTabItem); -} -function isResolvedText(props) { - return isObject$1(props) && "text" in props && isStringValue(props.text); -} -function isResolvedTextField(props) { - return isObject$1(props) && "label" in props && isStringValue(props.label); -} -function isResolvedVideo(props) { - return isObject$1(props) && "url" in props && isStringValue(props.url); -} - -/** -* Processes and consolidates A2UIProtocolMessage objects into a structured, -* hierarchical model of UI surfaces. -*/ -var A2uiMessageProcessor = class A2uiMessageProcessor { - static { - this.DEFAULT_SURFACE_ID = "@default"; - } - #mapCtor = Map; - #arrayCtor = Array; - #setCtor = Set; - #objCtor = Object; - #surfaces; - constructor(opts = { - mapCtor: Map, - arrayCtor: Array, - setCtor: Set, - objCtor: Object - }) { - this.opts = opts; - this.#arrayCtor = opts.arrayCtor; - this.#mapCtor = opts.mapCtor; - this.#setCtor = opts.setCtor; - this.#objCtor = opts.objCtor; - this.#surfaces = new opts.mapCtor(); - } - getSurfaces() { - return this.#surfaces; - } - clearSurfaces() { - this.#surfaces.clear(); - } - processMessages(messages) { - for (const message of messages) { - if (message.beginRendering) { - this.#handleBeginRendering(message.beginRendering, message.beginRendering.surfaceId); - } - if (message.surfaceUpdate) { - this.#handleSurfaceUpdate(message.surfaceUpdate, message.surfaceUpdate.surfaceId); - } - if (message.dataModelUpdate) { - this.#handleDataModelUpdate(message.dataModelUpdate, message.dataModelUpdate.surfaceId); - } - if (message.deleteSurface) { - this.#handleDeleteSurface(message.deleteSurface); - } - } - } - /** - * Retrieves the data for a given component node and a relative path string. - * This correctly handles the special `.` path, which refers to the node's - * own data context. - */ - getData(node, relativePath, surfaceId = A2uiMessageProcessor.DEFAULT_SURFACE_ID) { - const surface = this.#getOrCreateSurface(surfaceId); - if (!surface) return null; - let finalPath; - if (relativePath === "." || relativePath === "") { - finalPath = node.dataContextPath ?? "/"; - } else { - finalPath = this.resolvePath(relativePath, node.dataContextPath); - } - return this.#getDataByPath(surface.dataModel, finalPath); - } - setData(node, relativePath, value, surfaceId = A2uiMessageProcessor.DEFAULT_SURFACE_ID) { - if (!node) { - console.warn("No component node set"); - return; - } - const surface = this.#getOrCreateSurface(surfaceId); - if (!surface) return; - let finalPath; - if (relativePath === "." || relativePath === "") { - finalPath = node.dataContextPath ?? "/"; - } else { - finalPath = this.resolvePath(relativePath, node.dataContextPath); - } - this.#setDataByPath(surface.dataModel, finalPath, value); - } - resolvePath(path, dataContextPath) { - if (path.startsWith("/")) { - return path; - } - if (dataContextPath && dataContextPath !== "/") { - return dataContextPath.endsWith("/") ? `${dataContextPath}${path}` : `${dataContextPath}/${path}`; - } - return `/${path}`; - } - #parseIfJsonString(value) { - if (typeof value !== "string") { - return value; - } - const trimmedValue = value.trim(); - if (trimmedValue.startsWith("{") && trimmedValue.endsWith("}") || trimmedValue.startsWith("[") && trimmedValue.endsWith("]")) { - try { - return JSON.parse(value); - } catch (e$14) { - console.warn(`Failed to parse potential JSON string: "${value.substring(0, 50)}..."`, e$14); - return value; - } - } - return value; - } - /** - * Converts a specific array format [{key: "...", value_string: "..."}, ...] - * into a standard Map. It also attempts to parse any string values that - * appear to be stringified JSON. - */ - #convertKeyValueArrayToMap(arr) { - const map$1 = new this.#mapCtor(); - for (const item of arr) { - if (!isObject$1(item) || !("key" in item)) continue; - const key = item.key; - const valueKey = this.#findValueKey(item); - if (!valueKey) continue; - let value = item[valueKey]; - if (valueKey === "valueMap" && Array.isArray(value)) { - value = this.#convertKeyValueArrayToMap(value); - } else if (typeof value === "string") { - value = this.#parseIfJsonString(value); - } - this.#setDataByPath(map$1, key, value); - } - return map$1; - } - #setDataByPath(root, path, value) { - if (Array.isArray(value) && (value.length === 0 || isObject$1(value[0]) && "key" in value[0])) { - if (value.length === 1 && isObject$1(value[0]) && value[0].key === ".") { - const item = value[0]; - const valueKey = this.#findValueKey(item); - if (valueKey) { - value = item[valueKey]; - if (valueKey === "valueMap" && Array.isArray(value)) { - value = this.#convertKeyValueArrayToMap(value); - } else if (typeof value === "string") { - value = this.#parseIfJsonString(value); - } - } else { - value = this.#convertKeyValueArrayToMap(value); - } - } else { - value = this.#convertKeyValueArrayToMap(value); - } - } - const segments = this.#normalizePath(path).split("/").filter((s$9) => s$9); - if (segments.length === 0) { - if (value instanceof Map || isObject$1(value)) { - if (!(value instanceof Map) && isObject$1(value)) { - value = new this.#mapCtor(Object.entries(value)); - } - root.clear(); - for (const [key, v$2] of value.entries()) { - root.set(key, v$2); - } - } else { - console.error("Cannot set root of DataModel to a non-Map value."); - } - return; - } - let current = root; - for (let i$10 = 0; i$10 < segments.length - 1; i$10++) { - const segment = segments[i$10]; - let target; - if (current instanceof Map) { - target = current.get(segment); - } else if (Array.isArray(current) && /^\d+$/.test(segment)) { - target = current[parseInt(segment, 10)]; - } - if (target === undefined || typeof target !== "object" || target === null) { - target = new this.#mapCtor(); - if (current instanceof this.#mapCtor) { - current.set(segment, target); - } else if (Array.isArray(current)) { - current[parseInt(segment, 10)] = target; - } - } - current = target; - } - const finalSegment = segments[segments.length - 1]; - const storedValue = value; - if (current instanceof this.#mapCtor) { - current.set(finalSegment, storedValue); - } else if (Array.isArray(current) && /^\d+$/.test(finalSegment)) { - current[parseInt(finalSegment, 10)] = storedValue; - } - } - /** - * Normalizes a path string into a consistent, slash-delimited format. - * Converts bracket notation and dot notation in a two-pass. - * e.g., "bookRecommendations[0].title" -> "/bookRecommendations/0/title" - * e.g., "book.0.title" -> "/book/0/title" - */ - #normalizePath(path) { - const dotPath = path.replace(/\[(\d+)\]/g, ".$1"); - const segments = dotPath.split("."); - return "/" + segments.filter((s$9) => s$9.length > 0).join("/"); - } - #getDataByPath(root, path) { - const segments = this.#normalizePath(path).split("/").filter((s$9) => s$9); - let current = root; - for (const segment of segments) { - if (current === undefined || current === null) return null; - if (current instanceof Map) { - current = current.get(segment); - } else if (Array.isArray(current) && /^\d+$/.test(segment)) { - current = current[parseInt(segment, 10)]; - } else if (isObject$1(current)) { - current = current[segment]; - } else { - return null; - } - } - return current; - } - #getOrCreateSurface(surfaceId) { - let surface = this.#surfaces.get(surfaceId); - if (!surface) { - surface = new this.#objCtor({ - rootComponentId: null, - componentTree: null, - dataModel: new this.#mapCtor(), - components: new this.#mapCtor(), - styles: new this.#objCtor() - }); - this.#surfaces.set(surfaceId, surface); - } - return surface; - } - #handleBeginRendering(message, surfaceId) { - const surface = this.#getOrCreateSurface(surfaceId); - surface.rootComponentId = message.root; - surface.styles = message.styles ?? {}; - this.#rebuildComponentTree(surface); - } - #handleSurfaceUpdate(message, surfaceId) { - const surface = this.#getOrCreateSurface(surfaceId); - for (const component of message.components) { - surface.components.set(component.id, component); - } - this.#rebuildComponentTree(surface); - } - #handleDataModelUpdate(message, surfaceId) { - const surface = this.#getOrCreateSurface(surfaceId); - const path = message.path ?? "/"; - this.#setDataByPath(surface.dataModel, path, message.contents); - this.#rebuildComponentTree(surface); - } - #handleDeleteSurface(message) { - this.#surfaces.delete(message.surfaceId); - } - /** - * Starts at the root component of the surface and builds out the tree - * recursively. This process involves resolving all properties of the child - * components, and expanding on any explicit children lists or templates - * found in the structure. - * - * @param surface The surface to be built. - */ - #rebuildComponentTree(surface) { - if (!surface.rootComponentId) { - surface.componentTree = null; - return; - } - const visited = new this.#setCtor(); - surface.componentTree = this.#buildNodeRecursive(surface.rootComponentId, surface, visited, "/", ""); - } - /** Finds a value key in a map. */ - #findValueKey(value) { - return Object.keys(value).find((k$1) => k$1.startsWith("value")); - } - /** - * Builds out the nodes recursively. - */ - #buildNodeRecursive(baseComponentId, surface, visited, dataContextPath, idSuffix = "") { - const fullId = `${baseComponentId}${idSuffix}`; - const { components } = surface; - if (!components.has(baseComponentId)) { - return null; - } - if (visited.has(fullId)) { - throw new Error(`Circular dependency for component "${fullId}".`); - } - visited.add(fullId); - const componentData = components.get(baseComponentId); - const componentProps = componentData.component ?? {}; - const componentType = Object.keys(componentProps)[0]; - const unresolvedProperties = componentProps[componentType]; - const resolvedProperties = new this.#objCtor(); - if (isObject$1(unresolvedProperties)) { - for (const [key, value] of Object.entries(unresolvedProperties)) { - resolvedProperties[key] = this.#resolvePropertyValue(value, surface, visited, dataContextPath, idSuffix, key); - } - } - visited.delete(fullId); - const baseNode = { - id: fullId, - dataContextPath, - weight: componentData.weight ?? "initial" - }; - switch (componentType) { - case "Text": - if (!isResolvedText(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "Text", - properties: resolvedProperties - }); - case "Image": - if (!isResolvedImage(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "Image", - properties: resolvedProperties - }); - case "Icon": - if (!isResolvedIcon(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "Icon", - properties: resolvedProperties - }); - case "Video": - if (!isResolvedVideo(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "Video", - properties: resolvedProperties - }); - case "AudioPlayer": - if (!isResolvedAudioPlayer(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "AudioPlayer", - properties: resolvedProperties - }); - case "Row": - if (!isResolvedRow(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "Row", - properties: resolvedProperties - }); - case "Column": - if (!isResolvedColumn(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "Column", - properties: resolvedProperties - }); - case "List": - if (!isResolvedList(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "List", - properties: resolvedProperties - }); - case "Card": - if (!isResolvedCard(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "Card", - properties: resolvedProperties - }); - case "Tabs": - if (!isResolvedTabs(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "Tabs", - properties: resolvedProperties - }); - case "Divider": - if (!isResolvedDivider(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "Divider", - properties: resolvedProperties - }); - case "Modal": - if (!isResolvedModal(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "Modal", - properties: resolvedProperties - }); - case "Button": - if (!isResolvedButton(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "Button", - properties: resolvedProperties - }); - case "CheckBox": - if (!isResolvedCheckbox(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "CheckBox", - properties: resolvedProperties - }); - case "TextField": - if (!isResolvedTextField(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "TextField", - properties: resolvedProperties - }); - case "DateTimeInput": - if (!isResolvedDateTimeInput(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "DateTimeInput", - properties: resolvedProperties - }); - case "MultipleChoice": - if (!isResolvedMultipleChoice(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "MultipleChoice", - properties: resolvedProperties - }); - case "Slider": - if (!isResolvedSlider(resolvedProperties)) { - throw new Error(`Invalid data; expected ${componentType}`); - } - return new this.#objCtor({ - ...baseNode, - type: "Slider", - properties: resolvedProperties - }); - default: return new this.#objCtor({ - ...baseNode, - type: componentType, - properties: resolvedProperties - }); - } - } - /** - * Recursively resolves an individual property value. If a property indicates - * a child node (a string that matches a component ID), an explicitList of - * children, or a template, these will be built out here. - */ - #resolvePropertyValue(value, surface, visited, dataContextPath, idSuffix = "", propertyKey = null) { - const isComponentIdReferenceKey = (key) => key === "child" || key.endsWith("Child"); - if (typeof value === "string" && propertyKey && isComponentIdReferenceKey(propertyKey) && surface.components.has(value)) { - return this.#buildNodeRecursive(value, surface, visited, dataContextPath, idSuffix); - } - if (isComponentArrayReference(value)) { - if (value.explicitList) { - return value.explicitList.map((id) => this.#buildNodeRecursive(id, surface, visited, dataContextPath, idSuffix)); - } - if (value.template) { - const fullDataPath = this.resolvePath(value.template.dataBinding, dataContextPath); - const data = this.#getDataByPath(surface.dataModel, fullDataPath); - const template = value.template; - if (Array.isArray(data)) { - return data.map((_$1, index) => { - const parentIndices = dataContextPath.split("/").filter((segment) => /^\d+$/.test(segment)); - const newIndices = [...parentIndices, index]; - const newSuffix = `:${newIndices.join(":")}`; - const childDataContextPath = `${fullDataPath}/${index}`; - return this.#buildNodeRecursive(template.componentId, surface, visited, childDataContextPath, newSuffix); - }); - } - const mapCtor = this.#mapCtor; - if (data instanceof mapCtor) { - return Array.from(data.keys(), (key) => { - const newSuffix = `:${key}`; - const childDataContextPath = `${fullDataPath}/${key}`; - return this.#buildNodeRecursive(template.componentId, surface, visited, childDataContextPath, newSuffix); - }); - } - return new this.#arrayCtor(); - } - } - if (Array.isArray(value)) { - return value.map((item) => this.#resolvePropertyValue(item, surface, visited, dataContextPath, idSuffix, propertyKey)); - } - if (isObject$1(value)) { - const newObj = new this.#objCtor(); - for (const [key, propValue] of Object.entries(value)) { - let propertyValue = propValue; - if (isPath(key, propValue) && dataContextPath !== "/") { - propertyValue = propValue.replace(/^\.?\/item/, "").replace(/^\.?\/text/, "").replace(/^\.?\/label/, "").replace(/^\.?\//, ""); - newObj[key] = propertyValue; - continue; - } - newObj[key] = this.#resolvePropertyValue(propertyValue, surface, visited, dataContextPath, idSuffix, key); - } - return newObj; - } - return value; - } -}; - -var __defProp = Object.defineProperty; -var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { - enumerable: true, - configurable: true, - writable: true, - value -}) : obj[key] = value; -var __publicField = (obj, key, value) => { - __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); - return value; -}; -var __accessCheck = (obj, member, msg) => { - if (!member.has(obj)) throw TypeError("Cannot " + msg); -}; -var __privateIn = (member, obj) => { - if (Object(obj) !== obj) throw TypeError("Cannot use the \"in\" operator on this value"); - return member.has(obj); -}; -var __privateAdd = (obj, member, value) => { - if (member.has(obj)) throw TypeError("Cannot add the same private member more than once"); - member instanceof WeakSet ? member.add(obj) : member.set(obj, value); -}; -var __privateMethod = (obj, member, method) => { - __accessCheck(obj, member, "access private method"); - return method; -}; -/** -* @license -* Copyright Google LLC All Rights Reserved. -* -* Use of this source code is governed by an MIT-style license that can be -* found in the LICENSE file at https://angular.io/license -*/ -function defaultEquals(a$2, b$2) { - return Object.is(a$2, b$2); -} -/** -* @license -* Copyright Google LLC All Rights Reserved. -* -* Use of this source code is governed by an MIT-style license that can be -* found in the LICENSE file at https://angular.io/license -*/ -let activeConsumer = null; -let inNotificationPhase = false; -let epoch = 1; -const SIGNAL = /* @__PURE__ */ Symbol("SIGNAL"); -function setActiveConsumer(consumer) { - const prev = activeConsumer; - activeConsumer = consumer; - return prev; -} -function getActiveConsumer() { - return activeConsumer; -} -function isInNotificationPhase() { - return inNotificationPhase; -} -const REACTIVE_NODE = { - version: 0, - lastCleanEpoch: 0, - dirty: false, - producerNode: void 0, - producerLastReadVersion: void 0, - producerIndexOfThis: void 0, - nextProducerIndex: 0, - liveConsumerNode: void 0, - liveConsumerIndexOfThis: void 0, - consumerAllowSignalWrites: false, - consumerIsAlwaysLive: false, - producerMustRecompute: () => false, - producerRecomputeValue: () => {}, - consumerMarkedDirty: () => {}, - consumerOnSignalRead: () => {} -}; -function producerAccessed(node) { - if (inNotificationPhase) { - throw new Error(typeof ngDevMode !== "undefined" && ngDevMode ? `Assertion error: signal read during notification phase` : ""); - } - if (activeConsumer === null) { - return; - } - activeConsumer.consumerOnSignalRead(node); - const idx = activeConsumer.nextProducerIndex++; - assertConsumerNode(activeConsumer); - if (idx < activeConsumer.producerNode.length && activeConsumer.producerNode[idx] !== node) { - if (consumerIsLive(activeConsumer)) { - const staleProducer = activeConsumer.producerNode[idx]; - producerRemoveLiveConsumerAtIndex(staleProducer, activeConsumer.producerIndexOfThis[idx]); - } - } - if (activeConsumer.producerNode[idx] !== node) { - activeConsumer.producerNode[idx] = node; - activeConsumer.producerIndexOfThis[idx] = consumerIsLive(activeConsumer) ? producerAddLiveConsumer(node, activeConsumer, idx) : 0; - } - activeConsumer.producerLastReadVersion[idx] = node.version; -} -function producerIncrementEpoch() { - epoch++; -} -function producerUpdateValueVersion(node) { - if (!node.dirty && node.lastCleanEpoch === epoch) { - return; - } - if (!node.producerMustRecompute(node) && !consumerPollProducersForChange(node)) { - node.dirty = false; - node.lastCleanEpoch = epoch; - return; - } - node.producerRecomputeValue(node); - node.dirty = false; - node.lastCleanEpoch = epoch; -} -function producerNotifyConsumers(node) { - if (node.liveConsumerNode === void 0) { - return; - } - const prev = inNotificationPhase; - inNotificationPhase = true; - try { - for (const consumer of node.liveConsumerNode) { - if (!consumer.dirty) { - consumerMarkDirty(consumer); - } - } - } finally { - inNotificationPhase = prev; - } -} -function producerUpdatesAllowed() { - return (activeConsumer == null ? void 0 : activeConsumer.consumerAllowSignalWrites) !== false; -} -function consumerMarkDirty(node) { - var _a$1; - node.dirty = true; - producerNotifyConsumers(node); - (_a$1 = node.consumerMarkedDirty) == null ? void 0 : _a$1.call(node.wrapper ?? node); -} -function consumerBeforeComputation(node) { - node && (node.nextProducerIndex = 0); - return setActiveConsumer(node); -} -function consumerAfterComputation(node, prevConsumer) { - setActiveConsumer(prevConsumer); - if (!node || node.producerNode === void 0 || node.producerIndexOfThis === void 0 || node.producerLastReadVersion === void 0) { - return; - } - if (consumerIsLive(node)) { - for (let i$10 = node.nextProducerIndex; i$10 < node.producerNode.length; i$10++) { - producerRemoveLiveConsumerAtIndex(node.producerNode[i$10], node.producerIndexOfThis[i$10]); - } - } - while (node.producerNode.length > node.nextProducerIndex) { - node.producerNode.pop(); - node.producerLastReadVersion.pop(); - node.producerIndexOfThis.pop(); - } -} -function consumerPollProducersForChange(node) { - assertConsumerNode(node); - for (let i$10 = 0; i$10 < node.producerNode.length; i$10++) { - const producer = node.producerNode[i$10]; - const seenVersion = node.producerLastReadVersion[i$10]; - if (seenVersion !== producer.version) { - return true; - } - producerUpdateValueVersion(producer); - if (seenVersion !== producer.version) { - return true; - } - } - return false; -} -function producerAddLiveConsumer(node, consumer, indexOfThis) { - var _a$1; - assertProducerNode(node); - assertConsumerNode(node); - if (node.liveConsumerNode.length === 0) { - (_a$1 = node.watched) == null ? void 0 : _a$1.call(node.wrapper); - for (let i$10 = 0; i$10 < node.producerNode.length; i$10++) { - node.producerIndexOfThis[i$10] = producerAddLiveConsumer(node.producerNode[i$10], node, i$10); - } - } - node.liveConsumerIndexOfThis.push(indexOfThis); - return node.liveConsumerNode.push(consumer) - 1; -} -function producerRemoveLiveConsumerAtIndex(node, idx) { - var _a$1; - assertProducerNode(node); - assertConsumerNode(node); - if (typeof ngDevMode !== "undefined" && ngDevMode && idx >= node.liveConsumerNode.length) { - throw new Error(`Assertion error: active consumer index ${idx} is out of bounds of ${node.liveConsumerNode.length} consumers)`); - } - if (node.liveConsumerNode.length === 1) { - (_a$1 = node.unwatched) == null ? void 0 : _a$1.call(node.wrapper); - for (let i$10 = 0; i$10 < node.producerNode.length; i$10++) { - producerRemoveLiveConsumerAtIndex(node.producerNode[i$10], node.producerIndexOfThis[i$10]); - } - } - const lastIdx = node.liveConsumerNode.length - 1; - node.liveConsumerNode[idx] = node.liveConsumerNode[lastIdx]; - node.liveConsumerIndexOfThis[idx] = node.liveConsumerIndexOfThis[lastIdx]; - node.liveConsumerNode.length--; - node.liveConsumerIndexOfThis.length--; - if (idx < node.liveConsumerNode.length) { - const idxProducer = node.liveConsumerIndexOfThis[idx]; - const consumer = node.liveConsumerNode[idx]; - assertConsumerNode(consumer); - consumer.producerIndexOfThis[idxProducer] = idx; - } -} -function consumerIsLive(node) { - var _a$1; - return node.consumerIsAlwaysLive || (((_a$1 = node == null ? void 0 : node.liveConsumerNode) == null ? void 0 : _a$1.length) ?? 0) > 0; -} -function assertConsumerNode(node) { - node.producerNode ?? (node.producerNode = []); - node.producerIndexOfThis ?? (node.producerIndexOfThis = []); - node.producerLastReadVersion ?? (node.producerLastReadVersion = []); -} -function assertProducerNode(node) { - node.liveConsumerNode ?? (node.liveConsumerNode = []); - node.liveConsumerIndexOfThis ?? (node.liveConsumerIndexOfThis = []); -} -/** -* @license -* Copyright Google LLC All Rights Reserved. -* -* Use of this source code is governed by an MIT-style license that can be -* found in the LICENSE file at https://angular.io/license -*/ -function computedGet(node) { - producerUpdateValueVersion(node); - producerAccessed(node); - if (node.value === ERRORED) { - throw node.error; - } - return node.value; -} -function createComputed(computation) { - const node = Object.create(COMPUTED_NODE); - node.computation = computation; - const computed = () => computedGet(node); - computed[SIGNAL] = node; - return computed; -} -const UNSET = /* @__PURE__ */ Symbol("UNSET"); -const COMPUTING = /* @__PURE__ */ Symbol("COMPUTING"); -const ERRORED = /* @__PURE__ */ Symbol("ERRORED"); -const COMPUTED_NODE = /* @__PURE__ */ (() => { - return { - ...REACTIVE_NODE, - value: UNSET, - dirty: true, - error: null, - equal: defaultEquals, - producerMustRecompute(node) { - return node.value === UNSET || node.value === COMPUTING; - }, - producerRecomputeValue(node) { - if (node.value === COMPUTING) { - throw new Error("Detected cycle in computations."); - } - const oldValue = node.value; - node.value = COMPUTING; - const prevConsumer = consumerBeforeComputation(node); - let newValue; - let wasEqual = false; - try { - newValue = node.computation.call(node.wrapper); - const oldOk = oldValue !== UNSET && oldValue !== ERRORED; - wasEqual = oldOk && node.equal.call(node.wrapper, oldValue, newValue); - } catch (err) { - newValue = ERRORED; - node.error = err; - } finally { - consumerAfterComputation(node, prevConsumer); - } - if (wasEqual) { - node.value = oldValue; - return; - } - node.value = newValue; - node.version++; - } - }; -})(); -/** -* @license -* Copyright Google LLC All Rights Reserved. -* -* Use of this source code is governed by an MIT-style license that can be -* found in the LICENSE file at https://angular.io/license -*/ -function defaultThrowError() { - throw new Error(); -} -let throwInvalidWriteToSignalErrorFn = defaultThrowError; -function throwInvalidWriteToSignalError() { - throwInvalidWriteToSignalErrorFn(); -} -/** -* @license -* Copyright Google LLC All Rights Reserved. -* -* Use of this source code is governed by an MIT-style license that can be -* found in the LICENSE file at https://angular.io/license -*/ -function createSignal(initialValue) { - const node = Object.create(SIGNAL_NODE); - node.value = initialValue; - const getter = () => { - producerAccessed(node); - return node.value; - }; - getter[SIGNAL] = node; - return getter; -} -function signalGetFn() { - producerAccessed(this); - return this.value; -} -function signalSetFn(node, newValue) { - if (!producerUpdatesAllowed()) { - throwInvalidWriteToSignalError(); - } - if (!node.equal.call(node.wrapper, node.value, newValue)) { - node.value = newValue; - signalValueChanged(node); - } -} -const SIGNAL_NODE = /* @__PURE__ */ (() => { - return { - ...REACTIVE_NODE, - equal: defaultEquals, - value: void 0 - }; -})(); -function signalValueChanged(node) { - node.version++; - producerIncrementEpoch(); - producerNotifyConsumers(node); -} -/** -* @license -* Copyright 2024 Bloomberg Finance L.P. -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ -const NODE = Symbol("node"); -var Signal; -((Signal2) => { - var _a$1, _brand, brand_fn, _b, _brand2, brand_fn2; - class State { - constructor(initialValue, options = {}) { - __privateAdd(this, _brand); - __publicField(this, _a$1); - const ref = createSignal(initialValue); - const node = ref[SIGNAL]; - this[NODE] = node; - node.wrapper = this; - if (options) { - const equals = options.equals; - if (equals) { - node.equal = equals; - } - node.watched = options[Signal2.subtle.watched]; - node.unwatched = options[Signal2.subtle.unwatched]; - } - } - get() { - if (!(0, Signal2.isState)(this)) throw new TypeError("Wrong receiver type for Signal.State.prototype.get"); - return signalGetFn.call(this[NODE]); - } - set(newValue) { - if (!(0, Signal2.isState)(this)) throw new TypeError("Wrong receiver type for Signal.State.prototype.set"); - if (isInNotificationPhase()) { - throw new Error("Writes to signals not permitted during Watcher callback"); - } - const ref = this[NODE]; - signalSetFn(ref, newValue); - } - } - _a$1 = NODE; - _brand = new WeakSet(); - brand_fn = function() {}; - Signal2.isState = (s$9) => typeof s$9 === "object" && __privateIn(_brand, s$9); - Signal2.State = State; - class Computed { - constructor(computation, options) { - __privateAdd(this, _brand2); - __publicField(this, _b); - const ref = createComputed(computation); - const node = ref[SIGNAL]; - node.consumerAllowSignalWrites = true; - this[NODE] = node; - node.wrapper = this; - if (options) { - const equals = options.equals; - if (equals) { - node.equal = equals; - } - node.watched = options[Signal2.subtle.watched]; - node.unwatched = options[Signal2.subtle.unwatched]; - } - } - get() { - if (!(0, Signal2.isComputed)(this)) throw new TypeError("Wrong receiver type for Signal.Computed.prototype.get"); - return computedGet(this[NODE]); - } - } - _b = NODE; - _brand2 = new WeakSet(); - brand_fn2 = function() {}; - Signal2.isComputed = (c$7) => typeof c$7 === "object" && __privateIn(_brand2, c$7); - Signal2.Computed = Computed; - ((subtle2) => { - var _a2, _brand3, brand_fn3, _assertSignals, assertSignals_fn; - function untrack(cb) { - let output; - let prevActiveConsumer = null; - try { - prevActiveConsumer = setActiveConsumer(null); - output = cb(); - } finally { - setActiveConsumer(prevActiveConsumer); - } - return output; - } - subtle2.untrack = untrack; - function introspectSources(sink) { - var _a3; - if (!(0, Signal2.isComputed)(sink) && !(0, Signal2.isWatcher)(sink)) { - throw new TypeError("Called introspectSources without a Computed or Watcher argument"); - } - return ((_a3 = sink[NODE].producerNode) == null ? void 0 : _a3.map((n$13) => n$13.wrapper)) ?? []; - } - subtle2.introspectSources = introspectSources; - function introspectSinks(signal) { - var _a3; - if (!(0, Signal2.isComputed)(signal) && !(0, Signal2.isState)(signal)) { - throw new TypeError("Called introspectSinks without a Signal argument"); - } - return ((_a3 = signal[NODE].liveConsumerNode) == null ? void 0 : _a3.map((n$13) => n$13.wrapper)) ?? []; - } - subtle2.introspectSinks = introspectSinks; - function hasSinks(signal) { - if (!(0, Signal2.isComputed)(signal) && !(0, Signal2.isState)(signal)) { - throw new TypeError("Called hasSinks without a Signal argument"); - } - const liveConsumerNode = signal[NODE].liveConsumerNode; - if (!liveConsumerNode) return false; - return liveConsumerNode.length > 0; - } - subtle2.hasSinks = hasSinks; - function hasSources(signal) { - if (!(0, Signal2.isComputed)(signal) && !(0, Signal2.isWatcher)(signal)) { - throw new TypeError("Called hasSources without a Computed or Watcher argument"); - } - const producerNode = signal[NODE].producerNode; - if (!producerNode) return false; - return producerNode.length > 0; - } - subtle2.hasSources = hasSources; - class Watcher { - constructor(notify) { - __privateAdd(this, _brand3); - __privateAdd(this, _assertSignals); - __publicField(this, _a2); - let node = Object.create(REACTIVE_NODE); - node.wrapper = this; - node.consumerMarkedDirty = notify; - node.consumerIsAlwaysLive = true; - node.consumerAllowSignalWrites = false; - node.producerNode = []; - this[NODE] = node; - } - watch(...signals) { - if (!(0, Signal2.isWatcher)(this)) { - throw new TypeError("Called unwatch without Watcher receiver"); - } - __privateMethod(this, _assertSignals, assertSignals_fn).call(this, signals); - const node = this[NODE]; - node.dirty = false; - const prev = setActiveConsumer(node); - for (const signal of signals) { - producerAccessed(signal[NODE]); - } - setActiveConsumer(prev); - } - unwatch(...signals) { - if (!(0, Signal2.isWatcher)(this)) { - throw new TypeError("Called unwatch without Watcher receiver"); - } - __privateMethod(this, _assertSignals, assertSignals_fn).call(this, signals); - const node = this[NODE]; - assertConsumerNode(node); - for (let i$10 = node.producerNode.length - 1; i$10 >= 0; i$10--) { - if (signals.includes(node.producerNode[i$10].wrapper)) { - producerRemoveLiveConsumerAtIndex(node.producerNode[i$10], node.producerIndexOfThis[i$10]); - const lastIdx = node.producerNode.length - 1; - node.producerNode[i$10] = node.producerNode[lastIdx]; - node.producerIndexOfThis[i$10] = node.producerIndexOfThis[lastIdx]; - node.producerNode.length--; - node.producerIndexOfThis.length--; - node.nextProducerIndex--; - if (i$10 < node.producerNode.length) { - const idxConsumer = node.producerIndexOfThis[i$10]; - const producer = node.producerNode[i$10]; - assertProducerNode(producer); - producer.liveConsumerIndexOfThis[idxConsumer] = i$10; - } - } - } - } - getPending() { - if (!(0, Signal2.isWatcher)(this)) { - throw new TypeError("Called getPending without Watcher receiver"); - } - const node = this[NODE]; - return node.producerNode.filter((n$13) => n$13.dirty).map((n$13) => n$13.wrapper); - } - } - _a2 = NODE; - _brand3 = new WeakSet(); - brand_fn3 = function() {}; - _assertSignals = new WeakSet(); - assertSignals_fn = function(signals) { - for (const signal of signals) { - if (!(0, Signal2.isComputed)(signal) && !(0, Signal2.isState)(signal)) { - throw new TypeError("Called watch/unwatch without a Computed or State argument"); - } - } - }; - Signal2.isWatcher = (w$1) => __privateIn(_brand3, w$1); - subtle2.Watcher = Watcher; - function currentComputed() { - var _a3; - return (_a3 = getActiveConsumer()) == null ? void 0 : _a3.wrapper; - } - subtle2.currentComputed = currentComputed; - subtle2.watched = Symbol("watched"); - subtle2.unwatched = Symbol("unwatched"); - })(Signal2.subtle || (Signal2.subtle = {})); -})(Signal || (Signal = {})); - -/** -* equality check here is always false so that we can dirty the storage -* via setting to _anything_ -* -* -* This is for a pattern where we don't *directly* use signals to back the values used in collections -* so that instanceof checks and getters and other native features "just work" without having -* to do nested proxying. -* -* (though, see deep.ts for nested / deep behavior) -*/ -const createStorage = (initial = null) => new Signal.State(initial, { equals: () => false }); -/** -* Just an alias for brevity -*/ -const BOUND_FUNS = new WeakMap(); -function fnCacheFor(context) { - let fnCache = BOUND_FUNS.get(context); - if (!fnCache) { - fnCache = new Map(); - BOUND_FUNS.set(context, fnCache); - } - return fnCache; -} - -const ARRAY_GETTER_METHODS = new Set([ - Symbol.iterator, - "concat", - "entries", - "every", - "filter", - "find", - "findIndex", - "flat", - "flatMap", - "forEach", - "includes", - "indexOf", - "join", - "keys", - "lastIndexOf", - "map", - "reduce", - "reduceRight", - "slice", - "some", - "values" -]); -const ARRAY_WRITE_THEN_READ_METHODS = new Set([ - "fill", - "push", - "unshift" -]); -function convertToInt(prop) { - if (typeof prop === "symbol") return null; - const num = Number(prop); - if (isNaN(num)) return null; - return num % 1 === 0 ? num : null; -} -var SignalArray = class SignalArray { - /** - * Creates an array from an iterable object. - * @param iterable An iterable object to convert to an array. - */ - /** - * Creates an array from an iterable object. - * @param iterable An iterable object to convert to an array. - * @param mapfn A mapping function to call on every element of the array. - * @param thisArg Value of 'this' used to invoke the mapfn. - */ - static from(iterable, mapfn, thisArg) { - return mapfn ? new SignalArray(Array.from(iterable, mapfn, thisArg)) : new SignalArray(Array.from(iterable)); - } - static of(...arr) { - return new SignalArray(arr); - } - constructor(arr = []) { - let clone = arr.slice(); - let self = this; - let boundFns = new Map(); - /** - Flag to track whether we have *just* intercepted a call to `.push()` or - `.unshift()`, since in those cases (and only those cases!) the `Array` - itself checks `.length` to return from the function call. - */ - let nativelyAccessingLengthFromPushOrUnshift = false; - return new Proxy(clone, { - get(target, prop) { - let index = convertToInt(prop); - if (index !== null) { - self.#readStorageFor(index); - self.#collection.get(); - return target[index]; - } - if (prop === "length") { - if (nativelyAccessingLengthFromPushOrUnshift) { - nativelyAccessingLengthFromPushOrUnshift = false; - } else { - self.#collection.get(); - } - return target[prop]; - } - if (ARRAY_WRITE_THEN_READ_METHODS.has(prop)) { - nativelyAccessingLengthFromPushOrUnshift = true; - } - if (ARRAY_GETTER_METHODS.has(prop)) { - let fn = boundFns.get(prop); - if (fn === undefined) { - fn = (...args) => { - self.#collection.get(); - return target[prop](...args); - }; - boundFns.set(prop, fn); - } - return fn; - } - return target[prop]; - }, - set(target, prop, value) { - target[prop] = value; - let index = convertToInt(prop); - if (index !== null) { - self.#dirtyStorageFor(index); - self.#collection.set(null); - } else if (prop === "length") { - self.#collection.set(null); - } - return true; - }, - getPrototypeOf() { - return SignalArray.prototype; - } - }); - } - #collection = createStorage(); - #storages = new Map(); - #readStorageFor(index) { - let storage = this.#storages.get(index); - if (storage === undefined) { - storage = createStorage(); - this.#storages.set(index, storage); - } - storage.get(); - } - #dirtyStorageFor(index) { - const storage = this.#storages.get(index); - if (storage) { - storage.set(null); - } - } -}; -Object.setPrototypeOf(SignalArray.prototype, Array.prototype); -function signalArray(x$1) { - return new SignalArray(x$1); -} - -var SignalMap = class { - collection = createStorage(); - storages = new Map(); - vals; - readStorageFor(key) { - const { storages } = this; - let storage = storages.get(key); - if (storage === undefined) { - storage = createStorage(); - storages.set(key, storage); - } - storage.get(); - } - dirtyStorageFor(key) { - const storage = this.storages.get(key); - if (storage) { - storage.set(null); - } - } - constructor(existing) { - this.vals = existing ? new Map(existing) : new Map(); - } - get(key) { - this.readStorageFor(key); - return this.vals.get(key); - } - has(key) { - this.readStorageFor(key); - return this.vals.has(key); - } - entries() { - this.collection.get(); - return this.vals.entries(); - } - keys() { - this.collection.get(); - return this.vals.keys(); - } - values() { - this.collection.get(); - return this.vals.values(); - } - forEach(fn) { - this.collection.get(); - this.vals.forEach(fn); - } - get size() { - this.collection.get(); - return this.vals.size; - } - [Symbol.iterator]() { - this.collection.get(); - return this.vals[Symbol.iterator](); - } - get [Symbol.toStringTag]() { - return this.vals[Symbol.toStringTag]; - } - set(key, value) { - this.dirtyStorageFor(key); - this.collection.set(null); - this.vals.set(key, value); - return this; - } - delete(key) { - this.dirtyStorageFor(key); - this.collection.set(null); - return this.vals.delete(key); - } - clear() { - this.storages.forEach((s$9) => s$9.set(null)); - this.collection.set(null); - this.vals.clear(); - } -}; -Object.setPrototypeOf(SignalMap.prototype, Map.prototype); - -/** -* Implementation based of tracked-built-ins' TrackedObject -* https://github.com/tracked-tools/tracked-built-ins/blob/master/addon/src/-private/object.js -*/ -var SignalObjectImpl = class SignalObjectImpl { - static fromEntries(entries) { - return new SignalObjectImpl(Object.fromEntries(entries)); - } - #storages = new Map(); - #collection = createStorage(); - constructor(obj = {}) { - let proto = Object.getPrototypeOf(obj); - let descs = Object.getOwnPropertyDescriptors(obj); - let clone = Object.create(proto); - for (let prop in descs) { - Object.defineProperty(clone, prop, descs[prop]); - } - let self = this; - return new Proxy(clone, { - get(target, prop, receiver) { - self.#readStorageFor(prop); - return Reflect.get(target, prop, receiver); - }, - has(target, prop) { - self.#readStorageFor(prop); - return prop in target; - }, - ownKeys(target) { - self.#collection.get(); - return Reflect.ownKeys(target); - }, - set(target, prop, value, receiver) { - let result = Reflect.set(target, prop, value, receiver); - self.#dirtyStorageFor(prop); - self.#dirtyCollection(); - return result; - }, - deleteProperty(target, prop) { - if (prop in target) { - delete target[prop]; - self.#dirtyStorageFor(prop); - self.#dirtyCollection(); - } - return true; - }, - getPrototypeOf() { - return SignalObjectImpl.prototype; - } - }); - } - #readStorageFor(key) { - let storage = this.#storages.get(key); - if (storage === undefined) { - storage = createStorage(); - this.#storages.set(key, storage); - } - storage.get(); - } - #dirtyStorageFor(key) { - const storage = this.#storages.get(key); - if (storage) { - storage.set(null); - } - } - #dirtyCollection() { - this.#collection.set(null); - } -}; -/** -* Create a reactive Object, backed by Signals, using a Proxy. -* This allows dynamic creation and deletion of signals using the object primitive -* APIs that most folks are familiar with -- the only difference is instantiation. -* ```js -* const obj = new SignalObject({ foo: 123 }); -* -* obj.foo // 123 -* obj.foo = 456 -* obj.foo // 456 -* obj.bar = 2 -* obj.bar // 2 -* ``` -*/ -const SignalObject = SignalObjectImpl; -function signalObject(obj) { - return new SignalObject(obj); -} - -var SignalSet = class { - collection = createStorage(); - storages = new Map(); - vals; - storageFor(key) { - const storages = this.storages; - let storage = storages.get(key); - if (storage === undefined) { - storage = createStorage(); - storages.set(key, storage); - } - return storage; - } - dirtyStorageFor(key) { - const storage = this.storages.get(key); - if (storage) { - storage.set(null); - } - } - constructor(existing) { - this.vals = new Set(existing); - } - has(value) { - this.storageFor(value).get(); - return this.vals.has(value); - } - entries() { - this.collection.get(); - return this.vals.entries(); - } - keys() { - this.collection.get(); - return this.vals.keys(); - } - values() { - this.collection.get(); - return this.vals.values(); - } - forEach(fn) { - this.collection.get(); - this.vals.forEach(fn); - } - get size() { - this.collection.get(); - return this.vals.size; - } - [Symbol.iterator]() { - this.collection.get(); - return this.vals[Symbol.iterator](); - } - get [Symbol.toStringTag]() { - return this.vals[Symbol.toStringTag]; - } - add(value) { - this.dirtyStorageFor(value); - this.collection.set(null); - this.vals.add(value); - return this; - } - delete(value) { - this.dirtyStorageFor(value); - this.collection.set(null); - return this.vals.delete(value); - } - clear() { - this.storages.forEach((s$9) => s$9.set(null)); - this.collection.set(null); - this.vals.clear(); - } -}; -Object.setPrototypeOf(SignalSet.prototype, Set.prototype); - -function create() { - return new A2uiMessageProcessor({ - arrayCtor: SignalArray, - mapCtor: SignalMap, - objCtor: SignalObject, - setCtor: SignalSet - }); -} - -var server_to_client_with_standard_catalog_default = { - title: "A2UI Message Schema", - description: "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces. A message MUST contain exactly ONE of the action properties: 'beginRendering', 'surfaceUpdate', 'dataModelUpdate', or 'deleteSurface'.", - type: "object", - additionalProperties: false, - properties: { - "beginRendering": { - "type": "object", - "description": "Signals the client to begin rendering a surface with a root component and specific styles.", - "additionalProperties": false, - "properties": { - "surfaceId": { - "type": "string", - "description": "The unique identifier for the UI surface to be rendered." - }, - "root": { - "type": "string", - "description": "The ID of the root component to render." - }, - "styles": { - "type": "object", - "description": "Styling information for the UI.", - "additionalProperties": false, - "properties": { - "font": { - "type": "string", - "description": "The primary font for the UI." - }, - "primaryColor": { - "type": "string", - "description": "The primary UI color as a hexadecimal code (e.g., '#00BFFF').", - "pattern": "^#[0-9a-fA-F]{6}$" - } - } - } - }, - "required": ["root", "surfaceId"] - }, - "surfaceUpdate": { - "type": "object", - "description": "Updates a surface with a new set of components.", - "additionalProperties": false, - "properties": { - "surfaceId": { - "type": "string", - "description": "The unique identifier for the UI surface to be updated. If you are adding a new surface this *must* be a new, unique identified that has never been used for any existing surfaces shown." - }, - "components": { - "type": "array", - "description": "A list containing all UI components for the surface.", - "minItems": 1, - "items": { - "type": "object", - "description": "Represents a *single* component in a UI widget tree. This component could be one of many supported types.", - "additionalProperties": false, - "properties": { - "id": { - "type": "string", - "description": "The unique identifier for this component." - }, - "weight": { - "type": "number", - "description": "The relative weight of this component within a Row or Column. This corresponds to the CSS 'flex-grow' property. Note: this may ONLY be set when the component is a direct descendant of a Row or Column." - }, - "component": { - "type": "object", - "description": "A wrapper object that MUST contain exactly one key, which is the name of the component type (e.g., 'Heading'). The value is an object containing the properties for that specific component.", - "additionalProperties": false, - "properties": { - "Text": { - "type": "object", - "additionalProperties": false, - "properties": { - "text": { - "type": "object", - "description": "The text content to display. This can be a literal string or a reference to a value in the data model ('path', e.g., '/doc/title'). While simple Markdown formatting is supported (i.e. without HTML, images, or links), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.", - "additionalProperties": false, - "properties": { - "literalString": { "type": "string" }, - "path": { "type": "string" } - } - }, - "usageHint": { - "type": "string", - "description": "A hint for the base text style. One of:\n- `h1`: Largest heading.\n- `h2`: Second largest heading.\n- `h3`: Third largest heading.\n- `h4`: Fourth largest heading.\n- `h5`: Fifth largest heading.\n- `caption`: Small text for captions.\n- `body`: Standard body text.", - "enum": [ - "h1", - "h2", - "h3", - "h4", - "h5", - "caption", - "body" - ] - } - }, - "required": ["text"] - }, - "Image": { - "type": "object", - "additionalProperties": false, - "properties": { - "url": { - "type": "object", - "description": "The URL of the image to display. This can be a literal string ('literal') or a reference to a value in the data model ('path', e.g. '/thumbnail/url').", - "additionalProperties": false, - "properties": { - "literalString": { "type": "string" }, - "path": { "type": "string" } - } - }, - "fit": { - "type": "string", - "description": "Specifies how the image should be resized to fit its container. This corresponds to the CSS 'object-fit' property.", - "enum": [ - "contain", - "cover", - "fill", - "none", - "scale-down" - ] - }, - "usageHint": { - "type": "string", - "description": "A hint for the image size and style. One of:\n- `icon`: Small square icon.\n- `avatar`: Circular avatar image.\n- `smallFeature`: Small feature image.\n- `mediumFeature`: Medium feature image.\n- `largeFeature`: Large feature image.\n- `header`: Full-width, full bleed, header image.", - "enum": [ - "icon", - "avatar", - "smallFeature", - "mediumFeature", - "largeFeature", - "header" - ] - } - }, - "required": ["url"] - }, - "Icon": { - "type": "object", - "additionalProperties": false, - "properties": { "name": { - "type": "object", - "description": "The name of the icon to display. This can be a literal string or a reference to a value in the data model ('path', e.g. '/form/submit').", - "additionalProperties": false, - "properties": { - "literalString": { - "type": "string", - "enum": [ - "accountCircle", - "add", - "arrowBack", - "arrowForward", - "attachFile", - "calendarToday", - "call", - "camera", - "check", - "close", - "delete", - "download", - "edit", - "event", - "error", - "favorite", - "favoriteOff", - "folder", - "help", - "home", - "info", - "locationOn", - "lock", - "lockOpen", - "mail", - "menu", - "moreVert", - "moreHoriz", - "notificationsOff", - "notifications", - "payment", - "person", - "phone", - "photo", - "print", - "refresh", - "search", - "send", - "settings", - "share", - "shoppingCart", - "star", - "starHalf", - "starOff", - "upload", - "visibility", - "visibilityOff", - "warning" - ] - }, - "path": { "type": "string" } - } - } }, - "required": ["name"] - }, - "Video": { - "type": "object", - "additionalProperties": false, - "properties": { "url": { - "type": "object", - "description": "The URL of the video to display. This can be a literal string or a reference to a value in the data model ('path', e.g. '/video/url').", - "additionalProperties": false, - "properties": { - "literalString": { "type": "string" }, - "path": { "type": "string" } - } - } }, - "required": ["url"] - }, - "AudioPlayer": { - "type": "object", - "additionalProperties": false, - "properties": { - "url": { - "type": "object", - "description": "The URL of the audio to be played. This can be a literal string ('literal') or a reference to a value in the data model ('path', e.g. '/song/url').", - "additionalProperties": false, - "properties": { - "literalString": { "type": "string" }, - "path": { "type": "string" } - } - }, - "description": { - "type": "object", - "description": "A description of the audio, such as a title or summary. This can be a literal string or a reference to a value in the data model ('path', e.g. '/song/title').", - "additionalProperties": false, - "properties": { - "literalString": { "type": "string" }, - "path": { "type": "string" } - } - } - }, - "required": ["url"] - }, - "Row": { - "type": "object", - "additionalProperties": false, - "properties": { - "children": { - "type": "object", - "description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.", - "additionalProperties": false, - "properties": { - "explicitList": { - "type": "array", - "items": { "type": "string" } - }, - "template": { - "type": "object", - "description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.", - "additionalProperties": false, - "properties": { - "componentId": { "type": "string" }, - "dataBinding": { "type": "string" } - }, - "required": ["componentId", "dataBinding"] - } - } - }, - "distribution": { - "type": "string", - "description": "Defines the arrangement of children along the main axis (horizontally). This corresponds to the CSS 'justify-content' property.", - "enum": [ - "center", - "end", - "spaceAround", - "spaceBetween", - "spaceEvenly", - "start" - ] - }, - "alignment": { - "type": "string", - "description": "Defines the alignment of children along the cross axis (vertically). This corresponds to the CSS 'align-items' property.", - "enum": [ - "start", - "center", - "end", - "stretch" - ] - } - }, - "required": ["children"] - }, - "Column": { - "type": "object", - "additionalProperties": false, - "properties": { - "children": { - "type": "object", - "description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.", - "additionalProperties": false, - "properties": { - "explicitList": { - "type": "array", - "items": { "type": "string" } - }, - "template": { - "type": "object", - "description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.", - "additionalProperties": false, - "properties": { - "componentId": { "type": "string" }, - "dataBinding": { "type": "string" } - }, - "required": ["componentId", "dataBinding"] - } - } - }, - "distribution": { - "type": "string", - "description": "Defines the arrangement of children along the main axis (vertically). This corresponds to the CSS 'justify-content' property.", - "enum": [ - "start", - "center", - "end", - "spaceBetween", - "spaceAround", - "spaceEvenly" - ] - }, - "alignment": { - "type": "string", - "description": "Defines the alignment of children along the cross axis (horizontally). This corresponds to the CSS 'align-items' property.", - "enum": [ - "center", - "end", - "start", - "stretch" - ] - } - }, - "required": ["children"] - }, - "List": { - "type": "object", - "additionalProperties": false, - "properties": { - "children": { - "type": "object", - "description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.", - "additionalProperties": false, - "properties": { - "explicitList": { - "type": "array", - "items": { "type": "string" } - }, - "template": { - "type": "object", - "description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.", - "additionalProperties": false, - "properties": { - "componentId": { "type": "string" }, - "dataBinding": { "type": "string" } - }, - "required": ["componentId", "dataBinding"] - } - } - }, - "direction": { - "type": "string", - "description": "The direction in which the list items are laid out.", - "enum": ["vertical", "horizontal"] - }, - "alignment": { - "type": "string", - "description": "Defines the alignment of children along the cross axis.", - "enum": [ - "start", - "center", - "end", - "stretch" - ] - } - }, - "required": ["children"] - }, - "Card": { - "type": "object", - "additionalProperties": false, - "properties": { "child": { - "type": "string", - "description": "The ID of the component to be rendered inside the card." - } }, - "required": ["child"] - }, - "Tabs": { - "type": "object", - "additionalProperties": false, - "properties": { "tabItems": { - "type": "array", - "description": "An array of objects, where each object defines a tab with a title and a child component.", - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "title": { - "type": "object", - "description": "The tab title. Defines the value as either a literal value or a path to data model value (e.g. '/options/title').", - "additionalProperties": false, - "properties": { - "literalString": { "type": "string" }, - "path": { "type": "string" } - } - }, - "child": { "type": "string" } - }, - "required": ["title", "child"] - } - } }, - "required": ["tabItems"] - }, - "Divider": { - "type": "object", - "additionalProperties": false, - "properties": { "axis": { - "type": "string", - "description": "The orientation of the divider.", - "enum": ["horizontal", "vertical"] - } } - }, - "Modal": { - "type": "object", - "additionalProperties": false, - "properties": { - "entryPointChild": { - "type": "string", - "description": "The ID of the component that opens the modal when interacted with (e.g., a button)." - }, - "contentChild": { - "type": "string", - "description": "The ID of the component to be displayed inside the modal." - } - }, - "required": ["entryPointChild", "contentChild"] - }, - "Button": { - "type": "object", - "additionalProperties": false, - "properties": { - "child": { - "type": "string", - "description": "The ID of the component to display in the button, typically a Text component." - }, - "primary": { - "type": "boolean", - "description": "Indicates if this button should be styled as the primary action." - }, - "action": { - "type": "object", - "description": "The client-side action to be dispatched when the button is clicked. It includes the action's name and an optional context payload.", - "additionalProperties": false, - "properties": { - "name": { "type": "string" }, - "context": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "key": { "type": "string" }, - "value": { - "type": "object", - "description": "Defines the value to be included in the context as either a literal value or a path to a data model value (e.g. '/user/name').", - "additionalProperties": false, - "properties": { - "path": { "type": "string" }, - "literalString": { "type": "string" }, - "literalNumber": { "type": "number" }, - "literalBoolean": { "type": "boolean" } - } - } - }, - "required": ["key", "value"] - } - } - }, - "required": ["name"] - } - }, - "required": ["child", "action"] - }, - "CheckBox": { - "type": "object", - "additionalProperties": false, - "properties": { - "label": { - "type": "object", - "description": "The text to display next to the checkbox. Defines the value as either a literal value or a path to data model ('path', e.g. '/option/label').", - "additionalProperties": false, - "properties": { - "literalString": { "type": "string" }, - "path": { "type": "string" } - } - }, - "value": { - "type": "object", - "description": "The current state of the checkbox (true for checked, false for unchecked). This can be a literal boolean ('literalBoolean') or a reference to a value in the data model ('path', e.g. '/filter/open').", - "additionalProperties": false, - "properties": { - "literalBoolean": { "type": "boolean" }, - "path": { "type": "string" } - } - } - }, - "required": ["label", "value"] - }, - "TextField": { - "type": "object", - "additionalProperties": false, - "properties": { - "label": { - "type": "object", - "description": "The text label for the input field. This can be a literal string or a reference to a value in the data model ('path, e.g. '/user/name').", - "additionalProperties": false, - "properties": { - "literalString": { "type": "string" }, - "path": { "type": "string" } - } - }, - "text": { - "type": "object", - "description": "The value of the text field. This can be a literal string or a reference to a value in the data model ('path', e.g. '/user/name').", - "additionalProperties": false, - "properties": { - "literalString": { "type": "string" }, - "path": { "type": "string" } - } - }, - "textFieldType": { - "type": "string", - "description": "The type of input field to display.", - "enum": [ - "date", - "longText", - "number", - "shortText", - "obscured" - ] - }, - "validationRegexp": { - "type": "string", - "description": "A regular expression used for client-side validation of the input." - } - }, - "required": ["label"] - }, - "DateTimeInput": { - "type": "object", - "additionalProperties": false, - "properties": { - "value": { - "type": "object", - "description": "The selected date and/or time value. This can be a literal string ('literalString') or a reference to a value in the data model ('path', e.g. '/user/dob').", - "additionalProperties": false, - "properties": { - "literalString": { "type": "string" }, - "path": { "type": "string" } - } - }, - "enableDate": { - "type": "boolean", - "description": "If true, allows the user to select a date." - }, - "enableTime": { - "type": "boolean", - "description": "If true, allows the user to select a time." - }, - "outputFormat": { - "type": "string", - "description": "The desired format for the output string after a date or time is selected." - } - }, - "required": ["value"] - }, - "MultipleChoice": { - "type": "object", - "additionalProperties": false, - "properties": { - "selections": { - "type": "object", - "description": "The currently selected values for the component. This can be a literal array of strings or a path to an array in the data model('path', e.g. '/hotel/options').", - "additionalProperties": false, - "properties": { - "literalArray": { - "type": "array", - "items": { "type": "string" } - }, - "path": { "type": "string" } - } - }, - "options": { - "type": "array", - "description": "An array of available options for the user to choose from.", - "items": { - "type": "object", - "additionalProperties": false, - "properties": { - "label": { - "type": "object", - "description": "The text to display for this option. This can be a literal string or a reference to a value in the data model (e.g. '/option/label').", - "additionalProperties": false, - "properties": { - "literalString": { "type": "string" }, - "path": { "type": "string" } - } - }, - "value": { - "type": "string", - "description": "The value to be associated with this option when selected." - } - }, - "required": ["label", "value"] - } - }, - "maxAllowedSelections": { - "type": "integer", - "description": "The maximum number of options that the user is allowed to select." - } - }, - "required": ["selections", "options"] - }, - "Slider": { - "type": "object", - "additionalProperties": false, - "properties": { - "value": { - "type": "object", - "description": "The current value of the slider. This can be a literal number ('literalNumber') or a reference to a value in the data model ('path', e.g. '/restaurant/cost').", - "additionalProperties": false, - "properties": { - "literalNumber": { "type": "number" }, - "path": { "type": "string" } - } - }, - "minValue": { - "type": "number", - "description": "The minimum value of the slider." - }, - "maxValue": { - "type": "number", - "description": "The maximum value of the slider." - } - }, - "required": ["value"] - } - } - } - }, - "required": ["id", "component"] - } - } - }, - "required": ["surfaceId", "components"] - }, - "dataModelUpdate": { - "type": "object", - "description": "Updates the data model for a surface.", - "additionalProperties": false, - "properties": { - "surfaceId": { - "type": "string", - "description": "The unique identifier for the UI surface this data model update applies to." - }, - "path": { - "type": "string", - "description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', the entire data model will be replaced." - }, - "contents": { - "type": "array", - "description": "An array of data entries. Each entry must contain a 'key' and exactly one corresponding typed 'value*' property.", - "items": { - "type": "object", - "description": "A single data entry. Exactly one 'value*' property should be provided alongside the key.", - "additionalProperties": false, - "properties": { - "key": { - "type": "string", - "description": "The key for this data entry." - }, - "valueString": { "type": "string" }, - "valueNumber": { "type": "number" }, - "valueBoolean": { "type": "boolean" }, - "valueMap": { - "description": "Represents a map as an adjacency list.", - "type": "array", - "items": { - "type": "object", - "description": "One entry in the map. Exactly one 'value*' property should be provided alongside the key.", - "additionalProperties": false, - "properties": { - "key": { "type": "string" }, - "valueString": { "type": "string" }, - "valueNumber": { "type": "number" }, - "valueBoolean": { "type": "boolean" } - }, - "required": ["key"] - } - } - }, - "required": ["key"] - } - } - }, - "required": ["contents", "surfaceId"] - }, - "deleteSurface": { - "type": "object", - "description": "Signals the client to delete the surface identified by 'surfaceId'.", - "additionalProperties": false, - "properties": { "surfaceId": { - "type": "string", - "description": "The unique identifier for the UI surface to be deleted." - } }, - "required": ["surfaceId"] - } - } -}; - -const Data = { - createSignalA2uiMessageProcessor: create, - A2uiMessageProcessor, - Guards: guards_exports -}; -const Schemas = { A2UIClientEventMessage: server_to_client_with_standard_catalog_default }; - -/** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -const t$1 = (t$7) => (e$14, o$15) => { - void 0 !== o$15 ? o$15.addInitializer(() => { - customElements.define(t$7, e$14); - }) : customElements.define(t$7, e$14); -}; - -/** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ const o$9 = { - attribute: !0, - type: String, - converter: u$3, - reflect: !1, - hasChanged: f$3 -}, r$7 = (t$7 = o$9, e$14, r$12) => { - const { kind: n$13, metadata: i$10 } = r$12; - let s$9 = globalThis.litPropertyMetadata.get(i$10); - if (void 0 === s$9 && globalThis.litPropertyMetadata.set(i$10, s$9 = new Map()), "setter" === n$13 && ((t$7 = Object.create(t$7)).wrapped = !0), s$9.set(r$12.name, t$7), "accessor" === n$13) { - const { name: o$15 } = r$12; - return { - set(r$13) { - const n$14 = e$14.get.call(this); - e$14.set.call(this, r$13), this.requestUpdate(o$15, n$14, t$7, !0, r$13); - }, - init(e$15) { - return void 0 !== e$15 && this.C(o$15, void 0, t$7, e$15), e$15; - } - }; - } - if ("setter" === n$13) { - const { name: o$15 } = r$12; - return function(r$13) { - const n$14 = this[o$15]; - e$14.call(this, r$13), this.requestUpdate(o$15, n$14, t$7, !0, r$13); - }; - } - throw Error("Unsupported decorator location: " + n$13); -}; -function n$6(t$7) { - return (e$14, o$15) => "object" == typeof o$15 ? r$7(t$7, e$14, o$15) : ((t$8, e$15, o$16) => { - const r$12 = e$15.hasOwnProperty(o$16); - return e$15.constructor.createProperty(o$16, t$8), r$12 ? Object.getOwnPropertyDescriptor(e$15, o$16) : void 0; - })(t$7, e$14, o$15); -} - -/** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ function r$6(r$12) { - return n$6({ - ...r$12, - state: !0, - attribute: !1 - }); -} - -/** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -function t(t) { - return (n$13, o$15) => { - const c$7 = "function" == typeof n$13 ? n$13 : n$13[o$15]; - Object.assign(c$7, t); - }; -} - -/** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -const e$6 = (e$14, t$7, c$7) => (c$7.configurable = !0, c$7.enumerable = !0, Reflect.decorate && "object" != typeof t$7 && Object.defineProperty(e$14, t$7, c$7), c$7); - -/** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ function e$5(e$14, r$12) { - return (n$13, s$9, i$10) => { - const o$15 = (t$7) => t$7.renderRoot?.querySelector(e$14) ?? null; - if (r$12) { - const { get: e$15, set: r$13 } = "object" == typeof s$9 ? n$13 : i$10 ?? (() => { - const t$7 = Symbol(); - return { - get() { - return this[t$7]; - }, - set(e$16) { - this[t$7] = e$16; - } - }; - })(); - return e$6(n$13, s$9, { get() { - let t$7 = e$15.call(this); - return void 0 === t$7 && (t$7 = o$15(this), (null !== t$7 || this.hasUpdated) && r$13.call(this, t$7)), t$7; - } }); - } - return e$6(n$13, s$9, { get() { - return o$15(this); - } }); - }; -} - -/** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -let e$4; -function r$5(r$12) { - return (n$13, o$15) => e$6(n$13, o$15, { get() { - return (this.renderRoot ?? (e$4 ??= document.createDocumentFragment())).querySelectorAll(r$12); - } }); -} - -/** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -function r$4(r$12) { - return (n$13, e$14) => e$6(n$13, e$14, { async get() { - return await this.updateComplete, this.renderRoot?.querySelector(r$12) ?? null; - } }); -} - -/** -* @license -* Copyright 2021 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ function o$8(o$15) { - return (e$14, n$13) => { - const { slot: r$12, selector: s$9 } = o$15 ?? {}, c$7 = "slot" + (r$12 ? `[name=${r$12}]` : ":not([name])"); - return e$6(e$14, n$13, { get() { - const t$7 = this.renderRoot?.querySelector(c$7), e$15 = t$7?.assignedElements(o$15) ?? []; - return void 0 === s$9 ? e$15 : e$15.filter((t$8) => t$8.matches(s$9)); - } }); - }; -} - -/** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ function n$5(n$13) { - return (o$15, r$12) => { - const { slot: e$14 } = n$13 ?? {}, s$9 = "slot" + (e$14 ? `[name=${e$14}]` : ":not([name])"); - return e$6(o$15, r$12, { get() { - const t$7 = this.renderRoot?.querySelector(s$9); - return t$7?.assignedNodes(n$13) ?? []; - } }); - }; -} - -/** -* @license -* Copyright 2023 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ let i$2 = !1; -const s$1 = new Signal.subtle.Watcher(() => { - i$2 || (i$2 = !0, queueMicrotask(() => { - i$2 = !1; - for (const t$7 of s$1.getPending()) t$7.get(); - s$1.watch(); - })); -}), h$3 = Symbol("SignalWatcherBrand"), e$3 = new FinalizationRegistry((i$10) => { - i$10.unwatch(...Signal.subtle.introspectSources(i$10)); -}), n$4 = new WeakMap(); -function o$7(i$10) { - return !0 === i$10[h$3] ? (console.warn("SignalWatcher should not be applied to the same class more than once."), i$10) : class extends i$10 { - constructor() { - super(...arguments), this._$St = new Map(), this._$So = new Signal.State(0), this._$Si = !1; - } - _$Sl() { - var t$7, i$11; - const s$9 = [], h$7 = []; - this._$St.forEach((t$8, i$12) => { - ((null == t$8 ? void 0 : t$8.beforeUpdate) ? s$9 : h$7).push(i$12); - }); - const e$14 = null === (t$7 = this.h) || void 0 === t$7 ? void 0 : t$7.getPending().filter((t$8) => t$8 !== this._$Su && !this._$St.has(t$8)); - s$9.forEach((t$8) => t$8.get()), null === (i$11 = this._$Su) || void 0 === i$11 || i$11.get(), e$14.forEach((t$8) => t$8.get()), h$7.forEach((t$8) => t$8.get()); - } - _$Sv() { - this.isUpdatePending || queueMicrotask(() => { - this.isUpdatePending || this._$Sl(); - }); - } - _$S_() { - if (void 0 !== this.h) return; - this._$Su = new Signal.Computed(() => { - this._$So.get(), super.performUpdate(); - }); - const i$11 = this.h = new Signal.subtle.Watcher(function() { - const t$7 = n$4.get(this); - void 0 !== t$7 && (!1 === t$7._$Si && (new Set(this.getPending()).has(t$7._$Su) ? t$7.requestUpdate() : t$7._$Sv()), this.watch()); - }); - n$4.set(i$11, this), e$3.register(this, i$11), i$11.watch(this._$Su), i$11.watch(...Array.from(this._$St).map(([t$7]) => t$7)); - } - _$Sp() { - if (void 0 === this.h) return; - let i$11 = !1; - this.h.unwatch(...Signal.subtle.introspectSources(this.h).filter((t$7) => { - var s$9; - const h$7 = !0 !== (null === (s$9 = this._$St.get(t$7)) || void 0 === s$9 ? void 0 : s$9.manualDispose); - return h$7 && this._$St.delete(t$7), i$11 || (i$11 = !h$7), h$7; - })), i$11 || (this._$Su = void 0, this.h = void 0, this._$St.clear()); - } - updateEffect(i$11, s$9) { - var h$7; - this._$S_(); - const e$14 = new Signal.Computed(() => { - i$11(); - }); - return this.h.watch(e$14), this._$St.set(e$14, s$9), null !== (h$7 = null == s$9 ? void 0 : s$9.beforeUpdate) && void 0 !== h$7 && h$7 ? Signal.subtle.untrack(() => e$14.get()) : this.updateComplete.then(() => Signal.subtle.untrack(() => e$14.get())), () => { - this._$St.delete(e$14), this.h.unwatch(e$14), !1 === this.isConnected && this._$Sp(); - }; - } - performUpdate() { - this.isUpdatePending && (this._$S_(), this._$Si = !0, this._$So.set(this._$So.get() + 1), this._$Si = !1, this._$Sl()); - } - connectedCallback() { - super.connectedCallback(), this.requestUpdate(); - } - disconnectedCallback() { - super.disconnectedCallback(), queueMicrotask(() => { - !1 === this.isConnected && this._$Sp(); - }); - } - }; -} - -/** -* @license -* Copyright 2017 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ const s = (i$10, t$7) => { - const e$14 = i$10._$AN; - if (void 0 === e$14) return !1; - for (const i$11 of e$14) i$11._$AO?.(t$7, !1), s(i$11, t$7); - return !0; -}, o$6 = (i$10) => { - let t$7, e$14; - do { - if (void 0 === (t$7 = i$10._$AM)) break; - e$14 = t$7._$AN, e$14.delete(i$10), i$10 = t$7; - } while (0 === e$14?.size); -}, r$3 = (i$10) => { - for (let t$7; t$7 = i$10._$AM; i$10 = t$7) { - let e$14 = t$7._$AN; - if (void 0 === e$14) t$7._$AN = e$14 = new Set(); - else if (e$14.has(i$10)) break; - e$14.add(i$10), c(t$7); - } -}; -function h$2(i$10) { - void 0 !== this._$AN ? (o$6(this), this._$AM = i$10, r$3(this)) : this._$AM = i$10; -} -function n$3(i$10, t$7 = !1, e$14 = 0) { - const r$12 = this._$AH, h$7 = this._$AN; - if (void 0 !== h$7 && 0 !== h$7.size) if (t$7) if (Array.isArray(r$12)) for (let i$11 = e$14; i$11 < r$12.length; i$11++) s(r$12[i$11], !1), o$6(r$12[i$11]); - else null != r$12 && (s(r$12, !1), o$6(r$12)); - else s(this, i$10); -} -const c = (i$10) => { - i$10.type == t$4.CHILD && (i$10._$AP ??= n$3, i$10._$AQ ??= h$2); -}; -var f = class extends i$5 { - constructor() { - super(...arguments), this._$AN = void 0; - } - _$AT(i$10, t$7, e$14) { - super._$AT(i$10, t$7, e$14), r$3(this), this.isConnected = i$10._$AU; - } - _$AO(i$10, t$7 = !0) { - i$10 !== this.isConnected && (this.isConnected = i$10, i$10 ? this.reconnected?.() : this.disconnected?.()), t$7 && (s(this, i$10), o$6(this)); - } - setValue(t$7) { - if (r$8(this._$Ct)) this._$Ct._$AI(t$7, this); - else { - const i$10 = [...this._$Ct._$AH]; - i$10[this._$Ci] = t$7, this._$Ct._$AI(i$10, this, 0); - } - } - disconnected() {} - reconnected() {} -}; - -/** -* @license -* Copyright 2023 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -let o$5 = !1; -const n$2 = new Signal.subtle.Watcher(async () => { - o$5 || (o$5 = !0, queueMicrotask(() => { - o$5 = !1; - for (const i$10 of n$2.getPending()) i$10.get(); - n$2.watch(); - })); -}); -var r$2 = class extends f { - _$S_() { - var i$10, t$7; - void 0 === this._$Sm && (this._$Sj = new Signal.Computed(() => { - var i$11; - const t$8 = null === (i$11 = this._$SW) || void 0 === i$11 ? void 0 : i$11.get(); - return this.setValue(t$8), t$8; - }), this._$Sm = null !== (t$7 = null === (i$10 = this._$Sk) || void 0 === i$10 ? void 0 : i$10.h) && void 0 !== t$7 ? t$7 : n$2, this._$Sm.watch(this._$Sj), Signal.subtle.untrack(() => { - var i$11; - return null === (i$11 = this._$Sj) || void 0 === i$11 ? void 0 : i$11.get(); - })); - } - _$Sp() { - void 0 !== this._$Sm && (this._$Sm.unwatch(this._$SW), this._$Sm = void 0); - } - render(i$10) { - return Signal.subtle.untrack(() => i$10.get()); - } - update(i$10, [t$7]) { - var o$15, n$13; - return null !== (o$15 = this._$Sk) && void 0 !== o$15 || (this._$Sk = null === (n$13 = i$10.options) || void 0 === n$13 ? void 0 : n$13.host), t$7 !== this._$SW && void 0 !== this._$SW && this._$Sp(), this._$SW = t$7, this._$S_(), Signal.subtle.untrack(() => this._$SW.get()); - } - disconnected() { - this._$Sp(); - } - reconnected() { - this._$S_(); - } -}; -const h$1 = e$10(r$2); - -/** -* @license -* Copyright 2023 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ const m = (o$15) => (t$7, ...m) => o$15(t$7, ...m.map((o$16) => o$16 instanceof Signal.State || o$16 instanceof Signal.Computed ? h$1(o$16) : o$16)), l$1 = m(b), r$1 = m(w); - -/** -* @license -* Copyright 2023 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ const l = Signal.State, o$4 = Signal.Computed, r = (l, o$15) => new Signal.State(l, o$15), i$1 = (l, o$15) => new Signal.Computed(l, o$15); - -/** -* @license -* Copyright 2021 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ -function* o$3(o$15, f$4) { - if (void 0 !== o$15) { - let i$10 = 0; - for (const t$7 of o$15) yield f$4(t$7, i$10++); - } -} - -let pending = false; -let watcher = new Signal.subtle.Watcher(() => { - if (!pending) { - pending = true; - queueMicrotask(() => { - pending = false; - flushPending(); - }); - } -}); -function flushPending() { - for (const signal of watcher.getPending()) { - signal.get(); - } - watcher.watch(); -} -/** -* ⚠️ WARNING: Nothing unwatches ⚠️ -* This will produce a memory leak. -*/ -function effect(cb) { - let c$7 = new Signal.Computed(() => cb()); - watcher.watch(c$7); - c$7.get(); - return () => { - watcher.unwatch(c$7); - }; -} - -const themeContext = n$7("A2UITheme"); - -const structuralStyles = r$11(structuralStyles$1); - -var ComponentRegistry = class { - constructor() { - this.registry = new Map(); - } - register(typeName, constructor, tagName) { - if (!/^[a-zA-Z0-9]+$/.test(typeName)) { - throw new Error(`[Registry] Invalid typeName '${typeName}'. Must be alphanumeric.`); - } - this.registry.set(typeName, constructor); - const actualTagName = tagName || `a2ui-custom-${typeName.toLowerCase()}`; - const existingName = customElements.getName(constructor); - if (existingName) { - if (existingName !== actualTagName) { - throw new Error(`Component ${typeName} is already registered as ${existingName}, but requested as ${actualTagName}.`); - } - return; - } - if (!customElements.get(actualTagName)) { - customElements.define(actualTagName, constructor); - } - } - get(typeName) { - return this.registry.get(typeName); - } -}; -const componentRegistry = new ComponentRegistry(); - -var __runInitializers$19 = void 0 && (void 0).__runInitializers || function(thisArg, initializers, value) { - var useValue = arguments.length > 2; - for (var i$10 = 0; i$10 < initializers.length; i$10++) { - value = useValue ? initializers[i$10].call(thisArg, value) : initializers[i$10].call(thisArg); - } - return useValue ? value : void 0; -}; -var __esDecorate$19 = void 0 && (void 0).__esDecorate || function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { - function accept(f$4) { - if (f$4 !== void 0 && typeof f$4 !== "function") throw new TypeError("Function expected"); - return f$4; - } - var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; - var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; - var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); - var _$1, done = false; - for (var i$10 = decorators.length - 1; i$10 >= 0; i$10--) { - var context = {}; - for (var p$3 in contextIn) context[p$3] = p$3 === "access" ? {} : contextIn[p$3]; - for (var p$3 in contextIn.access) context.access[p$3] = contextIn.access[p$3]; - context.addInitializer = function(f$4) { - if (done) throw new TypeError("Cannot add initializers after decoration has completed"); - extraInitializers.push(accept(f$4 || null)); - }; - var result = (0, decorators[i$10])(kind === "accessor" ? { - get: descriptor.get, - set: descriptor.set - } : descriptor[key], context); - if (kind === "accessor") { - if (result === void 0) continue; - if (result === null || typeof result !== "object") throw new TypeError("Object expected"); - if (_$1 = accept(result.get)) descriptor.get = _$1; - if (_$1 = accept(result.set)) descriptor.set = _$1; - if (_$1 = accept(result.init)) initializers.unshift(_$1); - } else if (_$1 = accept(result)) { - if (kind === "field") initializers.unshift(_$1); - else descriptor[key] = _$1; - } - } - if (target) Object.defineProperty(target, contextIn.name, descriptor); - done = true; -}; -let Root = (() => { - let _classDecorators = [t$1("a2ui-root")]; - let _classDescriptor; - let _classExtraInitializers = []; - let _classThis; - let _classSuper = o$7(i$6); - let _instanceExtraInitializers = []; - let _surfaceId_decorators; - let _surfaceId_initializers = []; - let _surfaceId_extraInitializers = []; - let _component_decorators; - let _component_initializers = []; - let _component_extraInitializers = []; - let _theme_decorators; - let _theme_initializers = []; - let _theme_extraInitializers = []; - let _childComponents_decorators; - let _childComponents_initializers = []; - let _childComponents_extraInitializers = []; - let _processor_decorators; - let _processor_initializers = []; - let _processor_extraInitializers = []; - let _dataContextPath_decorators; - let _dataContextPath_initializers = []; - let _dataContextPath_extraInitializers = []; - let _enableCustomElements_decorators; - let _enableCustomElements_initializers = []; - let _enableCustomElements_extraInitializers = []; - let _set_weight_decorators; - var Root = class extends _classSuper { - static { - _classThis = this; - } - static { - const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0; - _surfaceId_decorators = [n$6()]; - _component_decorators = [n$6()]; - _theme_decorators = [c$1({ context: themeContext })]; - _childComponents_decorators = [n$6({ attribute: false })]; - _processor_decorators = [n$6({ attribute: false })]; - _dataContextPath_decorators = [n$6()]; - _enableCustomElements_decorators = [n$6()]; - _set_weight_decorators = [n$6()]; - __esDecorate$19(this, null, _surfaceId_decorators, { - kind: "accessor", - name: "surfaceId", - static: false, - private: false, - access: { - has: (obj) => "surfaceId" in obj, - get: (obj) => obj.surfaceId, - set: (obj, value) => { - obj.surfaceId = value; - } - }, - metadata: _metadata - }, _surfaceId_initializers, _surfaceId_extraInitializers); - __esDecorate$19(this, null, _component_decorators, { - kind: "accessor", - name: "component", - static: false, - private: false, - access: { - has: (obj) => "component" in obj, - get: (obj) => obj.component, - set: (obj, value) => { - obj.component = value; - } - }, - metadata: _metadata - }, _component_initializers, _component_extraInitializers); - __esDecorate$19(this, null, _theme_decorators, { - kind: "accessor", - name: "theme", - static: false, - private: false, - access: { - has: (obj) => "theme" in obj, - get: (obj) => obj.theme, - set: (obj, value) => { - obj.theme = value; - } - }, - metadata: _metadata - }, _theme_initializers, _theme_extraInitializers); - __esDecorate$19(this, null, _childComponents_decorators, { - kind: "accessor", - name: "childComponents", - static: false, - private: false, - access: { - has: (obj) => "childComponents" in obj, - get: (obj) => obj.childComponents, - set: (obj, value) => { - obj.childComponents = value; - } - }, - metadata: _metadata - }, _childComponents_initializers, _childComponents_extraInitializers); - __esDecorate$19(this, null, _processor_decorators, { - kind: "accessor", - name: "processor", - static: false, - private: false, - access: { - has: (obj) => "processor" in obj, - get: (obj) => obj.processor, - set: (obj, value) => { - obj.processor = value; - } - }, - metadata: _metadata - }, _processor_initializers, _processor_extraInitializers); - __esDecorate$19(this, null, _dataContextPath_decorators, { - kind: "accessor", - name: "dataContextPath", - static: false, - private: false, - access: { - has: (obj) => "dataContextPath" in obj, - get: (obj) => obj.dataContextPath, - set: (obj, value) => { - obj.dataContextPath = value; - } - }, - metadata: _metadata - }, _dataContextPath_initializers, _dataContextPath_extraInitializers); - __esDecorate$19(this, null, _enableCustomElements_decorators, { - kind: "accessor", - name: "enableCustomElements", - static: false, - private: false, - access: { - has: (obj) => "enableCustomElements" in obj, - get: (obj) => obj.enableCustomElements, - set: (obj, value) => { - obj.enableCustomElements = value; - } - }, - metadata: _metadata - }, _enableCustomElements_initializers, _enableCustomElements_extraInitializers); - __esDecorate$19(this, null, _set_weight_decorators, { - kind: "setter", - name: "weight", - static: false, - private: false, - access: { - has: (obj) => "weight" in obj, - set: (obj, value) => { - obj.weight = value; - } - }, - metadata: _metadata - }, null, _instanceExtraInitializers); - __esDecorate$19(null, _classDescriptor = { value: _classThis }, _classDecorators, { - kind: "class", - name: _classThis.name, - metadata: _metadata - }, null, _classExtraInitializers); - Root = _classThis = _classDescriptor.value; - if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { - enumerable: true, - configurable: true, - writable: true, - value: _metadata - }); - } - #surfaceId_accessor_storage = (__runInitializers$19(this, _instanceExtraInitializers), __runInitializers$19(this, _surfaceId_initializers, null)); - get surfaceId() { - return this.#surfaceId_accessor_storage; - } - set surfaceId(value) { - this.#surfaceId_accessor_storage = value; - } - #component_accessor_storage = (__runInitializers$19(this, _surfaceId_extraInitializers), __runInitializers$19(this, _component_initializers, null)); - get component() { - return this.#component_accessor_storage; - } - set component(value) { - this.#component_accessor_storage = value; - } - #theme_accessor_storage = (__runInitializers$19(this, _component_extraInitializers), __runInitializers$19(this, _theme_initializers, void 0)); - get theme() { - return this.#theme_accessor_storage; - } - set theme(value) { - this.#theme_accessor_storage = value; - } - #childComponents_accessor_storage = (__runInitializers$19(this, _theme_extraInitializers), __runInitializers$19(this, _childComponents_initializers, null)); - get childComponents() { - return this.#childComponents_accessor_storage; - } - set childComponents(value) { - this.#childComponents_accessor_storage = value; - } - #processor_accessor_storage = (__runInitializers$19(this, _childComponents_extraInitializers), __runInitializers$19(this, _processor_initializers, null)); - get processor() { - return this.#processor_accessor_storage; - } - set processor(value) { - this.#processor_accessor_storage = value; - } - #dataContextPath_accessor_storage = (__runInitializers$19(this, _processor_extraInitializers), __runInitializers$19(this, _dataContextPath_initializers, "")); - get dataContextPath() { - return this.#dataContextPath_accessor_storage; - } - set dataContextPath(value) { - this.#dataContextPath_accessor_storage = value; - } - #enableCustomElements_accessor_storage = (__runInitializers$19(this, _dataContextPath_extraInitializers), __runInitializers$19(this, _enableCustomElements_initializers, false)); - get enableCustomElements() { - return this.#enableCustomElements_accessor_storage; - } - set enableCustomElements(value) { - this.#enableCustomElements_accessor_storage = value; - } - set weight(weight) { - this.#weight = weight; - this.style.setProperty("--weight", `${weight}`); - } - get weight() { - return this.#weight; - } - #weight = (__runInitializers$19(this, _enableCustomElements_extraInitializers), 1); - static { - this.styles = [structuralStyles, i$9` - :host { - display: flex; - flex-direction: column; - gap: 8px; - max-height: 80%; - } - `]; - } - /** - * Holds the cleanup function for our effect. - * We need this to stop the effect when the component is disconnected. - */ - #lightDomEffectDisposer = null; - willUpdate(changedProperties) { - if (changedProperties.has("childComponents")) { - if (this.#lightDomEffectDisposer) { - this.#lightDomEffectDisposer(); - } - this.#lightDomEffectDisposer = effect(() => { - const allChildren = this.childComponents ?? null; - const lightDomTemplate = this.renderComponentTree(allChildren); - D(lightDomTemplate, this, { host: this }); - }); - } - } - /** - * Clean up the effect when the component is removed from the DOM. - */ - disconnectedCallback() { - super.disconnectedCallback(); - if (this.#lightDomEffectDisposer) { - this.#lightDomEffectDisposer(); - } - } - /** - * Turns the SignalMap into a renderable TemplateResult for Lit. - */ - renderComponentTree(components) { - if (!components) { - return A; - } - if (!Array.isArray(components)) { - return A; - } - return b` ${o$3(components, (component) => { - if (this.enableCustomElements) { - const registeredCtor = componentRegistry.get(component.type); - const elCtor = registeredCtor || customElements.get(component.type); - if (elCtor) { - const node = component; - const el = new elCtor(); - el.id = node.id; - if (node.slotName) { - el.slot = node.slotName; - } - el.component = node; - el.weight = node.weight ?? "initial"; - el.processor = this.processor; - el.surfaceId = this.surfaceId; - el.dataContextPath = node.dataContextPath ?? "/"; - for (const [prop, val] of Object.entries(component.properties)) { - el[prop] = val; - } - return b`${el}`; - } - } - switch (component.type) { - case "List": { - const node = component; - const childComponents = node.properties.children; - return b``; - } - case "Card": { - const node = component; - let childComponents = node.properties.children; - if (!childComponents && node.properties.child) { - childComponents = [node.properties.child]; - } - return b``; - } - case "Column": { - const node = component; - return b``; - } - case "Row": { - const node = component; - return b``; - } - case "Image": { - const node = component; - return b``; - } - case "Icon": { - const node = component; - return b``; - } - case "AudioPlayer": { - const node = component; - return b``; - } - case "Button": { - const node = component; - return b``; - } - case "Text": { - const node = component; - return b``; - } - case "CheckBox": { - const node = component; - return b``; - } - case "DateTimeInput": { - const node = component; - return b``; - } - case "Divider": { - const node = component; - return b``; - } - case "MultipleChoice": { - const node = component; - return b``; - } - case "Slider": { - const node = component; - return b``; - } - case "TextField": { - const node = component; - return b``; - } - case "Video": { - const node = component; - return b``; - } - case "Tabs": { - const node = component; - const titles = []; - const childComponents = []; - if (node.properties.tabItems) { - for (const item of node.properties.tabItems) { - titles.push(item.title); - childComponents.push(item.child); - } - } - return b``; - } - case "Modal": { - const node = component; - const childComponents = [node.properties.entryPointChild, node.properties.contentChild]; - node.properties.entryPointChild.slotName = "entry"; - return b``; - } - default: { - return this.renderCustomComponent(component); - } - } - })}`; - } - renderCustomComponent(component) { - if (!this.enableCustomElements) { - return; - } - const node = component; - const registeredCtor = componentRegistry.get(component.type); - const elCtor = registeredCtor || customElements.get(component.type); - if (!elCtor) { - return b`Unknown element ${component.type}`; - } - const el = new elCtor(); - el.id = node.id; - if (node.slotName) { - el.slot = node.slotName; - } - el.component = node; - el.weight = node.weight ?? "initial"; - el.processor = this.processor; - el.surfaceId = this.surfaceId; - el.dataContextPath = node.dataContextPath ?? "/"; - for (const [prop, val] of Object.entries(component.properties)) { - el[prop] = val; - } - return b`${el}`; - } - render() { - return b``; - } - static { - __runInitializers$19(_classThis, _classExtraInitializers); - } - }; - return Root = _classThis; -})(); - -/** -* @license -* Copyright 2018 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ const e$2 = e$10(class extends i$5 { - constructor(t$7) { - if (super(t$7), t$7.type !== t$4.ATTRIBUTE || "class" !== t$7.name || t$7.strings?.length > 2) throw Error("`classMap()` can only be used in the `class` attribute and must be the only part in the attribute."); - } - render(t$7) { - return " " + Object.keys(t$7).filter((s$9) => t$7[s$9]).join(" ") + " "; - } - update(s$9, [i$10]) { - if (void 0 === this.st) { - this.st = new Set(), void 0 !== s$9.strings && (this.nt = new Set(s$9.strings.join(" ").split(/\s/).filter((t$7) => "" !== t$7))); - for (const t$7 in i$10) i$10[t$7] && !this.nt?.has(t$7) && this.st.add(t$7); - return this.render(i$10); - } - const r$12 = s$9.element.classList; - for (const t$7 of this.st) t$7 in i$10 || (r$12.remove(t$7), this.st.delete(t$7)); - for (const t$7 in i$10) { - const s$10 = !!i$10[t$7]; - s$10 === this.st.has(t$7) || this.nt?.has(t$7) || (s$10 ? (r$12.add(t$7), this.st.add(t$7)) : (r$12.remove(t$7), this.st.delete(t$7))); - } - return E; - } -}); - -/** -* @license -* Copyright 2018 Google LLC -* SPDX-License-Identifier: BSD-3-Clause -*/ const n$1 = "important", i = " !" + n$1, o$2 = e$10(class extends i$5 { - constructor(t$7) { - if (super(t$7), t$7.type !== t$4.ATTRIBUTE || "style" !== t$7.name || t$7.strings?.length > 2) throw Error("The `styleMap` directive must be used in the `style` attribute and must be the only part in the attribute."); - } - render(t$7) { - return Object.keys(t$7).reduce((e$14, r$12) => { - const s$9 = t$7[r$12]; - return null == s$9 ? e$14 : e$14 + `${r$12 = r$12.includes("-") ? r$12 : r$12.replace(/(?:^(webkit|moz|ms|o)|)(?=[A-Z])/g, "-$&").toLowerCase()}:${s$9};`; - }, ""); - } - update(e$14, [r$12]) { - const { style: s$9 } = e$14.element; - if (void 0 === this.ft) return this.ft = new Set(Object.keys(r$12)), this.render(r$12); - for (const t$7 of this.ft) null == r$12[t$7] && (this.ft.delete(t$7), t$7.includes("-") ? s$9.removeProperty(t$7) : s$9[t$7] = null); - for (const t$7 in r$12) { - const e$15 = r$12[t$7]; - if (null != e$15) { - this.ft.add(t$7); - const r$13 = "string" == typeof e$15 && e$15.endsWith(i); - t$7.includes("-") || r$13 ? s$9.setProperty(t$7, r$13 ? e$15.slice(0, -11) : e$15, r$13 ? n$1 : "") : s$9[t$7] = e$15; - } - } - return E; - } -}); - -var __esDecorate$18 = void 0 && (void 0).__esDecorate || function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { - function accept(f$4) { - if (f$4 !== void 0 && typeof f$4 !== "function") throw new TypeError("Function expected"); - return f$4; - } - var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; - var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; - var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); - var _$1, done = false; - for (var i$10 = decorators.length - 1; i$10 >= 0; i$10--) { - var context = {}; - for (var p$3 in contextIn) context[p$3] = p$3 === "access" ? {} : contextIn[p$3]; - for (var p$3 in contextIn.access) context.access[p$3] = contextIn.access[p$3]; - context.addInitializer = function(f$4) { - if (done) throw new TypeError("Cannot add initializers after decoration has completed"); - extraInitializers.push(accept(f$4 || null)); - }; - var result = (0, decorators[i$10])(kind === "accessor" ? { - get: descriptor.get, - set: descriptor.set - } : descriptor[key], context); - if (kind === "accessor") { - if (result === void 0) continue; - if (result === null || typeof result !== "object") throw new TypeError("Object expected"); - if (_$1 = accept(result.get)) descriptor.get = _$1; - if (_$1 = accept(result.set)) descriptor.set = _$1; - if (_$1 = accept(result.init)) initializers.unshift(_$1); - } else if (_$1 = accept(result)) { - if (kind === "field") initializers.unshift(_$1); - else descriptor[key] = _$1; - } - } - if (target) Object.defineProperty(target, contextIn.name, descriptor); - done = true; -}; -var __runInitializers$18 = void 0 && (void 0).__runInitializers || function(thisArg, initializers, value) { - var useValue = arguments.length > 2; - for (var i$10 = 0; i$10 < initializers.length; i$10++) { - value = useValue ? initializers[i$10].call(thisArg, value) : initializers[i$10].call(thisArg); - } - return useValue ? value : void 0; -}; -let Audio = (() => { - let _classDecorators = [t$1("a2ui-audioplayer")]; - let _classDescriptor; - let _classExtraInitializers = []; - let _classThis; - let _classSuper = Root; - let _url_decorators; - let _url_initializers = []; - let _url_extraInitializers = []; - var Audio = class extends _classSuper { - static { - _classThis = this; - } - static { - const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0; - _url_decorators = [n$6()]; - __esDecorate$18(this, null, _url_decorators, { - kind: "accessor", - name: "url", - static: false, - private: false, - access: { - has: (obj) => "url" in obj, - get: (obj) => obj.url, - set: (obj, value) => { - obj.url = value; - } - }, - metadata: _metadata - }, _url_initializers, _url_extraInitializers); - __esDecorate$18(null, _classDescriptor = { value: _classThis }, _classDecorators, { - kind: "class", - name: _classThis.name, - metadata: _metadata - }, null, _classExtraInitializers); - Audio = _classThis = _classDescriptor.value; - if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { - enumerable: true, - configurable: true, - writable: true, - value: _metadata - }); - } - #url_accessor_storage = __runInitializers$18(this, _url_initializers, null); - get url() { - return this.#url_accessor_storage; - } - set url(value) { - this.#url_accessor_storage = value; - } - static { - this.styles = [structuralStyles, i$9` - * { - box-sizing: border-box; - } - - :host { - display: block; - flex: var(--weight); - min-height: 0; - overflow: auto; - } - - audio { - display: block; - width: 100%; - } - `]; - } - #renderAudio() { - if (!this.url) { - return A; - } - if (this.url && typeof this.url === "object") { - if ("literalString" in this.url) { - return b`"; -}; -default_rules.code_block = function(tokens, idx, options, env, slf) { - const token = tokens[idx]; - return "" + escapeHtml(tokens[idx].content) + "\n"; -}; -default_rules.fence = function(tokens, idx, options, env, slf) { - const token = tokens[idx]; - const info = token.info ? unescapeAll(token.info).trim() : ""; - let langName = ""; - let langAttrs = ""; - if (info) { - const arr = info.split(/(\s+)/g); - langName = arr[0]; - langAttrs = arr.slice(2).join(""); - } - let highlighted; - if (options.highlight) { - highlighted = options.highlight(token.content, langName, langAttrs) || escapeHtml(token.content); - } else { - highlighted = escapeHtml(token.content); - } - if (highlighted.indexOf("${highlighted}\n`; - } - return `
    ${highlighted}
    \n`; -}; -default_rules.image = function(tokens, idx, options, env, slf) { - const token = tokens[idx]; - token.attrs[token.attrIndex("alt")][1] = slf.renderInlineAsText(token.children, options, env); - return slf.renderToken(tokens, idx, options); -}; -default_rules.hardbreak = function(tokens, idx, options) { - return options.xhtmlOut ? "
    \n" : "
    \n"; -}; -default_rules.softbreak = function(tokens, idx, options) { - return options.breaks ? options.xhtmlOut ? "
    \n" : "
    \n" : "\n"; -}; -default_rules.text = function(tokens, idx) { - return escapeHtml(tokens[idx].content); -}; -default_rules.html_block = function(tokens, idx) { - return tokens[idx].content; -}; -default_rules.html_inline = function(tokens, idx) { - return tokens[idx].content; -}; -/** -* new Renderer() -* -* Creates new [[Renderer]] instance and fill [[Renderer#rules]] with defaults. -**/ -function Renderer() { - /** - * Renderer#rules -> Object - * - * Contains render rules for tokens. Can be updated and extended. - * - * ##### Example - * - * ```javascript - * var md = require('markdown-it')(); - * - * md.renderer.rules.strong_open = function () { return ''; }; - * md.renderer.rules.strong_close = function () { return ''; }; - * - * var result = md.renderInline(...); - * ``` - * - * Each rule is called as independent static function with fixed signature: - * - * ```javascript - * function my_token_render(tokens, idx, options, env, renderer) { - * // ... - * return renderedHTML; - * } - * ``` - * - * See [source code](https://github.com/markdown-it/markdown-it/blob/master/lib/renderer.mjs) - * for more details and examples. - **/ - this.rules = assign$1({}, default_rules); -} -/** -* Renderer.renderAttrs(token) -> String -* -* Render token attributes to string. -**/ -Renderer.prototype.renderAttrs = function renderAttrs(token) { - let i$10, l$5, result; - if (!token.attrs) { - return ""; - } - result = ""; - for (i$10 = 0, l$5 = token.attrs.length; i$10 < l$5; i$10++) { - result += " " + escapeHtml(token.attrs[i$10][0]) + "=\"" + escapeHtml(token.attrs[i$10][1]) + "\""; - } - return result; -}; -/** -* Renderer.renderToken(tokens, idx, options) -> String -* - tokens (Array): list of tokens -* - idx (Numbed): token index to render -* - options (Object): params of parser instance -* -* Default token renderer. Can be overriden by custom function -* in [[Renderer#rules]]. -**/ -Renderer.prototype.renderToken = function renderToken(tokens, idx, options) { - const token = tokens[idx]; - let result = ""; - if (token.hidden) { - return ""; - } - if (token.block && token.nesting !== -1 && idx && tokens[idx - 1].hidden) { - result += "\n"; - } - result += (token.nesting === -1 ? "\n" : ">"; - return result; -}; -/** -* Renderer.renderInline(tokens, options, env) -> String -* - tokens (Array): list on block tokens to render -* - options (Object): params of parser instance -* - env (Object): additional data from parsed input (references, for example) -* -* The same as [[Renderer.render]], but for single token of `inline` type. -**/ -Renderer.prototype.renderInline = function(tokens, options, env) { - let result = ""; - const rules = this.rules; - for (let i$10 = 0, len = tokens.length; i$10 < len; i$10++) { - const type$2 = tokens[i$10].type; - if (typeof rules[type$2] !== "undefined") { - result += rules[type$2](tokens, i$10, options, env, this); - } else { - result += this.renderToken(tokens, i$10, options); - } - } - return result; -}; -/** internal -* Renderer.renderInlineAsText(tokens, options, env) -> String -* - tokens (Array): list on block tokens to render -* - options (Object): params of parser instance -* - env (Object): additional data from parsed input (references, for example) -* -* Special kludge for image `alt` attributes to conform CommonMark spec. -* Don't try to use it! Spec requires to show `alt` content with stripped markup, -* instead of simple escaping. -**/ -Renderer.prototype.renderInlineAsText = function(tokens, options, env) { - let result = ""; - for (let i$10 = 0, len = tokens.length; i$10 < len; i$10++) { - switch (tokens[i$10].type) { - case "text": - result += tokens[i$10].content; - break; - case "image": - result += this.renderInlineAsText(tokens[i$10].children, options, env); - break; - case "html_inline": - case "html_block": - result += tokens[i$10].content; - break; - case "softbreak": - case "hardbreak": - result += "\n"; - break; - default: - } - } - return result; -}; -/** -* Renderer.render(tokens, options, env) -> String -* - tokens (Array): list on block tokens to render -* - options (Object): params of parser instance -* - env (Object): additional data from parsed input (references, for example) -* -* Takes token stream and generates HTML. Probably, you will never need to call -* this method directly. -**/ -Renderer.prototype.render = function(tokens, options, env) { - let result = ""; - const rules = this.rules; - for (let i$10 = 0, len = tokens.length; i$10 < len; i$10++) { - const type$2 = tokens[i$10].type; - if (type$2 === "inline") { - result += this.renderInline(tokens[i$10].children, options, env); - } else if (typeof rules[type$2] !== "undefined") { - result += rules[type$2](tokens, i$10, options, env, this); - } else { - result += this.renderToken(tokens, i$10, options, env); - } - } - return result; -}; -var renderer_default = Renderer; - -/** -* class Ruler -* -* Helper class, used by [[MarkdownIt#core]], [[MarkdownIt#block]] and -* [[MarkdownIt#inline]] to manage sequences of functions (rules): -* -* - keep rules in defined order -* - assign the name to each rule -* - enable/disable rules -* - add/replace rules -* - allow assign rules to additional named chains (in the same) -* - cacheing lists of active rules -* -* You will not need use this class directly until write plugins. For simple -* rules control use [[MarkdownIt.disable]], [[MarkdownIt.enable]] and -* [[MarkdownIt.use]]. -**/ -/** -* new Ruler() -**/ -function Ruler() { - this.__rules__ = []; - this.__cache__ = null; -} -Ruler.prototype.__find__ = function(name) { - for (let i$10 = 0; i$10 < this.__rules__.length; i$10++) { - if (this.__rules__[i$10].name === name) { - return i$10; - } - } - return -1; -}; -Ruler.prototype.__compile__ = function() { - const self = this; - const chains = [""]; - self.__rules__.forEach(function(rule) { - if (!rule.enabled) { - return; - } - rule.alt.forEach(function(altName) { - if (chains.indexOf(altName) < 0) { - chains.push(altName); - } - }); - }); - self.__cache__ = {}; - chains.forEach(function(chain) { - self.__cache__[chain] = []; - self.__rules__.forEach(function(rule) { - if (!rule.enabled) { - return; - } - if (chain && rule.alt.indexOf(chain) < 0) { - return; - } - self.__cache__[chain].push(rule.fn); - }); - }); -}; -/** -* Ruler.at(name, fn [, options]) -* - name (String): rule name to replace. -* - fn (Function): new rule function. -* - options (Object): new rule options (not mandatory). -* -* Replace rule by name with new function & options. Throws error if name not -* found. -* -* ##### Options: -* -* - __alt__ - array with names of "alternate" chains. -* -* ##### Example -* -* Replace existing typographer replacement rule with new one: -* -* ```javascript -* var md = require('markdown-it')(); -* -* md.core.ruler.at('replacements', function replace(state) { -* //... -* }); -* ``` -**/ -Ruler.prototype.at = function(name, fn, options) { - const index = this.__find__(name); - const opt = options || {}; - if (index === -1) { - throw new Error("Parser rule not found: " + name); - } - this.__rules__[index].fn = fn; - this.__rules__[index].alt = opt.alt || []; - this.__cache__ = null; -}; -/** -* Ruler.before(beforeName, ruleName, fn [, options]) -* - beforeName (String): new rule will be added before this one. -* - ruleName (String): name of added rule. -* - fn (Function): rule function. -* - options (Object): rule options (not mandatory). -* -* Add new rule to chain before one with given name. See also -* [[Ruler.after]], [[Ruler.push]]. -* -* ##### Options: -* -* - __alt__ - array with names of "alternate" chains. -* -* ##### Example -* -* ```javascript -* var md = require('markdown-it')(); -* -* md.block.ruler.before('paragraph', 'my_rule', function replace(state) { -* //... -* }); -* ``` -**/ -Ruler.prototype.before = function(beforeName, ruleName, fn, options) { - const index = this.__find__(beforeName); - const opt = options || {}; - if (index === -1) { - throw new Error("Parser rule not found: " + beforeName); - } - this.__rules__.splice(index, 0, { - name: ruleName, - enabled: true, - fn, - alt: opt.alt || [] - }); - this.__cache__ = null; -}; -/** -* Ruler.after(afterName, ruleName, fn [, options]) -* - afterName (String): new rule will be added after this one. -* - ruleName (String): name of added rule. -* - fn (Function): rule function. -* - options (Object): rule options (not mandatory). -* -* Add new rule to chain after one with given name. See also -* [[Ruler.before]], [[Ruler.push]]. -* -* ##### Options: -* -* - __alt__ - array with names of "alternate" chains. -* -* ##### Example -* -* ```javascript -* var md = require('markdown-it')(); -* -* md.inline.ruler.after('text', 'my_rule', function replace(state) { -* //... -* }); -* ``` -**/ -Ruler.prototype.after = function(afterName, ruleName, fn, options) { - const index = this.__find__(afterName); - const opt = options || {}; - if (index === -1) { - throw new Error("Parser rule not found: " + afterName); - } - this.__rules__.splice(index + 1, 0, { - name: ruleName, - enabled: true, - fn, - alt: opt.alt || [] - }); - this.__cache__ = null; -}; -/** -* Ruler.push(ruleName, fn [, options]) -* - ruleName (String): name of added rule. -* - fn (Function): rule function. -* - options (Object): rule options (not mandatory). -* -* Push new rule to the end of chain. See also -* [[Ruler.before]], [[Ruler.after]]. -* -* ##### Options: -* -* - __alt__ - array with names of "alternate" chains. -* -* ##### Example -* -* ```javascript -* var md = require('markdown-it')(); -* -* md.core.ruler.push('my_rule', function replace(state) { -* //... -* }); -* ``` -**/ -Ruler.prototype.push = function(ruleName, fn, options) { - const opt = options || {}; - this.__rules__.push({ - name: ruleName, - enabled: true, - fn, - alt: opt.alt || [] - }); - this.__cache__ = null; -}; -/** -* Ruler.enable(list [, ignoreInvalid]) -> Array -* - list (String|Array): list of rule names to enable. -* - ignoreInvalid (Boolean): set `true` to ignore errors when rule not found. -* -* Enable rules with given names. If any rule name not found - throw Error. -* Errors can be disabled by second param. -* -* Returns list of found rule names (if no exception happened). -* -* See also [[Ruler.disable]], [[Ruler.enableOnly]]. -**/ -Ruler.prototype.enable = function(list$1, ignoreInvalid) { - if (!Array.isArray(list$1)) { - list$1 = [list$1]; - } - const result = []; - list$1.forEach(function(name) { - const idx = this.__find__(name); - if (idx < 0) { - if (ignoreInvalid) { - return; - } - throw new Error("Rules manager: invalid rule name " + name); - } - this.__rules__[idx].enabled = true; - result.push(name); - }, this); - this.__cache__ = null; - return result; -}; -/** -* Ruler.enableOnly(list [, ignoreInvalid]) -* - list (String|Array): list of rule names to enable (whitelist). -* - ignoreInvalid (Boolean): set `true` to ignore errors when rule not found. -* -* Enable rules with given names, and disable everything else. If any rule name -* not found - throw Error. Errors can be disabled by second param. -* -* See also [[Ruler.disable]], [[Ruler.enable]]. -**/ -Ruler.prototype.enableOnly = function(list$1, ignoreInvalid) { - if (!Array.isArray(list$1)) { - list$1 = [list$1]; - } - this.__rules__.forEach(function(rule) { - rule.enabled = false; - }); - this.enable(list$1, ignoreInvalid); -}; -/** -* Ruler.disable(list [, ignoreInvalid]) -> Array -* - list (String|Array): list of rule names to disable. -* - ignoreInvalid (Boolean): set `true` to ignore errors when rule not found. -* -* Disable rules with given names. If any rule name not found - throw Error. -* Errors can be disabled by second param. -* -* Returns list of found rule names (if no exception happened). -* -* See also [[Ruler.enable]], [[Ruler.enableOnly]]. -**/ -Ruler.prototype.disable = function(list$1, ignoreInvalid) { - if (!Array.isArray(list$1)) { - list$1 = [list$1]; - } - const result = []; - list$1.forEach(function(name) { - const idx = this.__find__(name); - if (idx < 0) { - if (ignoreInvalid) { - return; - } - throw new Error("Rules manager: invalid rule name " + name); - } - this.__rules__[idx].enabled = false; - result.push(name); - }, this); - this.__cache__ = null; - return result; -}; -/** -* Ruler.getRules(chainName) -> Array -* -* Return array of active functions (rules) for given chain name. It analyzes -* rules configuration, compiles caches if not exists and returns result. -* -* Default chain name is `''` (empty string). It can't be skipped. That's -* done intentionally, to keep signature monomorphic for high speed. -**/ -Ruler.prototype.getRules = function(chainName) { - if (this.__cache__ === null) { - this.__compile__(); - } - return this.__cache__[chainName] || []; -}; -var ruler_default = Ruler; - -/** -* class Token -**/ -/** -* new Token(type, tag, nesting) -* -* Create new token and fill passed properties. -**/ -function Token(type$2, tag, nesting) { - /** - * Token#type -> String - * - * Type of the token (string, e.g. "paragraph_open") - **/ - this.type = type$2; - /** - * Token#tag -> String - * - * html tag name, e.g. "p" - **/ - this.tag = tag; - /** - * Token#attrs -> Array - * - * Html attributes. Format: `[ [ name1, value1 ], [ name2, value2 ] ]` - **/ - this.attrs = null; - /** - * Token#map -> Array - * - * Source map info. Format: `[ line_begin, line_end ]` - **/ - this.map = null; - /** - * Token#nesting -> Number - * - * Level change (number in {-1, 0, 1} set), where: - * - * - `1` means the tag is opening - * - `0` means the tag is self-closing - * - `-1` means the tag is closing - **/ - this.nesting = nesting; - /** - * Token#level -> Number - * - * nesting level, the same as `state.level` - **/ - this.level = 0; - /** - * Token#children -> Array - * - * An array of child nodes (inline and img tokens) - **/ - this.children = null; - /** - * Token#content -> String - * - * In a case of self-closing tag (code, html, fence, etc.), - * it has contents of this tag. - **/ - this.content = ""; - /** - * Token#markup -> String - * - * '*' or '_' for emphasis, fence string for fence, etc. - **/ - this.markup = ""; - /** - * Token#info -> String - * - * Additional information: - * - * - Info string for "fence" tokens - * - The value "auto" for autolink "link_open" and "link_close" tokens - * - The string value of the item marker for ordered-list "list_item_open" tokens - **/ - this.info = ""; - /** - * Token#meta -> Object - * - * A place for plugins to store an arbitrary data - **/ - this.meta = null; - /** - * Token#block -> Boolean - * - * True for block-level tokens, false for inline tokens. - * Used in renderer to calculate line breaks - **/ - this.block = false; - /** - * Token#hidden -> Boolean - * - * If it's true, ignore this element when rendering. Used for tight lists - * to hide paragraphs. - **/ - this.hidden = false; -} -/** -* Token.attrIndex(name) -> Number -* -* Search attribute index by name. -**/ -Token.prototype.attrIndex = function attrIndex(name) { - if (!this.attrs) { - return -1; - } - const attrs = this.attrs; - for (let i$10 = 0, len = attrs.length; i$10 < len; i$10++) { - if (attrs[i$10][0] === name) { - return i$10; - } - } - return -1; -}; -/** -* Token.attrPush(attrData) -* -* Add `[ name, value ]` attribute to list. Init attrs if necessary -**/ -Token.prototype.attrPush = function attrPush(attrData) { - if (this.attrs) { - this.attrs.push(attrData); - } else { - this.attrs = [attrData]; - } -}; -/** -* Token.attrSet(name, value) -* -* Set `name` attribute to `value`. Override old value if exists. -**/ -Token.prototype.attrSet = function attrSet(name, value) { - const idx = this.attrIndex(name); - const attrData = [name, value]; - if (idx < 0) { - this.attrPush(attrData); - } else { - this.attrs[idx] = attrData; - } -}; -/** -* Token.attrGet(name) -* -* Get the value of attribute `name`, or null if it does not exist. -**/ -Token.prototype.attrGet = function attrGet(name) { - const idx = this.attrIndex(name); - let value = null; - if (idx >= 0) { - value = this.attrs[idx][1]; - } - return value; -}; -/** -* Token.attrJoin(name, value) -* -* Join value to existing attribute via space. Or create new attribute if not -* exists. Useful to operate with token classes. -**/ -Token.prototype.attrJoin = function attrJoin(name, value) { - const idx = this.attrIndex(name); - if (idx < 0) { - this.attrPush([name, value]); - } else { - this.attrs[idx][1] = this.attrs[idx][1] + " " + value; - } -}; -var token_default = Token; - -function StateCore(src, md, env) { - this.src = src; - this.env = env; - this.tokens = []; - this.inlineMode = false; - this.md = md; -} -StateCore.prototype.Token = token_default; -var state_core_default = StateCore; - -const NEWLINES_RE = /\r\n?|\n/g; -const NULL_RE = /\0/g; -function normalize(state) { - let str; - str = state.src.replace(NEWLINES_RE, "\n"); - str = str.replace(NULL_RE, "�"); - state.src = str; -} - -function block(state) { - let token; - if (state.inlineMode) { - token = new state.Token("inline", "", 0); - token.content = state.src; - token.map = [0, 1]; - token.children = []; - state.tokens.push(token); - } else { - state.md.block.parse(state.src, state.md, state.env, state.tokens); - } -} - -function inline(state) { - const tokens = state.tokens; - for (let i$10 = 0, l$5 = tokens.length; i$10 < l$5; i$10++) { - const tok = tokens[i$10]; - if (tok.type === "inline") { - state.md.inline.parse(tok.content, state.md, state.env, tok.children); - } - } -} - -function isLinkOpen$1(str) { - return /^\s]/i.test(str); -} -function isLinkClose$1(str) { - return /^<\/a\s*>/i.test(str); -} -function linkify$1(state) { - const blockTokens = state.tokens; - if (!state.md.options.linkify) { - return; - } - for (let j$2 = 0, l$5 = blockTokens.length; j$2 < l$5; j$2++) { - if (blockTokens[j$2].type !== "inline" || !state.md.linkify.pretest(blockTokens[j$2].content)) { - continue; - } - let tokens = blockTokens[j$2].children; - let htmlLinkLevel = 0; - for (let i$10 = tokens.length - 1; i$10 >= 0; i$10--) { - const currentToken = tokens[i$10]; - if (currentToken.type === "link_close") { - i$10--; - while (tokens[i$10].level !== currentToken.level && tokens[i$10].type !== "link_open") { - i$10--; - } - continue; - } - if (currentToken.type === "html_inline") { - if (isLinkOpen$1(currentToken.content) && htmlLinkLevel > 0) { - htmlLinkLevel--; - } - if (isLinkClose$1(currentToken.content)) { - htmlLinkLevel++; - } - } - if (htmlLinkLevel > 0) { - continue; - } - if (currentToken.type === "text" && state.md.linkify.test(currentToken.content)) { - const text$1 = currentToken.content; - let links = state.md.linkify.match(text$1); - const nodes = []; - let level = currentToken.level; - let lastPos = 0; - if (links.length > 0 && links[0].index === 0 && i$10 > 0 && tokens[i$10 - 1].type === "text_special") { - links = links.slice(1); - } - for (let ln = 0; ln < links.length; ln++) { - const url = links[ln].url; - const fullUrl = state.md.normalizeLink(url); - if (!state.md.validateLink(fullUrl)) { - continue; - } - let urlText = links[ln].text; - if (!links[ln].schema) { - urlText = state.md.normalizeLinkText("http://" + urlText).replace(/^http:\/\//, ""); - } else if (links[ln].schema === "mailto:" && !/^mailto:/i.test(urlText)) { - urlText = state.md.normalizeLinkText("mailto:" + urlText).replace(/^mailto:/, ""); - } else { - urlText = state.md.normalizeLinkText(urlText); - } - const pos = links[ln].index; - if (pos > lastPos) { - const token = new state.Token("text", "", 0); - token.content = text$1.slice(lastPos, pos); - token.level = level; - nodes.push(token); - } - const token_o = new state.Token("link_open", "a", 1); - token_o.attrs = [["href", fullUrl]]; - token_o.level = level++; - token_o.markup = "linkify"; - token_o.info = "auto"; - nodes.push(token_o); - const token_t = new state.Token("text", "", 0); - token_t.content = urlText; - token_t.level = level; - nodes.push(token_t); - const token_c = new state.Token("link_close", "a", -1); - token_c.level = --level; - token_c.markup = "linkify"; - token_c.info = "auto"; - nodes.push(token_c); - lastPos = links[ln].lastIndex; - } - if (lastPos < text$1.length) { - const token = new state.Token("text", "", 0); - token.content = text$1.slice(lastPos); - token.level = level; - nodes.push(token); - } - blockTokens[j$2].children = tokens = arrayReplaceAt(tokens, i$10, nodes); - } - } - } -} - -const RARE_RE = /\+-|\.\.|\?\?\?\?|!!!!|,,|--/; -const SCOPED_ABBR_TEST_RE = /\((c|tm|r)\)/i; -const SCOPED_ABBR_RE = /\((c|tm|r)\)/gi; -const SCOPED_ABBR = { - c: "©", - r: "®", - tm: "™" -}; -function replaceFn(match, name) { - return SCOPED_ABBR[name.toLowerCase()]; -} -function replace_scoped(inlineTokens) { - let inside_autolink = 0; - for (let i$10 = inlineTokens.length - 1; i$10 >= 0; i$10--) { - const token = inlineTokens[i$10]; - if (token.type === "text" && !inside_autolink) { - token.content = token.content.replace(SCOPED_ABBR_RE, replaceFn); - } - if (token.type === "link_open" && token.info === "auto") { - inside_autolink--; - } - if (token.type === "link_close" && token.info === "auto") { - inside_autolink++; - } - } -} -function replace_rare(inlineTokens) { - let inside_autolink = 0; - for (let i$10 = inlineTokens.length - 1; i$10 >= 0; i$10--) { - const token = inlineTokens[i$10]; - if (token.type === "text" && !inside_autolink) { - if (RARE_RE.test(token.content)) { - token.content = token.content.replace(/\+-/g, "±").replace(/\.{2,}/g, "…").replace(/([?!])…/g, "$1..").replace(/([?!]){4,}/g, "$1$1$1").replace(/,{2,}/g, ",").replace(/(^|[^-])---(?=[^-]|$)/gm, "$1—").replace(/(^|\s)--(?=\s|$)/gm, "$1–").replace(/(^|[^-\s])--(?=[^-\s]|$)/gm, "$1–"); - } - } - if (token.type === "link_open" && token.info === "auto") { - inside_autolink--; - } - if (token.type === "link_close" && token.info === "auto") { - inside_autolink++; - } - } -} -function replace(state) { - let blkIdx; - if (!state.md.options.typographer) { - return; - } - for (blkIdx = state.tokens.length - 1; blkIdx >= 0; blkIdx--) { - if (state.tokens[blkIdx].type !== "inline") { - continue; - } - if (SCOPED_ABBR_TEST_RE.test(state.tokens[blkIdx].content)) { - replace_scoped(state.tokens[blkIdx].children); - } - if (RARE_RE.test(state.tokens[blkIdx].content)) { - replace_rare(state.tokens[blkIdx].children); - } - } -} - -const QUOTE_TEST_RE = /['"]/; -const QUOTE_RE = /['"]/g; -const APOSTROPHE = "’"; -function replaceAt(str, index, ch) { - return str.slice(0, index) + ch + str.slice(index + 1); -} -function process_inlines(tokens, state) { - let j$2; - const stack = []; - for (let i$10 = 0; i$10 < tokens.length; i$10++) { - const token = tokens[i$10]; - const thisLevel = tokens[i$10].level; - for (j$2 = stack.length - 1; j$2 >= 0; j$2--) { - if (stack[j$2].level <= thisLevel) { - break; - } - } - stack.length = j$2 + 1; - if (token.type !== "text") { - continue; - } - let text$1 = token.content; - let pos = 0; - let max = text$1.length; - OUTER: while (pos < max) { - QUOTE_RE.lastIndex = pos; - const t$7 = QUOTE_RE.exec(text$1); - if (!t$7) { - break; - } - let canOpen = true; - let canClose = true; - pos = t$7.index + 1; - const isSingle = t$7[0] === "'"; - let lastChar = 32; - if (t$7.index - 1 >= 0) { - lastChar = text$1.charCodeAt(t$7.index - 1); - } else { - for (j$2 = i$10 - 1; j$2 >= 0; j$2--) { - if (tokens[j$2].type === "softbreak" || tokens[j$2].type === "hardbreak") break; - if (!tokens[j$2].content) continue; - lastChar = tokens[j$2].content.charCodeAt(tokens[j$2].content.length - 1); - break; - } - } - let nextChar = 32; - if (pos < max) { - nextChar = text$1.charCodeAt(pos); - } else { - for (j$2 = i$10 + 1; j$2 < tokens.length; j$2++) { - if (tokens[j$2].type === "softbreak" || tokens[j$2].type === "hardbreak") break; - if (!tokens[j$2].content) continue; - nextChar = tokens[j$2].content.charCodeAt(0); - break; - } - } - const isLastPunctChar = isMdAsciiPunct(lastChar) || isPunctChar(String.fromCharCode(lastChar)); - const isNextPunctChar = isMdAsciiPunct(nextChar) || isPunctChar(String.fromCharCode(nextChar)); - const isLastWhiteSpace = isWhiteSpace(lastChar); - const isNextWhiteSpace = isWhiteSpace(nextChar); - if (isNextWhiteSpace) { - canOpen = false; - } else if (isNextPunctChar) { - if (!(isLastWhiteSpace || isLastPunctChar)) { - canOpen = false; - } - } - if (isLastWhiteSpace) { - canClose = false; - } else if (isLastPunctChar) { - if (!(isNextWhiteSpace || isNextPunctChar)) { - canClose = false; - } - } - if (nextChar === 34 && t$7[0] === "\"") { - if (lastChar >= 48 && lastChar <= 57) { - canClose = canOpen = false; - } - } - if (canOpen && canClose) { - canOpen = isLastPunctChar; - canClose = isNextPunctChar; - } - if (!canOpen && !canClose) { - if (isSingle) { - token.content = replaceAt(token.content, t$7.index, APOSTROPHE); - } - continue; - } - if (canClose) { - for (j$2 = stack.length - 1; j$2 >= 0; j$2--) { - let item = stack[j$2]; - if (stack[j$2].level < thisLevel) { - break; - } - if (item.single === isSingle && stack[j$2].level === thisLevel) { - item = stack[j$2]; - let openQuote; - let closeQuote; - if (isSingle) { - openQuote = state.md.options.quotes[2]; - closeQuote = state.md.options.quotes[3]; - } else { - openQuote = state.md.options.quotes[0]; - closeQuote = state.md.options.quotes[1]; - } - token.content = replaceAt(token.content, t$7.index, closeQuote); - tokens[item.token].content = replaceAt(tokens[item.token].content, item.pos, openQuote); - pos += closeQuote.length - 1; - if (item.token === i$10) { - pos += openQuote.length - 1; - } - text$1 = token.content; - max = text$1.length; - stack.length = j$2; - continue OUTER; - } - } - } - if (canOpen) { - stack.push({ - token: i$10, - pos: t$7.index, - single: isSingle, - level: thisLevel - }); - } else if (canClose && isSingle) { - token.content = replaceAt(token.content, t$7.index, APOSTROPHE); - } - } - } -} -function smartquotes(state) { - if (!state.md.options.typographer) { - return; - } - for (let blkIdx = state.tokens.length - 1; blkIdx >= 0; blkIdx--) { - if (state.tokens[blkIdx].type !== "inline" || !QUOTE_TEST_RE.test(state.tokens[blkIdx].content)) { - continue; - } - process_inlines(state.tokens[blkIdx].children, state); - } -} - -function text_join(state) { - let curr, last; - const blockTokens = state.tokens; - const l$5 = blockTokens.length; - for (let j$2 = 0; j$2 < l$5; j$2++) { - if (blockTokens[j$2].type !== "inline") continue; - const tokens = blockTokens[j$2].children; - const max = tokens.length; - for (curr = 0; curr < max; curr++) { - if (tokens[curr].type === "text_special") { - tokens[curr].type = "text"; - } - } - for (curr = last = 0; curr < max; curr++) { - if (tokens[curr].type === "text" && curr + 1 < max && tokens[curr + 1].type === "text") { - tokens[curr + 1].content = tokens[curr].content + tokens[curr + 1].content; - } else { - if (curr !== last) { - tokens[last] = tokens[curr]; - } - last++; - } - } - if (curr !== last) { - tokens.length = last; - } - } -} - -/** internal -* class Core -* -* Top-level rules executor. Glues block/inline parsers and does intermediate -* transformations. -**/ -const _rules$2 = [ - ["normalize", normalize], - ["block", block], - ["inline", inline], - ["linkify", linkify$1], - ["replacements", replace], - ["smartquotes", smartquotes], - ["text_join", text_join] -]; -/** -* new Core() -**/ -function Core() { - /** - * Core#ruler -> Ruler - * - * [[Ruler]] instance. Keep configuration of core rules. - **/ - this.ruler = new ruler_default(); - for (let i$10 = 0; i$10 < _rules$2.length; i$10++) { - this.ruler.push(_rules$2[i$10][0], _rules$2[i$10][1]); - } -} -/** -* Core.process(state) -* -* Executes core chain rules. -**/ -Core.prototype.process = function(state) { - const rules = this.ruler.getRules(""); - for (let i$10 = 0, l$5 = rules.length; i$10 < l$5; i$10++) { - rules[i$10](state); - } -}; -Core.prototype.State = state_core_default; -var parser_core_default = Core; - -function StateBlock(src, md, env, tokens) { - this.src = src; - this.md = md; - this.env = env; - this.tokens = tokens; - this.bMarks = []; - this.eMarks = []; - this.tShift = []; - this.sCount = []; - this.bsCount = []; - this.blkIndent = 0; - this.line = 0; - this.lineMax = 0; - this.tight = false; - this.ddIndent = -1; - this.listIndent = -1; - this.parentType = "root"; - this.level = 0; - const s$9 = this.src; - for (let start = 0, pos = 0, indent = 0, offset = 0, len = s$9.length, indent_found = false; pos < len; pos++) { - const ch = s$9.charCodeAt(pos); - if (!indent_found) { - if (isSpace(ch)) { - indent++; - if (ch === 9) { - offset += 4 - offset % 4; - } else { - offset++; - } - continue; - } else { - indent_found = true; - } - } - if (ch === 10 || pos === len - 1) { - if (ch !== 10) { - pos++; - } - this.bMarks.push(start); - this.eMarks.push(pos); - this.tShift.push(indent); - this.sCount.push(offset); - this.bsCount.push(0); - indent_found = false; - indent = 0; - offset = 0; - start = pos + 1; - } - } - this.bMarks.push(s$9.length); - this.eMarks.push(s$9.length); - this.tShift.push(0); - this.sCount.push(0); - this.bsCount.push(0); - this.lineMax = this.bMarks.length - 1; -} -StateBlock.prototype.push = function(type$2, tag, nesting) { - const token = new token_default(type$2, tag, nesting); - token.block = true; - if (nesting < 0) this.level--; - token.level = this.level; - if (nesting > 0) this.level++; - this.tokens.push(token); - return token; -}; -StateBlock.prototype.isEmpty = function isEmpty(line) { - return this.bMarks[line] + this.tShift[line] >= this.eMarks[line]; -}; -StateBlock.prototype.skipEmptyLines = function skipEmptyLines(from) { - for (let max = this.lineMax; from < max; from++) { - if (this.bMarks[from] + this.tShift[from] < this.eMarks[from]) { - break; - } - } - return from; -}; -StateBlock.prototype.skipSpaces = function skipSpaces(pos) { - for (let max = this.src.length; pos < max; pos++) { - const ch = this.src.charCodeAt(pos); - if (!isSpace(ch)) { - break; - } - } - return pos; -}; -StateBlock.prototype.skipSpacesBack = function skipSpacesBack(pos, min) { - if (pos <= min) { - return pos; - } - while (pos > min) { - if (!isSpace(this.src.charCodeAt(--pos))) { - return pos + 1; - } - } - return pos; -}; -StateBlock.prototype.skipChars = function skipChars(pos, code$1) { - for (let max = this.src.length; pos < max; pos++) { - if (this.src.charCodeAt(pos) !== code$1) { - break; - } - } - return pos; -}; -StateBlock.prototype.skipCharsBack = function skipCharsBack(pos, code$1, min) { - if (pos <= min) { - return pos; - } - while (pos > min) { - if (code$1 !== this.src.charCodeAt(--pos)) { - return pos + 1; - } - } - return pos; -}; -StateBlock.prototype.getLines = function getLines(begin, end, indent, keepLastLF) { - if (begin >= end) { - return ""; - } - const queue = new Array(end - begin); - for (let i$10 = 0, line = begin; line < end; line++, i$10++) { - let lineIndent = 0; - const lineStart = this.bMarks[line]; - let first = lineStart; - let last; - if (line + 1 < end || keepLastLF) { - last = this.eMarks[line] + 1; - } else { - last = this.eMarks[line]; - } - while (first < last && lineIndent < indent) { - const ch = this.src.charCodeAt(first); - if (isSpace(ch)) { - if (ch === 9) { - lineIndent += 4 - (lineIndent + this.bsCount[line]) % 4; - } else { - lineIndent++; - } - } else if (first - lineStart < this.tShift[line]) { - lineIndent++; - } else { - break; - } - first++; - } - if (lineIndent > indent) { - queue[i$10] = new Array(lineIndent - indent + 1).join(" ") + this.src.slice(first, last); - } else { - queue[i$10] = this.src.slice(first, last); - } - } - return queue.join(""); -}; -StateBlock.prototype.Token = token_default; -var state_block_default = StateBlock; - -const MAX_AUTOCOMPLETED_CELLS = 65536; -function getLine(state, line) { - const pos = state.bMarks[line] + state.tShift[line]; - const max = state.eMarks[line]; - return state.src.slice(pos, max); -} -function escapedSplit(str) { - const result = []; - const max = str.length; - let pos = 0; - let ch = str.charCodeAt(pos); - let isEscaped = false; - let lastPos = 0; - let current = ""; - while (pos < max) { - if (ch === 124) { - if (!isEscaped) { - result.push(current + str.substring(lastPos, pos)); - current = ""; - lastPos = pos + 1; - } else { - current += str.substring(lastPos, pos - 1); - lastPos = pos; - } - } - isEscaped = ch === 92; - pos++; - ch = str.charCodeAt(pos); - } - result.push(current + str.substring(lastPos)); - return result; -} -function table(state, startLine, endLine, silent) { - if (startLine + 2 > endLine) { - return false; - } - let nextLine = startLine + 1; - if (state.sCount[nextLine] < state.blkIndent) { - return false; - } - if (state.sCount[nextLine] - state.blkIndent >= 4) { - return false; - } - let pos = state.bMarks[nextLine] + state.tShift[nextLine]; - if (pos >= state.eMarks[nextLine]) { - return false; - } - const firstCh = state.src.charCodeAt(pos++); - if (firstCh !== 124 && firstCh !== 45 && firstCh !== 58) { - return false; - } - if (pos >= state.eMarks[nextLine]) { - return false; - } - const secondCh = state.src.charCodeAt(pos++); - if (secondCh !== 124 && secondCh !== 45 && secondCh !== 58 && !isSpace(secondCh)) { - return false; - } - if (firstCh === 45 && isSpace(secondCh)) { - return false; - } - while (pos < state.eMarks[nextLine]) { - const ch = state.src.charCodeAt(pos); - if (ch !== 124 && ch !== 45 && ch !== 58 && !isSpace(ch)) { - return false; - } - pos++; - } - let lineText = getLine(state, startLine + 1); - let columns = lineText.split("|"); - const aligns = []; - for (let i$10 = 0; i$10 < columns.length; i$10++) { - const t$7 = columns[i$10].trim(); - if (!t$7) { - if (i$10 === 0 || i$10 === columns.length - 1) { - continue; - } else { - return false; - } - } - if (!/^:?-+:?$/.test(t$7)) { - return false; - } - if (t$7.charCodeAt(t$7.length - 1) === 58) { - aligns.push(t$7.charCodeAt(0) === 58 ? "center" : "right"); - } else if (t$7.charCodeAt(0) === 58) { - aligns.push("left"); - } else { - aligns.push(""); - } - } - lineText = getLine(state, startLine).trim(); - if (lineText.indexOf("|") === -1) { - return false; - } - if (state.sCount[startLine] - state.blkIndent >= 4) { - return false; - } - columns = escapedSplit(lineText); - if (columns.length && columns[0] === "") columns.shift(); - if (columns.length && columns[columns.length - 1] === "") columns.pop(); - const columnCount = columns.length; - if (columnCount === 0 || columnCount !== aligns.length) { - return false; - } - if (silent) { - return true; - } - const oldParentType = state.parentType; - state.parentType = "table"; - const terminatorRules = state.md.block.ruler.getRules("blockquote"); - const token_to = state.push("table_open", "table", 1); - const tableLines = [startLine, 0]; - token_to.map = tableLines; - const token_tho = state.push("thead_open", "thead", 1); - token_tho.map = [startLine, startLine + 1]; - const token_htro = state.push("tr_open", "tr", 1); - token_htro.map = [startLine, startLine + 1]; - for (let i$10 = 0; i$10 < columns.length; i$10++) { - const token_ho = state.push("th_open", "th", 1); - if (aligns[i$10]) { - token_ho.attrs = [["style", "text-align:" + aligns[i$10]]]; - } - const token_il = state.push("inline", "", 0); - token_il.content = columns[i$10].trim(); - token_il.children = []; - state.push("th_close", "th", -1); - } - state.push("tr_close", "tr", -1); - state.push("thead_close", "thead", -1); - let tbodyLines; - let autocompletedCells = 0; - for (nextLine = startLine + 2; nextLine < endLine; nextLine++) { - if (state.sCount[nextLine] < state.blkIndent) { - break; - } - let terminate = false; - for (let i$10 = 0, l$5 = terminatorRules.length; i$10 < l$5; i$10++) { - if (terminatorRules[i$10](state, nextLine, endLine, true)) { - terminate = true; - break; - } - } - if (terminate) { - break; - } - lineText = getLine(state, nextLine).trim(); - if (!lineText) { - break; - } - if (state.sCount[nextLine] - state.blkIndent >= 4) { - break; - } - columns = escapedSplit(lineText); - if (columns.length && columns[0] === "") columns.shift(); - if (columns.length && columns[columns.length - 1] === "") columns.pop(); - autocompletedCells += columnCount - columns.length; - if (autocompletedCells > MAX_AUTOCOMPLETED_CELLS) { - break; - } - if (nextLine === startLine + 2) { - const token_tbo = state.push("tbody_open", "tbody", 1); - token_tbo.map = tbodyLines = [startLine + 2, 0]; - } - const token_tro = state.push("tr_open", "tr", 1); - token_tro.map = [nextLine, nextLine + 1]; - for (let i$10 = 0; i$10 < columnCount; i$10++) { - const token_tdo = state.push("td_open", "td", 1); - if (aligns[i$10]) { - token_tdo.attrs = [["style", "text-align:" + aligns[i$10]]]; - } - const token_il = state.push("inline", "", 0); - token_il.content = columns[i$10] ? columns[i$10].trim() : ""; - token_il.children = []; - state.push("td_close", "td", -1); - } - state.push("tr_close", "tr", -1); - } - if (tbodyLines) { - state.push("tbody_close", "tbody", -1); - tbodyLines[1] = nextLine; - } - state.push("table_close", "table", -1); - tableLines[1] = nextLine; - state.parentType = oldParentType; - state.line = nextLine; - return true; -} - -function code(state, startLine, endLine) { - if (state.sCount[startLine] - state.blkIndent < 4) { - return false; - } - let nextLine = startLine + 1; - let last = nextLine; - while (nextLine < endLine) { - if (state.isEmpty(nextLine)) { - nextLine++; - continue; - } - if (state.sCount[nextLine] - state.blkIndent >= 4) { - nextLine++; - last = nextLine; - continue; - } - break; - } - state.line = last; - const token = state.push("code_block", "code", 0); - token.content = state.getLines(startLine, last, 4 + state.blkIndent, false) + "\n"; - token.map = [startLine, state.line]; - return true; -} - -function fence(state, startLine, endLine, silent) { - let pos = state.bMarks[startLine] + state.tShift[startLine]; - let max = state.eMarks[startLine]; - if (state.sCount[startLine] - state.blkIndent >= 4) { - return false; - } - if (pos + 3 > max) { - return false; - } - const marker = state.src.charCodeAt(pos); - if (marker !== 126 && marker !== 96) { - return false; - } - let mem = pos; - pos = state.skipChars(pos, marker); - let len = pos - mem; - if (len < 3) { - return false; - } - const markup = state.src.slice(mem, pos); - const params = state.src.slice(pos, max); - if (marker === 96) { - if (params.indexOf(String.fromCharCode(marker)) >= 0) { - return false; - } - } - if (silent) { - return true; - } - let nextLine = startLine; - let haveEndMarker = false; - for (;;) { - nextLine++; - if (nextLine >= endLine) { - break; - } - pos = mem = state.bMarks[nextLine] + state.tShift[nextLine]; - max = state.eMarks[nextLine]; - if (pos < max && state.sCount[nextLine] < state.blkIndent) { - break; - } - if (state.src.charCodeAt(pos) !== marker) { - continue; - } - if (state.sCount[nextLine] - state.blkIndent >= 4) { - continue; - } - pos = state.skipChars(pos, marker); - if (pos - mem < len) { - continue; - } - pos = state.skipSpaces(pos); - if (pos < max) { - continue; - } - haveEndMarker = true; - break; - } - len = state.sCount[startLine]; - state.line = nextLine + (haveEndMarker ? 1 : 0); - const token = state.push("fence", "code", 0); - token.info = params; - token.content = state.getLines(startLine + 1, nextLine, len, true); - token.markup = markup; - token.map = [startLine, state.line]; - return true; -} - -function blockquote(state, startLine, endLine, silent) { - let pos = state.bMarks[startLine] + state.tShift[startLine]; - let max = state.eMarks[startLine]; - const oldLineMax = state.lineMax; - if (state.sCount[startLine] - state.blkIndent >= 4) { - return false; - } - if (state.src.charCodeAt(pos) !== 62) { - return false; - } - if (silent) { - return true; - } - const oldBMarks = []; - const oldBSCount = []; - const oldSCount = []; - const oldTShift = []; - const terminatorRules = state.md.block.ruler.getRules("blockquote"); - const oldParentType = state.parentType; - state.parentType = "blockquote"; - let lastLineEmpty = false; - let nextLine; - for (nextLine = startLine; nextLine < endLine; nextLine++) { - const isOutdented = state.sCount[nextLine] < state.blkIndent; - pos = state.bMarks[nextLine] + state.tShift[nextLine]; - max = state.eMarks[nextLine]; - if (pos >= max) { - break; - } - if (state.src.charCodeAt(pos++) === 62 && !isOutdented) { - let initial = state.sCount[nextLine] + 1; - let spaceAfterMarker; - let adjustTab; - if (state.src.charCodeAt(pos) === 32) { - pos++; - initial++; - adjustTab = false; - spaceAfterMarker = true; - } else if (state.src.charCodeAt(pos) === 9) { - spaceAfterMarker = true; - if ((state.bsCount[nextLine] + initial) % 4 === 3) { - pos++; - initial++; - adjustTab = false; - } else { - adjustTab = true; - } - } else { - spaceAfterMarker = false; - } - let offset = initial; - oldBMarks.push(state.bMarks[nextLine]); - state.bMarks[nextLine] = pos; - while (pos < max) { - const ch = state.src.charCodeAt(pos); - if (isSpace(ch)) { - if (ch === 9) { - offset += 4 - (offset + state.bsCount[nextLine] + (adjustTab ? 1 : 0)) % 4; - } else { - offset++; - } - } else { - break; - } - pos++; - } - lastLineEmpty = pos >= max; - oldBSCount.push(state.bsCount[nextLine]); - state.bsCount[nextLine] = state.sCount[nextLine] + 1 + (spaceAfterMarker ? 1 : 0); - oldSCount.push(state.sCount[nextLine]); - state.sCount[nextLine] = offset - initial; - oldTShift.push(state.tShift[nextLine]); - state.tShift[nextLine] = pos - state.bMarks[nextLine]; - continue; - } - if (lastLineEmpty) { - break; - } - let terminate = false; - for (let i$10 = 0, l$5 = terminatorRules.length; i$10 < l$5; i$10++) { - if (terminatorRules[i$10](state, nextLine, endLine, true)) { - terminate = true; - break; - } - } - if (terminate) { - state.lineMax = nextLine; - if (state.blkIndent !== 0) { - oldBMarks.push(state.bMarks[nextLine]); - oldBSCount.push(state.bsCount[nextLine]); - oldTShift.push(state.tShift[nextLine]); - oldSCount.push(state.sCount[nextLine]); - state.sCount[nextLine] -= state.blkIndent; - } - break; - } - oldBMarks.push(state.bMarks[nextLine]); - oldBSCount.push(state.bsCount[nextLine]); - oldTShift.push(state.tShift[nextLine]); - oldSCount.push(state.sCount[nextLine]); - state.sCount[nextLine] = -1; - } - const oldIndent = state.blkIndent; - state.blkIndent = 0; - const token_o = state.push("blockquote_open", "blockquote", 1); - token_o.markup = ">"; - const lines = [startLine, 0]; - token_o.map = lines; - state.md.block.tokenize(state, startLine, nextLine); - const token_c = state.push("blockquote_close", "blockquote", -1); - token_c.markup = ">"; - state.lineMax = oldLineMax; - state.parentType = oldParentType; - lines[1] = state.line; - for (let i$10 = 0; i$10 < oldTShift.length; i$10++) { - state.bMarks[i$10 + startLine] = oldBMarks[i$10]; - state.tShift[i$10 + startLine] = oldTShift[i$10]; - state.sCount[i$10 + startLine] = oldSCount[i$10]; - state.bsCount[i$10 + startLine] = oldBSCount[i$10]; - } - state.blkIndent = oldIndent; - return true; -} - -function hr(state, startLine, endLine, silent) { - const max = state.eMarks[startLine]; - if (state.sCount[startLine] - state.blkIndent >= 4) { - return false; - } - let pos = state.bMarks[startLine] + state.tShift[startLine]; - const marker = state.src.charCodeAt(pos++); - if (marker !== 42 && marker !== 45 && marker !== 95) { - return false; - } - let cnt = 1; - while (pos < max) { - const ch = state.src.charCodeAt(pos++); - if (ch !== marker && !isSpace(ch)) { - return false; - } - if (ch === marker) { - cnt++; - } - } - if (cnt < 3) { - return false; - } - if (silent) { - return true; - } - state.line = startLine + 1; - const token = state.push("hr", "hr", 0); - token.map = [startLine, state.line]; - token.markup = Array(cnt + 1).join(String.fromCharCode(marker)); - return true; -} - -function skipBulletListMarker(state, startLine) { - const max = state.eMarks[startLine]; - let pos = state.bMarks[startLine] + state.tShift[startLine]; - const marker = state.src.charCodeAt(pos++); - if (marker !== 42 && marker !== 45 && marker !== 43) { - return -1; - } - if (pos < max) { - const ch = state.src.charCodeAt(pos); - if (!isSpace(ch)) { - return -1; - } - } - return pos; -} -function skipOrderedListMarker(state, startLine) { - const start = state.bMarks[startLine] + state.tShift[startLine]; - const max = state.eMarks[startLine]; - let pos = start; - if (pos + 1 >= max) { - return -1; - } - let ch = state.src.charCodeAt(pos++); - if (ch < 48 || ch > 57) { - return -1; - } - for (;;) { - if (pos >= max) { - return -1; - } - ch = state.src.charCodeAt(pos++); - if (ch >= 48 && ch <= 57) { - if (pos - start >= 10) { - return -1; - } - continue; - } - if (ch === 41 || ch === 46) { - break; - } - return -1; - } - if (pos < max) { - ch = state.src.charCodeAt(pos); - if (!isSpace(ch)) { - return -1; - } - } - return pos; -} -function markTightParagraphs(state, idx) { - const level = state.level + 2; - for (let i$10 = idx + 2, l$5 = state.tokens.length - 2; i$10 < l$5; i$10++) { - if (state.tokens[i$10].level === level && state.tokens[i$10].type === "paragraph_open") { - state.tokens[i$10 + 2].hidden = true; - state.tokens[i$10].hidden = true; - i$10 += 2; - } - } -} -function list(state, startLine, endLine, silent) { - let max, pos, start, token; - let nextLine = startLine; - let tight = true; - if (state.sCount[nextLine] - state.blkIndent >= 4) { - return false; - } - if (state.listIndent >= 0 && state.sCount[nextLine] - state.listIndent >= 4 && state.sCount[nextLine] < state.blkIndent) { - return false; - } - let isTerminatingParagraph = false; - if (silent && state.parentType === "paragraph") { - if (state.sCount[nextLine] >= state.blkIndent) { - isTerminatingParagraph = true; - } - } - let isOrdered; - let markerValue; - let posAfterMarker; - if ((posAfterMarker = skipOrderedListMarker(state, nextLine)) >= 0) { - isOrdered = true; - start = state.bMarks[nextLine] + state.tShift[nextLine]; - markerValue = Number(state.src.slice(start, posAfterMarker - 1)); - if (isTerminatingParagraph && markerValue !== 1) return false; - } else if ((posAfterMarker = skipBulletListMarker(state, nextLine)) >= 0) { - isOrdered = false; - } else { - return false; - } - if (isTerminatingParagraph) { - if (state.skipSpaces(posAfterMarker) >= state.eMarks[nextLine]) return false; - } - if (silent) { - return true; - } - const markerCharCode = state.src.charCodeAt(posAfterMarker - 1); - const listTokIdx = state.tokens.length; - if (isOrdered) { - token = state.push("ordered_list_open", "ol", 1); - if (markerValue !== 1) { - token.attrs = [["start", markerValue]]; - } - } else { - token = state.push("bullet_list_open", "ul", 1); - } - const listLines = [nextLine, 0]; - token.map = listLines; - token.markup = String.fromCharCode(markerCharCode); - let prevEmptyEnd = false; - const terminatorRules = state.md.block.ruler.getRules("list"); - const oldParentType = state.parentType; - state.parentType = "list"; - while (nextLine < endLine) { - pos = posAfterMarker; - max = state.eMarks[nextLine]; - const initial = state.sCount[nextLine] + posAfterMarker - (state.bMarks[nextLine] + state.tShift[nextLine]); - let offset = initial; - while (pos < max) { - const ch = state.src.charCodeAt(pos); - if (ch === 9) { - offset += 4 - (offset + state.bsCount[nextLine]) % 4; - } else if (ch === 32) { - offset++; - } else { - break; - } - pos++; - } - const contentStart = pos; - let indentAfterMarker; - if (contentStart >= max) { - indentAfterMarker = 1; - } else { - indentAfterMarker = offset - initial; - } - if (indentAfterMarker > 4) { - indentAfterMarker = 1; - } - const indent = initial + indentAfterMarker; - token = state.push("list_item_open", "li", 1); - token.markup = String.fromCharCode(markerCharCode); - const itemLines = [nextLine, 0]; - token.map = itemLines; - if (isOrdered) { - token.info = state.src.slice(start, posAfterMarker - 1); - } - const oldTight = state.tight; - const oldTShift = state.tShift[nextLine]; - const oldSCount = state.sCount[nextLine]; - const oldListIndent = state.listIndent; - state.listIndent = state.blkIndent; - state.blkIndent = indent; - state.tight = true; - state.tShift[nextLine] = contentStart - state.bMarks[nextLine]; - state.sCount[nextLine] = offset; - if (contentStart >= max && state.isEmpty(nextLine + 1)) { - state.line = Math.min(state.line + 2, endLine); - } else { - state.md.block.tokenize(state, nextLine, endLine, true); - } - if (!state.tight || prevEmptyEnd) { - tight = false; - } - prevEmptyEnd = state.line - nextLine > 1 && state.isEmpty(state.line - 1); - state.blkIndent = state.listIndent; - state.listIndent = oldListIndent; - state.tShift[nextLine] = oldTShift; - state.sCount[nextLine] = oldSCount; - state.tight = oldTight; - token = state.push("list_item_close", "li", -1); - token.markup = String.fromCharCode(markerCharCode); - nextLine = state.line; - itemLines[1] = nextLine; - if (nextLine >= endLine) { - break; - } - if (state.sCount[nextLine] < state.blkIndent) { - break; - } - if (state.sCount[nextLine] - state.blkIndent >= 4) { - break; - } - let terminate = false; - for (let i$10 = 0, l$5 = terminatorRules.length; i$10 < l$5; i$10++) { - if (terminatorRules[i$10](state, nextLine, endLine, true)) { - terminate = true; - break; - } - } - if (terminate) { - break; - } - if (isOrdered) { - posAfterMarker = skipOrderedListMarker(state, nextLine); - if (posAfterMarker < 0) { - break; - } - start = state.bMarks[nextLine] + state.tShift[nextLine]; - } else { - posAfterMarker = skipBulletListMarker(state, nextLine); - if (posAfterMarker < 0) { - break; - } - } - if (markerCharCode !== state.src.charCodeAt(posAfterMarker - 1)) { - break; - } - } - if (isOrdered) { - token = state.push("ordered_list_close", "ol", -1); - } else { - token = state.push("bullet_list_close", "ul", -1); - } - token.markup = String.fromCharCode(markerCharCode); - listLines[1] = nextLine; - state.line = nextLine; - state.parentType = oldParentType; - if (tight) { - markTightParagraphs(state, listTokIdx); - } - return true; -} - -function reference(state, startLine, _endLine, silent) { - let pos = state.bMarks[startLine] + state.tShift[startLine]; - let max = state.eMarks[startLine]; - let nextLine = startLine + 1; - if (state.sCount[startLine] - state.blkIndent >= 4) { - return false; - } - if (state.src.charCodeAt(pos) !== 91) { - return false; - } - function getNextLine(nextLine$1) { - const endLine = state.lineMax; - if (nextLine$1 >= endLine || state.isEmpty(nextLine$1)) { - return null; - } - let isContinuation = false; - if (state.sCount[nextLine$1] - state.blkIndent > 3) { - isContinuation = true; - } - if (state.sCount[nextLine$1] < 0) { - isContinuation = true; - } - if (!isContinuation) { - const terminatorRules = state.md.block.ruler.getRules("reference"); - const oldParentType = state.parentType; - state.parentType = "reference"; - let terminate = false; - for (let i$10 = 0, l$5 = terminatorRules.length; i$10 < l$5; i$10++) { - if (terminatorRules[i$10](state, nextLine$1, endLine, true)) { - terminate = true; - break; - } - } - state.parentType = oldParentType; - if (terminate) { - return null; - } - } - const pos$1 = state.bMarks[nextLine$1] + state.tShift[nextLine$1]; - const max$1 = state.eMarks[nextLine$1]; - return state.src.slice(pos$1, max$1 + 1); - } - let str = state.src.slice(pos, max + 1); - max = str.length; - let labelEnd = -1; - for (pos = 1; pos < max; pos++) { - const ch = str.charCodeAt(pos); - if (ch === 91) { - return false; - } else if (ch === 93) { - labelEnd = pos; - break; - } else if (ch === 10) { - const lineContent = getNextLine(nextLine); - if (lineContent !== null) { - str += lineContent; - max = str.length; - nextLine++; - } - } else if (ch === 92) { - pos++; - if (pos < max && str.charCodeAt(pos) === 10) { - const lineContent = getNextLine(nextLine); - if (lineContent !== null) { - str += lineContent; - max = str.length; - nextLine++; - } - } - } - } - if (labelEnd < 0 || str.charCodeAt(labelEnd + 1) !== 58) { - return false; - } - for (pos = labelEnd + 2; pos < max; pos++) { - const ch = str.charCodeAt(pos); - if (ch === 10) { - const lineContent = getNextLine(nextLine); - if (lineContent !== null) { - str += lineContent; - max = str.length; - nextLine++; - } - } else if (isSpace(ch)) {} else { - break; - } - } - const destRes = state.md.helpers.parseLinkDestination(str, pos, max); - if (!destRes.ok) { - return false; - } - const href = state.md.normalizeLink(destRes.str); - if (!state.md.validateLink(href)) { - return false; - } - pos = destRes.pos; - const destEndPos = pos; - const destEndLineNo = nextLine; - const start = pos; - for (; pos < max; pos++) { - const ch = str.charCodeAt(pos); - if (ch === 10) { - const lineContent = getNextLine(nextLine); - if (lineContent !== null) { - str += lineContent; - max = str.length; - nextLine++; - } - } else if (isSpace(ch)) {} else { - break; - } - } - let titleRes = state.md.helpers.parseLinkTitle(str, pos, max); - while (titleRes.can_continue) { - const lineContent = getNextLine(nextLine); - if (lineContent === null) break; - str += lineContent; - pos = max; - max = str.length; - nextLine++; - titleRes = state.md.helpers.parseLinkTitle(str, pos, max, titleRes); - } - let title$1; - if (pos < max && start !== pos && titleRes.ok) { - title$1 = titleRes.str; - pos = titleRes.pos; - } else { - title$1 = ""; - pos = destEndPos; - nextLine = destEndLineNo; - } - while (pos < max) { - const ch = str.charCodeAt(pos); - if (!isSpace(ch)) { - break; - } - pos++; - } - if (pos < max && str.charCodeAt(pos) !== 10) { - if (title$1) { - title$1 = ""; - pos = destEndPos; - nextLine = destEndLineNo; - while (pos < max) { - const ch = str.charCodeAt(pos); - if (!isSpace(ch)) { - break; - } - pos++; - } - } - } - if (pos < max && str.charCodeAt(pos) !== 10) { - return false; - } - const label = normalizeReference(str.slice(1, labelEnd)); - if (!label) { - return false; - } - /* istanbul ignore if */ - if (silent) { - return true; - } - if (typeof state.env.references === "undefined") { - state.env.references = {}; - } - if (typeof state.env.references[label] === "undefined") { - state.env.references[label] = { - title: title$1, - href - }; - } - state.line = nextLine; - return true; -} - -var html_blocks_default = [ - "address", - "article", - "aside", - "base", - "basefont", - "blockquote", - "body", - "caption", - "center", - "col", - "colgroup", - "dd", - "details", - "dialog", - "dir", - "div", - "dl", - "dt", - "fieldset", - "figcaption", - "figure", - "footer", - "form", - "frame", - "frameset", - "h1", - "h2", - "h3", - "h4", - "h5", - "h6", - "head", - "header", - "hr", - "html", - "iframe", - "legend", - "li", - "link", - "main", - "menu", - "menuitem", - "nav", - "noframes", - "ol", - "optgroup", - "option", - "p", - "param", - "search", - "section", - "summary", - "table", - "tbody", - "td", - "tfoot", - "th", - "thead", - "title", - "tr", - "track", - "ul" -]; - -const attr_name = "[a-zA-Z_:][a-zA-Z0-9:._-]*"; -const unquoted = "[^\"'=<>`\\x00-\\x20]+"; -const single_quoted = "'[^']*'"; -const double_quoted = "\"[^\"]*\""; -const attr_value = "(?:" + unquoted + "|" + single_quoted + "|" + double_quoted + ")"; -const attribute = "(?:\\s+" + attr_name + "(?:\\s*=\\s*" + attr_value + ")?)"; -const open_tag = "<[A-Za-z][A-Za-z0-9\\-]*" + attribute + "*\\s*\\/?>"; -const close_tag = "<\\/[A-Za-z][A-Za-z0-9\\-]*\\s*>"; -const comment = ""; -const processing = "<[?][\\s\\S]*?[?]>"; -const declaration = "]*>"; -const cdata = ""; -const HTML_TAG_RE = new RegExp("^(?:" + open_tag + "|" + close_tag + "|" + comment + "|" + processing + "|" + declaration + "|" + cdata + ")"); -const HTML_OPEN_CLOSE_TAG_RE = new RegExp("^(?:" + open_tag + "|" + close_tag + ")"); - -const HTML_SEQUENCES = [ - [ - /^<(script|pre|style|textarea)(?=(\s|>|$))/i, - /<\/(script|pre|style|textarea)>/i, - true - ], - [ - /^/, - true - ], - [ - /^<\?/, - /\?>/, - true - ], - [ - /^/, - true - ], - [ - /^/, - true - ], - [ - new RegExp("^|$))", "i"), - /^$/, - true - ], - [ - new RegExp(HTML_OPEN_CLOSE_TAG_RE.source + "\\s*$"), - /^$/, - false - ] -]; -function html_block(state, startLine, endLine, silent) { - let pos = state.bMarks[startLine] + state.tShift[startLine]; - let max = state.eMarks[startLine]; - if (state.sCount[startLine] - state.blkIndent >= 4) { - return false; - } - if (!state.md.options.html) { - return false; - } - if (state.src.charCodeAt(pos) !== 60) { - return false; - } - let lineText = state.src.slice(pos, max); - let i$10 = 0; - for (; i$10 < HTML_SEQUENCES.length; i$10++) { - if (HTML_SEQUENCES[i$10][0].test(lineText)) { - break; - } - } - if (i$10 === HTML_SEQUENCES.length) { - return false; - } - if (silent) { - return HTML_SEQUENCES[i$10][2]; - } - let nextLine = startLine + 1; - if (!HTML_SEQUENCES[i$10][1].test(lineText)) { - for (; nextLine < endLine; nextLine++) { - if (state.sCount[nextLine] < state.blkIndent) { - break; - } - pos = state.bMarks[nextLine] + state.tShift[nextLine]; - max = state.eMarks[nextLine]; - lineText = state.src.slice(pos, max); - if (HTML_SEQUENCES[i$10][1].test(lineText)) { - if (lineText.length !== 0) { - nextLine++; - } - break; - } - } - } - state.line = nextLine; - const token = state.push("html_block", "", 0); - token.map = [startLine, nextLine]; - token.content = state.getLines(startLine, nextLine, state.blkIndent, true); - return true; -} - -function heading(state, startLine, endLine, silent) { - let pos = state.bMarks[startLine] + state.tShift[startLine]; - let max = state.eMarks[startLine]; - if (state.sCount[startLine] - state.blkIndent >= 4) { - return false; - } - let ch = state.src.charCodeAt(pos); - if (ch !== 35 || pos >= max) { - return false; - } - let level = 1; - ch = state.src.charCodeAt(++pos); - while (ch === 35 && pos < max && level <= 6) { - level++; - ch = state.src.charCodeAt(++pos); - } - if (level > 6 || pos < max && !isSpace(ch)) { - return false; - } - if (silent) { - return true; - } - max = state.skipSpacesBack(max, pos); - const tmp = state.skipCharsBack(max, 35, pos); - if (tmp > pos && isSpace(state.src.charCodeAt(tmp - 1))) { - max = tmp; - } - state.line = startLine + 1; - const token_o = state.push("heading_open", "h" + String(level), 1); - token_o.markup = "########".slice(0, level); - token_o.map = [startLine, state.line]; - const token_i = state.push("inline", "", 0); - token_i.content = state.src.slice(pos, max).trim(); - token_i.map = [startLine, state.line]; - token_i.children = []; - const token_c = state.push("heading_close", "h" + String(level), -1); - token_c.markup = "########".slice(0, level); - return true; -} - -function lheading(state, startLine, endLine) { - const terminatorRules = state.md.block.ruler.getRules("paragraph"); - if (state.sCount[startLine] - state.blkIndent >= 4) { - return false; - } - const oldParentType = state.parentType; - state.parentType = "paragraph"; - let level = 0; - let marker; - let nextLine = startLine + 1; - for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) { - if (state.sCount[nextLine] - state.blkIndent > 3) { - continue; - } - if (state.sCount[nextLine] >= state.blkIndent) { - let pos = state.bMarks[nextLine] + state.tShift[nextLine]; - const max = state.eMarks[nextLine]; - if (pos < max) { - marker = state.src.charCodeAt(pos); - if (marker === 45 || marker === 61) { - pos = state.skipChars(pos, marker); - pos = state.skipSpaces(pos); - if (pos >= max) { - level = marker === 61 ? 1 : 2; - break; - } - } - } - } - if (state.sCount[nextLine] < 0) { - continue; - } - let terminate = false; - for (let i$10 = 0, l$5 = terminatorRules.length; i$10 < l$5; i$10++) { - if (terminatorRules[i$10](state, nextLine, endLine, true)) { - terminate = true; - break; - } - } - if (terminate) { - break; - } - } - if (!level) { - return false; - } - const content = state.getLines(startLine, nextLine, state.blkIndent, false).trim(); - state.line = nextLine + 1; - const token_o = state.push("heading_open", "h" + String(level), 1); - token_o.markup = String.fromCharCode(marker); - token_o.map = [startLine, state.line]; - const token_i = state.push("inline", "", 0); - token_i.content = content; - token_i.map = [startLine, state.line - 1]; - token_i.children = []; - const token_c = state.push("heading_close", "h" + String(level), -1); - token_c.markup = String.fromCharCode(marker); - state.parentType = oldParentType; - return true; -} - -function paragraph(state, startLine, endLine) { - const terminatorRules = state.md.block.ruler.getRules("paragraph"); - const oldParentType = state.parentType; - let nextLine = startLine + 1; - state.parentType = "paragraph"; - for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) { - if (state.sCount[nextLine] - state.blkIndent > 3) { - continue; - } - if (state.sCount[nextLine] < 0) { - continue; - } - let terminate = false; - for (let i$10 = 0, l$5 = terminatorRules.length; i$10 < l$5; i$10++) { - if (terminatorRules[i$10](state, nextLine, endLine, true)) { - terminate = true; - break; - } - } - if (terminate) { - break; - } - } - const content = state.getLines(startLine, nextLine, state.blkIndent, false).trim(); - state.line = nextLine; - const token_o = state.push("paragraph_open", "p", 1); - token_o.map = [startLine, state.line]; - const token_i = state.push("inline", "", 0); - token_i.content = content; - token_i.map = [startLine, state.line]; - token_i.children = []; - state.push("paragraph_close", "p", -1); - state.parentType = oldParentType; - return true; -} - -/** internal -* class ParserBlock -* -* Block-level tokenizer. -**/ -const _rules$1 = [ - [ - "table", - table, - ["paragraph", "reference"] - ], - ["code", code], - [ - "fence", - fence, - [ - "paragraph", - "reference", - "blockquote", - "list" - ] - ], - [ - "blockquote", - blockquote, - [ - "paragraph", - "reference", - "blockquote", - "list" - ] - ], - [ - "hr", - hr, - [ - "paragraph", - "reference", - "blockquote", - "list" - ] - ], - [ - "list", - list, - [ - "paragraph", - "reference", - "blockquote" - ] - ], - ["reference", reference], - [ - "html_block", - html_block, - [ - "paragraph", - "reference", - "blockquote" - ] - ], - [ - "heading", - heading, - [ - "paragraph", - "reference", - "blockquote" - ] - ], - ["lheading", lheading], - ["paragraph", paragraph] -]; -/** -* new ParserBlock() -**/ -function ParserBlock() { - /** - * ParserBlock#ruler -> Ruler - * - * [[Ruler]] instance. Keep configuration of block rules. - **/ - this.ruler = new ruler_default(); - for (let i$10 = 0; i$10 < _rules$1.length; i$10++) { - this.ruler.push(_rules$1[i$10][0], _rules$1[i$10][1], { alt: (_rules$1[i$10][2] || []).slice() }); - } -} -ParserBlock.prototype.tokenize = function(state, startLine, endLine) { - const rules = this.ruler.getRules(""); - const len = rules.length; - const maxNesting = state.md.options.maxNesting; - let line = startLine; - let hasEmptyLines = false; - while (line < endLine) { - state.line = line = state.skipEmptyLines(line); - if (line >= endLine) { - break; - } - if (state.sCount[line] < state.blkIndent) { - break; - } - if (state.level >= maxNesting) { - state.line = endLine; - break; - } - const prevLine = state.line; - let ok = false; - for (let i$10 = 0; i$10 < len; i$10++) { - ok = rules[i$10](state, line, endLine, false); - if (ok) { - if (prevLine >= state.line) { - throw new Error("block rule didn't increment state.line"); - } - break; - } - } - if (!ok) throw new Error("none of the block rules matched"); - state.tight = !hasEmptyLines; - if (state.isEmpty(state.line - 1)) { - hasEmptyLines = true; - } - line = state.line; - if (line < endLine && state.isEmpty(line)) { - hasEmptyLines = true; - line++; - state.line = line; - } - } -}; -/** -* ParserBlock.parse(str, md, env, outTokens) -* -* Process input string and push block tokens into `outTokens` -**/ -ParserBlock.prototype.parse = function(src, md, env, outTokens) { - if (!src) { - return; - } - const state = new this.State(src, md, env, outTokens); - this.tokenize(state, state.line, state.lineMax); -}; -ParserBlock.prototype.State = state_block_default; -var parser_block_default = ParserBlock; - -function StateInline(src, md, env, outTokens) { - this.src = src; - this.env = env; - this.md = md; - this.tokens = outTokens; - this.tokens_meta = Array(outTokens.length); - this.pos = 0; - this.posMax = this.src.length; - this.level = 0; - this.pending = ""; - this.pendingLevel = 0; - this.cache = {}; - this.delimiters = []; - this._prev_delimiters = []; - this.backticks = {}; - this.backticksScanned = false; - this.linkLevel = 0; -} -StateInline.prototype.pushPending = function() { - const token = new token_default("text", "", 0); - token.content = this.pending; - token.level = this.pendingLevel; - this.tokens.push(token); - this.pending = ""; - return token; -}; -StateInline.prototype.push = function(type$2, tag, nesting) { - if (this.pending) { - this.pushPending(); - } - const token = new token_default(type$2, tag, nesting); - let token_meta = null; - if (nesting < 0) { - this.level--; - this.delimiters = this._prev_delimiters.pop(); - } - token.level = this.level; - if (nesting > 0) { - this.level++; - this._prev_delimiters.push(this.delimiters); - this.delimiters = []; - token_meta = { delimiters: this.delimiters }; - } - this.pendingLevel = this.level; - this.tokens.push(token); - this.tokens_meta.push(token_meta); - return token; -}; -StateInline.prototype.scanDelims = function(start, canSplitWord) { - const max = this.posMax; - const marker = this.src.charCodeAt(start); - const lastChar = start > 0 ? this.src.charCodeAt(start - 1) : 32; - let pos = start; - while (pos < max && this.src.charCodeAt(pos) === marker) { - pos++; - } - const count = pos - start; - const nextChar = pos < max ? this.src.charCodeAt(pos) : 32; - const isLastPunctChar = isMdAsciiPunct(lastChar) || isPunctChar(String.fromCharCode(lastChar)); - const isNextPunctChar = isMdAsciiPunct(nextChar) || isPunctChar(String.fromCharCode(nextChar)); - const isLastWhiteSpace = isWhiteSpace(lastChar); - const isNextWhiteSpace = isWhiteSpace(nextChar); - const left_flanking = !isNextWhiteSpace && (!isNextPunctChar || isLastWhiteSpace || isLastPunctChar); - const right_flanking = !isLastWhiteSpace && (!isLastPunctChar || isNextWhiteSpace || isNextPunctChar); - const can_open = left_flanking && (canSplitWord || !right_flanking || isLastPunctChar); - const can_close = right_flanking && (canSplitWord || !left_flanking || isNextPunctChar); - return { - can_open, - can_close, - length: count - }; -}; -StateInline.prototype.Token = token_default; -var state_inline_default = StateInline; - -function isTerminatorChar(ch) { - switch (ch) { - case 10: - case 33: - case 35: - case 36: - case 37: - case 38: - case 42: - case 43: - case 45: - case 58: - case 60: - case 61: - case 62: - case 64: - case 91: - case 92: - case 93: - case 94: - case 95: - case 96: - case 123: - case 125: - case 126: return true; - default: return false; - } -} -function text(state, silent) { - let pos = state.pos; - while (pos < state.posMax && !isTerminatorChar(state.src.charCodeAt(pos))) { - pos++; - } - if (pos === state.pos) { - return false; - } - if (!silent) { - state.pending += state.src.slice(state.pos, pos); - } - state.pos = pos; - return true; -} - -const SCHEME_RE = /(?:^|[^a-z0-9.+-])([a-z][a-z0-9.+-]*)$/i; -function linkify(state, silent) { - if (!state.md.options.linkify) return false; - if (state.linkLevel > 0) return false; - const pos = state.pos; - const max = state.posMax; - if (pos + 3 > max) return false; - if (state.src.charCodeAt(pos) !== 58) return false; - if (state.src.charCodeAt(pos + 1) !== 47) return false; - if (state.src.charCodeAt(pos + 2) !== 47) return false; - const match = state.pending.match(SCHEME_RE); - if (!match) return false; - const proto = match[1]; - const link$1 = state.md.linkify.matchAtStart(state.src.slice(pos - proto.length)); - if (!link$1) return false; - let url = link$1.url; - if (url.length <= proto.length) return false; - url = url.replace(/\*+$/, ""); - const fullUrl = state.md.normalizeLink(url); - if (!state.md.validateLink(fullUrl)) return false; - if (!silent) { - state.pending = state.pending.slice(0, -proto.length); - const token_o = state.push("link_open", "a", 1); - token_o.attrs = [["href", fullUrl]]; - token_o.markup = "linkify"; - token_o.info = "auto"; - const token_t = state.push("text", "", 0); - token_t.content = state.md.normalizeLinkText(url); - const token_c = state.push("link_close", "a", -1); - token_c.markup = "linkify"; - token_c.info = "auto"; - } - state.pos += url.length - proto.length; - return true; -} - -function newline(state, silent) { - let pos = state.pos; - if (state.src.charCodeAt(pos) !== 10) { - return false; - } - const pmax = state.pending.length - 1; - const max = state.posMax; - if (!silent) { - if (pmax >= 0 && state.pending.charCodeAt(pmax) === 32) { - if (pmax >= 1 && state.pending.charCodeAt(pmax - 1) === 32) { - let ws = pmax - 1; - while (ws >= 1 && state.pending.charCodeAt(ws - 1) === 32) ws--; - state.pending = state.pending.slice(0, ws); - state.push("hardbreak", "br", 0); - } else { - state.pending = state.pending.slice(0, -1); - state.push("softbreak", "br", 0); - } - } else { - state.push("softbreak", "br", 0); - } - } - pos++; - while (pos < max && isSpace(state.src.charCodeAt(pos))) { - pos++; - } - state.pos = pos; - return true; -} - -const ESCAPED = []; -for (let i$10 = 0; i$10 < 256; i$10++) { - ESCAPED.push(0); -} -"\\!\"#$%&'()*+,./:;<=>?@[]^_`{|}~-".split("").forEach(function(ch) { - ESCAPED[ch.charCodeAt(0)] = 1; -}); -function escape(state, silent) { - let pos = state.pos; - const max = state.posMax; - if (state.src.charCodeAt(pos) !== 92) return false; - pos++; - if (pos >= max) return false; - let ch1 = state.src.charCodeAt(pos); - if (ch1 === 10) { - if (!silent) { - state.push("hardbreak", "br", 0); - } - pos++; - while (pos < max) { - ch1 = state.src.charCodeAt(pos); - if (!isSpace(ch1)) break; - pos++; - } - state.pos = pos; - return true; - } - let escapedStr = state.src[pos]; - if (ch1 >= 55296 && ch1 <= 56319 && pos + 1 < max) { - const ch2 = state.src.charCodeAt(pos + 1); - if (ch2 >= 56320 && ch2 <= 57343) { - escapedStr += state.src[pos + 1]; - pos++; - } - } - const origStr = "\\" + escapedStr; - if (!silent) { - const token = state.push("text_special", "", 0); - if (ch1 < 256 && ESCAPED[ch1] !== 0) { - token.content = escapedStr; - } else { - token.content = origStr; - } - token.markup = origStr; - token.info = "escape"; - } - state.pos = pos + 1; - return true; -} - -function backtick(state, silent) { - let pos = state.pos; - const ch = state.src.charCodeAt(pos); - if (ch !== 96) { - return false; - } - const start = pos; - pos++; - const max = state.posMax; - while (pos < max && state.src.charCodeAt(pos) === 96) { - pos++; - } - const marker = state.src.slice(start, pos); - const openerLength = marker.length; - if (state.backticksScanned && (state.backticks[openerLength] || 0) <= start) { - if (!silent) state.pending += marker; - state.pos += openerLength; - return true; - } - let matchEnd = pos; - let matchStart; - while ((matchStart = state.src.indexOf("`", matchEnd)) !== -1) { - matchEnd = matchStart + 1; - while (matchEnd < max && state.src.charCodeAt(matchEnd) === 96) { - matchEnd++; - } - const closerLength = matchEnd - matchStart; - if (closerLength === openerLength) { - if (!silent) { - const token = state.push("code_inline", "code", 0); - token.markup = marker; - token.content = state.src.slice(pos, matchStart).replace(/\n/g, " ").replace(/^ (.+) $/, "$1"); - } - state.pos = matchEnd; - return true; - } - state.backticks[closerLength] = matchStart; - } - state.backticksScanned = true; - if (!silent) state.pending += marker; - state.pos += openerLength; - return true; -} - -function strikethrough_tokenize(state, silent) { - const start = state.pos; - const marker = state.src.charCodeAt(start); - if (silent) { - return false; - } - if (marker !== 126) { - return false; - } - const scanned = state.scanDelims(state.pos, true); - let len = scanned.length; - const ch = String.fromCharCode(marker); - if (len < 2) { - return false; - } - let token; - if (len % 2) { - token = state.push("text", "", 0); - token.content = ch; - len--; - } - for (let i$10 = 0; i$10 < len; i$10 += 2) { - token = state.push("text", "", 0); - token.content = ch + ch; - state.delimiters.push({ - marker, - length: 0, - token: state.tokens.length - 1, - end: -1, - open: scanned.can_open, - close: scanned.can_close - }); - } - state.pos += scanned.length; - return true; -} -function postProcess$1(state, delimiters) { - let token; - const loneMarkers = []; - const max = delimiters.length; - for (let i$10 = 0; i$10 < max; i$10++) { - const startDelim = delimiters[i$10]; - if (startDelim.marker !== 126) { - continue; - } - if (startDelim.end === -1) { - continue; - } - const endDelim = delimiters[startDelim.end]; - token = state.tokens[startDelim.token]; - token.type = "s_open"; - token.tag = "s"; - token.nesting = 1; - token.markup = "~~"; - token.content = ""; - token = state.tokens[endDelim.token]; - token.type = "s_close"; - token.tag = "s"; - token.nesting = -1; - token.markup = "~~"; - token.content = ""; - if (state.tokens[endDelim.token - 1].type === "text" && state.tokens[endDelim.token - 1].content === "~") { - loneMarkers.push(endDelim.token - 1); - } - } - while (loneMarkers.length) { - const i$10 = loneMarkers.pop(); - let j$2 = i$10 + 1; - while (j$2 < state.tokens.length && state.tokens[j$2].type === "s_close") { - j$2++; - } - j$2--; - if (i$10 !== j$2) { - token = state.tokens[j$2]; - state.tokens[j$2] = state.tokens[i$10]; - state.tokens[i$10] = token; - } - } -} -function strikethrough_postProcess(state) { - const tokens_meta = state.tokens_meta; - const max = state.tokens_meta.length; - postProcess$1(state, state.delimiters); - for (let curr = 0; curr < max; curr++) { - if (tokens_meta[curr] && tokens_meta[curr].delimiters) { - postProcess$1(state, tokens_meta[curr].delimiters); - } - } -} -var strikethrough_default = { - tokenize: strikethrough_tokenize, - postProcess: strikethrough_postProcess -}; - -function emphasis_tokenize(state, silent) { - const start = state.pos; - const marker = state.src.charCodeAt(start); - if (silent) { - return false; - } - if (marker !== 95 && marker !== 42) { - return false; - } - const scanned = state.scanDelims(state.pos, marker === 42); - for (let i$10 = 0; i$10 < scanned.length; i$10++) { - const token = state.push("text", "", 0); - token.content = String.fromCharCode(marker); - state.delimiters.push({ - marker, - length: scanned.length, - token: state.tokens.length - 1, - end: -1, - open: scanned.can_open, - close: scanned.can_close - }); - } - state.pos += scanned.length; - return true; -} -function postProcess(state, delimiters) { - const max = delimiters.length; - for (let i$10 = max - 1; i$10 >= 0; i$10--) { - const startDelim = delimiters[i$10]; - if (startDelim.marker !== 95 && startDelim.marker !== 42) { - continue; - } - if (startDelim.end === -1) { - continue; - } - const endDelim = delimiters[startDelim.end]; - const isStrong = i$10 > 0 && delimiters[i$10 - 1].end === startDelim.end + 1 && delimiters[i$10 - 1].marker === startDelim.marker && delimiters[i$10 - 1].token === startDelim.token - 1 && delimiters[startDelim.end + 1].token === endDelim.token + 1; - const ch = String.fromCharCode(startDelim.marker); - const token_o = state.tokens[startDelim.token]; - token_o.type = isStrong ? "strong_open" : "em_open"; - token_o.tag = isStrong ? "strong" : "em"; - token_o.nesting = 1; - token_o.markup = isStrong ? ch + ch : ch; - token_o.content = ""; - const token_c = state.tokens[endDelim.token]; - token_c.type = isStrong ? "strong_close" : "em_close"; - token_c.tag = isStrong ? "strong" : "em"; - token_c.nesting = -1; - token_c.markup = isStrong ? ch + ch : ch; - token_c.content = ""; - if (isStrong) { - state.tokens[delimiters[i$10 - 1].token].content = ""; - state.tokens[delimiters[startDelim.end + 1].token].content = ""; - i$10--; - } - } -} -function emphasis_post_process(state) { - const tokens_meta = state.tokens_meta; - const max = state.tokens_meta.length; - postProcess(state, state.delimiters); - for (let curr = 0; curr < max; curr++) { - if (tokens_meta[curr] && tokens_meta[curr].delimiters) { - postProcess(state, tokens_meta[curr].delimiters); - } - } -} -var emphasis_default = { - tokenize: emphasis_tokenize, - postProcess: emphasis_post_process -}; - -function link(state, silent) { - let code$1, label, res, ref; - let href = ""; - let title$1 = ""; - let start = state.pos; - let parseReference = true; - if (state.src.charCodeAt(state.pos) !== 91) { - return false; - } - const oldPos = state.pos; - const max = state.posMax; - const labelStart = state.pos + 1; - const labelEnd = state.md.helpers.parseLinkLabel(state, state.pos, true); - if (labelEnd < 0) { - return false; - } - let pos = labelEnd + 1; - if (pos < max && state.src.charCodeAt(pos) === 40) { - parseReference = false; - pos++; - for (; pos < max; pos++) { - code$1 = state.src.charCodeAt(pos); - if (!isSpace(code$1) && code$1 !== 10) { - break; - } - } - if (pos >= max) { - return false; - } - start = pos; - res = state.md.helpers.parseLinkDestination(state.src, pos, state.posMax); - if (res.ok) { - href = state.md.normalizeLink(res.str); - if (state.md.validateLink(href)) { - pos = res.pos; - } else { - href = ""; - } - start = pos; - for (; pos < max; pos++) { - code$1 = state.src.charCodeAt(pos); - if (!isSpace(code$1) && code$1 !== 10) { - break; - } - } - res = state.md.helpers.parseLinkTitle(state.src, pos, state.posMax); - if (pos < max && start !== pos && res.ok) { - title$1 = res.str; - pos = res.pos; - for (; pos < max; pos++) { - code$1 = state.src.charCodeAt(pos); - if (!isSpace(code$1) && code$1 !== 10) { - break; - } - } - } - } - if (pos >= max || state.src.charCodeAt(pos) !== 41) { - parseReference = true; - } - pos++; - } - if (parseReference) { - if (typeof state.env.references === "undefined") { - return false; - } - if (pos < max && state.src.charCodeAt(pos) === 91) { - start = pos + 1; - pos = state.md.helpers.parseLinkLabel(state, pos); - if (pos >= 0) { - label = state.src.slice(start, pos++); - } else { - pos = labelEnd + 1; - } - } else { - pos = labelEnd + 1; - } - if (!label) { - label = state.src.slice(labelStart, labelEnd); - } - ref = state.env.references[normalizeReference(label)]; - if (!ref) { - state.pos = oldPos; - return false; - } - href = ref.href; - title$1 = ref.title; - } - if (!silent) { - state.pos = labelStart; - state.posMax = labelEnd; - const token_o = state.push("link_open", "a", 1); - const attrs = [["href", href]]; - token_o.attrs = attrs; - if (title$1) { - attrs.push(["title", title$1]); - } - state.linkLevel++; - state.md.inline.tokenize(state); - state.linkLevel--; - state.push("link_close", "a", -1); - } - state.pos = pos; - state.posMax = max; - return true; -} - -function image(state, silent) { - let code$1, content, label, pos, ref, res, title$1, start; - let href = ""; - const oldPos = state.pos; - const max = state.posMax; - if (state.src.charCodeAt(state.pos) !== 33) { - return false; - } - if (state.src.charCodeAt(state.pos + 1) !== 91) { - return false; - } - const labelStart = state.pos + 2; - const labelEnd = state.md.helpers.parseLinkLabel(state, state.pos + 1, false); - if (labelEnd < 0) { - return false; - } - pos = labelEnd + 1; - if (pos < max && state.src.charCodeAt(pos) === 40) { - pos++; - for (; pos < max; pos++) { - code$1 = state.src.charCodeAt(pos); - if (!isSpace(code$1) && code$1 !== 10) { - break; - } - } - if (pos >= max) { - return false; - } - start = pos; - res = state.md.helpers.parseLinkDestination(state.src, pos, state.posMax); - if (res.ok) { - href = state.md.normalizeLink(res.str); - if (state.md.validateLink(href)) { - pos = res.pos; - } else { - href = ""; - } - } - start = pos; - for (; pos < max; pos++) { - code$1 = state.src.charCodeAt(pos); - if (!isSpace(code$1) && code$1 !== 10) { - break; - } - } - res = state.md.helpers.parseLinkTitle(state.src, pos, state.posMax); - if (pos < max && start !== pos && res.ok) { - title$1 = res.str; - pos = res.pos; - for (; pos < max; pos++) { - code$1 = state.src.charCodeAt(pos); - if (!isSpace(code$1) && code$1 !== 10) { - break; - } - } - } else { - title$1 = ""; - } - if (pos >= max || state.src.charCodeAt(pos) !== 41) { - state.pos = oldPos; - return false; - } - pos++; - } else { - if (typeof state.env.references === "undefined") { - return false; - } - if (pos < max && state.src.charCodeAt(pos) === 91) { - start = pos + 1; - pos = state.md.helpers.parseLinkLabel(state, pos); - if (pos >= 0) { - label = state.src.slice(start, pos++); - } else { - pos = labelEnd + 1; - } - } else { - pos = labelEnd + 1; - } - if (!label) { - label = state.src.slice(labelStart, labelEnd); - } - ref = state.env.references[normalizeReference(label)]; - if (!ref) { - state.pos = oldPos; - return false; - } - href = ref.href; - title$1 = ref.title; - } - if (!silent) { - content = state.src.slice(labelStart, labelEnd); - const tokens = []; - state.md.inline.parse(content, state.md, state.env, tokens); - const token = state.push("image", "img", 0); - const attrs = [["src", href], ["alt", ""]]; - token.attrs = attrs; - token.children = tokens; - token.content = content; - if (title$1) { - attrs.push(["title", title$1]); - } - } - state.pos = pos; - state.posMax = max; - return true; -} - -const EMAIL_RE = /^([a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)$/; -const AUTOLINK_RE = /^([a-zA-Z][a-zA-Z0-9+.-]{1,31}):([^<>\x00-\x20]*)$/; -function autolink(state, silent) { - let pos = state.pos; - if (state.src.charCodeAt(pos) !== 60) { - return false; - } - const start = state.pos; - const max = state.posMax; - for (;;) { - if (++pos >= max) return false; - const ch = state.src.charCodeAt(pos); - if (ch === 60) return false; - if (ch === 62) break; - } - const url = state.src.slice(start + 1, pos); - if (AUTOLINK_RE.test(url)) { - const fullUrl = state.md.normalizeLink(url); - if (!state.md.validateLink(fullUrl)) { - return false; - } - if (!silent) { - const token_o = state.push("link_open", "a", 1); - token_o.attrs = [["href", fullUrl]]; - token_o.markup = "autolink"; - token_o.info = "auto"; - const token_t = state.push("text", "", 0); - token_t.content = state.md.normalizeLinkText(url); - const token_c = state.push("link_close", "a", -1); - token_c.markup = "autolink"; - token_c.info = "auto"; - } - state.pos += url.length + 2; - return true; - } - if (EMAIL_RE.test(url)) { - const fullUrl = state.md.normalizeLink("mailto:" + url); - if (!state.md.validateLink(fullUrl)) { - return false; - } - if (!silent) { - const token_o = state.push("link_open", "a", 1); - token_o.attrs = [["href", fullUrl]]; - token_o.markup = "autolink"; - token_o.info = "auto"; - const token_t = state.push("text", "", 0); - token_t.content = state.md.normalizeLinkText(url); - const token_c = state.push("link_close", "a", -1); - token_c.markup = "autolink"; - token_c.info = "auto"; - } - state.pos += url.length + 2; - return true; - } - return false; -} - -function isLinkOpen(str) { - return /^\s]/i.test(str); -} -function isLinkClose(str) { - return /^<\/a\s*>/i.test(str); -} -function isLetter(ch) { - const lc = ch | 32; - return lc >= 97 && lc <= 122; -} -function html_inline(state, silent) { - if (!state.md.options.html) { - return false; - } - const max = state.posMax; - const pos = state.pos; - if (state.src.charCodeAt(pos) !== 60 || pos + 2 >= max) { - return false; - } - const ch = state.src.charCodeAt(pos + 1); - if (ch !== 33 && ch !== 63 && ch !== 47 && !isLetter(ch)) { - return false; - } - const match = state.src.slice(pos).match(HTML_TAG_RE); - if (!match) { - return false; - } - if (!silent) { - const token = state.push("html_inline", "", 0); - token.content = match[0]; - if (isLinkOpen(token.content)) state.linkLevel++; - if (isLinkClose(token.content)) state.linkLevel--; - } - state.pos += match[0].length; - return true; -} - -const DIGITAL_RE = /^&#((?:x[a-f0-9]{1,6}|[0-9]{1,7}));/i; -const NAMED_RE = /^&([a-z][a-z0-9]{1,31});/i; -function entity(state, silent) { - const pos = state.pos; - const max = state.posMax; - if (state.src.charCodeAt(pos) !== 38) return false; - if (pos + 1 >= max) return false; - const ch = state.src.charCodeAt(pos + 1); - if (ch === 35) { - const match = state.src.slice(pos).match(DIGITAL_RE); - if (match) { - if (!silent) { - const code$1 = match[1][0].toLowerCase() === "x" ? parseInt(match[1].slice(1), 16) : parseInt(match[1], 10); - const token = state.push("text_special", "", 0); - token.content = isValidEntityCode(code$1) ? fromCodePoint(code$1) : fromCodePoint(65533); - token.markup = match[0]; - token.info = "entity"; - } - state.pos += match[0].length; - return true; - } - } else { - const match = state.src.slice(pos).match(NAMED_RE); - if (match) { - const decoded = decodeHTML(match[0]); - if (decoded !== match[0]) { - if (!silent) { - const token = state.push("text_special", "", 0); - token.content = decoded; - token.markup = match[0]; - token.info = "entity"; - } - state.pos += match[0].length; - return true; - } - } - } - return false; -} - -function processDelimiters(delimiters) { - const openersBottom = {}; - const max = delimiters.length; - if (!max) return; - let headerIdx = 0; - let lastTokenIdx = -2; - const jumps = []; - for (let closerIdx = 0; closerIdx < max; closerIdx++) { - const closer = delimiters[closerIdx]; - jumps.push(0); - if (delimiters[headerIdx].marker !== closer.marker || lastTokenIdx !== closer.token - 1) { - headerIdx = closerIdx; - } - lastTokenIdx = closer.token; - closer.length = closer.length || 0; - if (!closer.close) continue; - if (!openersBottom.hasOwnProperty(closer.marker)) { - openersBottom[closer.marker] = [ - -1, - -1, - -1, - -1, - -1, - -1 - ]; - } - const minOpenerIdx = openersBottom[closer.marker][(closer.open ? 3 : 0) + closer.length % 3]; - let openerIdx = headerIdx - jumps[headerIdx] - 1; - let newMinOpenerIdx = openerIdx; - for (; openerIdx > minOpenerIdx; openerIdx -= jumps[openerIdx] + 1) { - const opener = delimiters[openerIdx]; - if (opener.marker !== closer.marker) continue; - if (opener.open && opener.end < 0) { - let isOddMatch = false; - if (opener.close || closer.open) { - if ((opener.length + closer.length) % 3 === 0) { - if (opener.length % 3 !== 0 || closer.length % 3 !== 0) { - isOddMatch = true; - } - } - } - if (!isOddMatch) { - const lastJump = openerIdx > 0 && !delimiters[openerIdx - 1].open ? jumps[openerIdx - 1] + 1 : 0; - jumps[closerIdx] = closerIdx - openerIdx + lastJump; - jumps[openerIdx] = lastJump; - closer.open = false; - opener.end = closerIdx; - opener.close = false; - newMinOpenerIdx = -1; - lastTokenIdx = -2; - break; - } - } - } - if (newMinOpenerIdx !== -1) { - openersBottom[closer.marker][(closer.open ? 3 : 0) + (closer.length || 0) % 3] = newMinOpenerIdx; - } - } -} -function link_pairs(state) { - const tokens_meta = state.tokens_meta; - const max = state.tokens_meta.length; - processDelimiters(state.delimiters); - for (let curr = 0; curr < max; curr++) { - if (tokens_meta[curr] && tokens_meta[curr].delimiters) { - processDelimiters(tokens_meta[curr].delimiters); - } - } -} - -function fragments_join(state) { - let curr, last; - let level = 0; - const tokens = state.tokens; - const max = state.tokens.length; - for (curr = last = 0; curr < max; curr++) { - if (tokens[curr].nesting < 0) level--; - tokens[curr].level = level; - if (tokens[curr].nesting > 0) level++; - if (tokens[curr].type === "text" && curr + 1 < max && tokens[curr + 1].type === "text") { - tokens[curr + 1].content = tokens[curr].content + tokens[curr + 1].content; - } else { - if (curr !== last) { - tokens[last] = tokens[curr]; - } - last++; - } - } - if (curr !== last) { - tokens.length = last; - } -} - -/** internal -* class ParserInline -* -* Tokenizes paragraph content. -**/ -const _rules = [ - ["text", text], - ["linkify", linkify], - ["newline", newline], - ["escape", escape], - ["backticks", backtick], - ["strikethrough", strikethrough_default.tokenize], - ["emphasis", emphasis_default.tokenize], - ["link", link], - ["image", image], - ["autolink", autolink], - ["html_inline", html_inline], - ["entity", entity] -]; -const _rules2 = [ - ["balance_pairs", link_pairs], - ["strikethrough", strikethrough_default.postProcess], - ["emphasis", emphasis_default.postProcess], - ["fragments_join", fragments_join] -]; -/** -* new ParserInline() -**/ -function ParserInline() { - /** - * ParserInline#ruler -> Ruler - * - * [[Ruler]] instance. Keep configuration of inline rules. - **/ - this.ruler = new ruler_default(); - for (let i$10 = 0; i$10 < _rules.length; i$10++) { - this.ruler.push(_rules[i$10][0], _rules[i$10][1]); - } - /** - * ParserInline#ruler2 -> Ruler - * - * [[Ruler]] instance. Second ruler used for post-processing - * (e.g. in emphasis-like rules). - **/ - this.ruler2 = new ruler_default(); - for (let i$10 = 0; i$10 < _rules2.length; i$10++) { - this.ruler2.push(_rules2[i$10][0], _rules2[i$10][1]); - } -} -ParserInline.prototype.skipToken = function(state) { - const pos = state.pos; - const rules = this.ruler.getRules(""); - const len = rules.length; - const maxNesting = state.md.options.maxNesting; - const cache = state.cache; - if (typeof cache[pos] !== "undefined") { - state.pos = cache[pos]; - return; - } - let ok = false; - if (state.level < maxNesting) { - for (let i$10 = 0; i$10 < len; i$10++) { - state.level++; - ok = rules[i$10](state, true); - state.level--; - if (ok) { - if (pos >= state.pos) { - throw new Error("inline rule didn't increment state.pos"); - } - break; - } - } - } else { - state.pos = state.posMax; - } - if (!ok) { - state.pos++; - } - cache[pos] = state.pos; -}; -ParserInline.prototype.tokenize = function(state) { - const rules = this.ruler.getRules(""); - const len = rules.length; - const end = state.posMax; - const maxNesting = state.md.options.maxNesting; - while (state.pos < end) { - const prevPos = state.pos; - let ok = false; - if (state.level < maxNesting) { - for (let i$10 = 0; i$10 < len; i$10++) { - ok = rules[i$10](state, false); - if (ok) { - if (prevPos >= state.pos) { - throw new Error("inline rule didn't increment state.pos"); - } - break; - } - } - } - if (ok) { - if (state.pos >= end) { - break; - } - continue; - } - state.pending += state.src[state.pos++]; - } - if (state.pending) { - state.pushPending(); - } -}; -/** -* ParserInline.parse(str, md, env, outTokens) -* -* Process input string and push inline tokens into `outTokens` -**/ -ParserInline.prototype.parse = function(str, md, env, outTokens) { - const state = new this.State(str, md, env, outTokens); - this.tokenize(state); - const rules = this.ruler2.getRules(""); - const len = rules.length; - for (let i$10 = 0; i$10 < len; i$10++) { - rules[i$10](state); - } -}; -ParserInline.prototype.State = state_inline_default; -var parser_inline_default = ParserInline; - -function re_default(opts) { - const re = {}; - opts = opts || {}; - re.src_Any = regex_default$5.source; - re.src_Cc = regex_default$4.source; - re.src_Z = regex_default.source; - re.src_P = regex_default$2.source; - re.src_ZPCc = [ - re.src_Z, - re.src_P, - re.src_Cc - ].join("|"); - re.src_ZCc = [re.src_Z, re.src_Cc].join("|"); - const text_separators = "[><|]"; - re.src_pseudo_letter = "(?:(?!" + text_separators + "|" + re.src_ZPCc + ")" + re.src_Any + ")"; - re.src_ip4 = "(?:(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"; - re.src_auth = "(?:(?:(?!" + re.src_ZCc + "|[@/\\[\\]()]).)+@)?"; - re.src_port = "(?::(?:6(?:[0-4]\\d{3}|5(?:[0-4]\\d{2}|5(?:[0-2]\\d|3[0-5])))|[1-5]?\\d{1,4}))?"; - re.src_host_terminator = "(?=$|" + text_separators + "|" + re.src_ZPCc + ")" + "(?!" + (opts["---"] ? "-(?!--)|" : "-|") + "_|:\\d|\\.-|\\.(?!$|" + re.src_ZPCc + "))"; - re.src_path = "(?:" + "[/?#]" + "(?:" + "(?!" + re.src_ZCc + "|" + text_separators + "|[()[\\]{}.,\"'?!\\-;]).|" + "\\[(?:(?!" + re.src_ZCc + "|\\]).)*\\]|" + "\\((?:(?!" + re.src_ZCc + "|[)]).)*\\)|" + "\\{(?:(?!" + re.src_ZCc + "|[}]).)*\\}|" + "\\\"(?:(?!" + re.src_ZCc + "|[\"]).)+\\\"|" + "\\'(?:(?!" + re.src_ZCc + "|[']).)+\\'|" + "\\'(?=" + re.src_pseudo_letter + "|[-])|" + "\\.{2,}[a-zA-Z0-9%/&]|" + "\\.(?!" + re.src_ZCc + "|[.]|$)|" + (opts["---"] ? "\\-(?!--(?:[^-]|$))(?:-*)|" : "\\-+|") + ",(?!" + re.src_ZCc + "|$)|" + ";(?!" + re.src_ZCc + "|$)|" + "\\!+(?!" + re.src_ZCc + "|[!]|$)|" + "\\?(?!" + re.src_ZCc + "|[?]|$)" + ")+" + "|\\/" + ")?"; - re.src_email_name = "[\\-;:&=\\+\\$,\\.a-zA-Z0-9_][\\-;:&=\\+\\$,\\\"\\.a-zA-Z0-9_]*"; - re.src_xn = "xn--[a-z0-9\\-]{1,59}"; - re.src_domain_root = "(?:" + re.src_xn + "|" + re.src_pseudo_letter + "{1,63}" + ")"; - re.src_domain = "(?:" + re.src_xn + "|" + "(?:" + re.src_pseudo_letter + ")" + "|" + "(?:" + re.src_pseudo_letter + "(?:-|" + re.src_pseudo_letter + "){0,61}" + re.src_pseudo_letter + ")" + ")"; - re.src_host = "(?:" + "(?:(?:(?:" + re.src_domain + ")\\.)*" + re.src_domain + ")" + ")"; - re.tpl_host_fuzzy = "(?:" + re.src_ip4 + "|" + "(?:(?:(?:" + re.src_domain + ")\\.)+(?:%TLDS%))" + ")"; - re.tpl_host_no_ip_fuzzy = "(?:(?:(?:" + re.src_domain + ")\\.)+(?:%TLDS%))"; - re.src_host_strict = re.src_host + re.src_host_terminator; - re.tpl_host_fuzzy_strict = re.tpl_host_fuzzy + re.src_host_terminator; - re.src_host_port_strict = re.src_host + re.src_port + re.src_host_terminator; - re.tpl_host_port_fuzzy_strict = re.tpl_host_fuzzy + re.src_port + re.src_host_terminator; - re.tpl_host_port_no_ip_fuzzy_strict = re.tpl_host_no_ip_fuzzy + re.src_port + re.src_host_terminator; - re.tpl_host_fuzzy_test = "localhost|www\\.|\\.\\d{1,3}\\.|(?:\\.(?:%TLDS%)(?:" + re.src_ZPCc + "|>|$))"; - re.tpl_email_fuzzy = "(^|" + text_separators + "|\"|\\(|" + re.src_ZCc + ")" + "(" + re.src_email_name + "@" + re.tpl_host_fuzzy_strict + ")"; - re.tpl_link_fuzzy = "(^|(?![.:/\\-_@])(?:[$+<=>^`||]|" + re.src_ZPCc + "))" + "((?![$+<=>^`||])" + re.tpl_host_port_fuzzy_strict + re.src_path + ")"; - re.tpl_link_no_ip_fuzzy = "(^|(?![.:/\\-_@])(?:[$+<=>^`||]|" + re.src_ZPCc + "))" + "((?![$+<=>^`||])" + re.tpl_host_port_no_ip_fuzzy_strict + re.src_path + ")"; - return re; -} - -function assign(obj) { - const sources = Array.prototype.slice.call(arguments, 1); - sources.forEach(function(source) { - if (!source) { - return; - } - Object.keys(source).forEach(function(key) { - obj[key] = source[key]; - }); - }); - return obj; -} -function _class(obj) { - return Object.prototype.toString.call(obj); -} -function isString(obj) { - return _class(obj) === "[object String]"; -} -function isObject(obj) { - return _class(obj) === "[object Object]"; -} -function isRegExp(obj) { - return _class(obj) === "[object RegExp]"; -} -function isFunction(obj) { - return _class(obj) === "[object Function]"; -} -function escapeRE(str) { - return str.replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&"); -} -const defaultOptions = { - fuzzyLink: true, - fuzzyEmail: true, - fuzzyIP: false -}; -function isOptionsObj(obj) { - return Object.keys(obj || {}).reduce(function(acc, k$1) { - return acc || defaultOptions.hasOwnProperty(k$1); - }, false); -} -const defaultSchemas = { - "http:": { validate: function(text$1, pos, self) { - const tail = text$1.slice(pos); - if (!self.re.http) { - self.re.http = new RegExp("^\\/\\/" + self.re.src_auth + self.re.src_host_port_strict + self.re.src_path, "i"); - } - if (self.re.http.test(tail)) { - return tail.match(self.re.http)[0].length; - } - return 0; - } }, - "https:": "http:", - "ftp:": "http:", - "//": { validate: function(text$1, pos, self) { - const tail = text$1.slice(pos); - if (!self.re.no_http) { - self.re.no_http = new RegExp("^" + self.re.src_auth + "(?:localhost|(?:(?:" + self.re.src_domain + ")\\.)+" + self.re.src_domain_root + ")" + self.re.src_port + self.re.src_host_terminator + self.re.src_path, "i"); - } - if (self.re.no_http.test(tail)) { - if (pos >= 3 && text$1[pos - 3] === ":") { - return 0; - } - if (pos >= 3 && text$1[pos - 3] === "/") { - return 0; - } - return tail.match(self.re.no_http)[0].length; - } - return 0; - } }, - "mailto:": { validate: function(text$1, pos, self) { - const tail = text$1.slice(pos); - if (!self.re.mailto) { - self.re.mailto = new RegExp("^" + self.re.src_email_name + "@" + self.re.src_host_strict, "i"); - } - if (self.re.mailto.test(tail)) { - return tail.match(self.re.mailto)[0].length; - } - return 0; - } } -}; -const tlds_2ch_src_re = "a[cdefgilmnoqrstuwxz]|b[abdefghijmnorstvwyz]|c[acdfghiklmnoruvwxyz]|d[ejkmoz]|e[cegrstu]|f[ijkmor]|g[abdefghilmnpqrstuwy]|h[kmnrtu]|i[delmnoqrst]|j[emop]|k[eghimnprwyz]|l[abcikrstuvy]|m[acdeghklmnopqrstuvwxyz]|n[acefgilopruz]|om|p[aefghklmnrstwy]|qa|r[eosuw]|s[abcdeghijklmnortuvxyz]|t[cdfghjklmnortvwz]|u[agksyz]|v[aceginu]|w[fs]|y[et]|z[amw]"; -const tlds_default = "biz|com|edu|gov|net|org|pro|web|xxx|aero|asia|coop|info|museum|name|shop|рф".split("|"); -function resetScanCache(self) { - self.__index__ = -1; - self.__text_cache__ = ""; -} -function createValidator(re) { - return function(text$1, pos) { - const tail = text$1.slice(pos); - if (re.test(tail)) { - return tail.match(re)[0].length; - } - return 0; - }; -} -function createNormalizer() { - return function(match, self) { - self.normalize(match); - }; -} -function compile(self) { - const re = self.re = re_default(self.__opts__); - const tlds = self.__tlds__.slice(); - self.onCompile(); - if (!self.__tlds_replaced__) { - tlds.push(tlds_2ch_src_re); - } - tlds.push(re.src_xn); - re.src_tlds = tlds.join("|"); - function untpl(tpl) { - return tpl.replace("%TLDS%", re.src_tlds); - } - re.email_fuzzy = RegExp(untpl(re.tpl_email_fuzzy), "i"); - re.link_fuzzy = RegExp(untpl(re.tpl_link_fuzzy), "i"); - re.link_no_ip_fuzzy = RegExp(untpl(re.tpl_link_no_ip_fuzzy), "i"); - re.host_fuzzy_test = RegExp(untpl(re.tpl_host_fuzzy_test), "i"); - const aliases = []; - self.__compiled__ = {}; - function schemaError(name, val) { - throw new Error("(LinkifyIt) Invalid schema \"" + name + "\": " + val); - } - Object.keys(self.__schemas__).forEach(function(name) { - const val = self.__schemas__[name]; - if (val === null) { - return; - } - const compiled = { - validate: null, - link: null - }; - self.__compiled__[name] = compiled; - if (isObject(val)) { - if (isRegExp(val.validate)) { - compiled.validate = createValidator(val.validate); - } else if (isFunction(val.validate)) { - compiled.validate = val.validate; - } else { - schemaError(name, val); - } - if (isFunction(val.normalize)) { - compiled.normalize = val.normalize; - } else if (!val.normalize) { - compiled.normalize = createNormalizer(); - } else { - schemaError(name, val); - } - return; - } - if (isString(val)) { - aliases.push(name); - return; - } - schemaError(name, val); - }); - aliases.forEach(function(alias) { - if (!self.__compiled__[self.__schemas__[alias]]) { - return; - } - self.__compiled__[alias].validate = self.__compiled__[self.__schemas__[alias]].validate; - self.__compiled__[alias].normalize = self.__compiled__[self.__schemas__[alias]].normalize; - }); - self.__compiled__[""] = { - validate: null, - normalize: createNormalizer() - }; - const slist = Object.keys(self.__compiled__).filter(function(name) { - return name.length > 0 && self.__compiled__[name]; - }).map(escapeRE).join("|"); - self.re.schema_test = RegExp("(^|(?!_)(?:[><|]|" + re.src_ZPCc + "))(" + slist + ")", "i"); - self.re.schema_search = RegExp("(^|(?!_)(?:[><|]|" + re.src_ZPCc + "))(" + slist + ")", "ig"); - self.re.schema_at_start = RegExp("^" + self.re.schema_search.source, "i"); - self.re.pretest = RegExp("(" + self.re.schema_test.source + ")|(" + self.re.host_fuzzy_test.source + ")|@", "i"); - resetScanCache(self); -} -/** -* class Match -* -* Match result. Single element of array, returned by [[LinkifyIt#match]] -**/ -function Match(self, shift) { - const start = self.__index__; - const end = self.__last_index__; - const text$1 = self.__text_cache__.slice(start, end); - /** - * Match#schema -> String - * - * Prefix (protocol) for matched string. - **/ - this.schema = self.__schema__.toLowerCase(); - /** - * Match#index -> Number - * - * First position of matched string. - **/ - this.index = start + shift; - /** - * Match#lastIndex -> Number - * - * Next position after matched string. - **/ - this.lastIndex = end + shift; - /** - * Match#raw -> String - * - * Matched string. - **/ - this.raw = text$1; - /** - * Match#text -> String - * - * Notmalized text of matched string. - **/ - this.text = text$1; - /** - * Match#url -> String - * - * Normalized url of matched string. - **/ - this.url = text$1; -} -function createMatch(self, shift) { - const match = new Match(self, shift); - self.__compiled__[match.schema].normalize(match, self); - return match; -} -/** -* class LinkifyIt -**/ -/** -* new LinkifyIt(schemas, options) -* - schemas (Object): Optional. Additional schemas to validate (prefix/validator) -* - options (Object): { fuzzyLink|fuzzyEmail|fuzzyIP: true|false } -* -* Creates new linkifier instance with optional additional schemas. -* Can be called without `new` keyword for convenience. -* -* By default understands: -* -* - `http(s)://...` , `ftp://...`, `mailto:...` & `//...` links -* - "fuzzy" links and emails (example.com, foo@bar.com). -* -* `schemas` is an object, where each key/value describes protocol/rule: -* -* - __key__ - link prefix (usually, protocol name with `:` at the end, `skype:` -* for example). `linkify-it` makes shure that prefix is not preceeded with -* alphanumeric char and symbols. Only whitespaces and punctuation allowed. -* - __value__ - rule to check tail after link prefix -* - _String_ - just alias to existing rule -* - _Object_ -* - _validate_ - validator function (should return matched length on success), -* or `RegExp`. -* - _normalize_ - optional function to normalize text & url of matched result -* (for example, for @twitter mentions). -* -* `options`: -* -* - __fuzzyLink__ - recognige URL-s without `http(s):` prefix. Default `true`. -* - __fuzzyIP__ - allow IPs in fuzzy links above. Can conflict with some texts -* like version numbers. Default `false`. -* - __fuzzyEmail__ - recognize emails without `mailto:` prefix. -* -**/ -function LinkifyIt(schemas, options) { - if (!(this instanceof LinkifyIt)) { - return new LinkifyIt(schemas, options); - } - if (!options) { - if (isOptionsObj(schemas)) { - options = schemas; - schemas = {}; - } - } - this.__opts__ = assign({}, defaultOptions, options); - this.__index__ = -1; - this.__last_index__ = -1; - this.__schema__ = ""; - this.__text_cache__ = ""; - this.__schemas__ = assign({}, defaultSchemas, schemas); - this.__compiled__ = {}; - this.__tlds__ = tlds_default; - this.__tlds_replaced__ = false; - this.re = {}; - compile(this); -} -/** chainable -* LinkifyIt#add(schema, definition) -* - schema (String): rule name (fixed pattern prefix) -* - definition (String|RegExp|Object): schema definition -* -* Add new rule definition. See constructor description for details. -**/ -LinkifyIt.prototype.add = function add(schema, definition) { - this.__schemas__[schema] = definition; - compile(this); - return this; -}; -/** chainable -* LinkifyIt#set(options) -* - options (Object): { fuzzyLink|fuzzyEmail|fuzzyIP: true|false } -* -* Set recognition options for links without schema. -**/ -LinkifyIt.prototype.set = function set(options) { - this.__opts__ = assign(this.__opts__, options); - return this; -}; -/** -* LinkifyIt#test(text) -> Boolean -* -* Searches linkifiable pattern and returns `true` on success or `false` on fail. -**/ -LinkifyIt.prototype.test = function test(text$1) { - this.__text_cache__ = text$1; - this.__index__ = -1; - if (!text$1.length) { - return false; - } - let m$3, ml, me, len, shift, next, re, tld_pos, at_pos; - if (this.re.schema_test.test(text$1)) { - re = this.re.schema_search; - re.lastIndex = 0; - while ((m$3 = re.exec(text$1)) !== null) { - len = this.testSchemaAt(text$1, m$3[2], re.lastIndex); - if (len) { - this.__schema__ = m$3[2]; - this.__index__ = m$3.index + m$3[1].length; - this.__last_index__ = m$3.index + m$3[0].length + len; - break; - } - } - } - if (this.__opts__.fuzzyLink && this.__compiled__["http:"]) { - tld_pos = text$1.search(this.re.host_fuzzy_test); - if (tld_pos >= 0) { - if (this.__index__ < 0 || tld_pos < this.__index__) { - if ((ml = text$1.match(this.__opts__.fuzzyIP ? this.re.link_fuzzy : this.re.link_no_ip_fuzzy)) !== null) { - shift = ml.index + ml[1].length; - if (this.__index__ < 0 || shift < this.__index__) { - this.__schema__ = ""; - this.__index__ = shift; - this.__last_index__ = ml.index + ml[0].length; - } - } - } - } - } - if (this.__opts__.fuzzyEmail && this.__compiled__["mailto:"]) { - at_pos = text$1.indexOf("@"); - if (at_pos >= 0) { - if ((me = text$1.match(this.re.email_fuzzy)) !== null) { - shift = me.index + me[1].length; - next = me.index + me[0].length; - if (this.__index__ < 0 || shift < this.__index__ || shift === this.__index__ && next > this.__last_index__) { - this.__schema__ = "mailto:"; - this.__index__ = shift; - this.__last_index__ = next; - } - } - } - } - return this.__index__ >= 0; -}; -/** -* LinkifyIt#pretest(text) -> Boolean -* -* Very quick check, that can give false positives. Returns true if link MAY BE -* can exists. Can be used for speed optimization, when you need to check that -* link NOT exists. -**/ -LinkifyIt.prototype.pretest = function pretest(text$1) { - return this.re.pretest.test(text$1); -}; -/** -* LinkifyIt#testSchemaAt(text, name, position) -> Number -* - text (String): text to scan -* - name (String): rule (schema) name -* - position (Number): text offset to check from -* -* Similar to [[LinkifyIt#test]] but checks only specific protocol tail exactly -* at given position. Returns length of found pattern (0 on fail). -**/ -LinkifyIt.prototype.testSchemaAt = function testSchemaAt(text$1, schema, pos) { - if (!this.__compiled__[schema.toLowerCase()]) { - return 0; - } - return this.__compiled__[schema.toLowerCase()].validate(text$1, pos, this); -}; -/** -* LinkifyIt#match(text) -> Array|null -* -* Returns array of found link descriptions or `null` on fail. We strongly -* recommend to use [[LinkifyIt#test]] first, for best speed. -* -* ##### Result match description -* -* - __schema__ - link schema, can be empty for fuzzy links, or `//` for -* protocol-neutral links. -* - __index__ - offset of matched text -* - __lastIndex__ - index of next char after mathch end -* - __raw__ - matched text -* - __text__ - normalized text -* - __url__ - link, generated from matched text -**/ -LinkifyIt.prototype.match = function match(text$1) { - const result = []; - let shift = 0; - if (this.__index__ >= 0 && this.__text_cache__ === text$1) { - result.push(createMatch(this, shift)); - shift = this.__last_index__; - } - let tail = shift ? text$1.slice(shift) : text$1; - while (this.test(tail)) { - result.push(createMatch(this, shift)); - tail = tail.slice(this.__last_index__); - shift += this.__last_index__; - } - if (result.length) { - return result; - } - return null; -}; -/** -* LinkifyIt#matchAtStart(text) -> Match|null -* -* Returns fully-formed (not fuzzy) link if it starts at the beginning -* of the string, and null otherwise. -**/ -LinkifyIt.prototype.matchAtStart = function matchAtStart(text$1) { - this.__text_cache__ = text$1; - this.__index__ = -1; - if (!text$1.length) return null; - const m$3 = this.re.schema_at_start.exec(text$1); - if (!m$3) return null; - const len = this.testSchemaAt(text$1, m$3[2], m$3[0].length); - if (!len) return null; - this.__schema__ = m$3[2]; - this.__index__ = m$3.index + m$3[1].length; - this.__last_index__ = m$3.index + m$3[0].length + len; - return createMatch(this, 0); -}; -/** chainable -* LinkifyIt#tlds(list [, keepOld]) -> this -* - list (Array): list of tlds -* - keepOld (Boolean): merge with current list if `true` (`false` by default) -* -* Load (or merge) new tlds list. Those are user for fuzzy links (without prefix) -* to avoid false positives. By default this algorythm used: -* -* - hostname with any 2-letter root zones are ok. -* - biz|com|edu|gov|net|org|pro|web|xxx|aero|asia|coop|info|museum|name|shop|рф -* are ok. -* - encoded (`xn--...`) root zones are ok. -* -* If list is replaced, then exact match for 2-chars root zones will be checked. -**/ -LinkifyIt.prototype.tlds = function tlds(list$1, keepOld) { - list$1 = Array.isArray(list$1) ? list$1 : [list$1]; - if (!keepOld) { - this.__tlds__ = list$1.slice(); - this.__tlds_replaced__ = true; - compile(this); - return this; - } - this.__tlds__ = this.__tlds__.concat(list$1).sort().filter(function(el, idx, arr) { - return el !== arr[idx - 1]; - }).reverse(); - compile(this); - return this; -}; -/** -* LinkifyIt#normalize(match) -* -* Default normalizer (if schema does not define it's own). -**/ -LinkifyIt.prototype.normalize = function normalize$1(match) { - if (!match.schema) { - match.url = "http://" + match.url; - } - if (match.schema === "mailto:" && !/^mailto:/i.test(match.url)) { - match.url = "mailto:" + match.url; - } -}; -/** -* LinkifyIt#onCompile() -* -* Override to modify basic RegExp-s. -**/ -LinkifyIt.prototype.onCompile = function onCompile() {}; -var linkify_it_default = LinkifyIt; - -/** Highest positive signed 32-bit float value */ -const maxInt = 2147483647; -/** Bootstring parameters */ -const base = 36; -const tMin = 1; -const tMax = 26; -const skew = 38; -const damp = 700; -const initialBias = 72; -const initialN = 128; -const delimiter = "-"; -/** Regular expressions */ -const regexPunycode = /^xn--/; -const regexNonASCII = /[^\0-\x7F]/; -const regexSeparators = /[\x2E\u3002\uFF0E\uFF61]/g; -/** Error messages */ -const errors = { - "overflow": "Overflow: input needs wider integers to process", - "not-basic": "Illegal input >= 0x80 (not a basic code point)", - "invalid-input": "Invalid input" -}; -/** Convenience shortcuts */ -const baseMinusTMin = base - tMin; -const floor = Math.floor; -const stringFromCharCode = String.fromCharCode; -/** -* A generic error utility function. -* @private -* @param {String} type The error type. -* @returns {Error} Throws a `RangeError` with the applicable error message. -*/ -function error(type$2) { - throw new RangeError(errors[type$2]); -} -/** -* A generic `Array#map` utility function. -* @private -* @param {Array} array The array to iterate over. -* @param {Function} callback The function that gets called for every array -* item. -* @returns {Array} A new array of values returned by the callback function. -*/ -function map(array, callback) { - const result = []; - let length = array.length; - while (length--) { - result[length] = callback(array[length]); - } - return result; -} -/** -* A simple `Array#map`-like wrapper to work with domain name strings or email -* addresses. -* @private -* @param {String} domain The domain name or email address. -* @param {Function} callback The function that gets called for every -* character. -* @returns {String} A new string of characters returned by the callback -* function. -*/ -function mapDomain(domain, callback) { - const parts = domain.split("@"); - let result = ""; - if (parts.length > 1) { - result = parts[0] + "@"; - domain = parts[1]; - } - domain = domain.replace(regexSeparators, "."); - const labels = domain.split("."); - const encoded = map(labels, callback).join("."); - return result + encoded; -} -/** -* Creates an array containing the numeric code points of each Unicode -* character in the string. While JavaScript uses UCS-2 internally, -* this function will convert a pair of surrogate halves (each of which -* UCS-2 exposes as separate characters) into a single code point, -* matching UTF-16. -* @see `punycode.ucs2.encode` -* @see -* @memberOf punycode.ucs2 -* @name decode -* @param {String} string The Unicode input string (UCS-2). -* @returns {Array} The new array of code points. -*/ -function ucs2decode(string) { - const output = []; - let counter = 0; - const length = string.length; - while (counter < length) { - const value = string.charCodeAt(counter++); - if (value >= 55296 && value <= 56319 && counter < length) { - const extra = string.charCodeAt(counter++); - if ((extra & 64512) == 56320) { - output.push(((value & 1023) << 10) + (extra & 1023) + 65536); - } else { - output.push(value); - counter--; - } - } else { - output.push(value); - } - } - return output; -} -/** -* Creates a string based on an array of numeric code points. -* @see `punycode.ucs2.decode` -* @memberOf punycode.ucs2 -* @name encode -* @param {Array} codePoints The array of numeric code points. -* @returns {String} The new Unicode string (UCS-2). -*/ -const ucs2encode = (codePoints) => String.fromCodePoint(...codePoints); -/** -* Converts a basic code point into a digit/integer. -* @see `digitToBasic()` -* @private -* @param {Number} codePoint The basic numeric code point value. -* @returns {Number} The numeric value of a basic code point (for use in -* representing integers) in the range `0` to `base - 1`, or `base` if -* the code point does not represent a value. -*/ -const basicToDigit = function(codePoint) { - if (codePoint >= 48 && codePoint < 58) { - return 26 + (codePoint - 48); - } - if (codePoint >= 65 && codePoint < 91) { - return codePoint - 65; - } - if (codePoint >= 97 && codePoint < 123) { - return codePoint - 97; - } - return base; -}; -/** -* Converts a digit/integer into a basic code point. -* @see `basicToDigit()` -* @private -* @param {Number} digit The numeric value of a basic code point. -* @returns {Number} The basic code point whose value (when used for -* representing integers) is `digit`, which needs to be in the range -* `0` to `base - 1`. If `flag` is non-zero, the uppercase form is -* used; else, the lowercase form is used. The behavior is undefined -* if `flag` is non-zero and `digit` has no uppercase form. -*/ -const digitToBasic = function(digit, flag) { - return digit + 22 + 75 * (digit < 26) - ((flag != 0) << 5); -}; -/** -* Bias adaptation function as per section 3.4 of RFC 3492. -* https://tools.ietf.org/html/rfc3492#section-3.4 -* @private -*/ -const adapt = function(delta, numPoints, firstTime) { - let k$1 = 0; - delta = firstTime ? floor(delta / damp) : delta >> 1; - delta += floor(delta / numPoints); - for (; delta > baseMinusTMin * tMax >> 1; k$1 += base) { - delta = floor(delta / baseMinusTMin); - } - return floor(k$1 + (baseMinusTMin + 1) * delta / (delta + skew)); -}; -/** -* Converts a Punycode string of ASCII-only symbols to a string of Unicode -* symbols. -* @memberOf punycode -* @param {String} input The Punycode string of ASCII-only symbols. -* @returns {String} The resulting string of Unicode symbols. -*/ -const decode = function(input) { - const output = []; - const inputLength = input.length; - let i$10 = 0; - let n$13 = initialN; - let bias = initialBias; - let basic = input.lastIndexOf(delimiter); - if (basic < 0) { - basic = 0; - } - for (let j$2 = 0; j$2 < basic; ++j$2) { - if (input.charCodeAt(j$2) >= 128) { - error("not-basic"); - } - output.push(input.charCodeAt(j$2)); - } - for (let index = basic > 0 ? basic + 1 : 0; index < inputLength;) { - const oldi = i$10; - for (let w$1 = 1, k$1 = base;; k$1 += base) { - if (index >= inputLength) { - error("invalid-input"); - } - const digit = basicToDigit(input.charCodeAt(index++)); - if (digit >= base) { - error("invalid-input"); - } - if (digit > floor((maxInt - i$10) / w$1)) { - error("overflow"); - } - i$10 += digit * w$1; - const t$7 = k$1 <= bias ? tMin : k$1 >= bias + tMax ? tMax : k$1 - bias; - if (digit < t$7) { - break; - } - const baseMinusT = base - t$7; - if (w$1 > floor(maxInt / baseMinusT)) { - error("overflow"); - } - w$1 *= baseMinusT; - } - const out = output.length + 1; - bias = adapt(i$10 - oldi, out, oldi == 0); - if (floor(i$10 / out) > maxInt - n$13) { - error("overflow"); - } - n$13 += floor(i$10 / out); - i$10 %= out; - output.splice(i$10++, 0, n$13); - } - return String.fromCodePoint(...output); -}; -/** -* Converts a string of Unicode symbols (e.g. a domain name label) to a -* Punycode string of ASCII-only symbols. -* @memberOf punycode -* @param {String} input The string of Unicode symbols. -* @returns {String} The resulting Punycode string of ASCII-only symbols. -*/ -const encode = function(input) { - const output = []; - input = ucs2decode(input); - const inputLength = input.length; - let n$13 = initialN; - let delta = 0; - let bias = initialBias; - for (const currentValue of input) { - if (currentValue < 128) { - output.push(stringFromCharCode(currentValue)); - } - } - const basicLength = output.length; - let handledCPCount = basicLength; - if (basicLength) { - output.push(delimiter); - } - while (handledCPCount < inputLength) { - let m$3 = maxInt; - for (const currentValue of input) { - if (currentValue >= n$13 && currentValue < m$3) { - m$3 = currentValue; - } - } - const handledCPCountPlusOne = handledCPCount + 1; - if (m$3 - n$13 > floor((maxInt - delta) / handledCPCountPlusOne)) { - error("overflow"); - } - delta += (m$3 - n$13) * handledCPCountPlusOne; - n$13 = m$3; - for (const currentValue of input) { - if (currentValue < n$13 && ++delta > maxInt) { - error("overflow"); - } - if (currentValue === n$13) { - let q = delta; - for (let k$1 = base;; k$1 += base) { - const t$7 = k$1 <= bias ? tMin : k$1 >= bias + tMax ? tMax : k$1 - bias; - if (q < t$7) { - break; - } - const qMinusT = q - t$7; - const baseMinusT = base - t$7; - output.push(stringFromCharCode(digitToBasic(t$7 + qMinusT % baseMinusT, 0))); - q = floor(qMinusT / baseMinusT); - } - output.push(stringFromCharCode(digitToBasic(q, 0))); - bias = adapt(delta, handledCPCountPlusOne, handledCPCount === basicLength); - delta = 0; - ++handledCPCount; - } - } - ++delta; - ++n$13; - } - return output.join(""); -}; -/** -* Converts a Punycode string representing a domain name or an email address -* to Unicode. Only the Punycoded parts of the input will be converted, i.e. -* it doesn't matter if you call it on a string that has already been -* converted to Unicode. -* @memberOf punycode -* @param {String} input The Punycoded domain name or email address to -* convert to Unicode. -* @returns {String} The Unicode representation of the given Punycode -* string. -*/ -const toUnicode = function(input) { - return mapDomain(input, function(string) { - return regexPunycode.test(string) ? decode(string.slice(4).toLowerCase()) : string; - }); -}; -/** -* Converts a Unicode string representing a domain name or an email address to -* Punycode. Only the non-ASCII parts of the domain name will be converted, -* i.e. it doesn't matter if you call it with a domain that's already in -* ASCII. -* @memberOf punycode -* @param {String} input The domain name or email address to convert, as a -* Unicode string. -* @returns {String} The Punycode representation of the given domain name or -* email address. -*/ -const toASCII = function(input) { - return mapDomain(input, function(string) { - return regexNonASCII.test(string) ? "xn--" + encode(string) : string; - }); -}; -/** Define the public API */ -const punycode = { - "version": "2.3.1", - "ucs2": { - "decode": ucs2decode, - "encode": ucs2encode - }, - "decode": decode, - "encode": encode, - "toASCII": toASCII, - "toUnicode": toUnicode -}; -var punycode_es6_default = punycode; - -var default_default = { - options: { - html: false, - xhtmlOut: false, - breaks: false, - langPrefix: "language-", - linkify: false, - typographer: false, - quotes: "“”‘’", - highlight: null, - maxNesting: 100 - }, - components: { - core: {}, - block: {}, - inline: {} - } -}; - -var zero_default = { - options: { - html: false, - xhtmlOut: false, - breaks: false, - langPrefix: "language-", - linkify: false, - typographer: false, - quotes: "“”‘’", - highlight: null, - maxNesting: 20 - }, - components: { - core: { rules: [ - "normalize", - "block", - "inline", - "text_join" - ] }, - block: { rules: ["paragraph"] }, - inline: { - rules: ["text"], - rules2: ["balance_pairs", "fragments_join"] - } - } -}; - -var commonmark_default = { - options: { - html: true, - xhtmlOut: true, - breaks: false, - langPrefix: "language-", - linkify: false, - typographer: false, - quotes: "“”‘’", - highlight: null, - maxNesting: 20 - }, - components: { - core: { rules: [ - "normalize", - "block", - "inline", - "text_join" - ] }, - block: { rules: [ - "blockquote", - "code", - "fence", - "heading", - "hr", - "html_block", - "lheading", - "list", - "reference", - "paragraph" - ] }, - inline: { - rules: [ - "autolink", - "backticks", - "emphasis", - "entity", - "escape", - "html_inline", - "image", - "link", - "newline", - "text" - ], - rules2: [ - "balance_pairs", - "emphasis", - "fragments_join" - ] - } - } -}; - -const config = { - default: default_default, - zero: zero_default, - commonmark: commonmark_default -}; -const BAD_PROTO_RE = /^(vbscript|javascript|file|data):/; -const GOOD_DATA_RE = /^data:image\/(gif|png|jpeg|webp);/; -function validateLink(url) { - const str = url.trim().toLowerCase(); - return BAD_PROTO_RE.test(str) ? GOOD_DATA_RE.test(str) : true; -} -const RECODE_HOSTNAME_FOR = [ - "http:", - "https:", - "mailto:" -]; -function normalizeLink(url) { - const parsed = parse_default(url, true); - if (parsed.hostname) { - if (!parsed.protocol || RECODE_HOSTNAME_FOR.indexOf(parsed.protocol) >= 0) { - try { - parsed.hostname = punycode_es6_default.toASCII(parsed.hostname); - } catch (er) {} - } - } - return encode_default(format(parsed)); -} -function normalizeLinkText(url) { - const parsed = parse_default(url, true); - if (parsed.hostname) { - if (!parsed.protocol || RECODE_HOSTNAME_FOR.indexOf(parsed.protocol) >= 0) { - try { - parsed.hostname = punycode_es6_default.toUnicode(parsed.hostname); - } catch (er) {} - } - } - return decode_default(format(parsed), decode_default.defaultChars + "%"); -} -/** -* class MarkdownIt -* -* Main parser/renderer class. -* -* ##### Usage -* -* ```javascript -* // node.js, "classic" way: -* var MarkdownIt = require('markdown-it'), -* md = new MarkdownIt(); -* var result = md.render('# markdown-it rulezz!'); -* -* // node.js, the same, but with sugar: -* var md = require('markdown-it')(); -* var result = md.render('# markdown-it rulezz!'); -* -* // browser without AMD, added to "window" on script load -* // Note, there are no dash. -* var md = window.markdownit(); -* var result = md.render('# markdown-it rulezz!'); -* ``` -* -* Single line rendering, without paragraph wrap: -* -* ```javascript -* var md = require('markdown-it')(); -* var result = md.renderInline('__markdown-it__ rulezz!'); -* ``` -**/ -/** -* new MarkdownIt([presetName, options]) -* - presetName (String): optional, `commonmark` / `zero` -* - options (Object) -* -* Creates parser instanse with given config. Can be called without `new`. -* -* ##### presetName -* -* MarkdownIt provides named presets as a convenience to quickly -* enable/disable active syntax rules and options for common use cases. -* -* - ["commonmark"](https://github.com/markdown-it/markdown-it/blob/master/lib/presets/commonmark.mjs) - -* configures parser to strict [CommonMark](http://commonmark.org/) mode. -* - [default](https://github.com/markdown-it/markdown-it/blob/master/lib/presets/default.mjs) - -* similar to GFM, used when no preset name given. Enables all available rules, -* but still without html, typographer & autolinker. -* - ["zero"](https://github.com/markdown-it/markdown-it/blob/master/lib/presets/zero.mjs) - -* all rules disabled. Useful to quickly setup your config via `.enable()`. -* For example, when you need only `bold` and `italic` markup and nothing else. -* -* ##### options: -* -* - __html__ - `false`. Set `true` to enable HTML tags in source. Be careful! -* That's not safe! You may need external sanitizer to protect output from XSS. -* It's better to extend features via plugins, instead of enabling HTML. -* - __xhtmlOut__ - `false`. Set `true` to add '/' when closing single tags -* (`
    `). This is needed only for full CommonMark compatibility. In real -* world you will need HTML output. -* - __breaks__ - `false`. Set `true` to convert `\n` in paragraphs into `
    `. -* - __langPrefix__ - `language-`. CSS language class prefix for fenced blocks. -* Can be useful for external highlighters. -* - __linkify__ - `false`. Set `true` to autoconvert URL-like text to links. -* - __typographer__ - `false`. Set `true` to enable [some language-neutral -* replacement](https://github.com/markdown-it/markdown-it/blob/master/lib/rules_core/replacements.mjs) + -* quotes beautification (smartquotes). -* - __quotes__ - `“”‘’`, String or Array. Double + single quotes replacement -* pairs, when typographer enabled and smartquotes on. For example, you can -* use `'«»„“'` for Russian, `'„“‚‘'` for German, and -* `['«\xA0', '\xA0»', '‹\xA0', '\xA0›']` for French (including nbsp). -* - __highlight__ - `null`. Highlighter function for fenced code blocks. -* Highlighter `function (str, lang)` should return escaped HTML. It can also -* return empty string if the source was not changed and should be escaped -* externaly. If result starts with ` or ``): -* -* ```javascript -* var hljs = require('highlight.js') // https://highlightjs.org/ -* -* // Actual default values -* var md = require('markdown-it')({ -* highlight: function (str, lang) { -* if (lang && hljs.getLanguage(lang)) { -* try { -* return '
    ' +
    -*                hljs.highlight(str, { language: lang, ignoreIllegals: true }).value +
    -*                '
    '; -* } catch (__) {} -* } -* -* return '
    ' + md.utils.escapeHtml(str) + '
    '; -* } -* }); -* ``` -* -**/ -function MarkdownIt(presetName, options) { - if (!(this instanceof MarkdownIt)) { - return new MarkdownIt(presetName, options); - } - if (!options) { - if (!isString$1(presetName)) { - options = presetName || {}; - presetName = "default"; - } - } - /** - * MarkdownIt#inline -> ParserInline - * - * Instance of [[ParserInline]]. You may need it to add new rules when - * writing plugins. For simple rules control use [[MarkdownIt.disable]] and - * [[MarkdownIt.enable]]. - **/ - this.inline = new parser_inline_default(); - /** - * MarkdownIt#block -> ParserBlock - * - * Instance of [[ParserBlock]]. You may need it to add new rules when - * writing plugins. For simple rules control use [[MarkdownIt.disable]] and - * [[MarkdownIt.enable]]. - **/ - this.block = new parser_block_default(); - /** - * MarkdownIt#core -> Core - * - * Instance of [[Core]] chain executor. You may need it to add new rules when - * writing plugins. For simple rules control use [[MarkdownIt.disable]] and - * [[MarkdownIt.enable]]. - **/ - this.core = new parser_core_default(); - /** - * MarkdownIt#renderer -> Renderer - * - * Instance of [[Renderer]]. Use it to modify output look. Or to add rendering - * rules for new token types, generated by plugins. - * - * ##### Example - * - * ```javascript - * var md = require('markdown-it')(); - * - * function myToken(tokens, idx, options, env, self) { - * //... - * return result; - * }; - * - * md.renderer.rules['my_token'] = myToken - * ``` - * - * See [[Renderer]] docs and [source code](https://github.com/markdown-it/markdown-it/blob/master/lib/renderer.mjs). - **/ - this.renderer = new renderer_default(); - /** - * MarkdownIt#linkify -> LinkifyIt - * - * [linkify-it](https://github.com/markdown-it/linkify-it) instance. - * Used by [linkify](https://github.com/markdown-it/markdown-it/blob/master/lib/rules_core/linkify.mjs) - * rule. - **/ - this.linkify = new linkify_it_default(); - /** - * MarkdownIt#validateLink(url) -> Boolean - * - * Link validation function. CommonMark allows too much in links. By default - * we disable `javascript:`, `vbscript:`, `file:` schemas, and almost all `data:...` schemas - * except some embedded image types. - * - * You can change this behaviour: - * - * ```javascript - * var md = require('markdown-it')(); - * // enable everything - * md.validateLink = function () { return true; } - * ``` - **/ - this.validateLink = validateLink; - /** - * MarkdownIt#normalizeLink(url) -> String - * - * Function used to encode link url to a machine-readable format, - * which includes url-encoding, punycode, etc. - **/ - this.normalizeLink = normalizeLink; - /** - * MarkdownIt#normalizeLinkText(url) -> String - * - * Function used to decode link url to a human-readable format` - **/ - this.normalizeLinkText = normalizeLinkText; - /** - * MarkdownIt#utils -> utils - * - * Assorted utility functions, useful to write plugins. See details - * [here](https://github.com/markdown-it/markdown-it/blob/master/lib/common/utils.mjs). - **/ - this.utils = utils_exports; - /** - * MarkdownIt#helpers -> helpers - * - * Link components parser functions, useful to write plugins. See details - * [here](https://github.com/markdown-it/markdown-it/blob/master/lib/helpers). - **/ - this.helpers = assign$1({}, helpers_exports); - this.options = {}; - this.configure(presetName); - if (options) { - this.set(options); - } -} -/** chainable -* MarkdownIt.set(options) -* -* Set parser options (in the same format as in constructor). Probably, you -* will never need it, but you can change options after constructor call. -* -* ##### Example -* -* ```javascript -* var md = require('markdown-it')() -* .set({ html: true, breaks: true }) -* .set({ typographer, true }); -* ``` -* -* __Note:__ To achieve the best possible performance, don't modify a -* `markdown-it` instance options on the fly. If you need multiple configurations -* it's best to create multiple instances and initialize each with separate -* config. -**/ -MarkdownIt.prototype.set = function(options) { - assign$1(this.options, options); - return this; -}; -/** chainable, internal -* MarkdownIt.configure(presets) -* -* Batch load of all options and compenent settings. This is internal method, -* and you probably will not need it. But if you will - see available presets -* and data structure [here](https://github.com/markdown-it/markdown-it/tree/master/lib/presets) -* -* We strongly recommend to use presets instead of direct config loads. That -* will give better compatibility with next versions. -**/ -MarkdownIt.prototype.configure = function(presets) { - const self = this; - if (isString$1(presets)) { - const presetName = presets; - presets = config[presetName]; - if (!presets) { - throw new Error("Wrong `markdown-it` preset \"" + presetName + "\", check name"); - } - } - if (!presets) { - throw new Error("Wrong `markdown-it` preset, can't be empty"); - } - if (presets.options) { - self.set(presets.options); - } - if (presets.components) { - Object.keys(presets.components).forEach(function(name) { - if (presets.components[name].rules) { - self[name].ruler.enableOnly(presets.components[name].rules); - } - if (presets.components[name].rules2) { - self[name].ruler2.enableOnly(presets.components[name].rules2); - } - }); - } - return this; -}; -/** chainable -* MarkdownIt.enable(list, ignoreInvalid) -* - list (String|Array): rule name or list of rule names to enable -* - ignoreInvalid (Boolean): set `true` to ignore errors when rule not found. -* -* Enable list or rules. It will automatically find appropriate components, -* containing rules with given names. If rule not found, and `ignoreInvalid` -* not set - throws exception. -* -* ##### Example -* -* ```javascript -* var md = require('markdown-it')() -* .enable(['sub', 'sup']) -* .disable('smartquotes'); -* ``` -**/ -MarkdownIt.prototype.enable = function(list$1, ignoreInvalid) { - let result = []; - if (!Array.isArray(list$1)) { - list$1 = [list$1]; - } - [ - "core", - "block", - "inline" - ].forEach(function(chain) { - result = result.concat(this[chain].ruler.enable(list$1, true)); - }, this); - result = result.concat(this.inline.ruler2.enable(list$1, true)); - const missed = list$1.filter(function(name) { - return result.indexOf(name) < 0; - }); - if (missed.length && !ignoreInvalid) { - throw new Error("MarkdownIt. Failed to enable unknown rule(s): " + missed); - } - return this; -}; -/** chainable -* MarkdownIt.disable(list, ignoreInvalid) -* - list (String|Array): rule name or list of rule names to disable. -* - ignoreInvalid (Boolean): set `true` to ignore errors when rule not found. -* -* The same as [[MarkdownIt.enable]], but turn specified rules off. -**/ -MarkdownIt.prototype.disable = function(list$1, ignoreInvalid) { - let result = []; - if (!Array.isArray(list$1)) { - list$1 = [list$1]; - } - [ - "core", - "block", - "inline" - ].forEach(function(chain) { - result = result.concat(this[chain].ruler.disable(list$1, true)); - }, this); - result = result.concat(this.inline.ruler2.disable(list$1, true)); - const missed = list$1.filter(function(name) { - return result.indexOf(name) < 0; - }); - if (missed.length && !ignoreInvalid) { - throw new Error("MarkdownIt. Failed to disable unknown rule(s): " + missed); - } - return this; -}; -/** chainable -* MarkdownIt.use(plugin, params) -* -* Load specified plugin with given params into current parser instance. -* It's just a sugar to call `plugin(md, params)` with curring. -* -* ##### Example -* -* ```javascript -* var iterator = require('markdown-it-for-inline'); -* var md = require('markdown-it')() -* .use(iterator, 'foo_replace', 'text', function (tokens, idx) { -* tokens[idx].content = tokens[idx].content.replace(/foo/g, 'bar'); -* }); -* ``` -**/ -MarkdownIt.prototype.use = function(plugin) { - const args = [this].concat(Array.prototype.slice.call(arguments, 1)); - plugin.apply(plugin, args); - return this; -}; -/** internal -* MarkdownIt.parse(src, env) -> Array -* - src (String): source string -* - env (Object): environment sandbox -* -* Parse input string and return list of block tokens (special token type -* "inline" will contain list of inline tokens). You should not call this -* method directly, until you write custom renderer (for example, to produce -* AST). -* -* `env` is used to pass data between "distributed" rules and return additional -* metadata like reference info, needed for the renderer. It also can be used to -* inject data in specific cases. Usually, you will be ok to pass `{}`, -* and then pass updated object to renderer. -**/ -MarkdownIt.prototype.parse = function(src, env) { - if (typeof src !== "string") { - throw new Error("Input data should be a String"); - } - const state = new this.core.State(src, this, env); - this.core.process(state); - return state.tokens; -}; -/** -* MarkdownIt.render(src [, env]) -> String -* - src (String): source string -* - env (Object): environment sandbox -* -* Render markdown string into html. It does all magic for you :). -* -* `env` can be used to inject additional metadata (`{}` by default). -* But you will not need it with high probability. See also comment -* in [[MarkdownIt.parse]]. -**/ -MarkdownIt.prototype.render = function(src, env) { - env = env || {}; - return this.renderer.render(this.parse(src, env), this.options, env); -}; -/** internal -* MarkdownIt.parseInline(src, env) -> Array -* - src (String): source string -* - env (Object): environment sandbox -* -* The same as [[MarkdownIt.parse]] but skip all block rules. It returns the -* block tokens list with the single `inline` element, containing parsed inline -* tokens in `children` property. Also updates `env` object. -**/ -MarkdownIt.prototype.parseInline = function(src, env) { - const state = new this.core.State(src, this, env); - state.inlineMode = true; - this.core.process(state); - return state.tokens; -}; -/** -* MarkdownIt.renderInline(src [, env]) -> String -* - src (String): source string -* - env (Object): environment sandbox -* -* Similar to [[MarkdownIt.render]] but for single paragraph content. Result -* will NOT be wrapped into `

    ` tags. -**/ -MarkdownIt.prototype.renderInline = function(src, env) { - env = env || {}; - return this.renderer.render(this.parseInline(src, env), this.options, env); -}; -var lib_default = MarkdownIt; - -/** -* This is only safe for (and intended to be used for) text node positions. If -* you are using attribute position, then this is only safe if the attribute -* value is surrounded by double-quotes, and is unsafe otherwise (because the -* value could break out of the attribute value and e.g. add another attribute). -*/ -function escapeNodeText(str) { - const frag = document.createElement("div"); - D(b`${str}`, frag); - return frag.innerHTML.replaceAll(//gim, ""); -} -function unescapeNodeText(str) { - if (!str) { - return ""; - } - const frag = document.createElement("textarea"); - frag.innerHTML = str; - return frag.value; -} - -var MarkdownDirective = class extends i$5 { - #markdownIt = lib_default({ highlight: (str, lang) => { - switch (lang) { - case "html": { - const iframe = document.createElement("iframe"); - iframe.classList.add("html-view"); - iframe.srcdoc = str; - iframe.sandbox = ""; - return iframe.innerHTML; - } - default: return escapeNodeText(str); - } - } }); - #lastValue = null; - #lastTagClassMap = null; - update(_part, [value, tagClassMap]) { - if (this.#lastValue === value && JSON.stringify(tagClassMap) === this.#lastTagClassMap) { - return E; - } - this.#lastValue = value; - this.#lastTagClassMap = JSON.stringify(tagClassMap); - return this.render(value, tagClassMap); - } - #originalClassMap = new Map(); - #applyTagClassMap(tagClassMap) { - Object.entries(tagClassMap).forEach(([tag]) => { - let tokenName; - switch (tag) { - case "p": - tokenName = "paragraph"; - break; - case "h1": - case "h2": - case "h3": - case "h4": - case "h5": - case "h6": - tokenName = "heading"; - break; - case "ul": - tokenName = "bullet_list"; - break; - case "ol": - tokenName = "ordered_list"; - break; - case "li": - tokenName = "list_item"; - break; - case "a": - tokenName = "link"; - break; - case "strong": - tokenName = "strong"; - break; - case "em": - tokenName = "em"; - break; - } - if (!tokenName) { - return; - } - const key = `${tokenName}_open`; - this.#markdownIt.renderer.rules[key] = (tokens, idx, options, _env, self) => { - const token = tokens[idx]; - const tokenClasses = tagClassMap[token.tag] ?? []; - for (const clazz of tokenClasses) { - token.attrJoin("class", clazz); - } - return self.renderToken(tokens, idx, options); - }; - }); - } - #unapplyTagClassMap() { - for (const [key] of this.#originalClassMap) { - delete this.#markdownIt.renderer.rules[key]; - } - this.#originalClassMap.clear(); - } - /** - * Renders the markdown string to HTML using MarkdownIt. - * - * Note: MarkdownIt doesn't enable HTML in its output, so we render the - * value directly without further sanitization. - * @see https://github.com/markdown-it/markdown-it/blob/master/docs/security.md - */ - render(value, tagClassMap) { - if (tagClassMap) { - this.#applyTagClassMap(tagClassMap); - } - const htmlString = this.#markdownIt.render(value); - this.#unapplyTagClassMap(); - return o(htmlString); - } -}; -const markdown = e$10(MarkdownDirective); -const markdownItStandalone = lib_default(); -function renderMarkdownToHtmlString(value) { - return markdownItStandalone.render(value); -} - -var __esDecorate$1 = void 0 && (void 0).__esDecorate || function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { - function accept(f$4) { - if (f$4 !== void 0 && typeof f$4 !== "function") throw new TypeError("Function expected"); - return f$4; - } - var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; - var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; - var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); - var _$1, done = false; - for (var i$10 = decorators.length - 1; i$10 >= 0; i$10--) { - var context = {}; - for (var p$3 in contextIn) context[p$3] = p$3 === "access" ? {} : contextIn[p$3]; - for (var p$3 in contextIn.access) context.access[p$3] = contextIn.access[p$3]; - context.addInitializer = function(f$4) { - if (done) throw new TypeError("Cannot add initializers after decoration has completed"); - extraInitializers.push(accept(f$4 || null)); - }; - var result = (0, decorators[i$10])(kind === "accessor" ? { - get: descriptor.get, - set: descriptor.set - } : descriptor[key], context); - if (kind === "accessor") { - if (result === void 0) continue; - if (result === null || typeof result !== "object") throw new TypeError("Object expected"); - if (_$1 = accept(result.get)) descriptor.get = _$1; - if (_$1 = accept(result.set)) descriptor.set = _$1; - if (_$1 = accept(result.init)) initializers.unshift(_$1); - } else if (_$1 = accept(result)) { - if (kind === "field") initializers.unshift(_$1); - else descriptor[key] = _$1; - } - } - if (target) Object.defineProperty(target, contextIn.name, descriptor); - done = true; -}; -var __runInitializers$1 = void 0 && (void 0).__runInitializers || function(thisArg, initializers, value) { - var useValue = arguments.length > 2; - for (var i$10 = 0; i$10 < initializers.length; i$10++) { - value = useValue ? initializers[i$10].call(thisArg, value) : initializers[i$10].call(thisArg); - } - return useValue ? value : void 0; -}; -let Text = (() => { - let _classDecorators = [t$1("a2ui-text")]; - let _classDescriptor; - let _classExtraInitializers = []; - let _classThis; - let _classSuper = Root; - let _text_decorators; - let _text_initializers = []; - let _text_extraInitializers = []; - let _usageHint_decorators; - let _usageHint_initializers = []; - let _usageHint_extraInitializers = []; - var Text = class extends _classSuper { - static { - _classThis = this; - } - static { - const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0; - _text_decorators = [n$6()]; - _usageHint_decorators = [n$6({ - reflect: true, - attribute: "usage-hint" - })]; - __esDecorate$1(this, null, _text_decorators, { - kind: "accessor", - name: "text", - static: false, - private: false, - access: { - has: (obj) => "text" in obj, - get: (obj) => obj.text, - set: (obj, value) => { - obj.text = value; - } - }, - metadata: _metadata - }, _text_initializers, _text_extraInitializers); - __esDecorate$1(this, null, _usageHint_decorators, { - kind: "accessor", - name: "usageHint", - static: false, - private: false, - access: { - has: (obj) => "usageHint" in obj, - get: (obj) => obj.usageHint, - set: (obj, value) => { - obj.usageHint = value; - } - }, - metadata: _metadata - }, _usageHint_initializers, _usageHint_extraInitializers); - __esDecorate$1(null, _classDescriptor = { value: _classThis }, _classDecorators, { - kind: "class", - name: _classThis.name, - metadata: _metadata - }, null, _classExtraInitializers); - Text = _classThis = _classDescriptor.value; - if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { - enumerable: true, - configurable: true, - writable: true, - value: _metadata - }); - } - #text_accessor_storage = __runInitializers$1(this, _text_initializers, null); - get text() { - return this.#text_accessor_storage; - } - set text(value) { - this.#text_accessor_storage = value; - } - #usageHint_accessor_storage = (__runInitializers$1(this, _text_extraInitializers), __runInitializers$1(this, _usageHint_initializers, null)); - get usageHint() { - return this.#usageHint_accessor_storage; - } - set usageHint(value) { - this.#usageHint_accessor_storage = value; - } - static { - this.styles = [structuralStyles, i$9` - :host { - display: block; - flex: var(--weight); - } - - h1, - h2, - h3, - h4, - h5 { - line-height: inherit; - font: inherit; - } - `]; - } - #renderText() { - let textValue = null; - if (this.text && typeof this.text === "object") { - if ("literalString" in this.text && this.text.literalString) { - textValue = this.text.literalString; - } else if ("literal" in this.text && this.text.literal !== undefined) { - textValue = this.text.literal; - } else if (this.text && "path" in this.text && this.text.path) { - if (!this.processor || !this.component) { - return b`(no model)`; - } - const value = this.processor.getData(this.component, this.text.path, this.surfaceId ?? A2uiMessageProcessor.DEFAULT_SURFACE_ID); - if (value !== null && value !== undefined) { - textValue = value.toString(); - } - } - } - if (textValue === null || textValue === undefined) { - return b`(empty)`; - } - let markdownText = textValue; - switch (this.usageHint) { - case "h1": - markdownText = `# ${markdownText}`; - break; - case "h2": - markdownText = `## ${markdownText}`; - break; - case "h3": - markdownText = `### ${markdownText}`; - break; - case "h4": - markdownText = `#### ${markdownText}`; - break; - case "h5": - markdownText = `##### ${markdownText}`; - break; - case "caption": - markdownText = `*${markdownText}*`; - break; - default: break; - } - return b`${markdown(markdownText, appendToAll(this.theme.markdown, [ - "ol", - "ul", - "li" - ], {}))}`; - } - #areHintedStyles(styles) { - if (typeof styles !== "object") return false; - if (Array.isArray(styles)) return false; - if (!styles) return false; - const expected = [ - "h1", - "h2", - "h3", - "h4", - "h5", - "h6", - "caption", - "body" - ]; - return expected.every((v$2) => v$2 in styles); - } - #getAdditionalStyles() { - let additionalStyles = {}; - const styles = this.theme.additionalStyles?.Text; - if (!styles) return additionalStyles; - if (this.#areHintedStyles(styles)) { - const hint = this.usageHint ?? "body"; - additionalStyles = styles[hint]; - } else { - additionalStyles = styles; - } - return additionalStyles; - } - render() { - const classes = merge(this.theme.components.Text.all, this.usageHint ? this.theme.components.Text[this.usageHint] : {}); - return b`

    - ${this.#renderText()} -
    `; - } - constructor() { - super(...arguments); - __runInitializers$1(this, _usageHint_extraInitializers); - } - static { - __runInitializers$1(_classThis, _classExtraInitializers); - } - }; - return Text = _classThis; -})(); - -var __esDecorate = void 0 && (void 0).__esDecorate || function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { - function accept(f$4) { - if (f$4 !== void 0 && typeof f$4 !== "function") throw new TypeError("Function expected"); - return f$4; - } - var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; - var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; - var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); - var _$1, done = false; - for (var i$10 = decorators.length - 1; i$10 >= 0; i$10--) { - var context = {}; - for (var p$3 in contextIn) context[p$3] = p$3 === "access" ? {} : contextIn[p$3]; - for (var p$3 in contextIn.access) context.access[p$3] = contextIn.access[p$3]; - context.addInitializer = function(f$4) { - if (done) throw new TypeError("Cannot add initializers after decoration has completed"); - extraInitializers.push(accept(f$4 || null)); - }; - var result = (0, decorators[i$10])(kind === "accessor" ? { - get: descriptor.get, - set: descriptor.set - } : descriptor[key], context); - if (kind === "accessor") { - if (result === void 0) continue; - if (result === null || typeof result !== "object") throw new TypeError("Object expected"); - if (_$1 = accept(result.get)) descriptor.get = _$1; - if (_$1 = accept(result.set)) descriptor.set = _$1; - if (_$1 = accept(result.init)) initializers.unshift(_$1); - } else if (_$1 = accept(result)) { - if (kind === "field") initializers.unshift(_$1); - else descriptor[key] = _$1; - } - } - if (target) Object.defineProperty(target, contextIn.name, descriptor); - done = true; -}; -var __runInitializers = void 0 && (void 0).__runInitializers || function(thisArg, initializers, value) { - var useValue = arguments.length > 2; - for (var i$10 = 0; i$10 < initializers.length; i$10++) { - value = useValue ? initializers[i$10].call(thisArg, value) : initializers[i$10].call(thisArg); - } - return useValue ? value : void 0; -}; -let Video = (() => { - let _classDecorators = [t$1("a2ui-video")]; - let _classDescriptor; - let _classExtraInitializers = []; - let _classThis; - let _classSuper = Root; - let _url_decorators; - let _url_initializers = []; - let _url_extraInitializers = []; - var Video = class extends _classSuper { - static { - _classThis = this; - } - static { - const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0; - _url_decorators = [n$6()]; - __esDecorate(this, null, _url_decorators, { - kind: "accessor", - name: "url", - static: false, - private: false, - access: { - has: (obj) => "url" in obj, - get: (obj) => obj.url, - set: (obj, value) => { - obj.url = value; - } - }, - metadata: _metadata - }, _url_initializers, _url_extraInitializers); - __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { - kind: "class", - name: _classThis.name, - metadata: _metadata - }, null, _classExtraInitializers); - Video = _classThis = _classDescriptor.value; - if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { - enumerable: true, - configurable: true, - writable: true, - value: _metadata - }); - } - #url_accessor_storage = __runInitializers(this, _url_initializers, null); - get url() { - return this.#url_accessor_storage; - } - set url(value) { - this.#url_accessor_storage = value; - } - static { - this.styles = [structuralStyles, i$9` - * { - box-sizing: border-box; - } - - :host { - display: block; - flex: var(--weight); - min-height: 0; - overflow: auto; - } - - video { - display: block; - width: 100%; - } - `]; - } - #renderVideo() { - if (!this.url) { - return A; - } - if (this.url && typeof this.url === "object") { - if ("literalString" in this.url) { - return b`