diff --git a/.agent/.DS_Store b/.agent/.DS_Store new file mode 100644 index 000000000..1f2c43e08 Binary files /dev/null and b/.agent/.DS_Store differ diff --git a/.agent/workflows/update_clawdbot.md b/.agent/workflows/update_clawdbot.md new file mode 100644 index 000000000..692ee84e4 --- /dev/null +++ b/.agent/workflows/update_clawdbot.md @@ -0,0 +1,366 @@ +--- +description: Update Clawdbot from upstream when branch has diverged (ahead/behind) +--- + +# Clawdbot Upstream Sync Workflow + +Use this workflow when your fork has diverged from upstream (e.g., "18 commits ahead, 29 commits behind"). + +## Quick Reference + +```bash +# Check divergence status +git fetch upstream && git rev-list --left-right --count main...upstream/main + +# Full sync (rebase preferred) +git fetch upstream && git rebase upstream/main && pnpm install && pnpm build && ./scripts/restart-mac.sh + +# Check for Swift 6.2 issues after sync +grep -r "FileManager\.default\|Thread\.isMainThread" src/ apps/ --include="*.swift" +``` + +--- + +## Step 1: Assess Divergence + +```bash +git fetch upstream +git log --oneline --left-right main...upstream/main | head -20 +``` + +This shows: +- `<` = your local commits (ahead) +- `>` = upstream commits you're missing (behind) + +**Decision point:** +- Few local commits, many upstream → **Rebase** (cleaner history) +- Many local commits or shared branch → **Merge** (preserves history) + +--- + +## Step 2A: Rebase Strategy (Preferred) + +Replays your commits on top of upstream. Results in linear history. + +```bash +# Ensure working tree is clean +git status + +# Rebase onto upstream +git rebase upstream/main +``` + +### Handling Rebase Conflicts + +```bash +# When conflicts occur: +# 1. Fix conflicts in the listed files +# 2. Stage resolved files +git add + +# 3. Continue rebase +git rebase --continue + +# If a commit is no longer needed (already in upstream): +git rebase --skip + +# To abort and return to original state: +git rebase --abort +``` + +### Common Conflict Patterns + +| File | Resolution | +|------|------------| +| `package.json` | Take upstream deps, keep local scripts if needed | +| `pnpm-lock.yaml` | Accept upstream, regenerate with `pnpm install` | +| `*.patch` files | Usually take upstream version | +| Source files | Merge logic carefully, prefer upstream structure | + +--- + +## Step 2B: Merge Strategy (Alternative) + +Preserves all history with a merge commit. + +```bash +git merge upstream/main --no-edit +``` + +Resolve conflicts same as rebase, then: +```bash +git add +git commit +``` + +--- + +## Step 3: Rebuild Everything + +After sync completes: + +```bash +# Install dependencies (regenerates lock if needed) +pnpm install + +# Build TypeScript +pnpm build + +# Build UI assets +pnpm ui:build + +# Run diagnostics +pnpm clawdbot doctor +``` + +--- + +## Step 4: Rebuild macOS App + +```bash +# Full rebuild, sign, and launch +./scripts/restart-mac.sh + +# Or just package without restart +pnpm mac:package +``` + +### Install to /Applications + +```bash +# Kill running app +pkill -x "Clawdbot" || true + +# Move old version +mv /Applications/Clawdbot.app /tmp/Clawdbot-backup.app + +# Install new build +cp -R dist/Clawdbot.app /Applications/ + +# Launch +open /Applications/Clawdbot.app +``` + +--- + +## Step 4A: Verify macOS App & Agent + +After rebuilding the macOS app, always verify it works correctly: + +```bash +# Check gateway health +pnpm clawdbot health + +# Verify no zombie processes +ps aux | grep -E "(clawdbot|gateway)" | grep -v grep + +# Test agent functionality by sending a verification message +pnpm clawdbot agent --message "Verification: macOS app rebuild successful - agent is responding." --session-id YOUR_TELEGRAM_SESSION_ID + +# Confirm the message was received on Telegram +# (Check your Telegram chat with the bot) +``` + +**Important:** Always wait for the Telegram verification message before proceeding. If the agent doesn't respond, troubleshoot the gateway or model configuration before pushing. + +--- + +## Step 5: Handle Swift/macOS Build Issues (Common After Upstream Sync) + +Upstream updates may introduce Swift 6.2 / macOS 26 SDK incompatibilities. Use analyze-mode for systematic debugging: + +### Analyze-Mode Investigation +```bash +# Gather context with parallel agents +morph-mcp_warpgrep_codebase_search search_string="Find deprecated FileManager.default and Thread.isMainThread usages in Swift files" repo_path="/Volumes/Main SSD/Developer/clawdis" +morph-mcp_warpgrep_codebase_search search_string="Locate Peekaboo submodule and macOS app Swift files with concurrency issues" repo_path="/Volumes/Main SSD/Developer/clawdis" +``` + +### Common Swift 6.2 Fixes + +**FileManager.default Deprecation:** +```bash +# Search for deprecated usage +grep -r "FileManager\.default" src/ apps/ --include="*.swift" + +# Replace with proper initialization +# OLD: FileManager.default +# NEW: FileManager() +``` + +**Thread.isMainThread Deprecation:** +```bash +# Search for deprecated usage +grep -r "Thread\.isMainThread" src/ apps/ --include="*.swift" + +# Replace with modern concurrency check +# OLD: Thread.isMainThread +# NEW: await MainActor.run { ... } or DispatchQueue.main.sync { ... } +``` + +### Peekaboo Submodule Fixes +```bash +# Check Peekaboo for concurrency issues +cd src/canvas-host/a2ui +grep -r "Thread\.isMainThread\|FileManager\.default" . --include="*.swift" + +# Fix and rebuild submodule +cd /Volumes/Main SSD/Developer/clawdis +pnpm canvas:a2ui:bundle +``` + +### macOS App Concurrency Fixes +```bash +# Check macOS app for issues +grep -r "Thread\.isMainThread\|FileManager\.default" apps/macos/ --include="*.swift" + +# Clean and rebuild after fixes +cd apps/macos && rm -rf .build .swiftpm +./scripts/restart-mac.sh +``` + +### Model Configuration Updates +If upstream introduced new model configurations: +```bash +# Check for OpenRouter API key requirements +grep -r "openrouter\|OPENROUTER" src/ --include="*.ts" --include="*.js" + +# Update clawdbot.json with fallback chains +# Add model fallback configurations as needed +``` + +--- + +## Step 6: Verify & Push + +```bash +# Verify everything works +pnpm clawdbot health +pnpm test + +# Push (force required after rebase) +git push origin main --force-with-lease + +# Or regular push after merge +git push origin main +``` + +--- + +## Troubleshooting + +### Build Fails After Sync + +```bash +# Clean and rebuild +rm -rf node_modules dist +pnpm install +pnpm build +``` + +### Type Errors (Bun/Node Incompatibility) + +Common issue: `fetch.preconnect` type mismatch. Fix by using `FetchLike` type instead of `typeof fetch`. + +### macOS App Crashes on Launch + +Usually resource bundle mismatch. Full rebuild required: +```bash +cd apps/macos && rm -rf .build .swiftpm +./scripts/restart-mac.sh +``` + +### Patch Failures + +```bash +# Check patch status +pnpm install 2>&1 | grep -i patch + +# If patches fail, they may need updating for new dep versions +# Check patches/ directory against package.json patchedDependencies +``` + +### Swift 6.2 / macOS 26 SDK Build Failures + +**Symptoms:** Build fails with deprecation warnings about `FileManager.default` or `Thread.isMainThread` + +**Search-Mode Investigation:** +```bash +# Exhaustive search for deprecated APIs +morph-mcp_warpgrep_codebase_search search_string="Find all Swift files using deprecated FileManager.default or Thread.isMainThread" repo_path="/Volumes/Main SSD/Developer/clawdis" +``` + +**Quick Fix Commands:** +```bash +# Find all affected files +find . -name "*.swift" -exec grep -l "FileManager\.default\|Thread\.isMainThread" {} \; + +# Replace FileManager.default with FileManager() +find . -name "*.swift" -exec sed -i '' 's/FileManager\.default/FileManager()/g' {} \; + +# For Thread.isMainThread, need manual review of each usage +grep -rn "Thread\.isMainThread" --include="*.swift" . +``` + +**Rebuild After Fixes:** +```bash +# Clean all build artifacts +rm -rf apps/macos/.build apps/macos/.swiftpm +rm -rf src/canvas-host/a2ui/.build + +# Rebuild Peekaboo bundle +pnpm canvas:a2ui:bundle + +# Full macOS rebuild +./scripts/restart-mac.sh +``` + +--- + +## Automation Script + +Save as `scripts/sync-upstream.sh`: + +```bash +#!/usr/bin/env bash +set -euo pipefail + +echo "==> Fetching upstream..." +git fetch upstream + +echo "==> Current divergence:" +git rev-list --left-right --count main...upstream/main + +echo "==> Rebasing onto upstream/main..." +git rebase upstream/main + +echo "==> Installing dependencies..." +pnpm install + +echo "==> Building..." +pnpm build +pnpm ui:build + +echo "==> Running doctor..." +pnpm clawdbot doctor + +echo "==> Rebuilding macOS app..." +./scripts/restart-mac.sh + +echo "==> Verifying gateway health..." +pnpm clawdbot health + +echo "==> Checking for Swift 6.2 compatibility issues..." +if grep -r "FileManager\.default\|Thread\.isMainThread" src/ apps/ --include="*.swift" --quiet; then + echo "⚠️ Found potential Swift 6.2 deprecated API usage" + echo " Run manual fixes or use analyze-mode investigation" +else + echo "✅ No obvious Swift deprecation issues found" +fi + +echo "==> Testing agent functionality..." +# Note: Update YOUR_TELEGRAM_SESSION_ID with actual session ID +pnpm clawdbot agent --message "Verification: Upstream sync and macOS rebuild completed successfully." --session-id YOUR_TELEGRAM_SESSION_ID || echo "Warning: Agent test failed - check Telegram for verification message" + +echo "==> Done! Check Telegram for verification message, then run 'git push --force-with-lease' when ready." +``` diff --git a/.github/workflows/install-smoke.yml b/.github/workflows/install-smoke.yml index b7e8e274e..84d1b7f32 100644 --- a/.github/workflows/install-smoke.yml +++ b/.github/workflows/install-smoke.yml @@ -29,5 +29,6 @@ jobs: CLAWDBOT_INSTALL_CLI_URL: https://clawd.bot/install-cli.sh CLAWDBOT_NO_ONBOARD: "1" CLAWDBOT_INSTALL_SMOKE_SKIP_CLI: "1" + CLAWDBOT_INSTALL_SMOKE_SKIP_NONROOT: ${{ github.event_name == 'pull_request' && '1' || '0' }} CLAWDBOT_INSTALL_SMOKE_PREVIOUS: "2026.1.11-4" run: pnpm test:install:smoke diff --git a/.gitignore b/.gitignore index 88e45373f..b2c1de9a2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules +**/node_modules/ .env docker-compose.extra.yml dist @@ -31,6 +32,11 @@ apps/ios/*.xcodeproj/ apps/ios/*.xcworkspace/ apps/ios/.swiftpm/ vendor/ +apps/ios/Clawdbot.xcodeproj/ +apps/ios/Clawdbot.xcodeproj/** +apps/macos/.build/** +**/*.bun-build +apps/ios/*.xcfilelist # Vendor build artifacts vendor/a2ui/renderers/lit/dist/ @@ -43,6 +49,8 @@ apps/ios/fastlane/Preview.html apps/ios/fastlane/screenshots/ apps/ios/fastlane/test_output/ apps/ios/fastlane/logs/ +apps/ios/fastlane/.env +apps/ios/fastlane/report.xml # fastlane build artifacts (local) apps/ios/*.ipa @@ -58,3 +66,6 @@ apps/ios/*.mobileprovision IDENTITY.md USER.md .tgz + +# local tooling +.serena/ diff --git a/.npmrc b/.npmrc index 39903bcbe..f0c783cb6 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1 @@ -allow-build-scripts=@whiskeysockets/baileys,sharp,esbuild,protobufjs,fs-ext,node-pty,@lydell/node-pty +allow-build-scripts=@whiskeysockets/baileys,sharp,esbuild,protobufjs,fs-ext,node-pty,@lydell/node-pty,@matrix-org/matrix-sdk-crypto-nodejs diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..6333f297c --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +src/canvas-host/a2ui/a2ui.bundle.js diff --git a/AGENTS.md b/AGENTS.md index ed08c1bb3..fbf1ecf79 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,12 @@ - Tests: colocated `*.test.ts`. - Docs: `docs/` (images, queue, Pi config). Built output lives in `dist/`. - Plugins/extensions: live under `extensions/*` (workspace packages). Keep plugin-only deps in the extension `package.json`; do not add them to the root `package.json` unless core uses them. +- Plugins: install runs `npm install --omit=dev` in plugin dir; runtime deps must live in `dependencies`. Avoid `workspace:*` in `dependencies` (npm install breaks); put `clawdbot` in `devDependencies` or `peerDependencies` instead (runtime resolves `clawdbot/plugin-sdk` via jiti alias). - Installers served from `https://clawd.bot/*`: live in the sibling repo `../clawd.bot` (`public/install.sh`, `public/install-cli.sh`, `public/install.ps1`). +- Messaging channels: always consider **all** built-in + extension channels when refactoring shared logic (routing, allowlists, pairing, command gating, onboarding, docs). + - Core channel docs: `docs/channels/` + - Core channel code: `src/telegram`, `src/discord`, `src/slack`, `src/signal`, `src/imessage`, `src/web` (WhatsApp web), `src/channels`, `src/routing` + - Extensions (channel plugins): `extensions/*` (e.g. `extensions/msteams`, `extensions/matrix`, `extensions/zalo`, `extensions/zalouser`, `extensions/voice-call`) ## Docs Linking (Mintlify) - Docs are hosted on Mintlify (docs.clawd.bot). @@ -18,6 +23,16 @@ - README (GitHub): keep absolute docs URLs (`https://docs.clawd.bot/...`) so links work on GitHub. - Docs content must be generic: no personal device names/hostnames/paths; use placeholders like `user@gateway-host` and “gateway host”. +## exe.dev VM ops (general) +- Access: stable path is `ssh exe.dev` then `ssh vm-name` (assume SSH key already set). +- SSH flaky: use exe.dev web terminal or Shelley (web agent); keep a tmux session for long ops. +- Update: `sudo npm i -g clawdbot@latest` (global install needs root on `/usr/lib/node_modules`). +- Config: use `clawdbot config set ...`; ensure `gateway.mode=local` is set. +- Discord: store raw token only (no `DISCORD_BOT_TOKEN=` prefix). +- Restart: stop old gateway and run: + `pkill -9 -f clawdbot-gateway || true; nohup clawdbot gateway run --bind loopback --port 18789 --force > /tmp/clawdbot-gateway.log 2>&1 &` +- Verify: `clawdbot channels status --probe`, `ss -ltnp | rg 18789`, `tail -n 120 /tmp/clawdbot-gateway.log`. + ## Build, Test, and Development Commands - Runtime baseline: Node **22+** (keep Node + Bun paths working). - Install deps: `pnpm install` @@ -25,6 +40,7 @@ - Prefer Bun for TypeScript execution (scripts, dev, tests): `bun ` / `bunx `. - Run CLI in dev: `pnpm clawdbot ...` (bun) or `pnpm dev`. - Node remains supported for running built output (`dist/*`) and production installs. +- Mac packaging (dev): `scripts/package-mac-app.sh` defaults to current arch. Release checklist: `docs/platforms/mac/release.md`. - Type-check/build: `pnpm build` (tsc) - Lint/format: `pnpm lint` (oxlint), `pnpm format` (oxfmt) - Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage` @@ -37,10 +53,16 @@ - Aim to keep files under ~700 LOC; guideline only (not a hard guardrail). Split/refactor when it improves clarity or testability. - Naming: use **Clawdbot** for product/app/docs headings; use `clawdbot` for CLI command, package/binary, paths, and config keys. +## Release Channels (Naming) +- stable: tagged releases only (e.g. `vYYYY.M.D`), npm dist-tag `latest`. +- beta: prerelease tags `vYYYY.M.D-beta.N`, npm dist-tag `beta` (may ship without macOS app). +- dev: moving head on `main` (no tag; git checkout main). + ## Testing Guidelines - Framework: Vitest with V8 coverage thresholds (70% lines/branches/functions/statements). - Naming: match source names with `*.test.ts`; e2e in `*.e2e.test.ts`. - Run `pnpm test` (or `pnpm test:coverage`) before pushing when you touch logic. +- Do not set test workers above 16; tried already. - Live tests (real keys): `CLAWDBOT_LIVE_TEST=1 pnpm test:live` (Clawdbot-only) or `LIVE=1 pnpm test:live` (includes provider live tests). Docker: `pnpm test:docker:live-models`, `pnpm test:docker:live-gateway`. Onboarding Docker E2E: `pnpm test:docker:onboard`. - Full kit + what’s covered: `docs/testing.md`. - Pure test additions/fixes generally do **not** need a changelog entry unless they alter user-facing behavior or the user asks for one. @@ -53,6 +75,9 @@ - Changelog workflow: keep latest released version at top (no `Unreleased`); after publishing, bump version and start a new top section. - PRs should summarize scope, note testing performed, and mention any user-facing changes or new flags. - PR review flow: when given a PR link, review via `gh pr view`/`gh pr diff` and do **not** change branches. +- PR review calls: prefer a single `gh pr view --json ...` to batch metadata/comments; run `gh pr diff` only when needed. +- Before starting a review when a GH Issue/PR is pasted: run `git pull`; if there are local changes or unpushed commits, stop and alert the user before reviewing. +- Goal: merge PRs. Prefer **rebase** when commits are clean; **squash** when history is messy. - PR merge flow: create a temp branch from `main`, merge the PR branch into it (prefer squash unless commit history is important; use rebase/merge when it is). Always try to merge the PR unless it’s truly difficult, then use another approach. If we squash, add the PR author as a co-contributor. Apply fixes, add changelog entry (include PR # + thanks), run full gate before the final commit, commit, merge back to `main`, delete the temp branch, and end on `main`. - If you review a PR and later do work on it, land via merge/squash (no direct-main commits) and always add the PR author as a co-contributor. - When working on a PR: add a changelog entry with the PR number and thank the contributor. @@ -62,7 +87,7 @@ - After merging a PR: run `bun scripts/update-clawtributors.ts` if the contributor is missing, then commit the regenerated README. ## Shorthand Commands -- `sync up`: if working tree is dirty, commit all changes (pick a sensible Conventional Commit message), then `git pull --rebase`; if rebase conflicts and cannot resolve, stop; otherwise `git push`. +- `sync`: if working tree is dirty, commit all changes (pick a sensible Conventional Commit message), then `git pull --rebase`; if rebase conflicts and cannot resolve, stop; otherwise `git push`. ### PR Workflow (Review vs Land) - **Review mode (PR link only):** read `gh pr view/diff`; **do not** switch branches; **do not** change code. @@ -80,6 +105,8 @@ ## Agent-Specific Notes - Vocabulary: "makeup" = "mac app". +- Never edit `node_modules` (global/Homebrew/npm/git installs too). Updates overwrite. Skill notes go in `tools.md` or `AGENTS.md`. +- Signal: "update fly" => `fly ssh console -a flawd-bot -C "bash -lc 'cd /data/clawd/clawdbot && git pull --rebase origin main'"` then `fly machines restart e825232f34d058 -a flawd-bot`. - When working on a GitHub Issue or PR, print the full URL at the end of the task. - When answering questions, respond with high-confidence answers only: verify in code; do not guess. - Never update the Carbon dependency. @@ -88,7 +115,7 @@ - CLI progress: use `src/cli/progress.ts` (`osc-progress` + `@clack/prompts` spinner); don’t hand-roll spinners/bars. - Status output: keep tables + ANSI-safe wrapping (`src/terminal/table.ts`); `status --all` = read-only/pasteable, `status --deep` = probes. - Gateway currently runs only as the menubar app; there is no separate LaunchAgent/helper label installed. Restart via the Clawdbot Mac app or `scripts/restart-mac.sh`; to verify/kill use `launchctl print gui/$UID | grep clawdbot` rather than assuming a fixed label. **When debugging on macOS, start/stop the gateway via the app, not ad-hoc tmux sessions; kill any temporary tunnels before handoff.** -- macOS logs: use `./scripts/clawlog.sh` (aka `vtlog`) to query unified logs for the Clawdbot subsystem; it supports follow/tail/category filters and expects passwordless sudo for `/usr/bin/log`. +- macOS logs: use `./scripts/clawlog.sh` to query unified logs for the Clawdbot subsystem; it supports follow/tail/category filters and expects passwordless sudo for `/usr/bin/log`. - If shared guardrails are available locally, review them; otherwise follow this repo's guidance. - SwiftUI state management (iOS/macOS): prefer the `Observation` framework (`@Observable`, `@Bindable`) over `ObservableObject`/`@StateObject`; don’t introduce new `ObservableObject` unless required for compatibility, and migrate existing usages when touching related code. - Connection providers: when adding a new connection, update every UI surface and docs (macOS app, web UI, mobile if applicable, onboarding/overview docs) and add matching status + configuration forms so provider lists and settings stay in sync. @@ -105,14 +132,17 @@ - **Multi-agent safety:** do **not** switch branches / check out a different branch unless explicitly requested. - **Multi-agent safety:** running multiple agents is OK as long as each agent has its own session. - **Multi-agent safety:** when you see unrecognized files, keep going; focus on your changes and commit only those. +- Lint/format churn: + - If staged+unstaged diffs are formatting-only, auto-resolve without asking. + - If commit/push already requested, auto-stage and include formatting-only follow-ups in the same commit (or a tiny follow-up commit if needed), no extra confirmation. + - Only ask when changes are semantic (logic/data/behavior). - Lobster seam: use the shared CLI palette in `src/terminal/palette.ts` (no hardcoded colors); apply palette to onboarding/config prompts and other TTY UI output as needed. - **Multi-agent safety:** focus reports on your edits; avoid guard-rail disclaimers unless truly blocked; when multiple agents touch the same file, continue if safe; end with a brief “other files present” note only if relevant. - Bug investigations: read source code of relevant npm dependencies and all related local code before concluding; aim for high-confidence root cause. - Code style: add brief comments for tricky logic; keep files under ~500 LOC when feasible (split/refactor as needed). - Tool schema guardrails (google-antigravity): avoid `Type.Union` in tool input schemas; no `anyOf`/`oneOf`/`allOf`. Use `stringEnum`/`optionalStringEnum` (Type.Unsafe enum) for string lists, and `Type.Optional(...)` instead of `... | null`. Keep top-level tool schema as `type: "object"` with `properties`. - Tool schema guardrails: avoid raw `format` property names in tool schemas; some validators treat `format` as a reserved keyword and reject the schema. -- When asked to open a “session” file, open the Pi session logs under `~/.clawdbot/agents/main/sessions/*.jsonl` (newest unless a specific ID is given), not the default `sessions.json`. If logs are needed from another machine, SSH via Tailscale and read the same path there. -- Menubar dimming + restart flow mirrors Trimmy: use `scripts/restart-mac.sh` (kills all Clawdbot variants, runs `swift build`, packages, relaunches). Icon dimming depends on MenuBarExtraAccess wiring in AppMain; keep `appearsDisabled` updates intact when touching the status item. +- When asked to open a “session” file, open the Pi session logs under `~/.clawdbot/agents//sessions/*.jsonl` (use the `agent=` value in the Runtime line of the system prompt; newest unless a specific ID is given), not the default `sessions.json`. If logs are needed from another machine, SSH via Tailscale and read the same path there. - Do not rebuild the macOS app over SSH; rebuilds must be run directly on the Mac. - Never send streaming/partial replies to external messaging surfaces (WhatsApp, Telegram); only final replies should be delivered there. Streaming/tool events may still go to internal UIs/control channel. - Voice wake forwarding tips: @@ -128,19 +158,3 @@ - Publish: `npm publish --access public --otp=""` (run from the package dir). - Verify without local npmrc side effects: `npm view version --userconfig "$(mktemp)"`. - Kill the tmux session after publish. - -## Exclamation Mark Escaping Workaround -The Claude Code Bash tool escapes `!` to `\\!` in command arguments. When using `clawdbot message send` with messages containing exclamation marks, use heredoc syntax: - -```bash -# WRONG - will send "Hello\\!" with backslash -clawdbot message send --to "+1234" --message 'Hello!' - -# CORRECT - use heredoc to avoid escaping -clawdbot message send --to "+1234" --message "$(cat <<'EOF' -Hello! -EOF -)" -``` - -This is a Claude Code quirk, not a clawdbot bug. diff --git a/CHANGELOG.md b/CHANGELOG.md index 93ea9893f..c274f668a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,173 +2,351 @@ Docs: https://docs.clawd.bot -## 2026.1.18-5 +## 2026.1.24 ### Changes -- Dependencies: update core + plugin deps (grammy, vitest, openai, Microsoft agents hosting, etc.). -- Agents: make inbound message envelopes configurable (timezone/timestamp/elapsed) and surface elapsed gaps. (#1150) — thanks @shiv19. +- Docs: expand FAQ (migration, scheduling, concurrency, model recommendations, OpenAI subscription auth, Pi sizing, hackable install, docs SSL workaround). +- Docs: add verbose installer troubleshooting guidance. +- Docs: update Fly.io guide notes. ### Fixes -- Configure: hide OpenRouter auto routing model from the model picker. (#1182) — thanks @zerone0x. +- Web UI: hide internal `message_id` hints in chat bubbles. +- Heartbeat: normalize target identifiers for consistent routing. -## 2026.1.18-4 +## 2026.1.23-1 + +### Fixes +- Packaging: include dist/tts output in npm tarball (fixes missing dist/tts/tts.js). + +## 2026.1.23 + +### Highlights +- TTS: move Telegram TTS into core + enable model-driven TTS tags by default for expressive audio replies. (#1559) Thanks @Glucksberg. https://docs.clawd.bot/tts +- Gateway: add `/tools/invoke` HTTP endpoint for direct tool calls (auth + tool policy enforced). (#1575) Thanks @vignesh07. https://docs.clawd.bot/gateway/tools-invoke-http-api +- Heartbeat: per-channel visibility controls (OK/alerts/indicator). (#1452) Thanks @dlauer. https://docs.clawd.bot/gateway/heartbeat +- Deploy: add Fly.io deployment support + guide. (#1570) https://docs.clawd.bot/platforms/fly +- Channels: add Tlon/Urbit channel plugin (DMs, group mentions, thread replies). (#1544) Thanks @wca4a. https://docs.clawd.bot/channels/tlon ### Changes -- macOS: switch PeekabooBridge integration to the tagged Swift Package Manager release (no submodule). -- macOS: stop syncing Peekaboo as a git submodule in postinstall. -- Swabble: use the tagged Commander Swift package release. -- CLI: add `clawdbot acp client` interactive ACP harness for debugging. -- Plugins: route command detection/text chunking helpers through the plugin runtime and drop runtime exports from the SDK. +- Channels: allow per-group tool allow/deny policies across built-in + plugin channels. (#1546) Thanks @adam91holt. https://docs.clawd.bot/multi-agent-sandbox-tools +- Agents: add Bedrock auto-discovery defaults + config overrides. (#1553) Thanks @fal3. https://docs.clawd.bot/bedrock +- CLI: add `clawdbot system` for system events + heartbeat controls; remove standalone `wake`. (commit 71203829d) https://docs.clawd.bot/cli/system +- CLI: add live auth probes to `clawdbot models status` for per-profile verification. (commit 40181afde) https://docs.clawd.bot/cli/models +- CLI: restart the gateway by default after `clawdbot update`; add `--no-restart` to skip it. (commit 2c85b1b40) +- Browser: add node-host proxy auto-routing for remote gateways (configurable per gateway/node). (commit c3cb26f7c) +- Plugins: add optional `llm-task` JSON-only tool for workflows. (#1498) Thanks @vignesh07. https://docs.clawd.bot/tools/llm-task +- Markdown: add per-channel table conversion (bullets for Signal/WhatsApp, code blocks elsewhere). (#1495) Thanks @odysseus0. +- Agents: keep system prompt time zone-only and move current time to `session_status` for better cache hits. (commit 66eec295b) +- Agents: remove redundant bash tool alias from tool registration/display. (#1571) Thanks @Takhoffman. +- Docs: add cron vs heartbeat decision guide (with Lobster workflow notes). (#1533) Thanks @JustYannicc. https://docs.clawd.bot/automation/cron-vs-heartbeat +- Docs: clarify HEARTBEAT.md empty file skips heartbeats, missing file still runs. (#1535) Thanks @JustYannicc. https://docs.clawd.bot/gateway/heartbeat + +### Fixes +- Sessions: accept non-UUID sessionIds for history/send/status while preserving agent scoping. (#1518) +- Heartbeat: accept plugin channel ids for heartbeat target validation + UI hints. +- Messaging/Sessions: mirror outbound sends into target session keys (threads + dmScope), create session entries on send, and normalize session key casing. (#1520, commit 4b6cdd1d3) +- Sessions: reject array-backed session stores to prevent silent wipes. (#1469) +- Gateway: compare Linux process start time to avoid PID recycling lock loops; keep locks unless stale. (#1572) Thanks @steipete. +- Gateway: accept null optional fields in exec approval requests. (#1511) Thanks @pvoo. +- Exec approvals: persist allowlist entry ids to keep macOS allowlist rows stable. (#1521) Thanks @ngutman. +- Exec: honor tools.exec ask/security defaults for elevated approvals (avoid unwanted prompts). (commit 5662a9cdf) +- Daemon: use platform PATH delimiters when building minimal service paths. (commit a4e57d3ac) +- Linux: include env-configured user bin roots in systemd PATH and align PATH audits. (#1512) Thanks @robbyczgw-cla. +- Tailscale: retry serve/funnel with sudo only for permission errors and keep original failure details. (#1551) Thanks @sweepies. +- Docker: update gateway command in docker-compose and Hetzner guide. (#1514) +- Agents: show tool error fallback when the last assistant turn only invoked tools (prevents silent stops). (commit 8ea8801d0) +- Agents: ignore IDENTITY.md template placeholders when parsing identity. (#1556) +- Agents: drop orphaned OpenAI Responses reasoning blocks on model switches. (#1562) Thanks @roshanasingh4. +- Agents: add CLI log hint to "agent failed before reply" messages. (#1550) Thanks @sweepies. +- Agents: warn and ignore tool allowlists that only reference unknown or unloaded plugin tools. (#1566) +- Agents: treat plugin-only tool allowlists as opt-ins; keep core tools enabled. (#1467) +- Agents: honor enqueue overrides for embedded runs to avoid queue deadlocks in tests. (commit 084002998) +- Slack: honor open groupPolicy for unlisted channels in message + slash gating. (#1563) Thanks @itsjaydesu. +- Discord: limit autoThread mention bypass to bot-owned threads; keep ack reactions mention-gated. (#1511) Thanks @pvoo. +- Discord: retry rate-limited allowlist resolution + command deploy to avoid gateway crashes. (commit f70ac0c7c) +- Mentions: ignore mentionPattern matches when another explicit mention is present in group chats (Slack/Discord/Telegram/WhatsApp). (commit d905ca0e0) +- Telegram: render markdown in media captions. (#1478) +- MS Teams: remove `.default` suffix from Graph scopes and Bot Framework probe scopes. (#1507, #1574) Thanks @Evizero. +- Browser: keep extension relay tabs controllable when the extension reuses a session id after switching tabs. (#1160) +- Voice wake: auto-save wake words on blur/submit across iOS/Android and align limits with macOS. (commit 69f645c66) +- UI: keep the Control UI sidebar visible while scrolling long pages. (#1515) Thanks @pookNast. +- UI: cache Control UI markdown rendering + memoize chat text extraction to reduce Safari typing jank. (commit d57cb2e1a) +- TUI: forward unknown slash commands, include Gateway commands in autocomplete, and render slash replies as system output. (commit 1af227b61, commit 8195497ce, commit 6fba598ea) +- CLI: auth probe output polish (table output, inline errors, reduced noise, and wrap fixes in `clawdbot models status`). (commit da3f2b489, commit 00ae21bed, commit 31e59cd58, commit f7dc27f2d, commit 438e782f8, commit 886752217, commit aabe0bed3, commit 81535d512, commit c63144ab1) +- Media: only parse `MEDIA:` tags when they start the line to avoid stripping prose mentions. (#1206) +- Media: preserve PNG alpha when possible; fall back to JPEG when still over size cap. (#1491) Thanks @robbyczgw-cla. +- Skills: gate bird Homebrew install to macOS. (#1569) Thanks @bradleypriest. + +## 2026.1.22 + +### Changes +- Highlight: Compaction safeguard now uses adaptive chunking, progressive fallback, and UI status + retries. (#1466) Thanks @dlauer. +- Providers: add Antigravity usage tracking to status output. (#1490) Thanks @patelhiren. +- Slack: add chat-type reply threading overrides via `replyToModeByChatType`. (#1442) Thanks @stefangalescu. +- BlueBubbles: add `asVoice` support for MP3/CAF voice memos in sendAttachment. (#1477, #1482) Thanks @Nicell. +- Onboarding: add hatch choice (TUI/Web/Later), token explainer, background dashboard seed on macOS, and showcase link. + +### Fixes +- BlueBubbles: stop typing indicator on idle/no-reply. (#1439) Thanks @Nicell. +- Message tool: keep path/filePath as-is for send; hydrate buffers only for sendAttachment. (#1444) Thanks @hopyky. +- Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla. +- Control UI: resolve local avatar URLs with basePath across injection + identity RPC. (#1457) Thanks @dlauer. +- Agents: sanitize assistant history text to strip tool-call markers. (#1456) Thanks @zerone0x. +- Discord: clarify Message Content Intent onboarding hint. (#1487) Thanks @kyleok. +- Gateway: stop the service before uninstalling and fail if it remains loaded. +- Agents: surface concrete API error details instead of generic AI service errors. +- Exec: fall back to non-PTY when PTY spawn fails (EBADF). (#1484) +- Exec approvals: allow per-segment allowlists for chained shell commands on gateway + node hosts. (#1458) Thanks @czekaj. +- Agents: make OpenAI sessions image-sanitize-only; gate tool-id/repair sanitization by provider. +- Doctor: honor CLAWDBOT_GATEWAY_TOKEN for auth checks and security audit token reuse. (#1448) Thanks @azade-c. +- Agents: make tool summaries more readable and only show optional params when set. +- Agents: honor SOUL.md guidance even when the file is nested or path-qualified. (#1434) Thanks @neooriginal. +- Matrix (plugin): persist m.direct for resolved DMs and harden room fallback. (#1436, #1486) Thanks @sibbl. +- CLI: prefer `~` for home paths in output. +- Mattermost (plugin): enforce pairing/allowlist gating, keep @username targets, and clarify plugin-only docs. (#1428) Thanks @damoahdominic. +- Agents: centralize transcript sanitization in the runner; keep tags and error turns intact. +- Auth: skip auth profiles in cooldown during initial selection and rotation. (#1316) Thanks @odrobnik. +- Agents/TUI: honor user-pinned auth profiles during cooldown and preserve search picker ranking. (#1432) Thanks @tobiasbischoff. +- Docs: fix gog auth services example to include docs scope. (#1454) Thanks @zerone0x. +- Slack: reduce WebClient retries to avoid duplicate sends. (#1481) +- Slack: read thread replies for message reads when threadId is provided (replies-only). (#1450) Thanks @rodrigouroz. +- Discord: honor accountId across message actions and cron deliveries. (#1492) Thanks @svkozak. +- macOS: prefer linked channels in gateway summary to avoid false “not linked” status. +- macOS/tests: fix gateway summary lookup after guard unwrap; prevent browser opens during tests. (ECID-1483) + +## 2026.1.21-2 + +### Fixes +- Control UI: ignore bootstrap identity placeholder text for avatar values and fall back to the default avatar. https://docs.clawd.bot/cli/agents https://docs.clawd.bot/web/control-ui +- Slack: remove deprecated `filetype` field from `files.uploadV2` to eliminate API warnings. (#1447) + +## 2026.1.21 + +### Changes +- Highlight: Lobster optional plugin tool for typed workflows + approval gates. https://docs.clawd.bot/tools/lobster +- Lobster: allow workflow file args via `argsJson` in the plugin tool. https://docs.clawd.bot/tools/lobster +- Heartbeat: allow running heartbeats in an explicit session key. (#1256) Thanks @zknicker. +- CLI: default exec approvals to the local host, add gateway/node targeting flags, and show target details in allowlist output. +- CLI: exec approvals mutations render tables instead of raw JSON. +- Exec approvals: support wildcard agent allowlists (`*`) across all agents. +- Exec approvals: allowlist matches resolved binary paths only, add safe stdin-only bins, and tighten allowlist shell parsing. +- Nodes: expose node PATH in status/describe and bootstrap PATH for node-host execution. +- CLI: flatten node service commands under `clawdbot node` and remove `service node` docs. +- CLI: move gateway service commands under `clawdbot gateway` and add `gateway probe` for reachability. +- Sessions: add per-channel reset overrides via `session.resetByChannel`. (#1353) Thanks @cash-echo-bot. +- Agents: add identity avatar config support and Control UI avatar rendering. (#1329, #1424) Thanks @dlauer. +- UI: show per-session assistant identity in the Control UI. (#1420) Thanks @robbyczgw-cla. +- CLI: add `clawdbot update wizard` for interactive channel selection and restart prompts. https://docs.clawd.bot/cli/update +- Signal: add typing indicators and DM read receipts via signal-cli. +- MSTeams: add file uploads, adaptive cards, and attachment handling improvements. (#1410) Thanks @Evizero. +- Onboarding: remove the run setup-token auth option (paste setup-token or reuse CLI creds instead). +- Docs: add troubleshooting entry for gateway.mode blocking gateway start. https://docs.clawd.bot/gateway/troubleshooting +- Docs: add /model allowlist troubleshooting note. (#1405) +- Docs: add per-message Gmail search example for gog. (#1220) Thanks @mbelinky. + +### Breaking +- **BREAKING:** Control UI now rejects insecure HTTP without device identity by default. Use HTTPS (Tailscale Serve) or set `gateway.controlUi.allowInsecureAuth: true` to allow token-only auth. https://docs.clawd.bot/web/control-ui#insecure-http +- **BREAKING:** Envelope and system event timestamps now default to host-local time (was UTC) so agents don’t have to constantly convert. + +### Fixes +- Nodes/macOS: prompt on allowlist miss for node exec approvals, persist allowlist decisions, and flatten node invoke errors. (#1394) Thanks @ngutman. +- Gateway: keep auto bind loopback-first and add explicit tailnet binding to avoid Tailscale taking over local UI. (#1380) +- Memory: prevent CLI hangs by deferring vector probes, adding sqlite-vec/embedding timeouts, and showing sync progress early. +- Agents: enforce 9-char alphanumeric tool call ids for Mistral providers. (#1372) Thanks @zerone0x. +- Embedded runner: persist injected history images so attachments aren’t reloaded each turn. (#1374) Thanks @Nicell. +- Nodes tool: include agent/node/gateway context in tool failure logs to speed approval debugging. +- macOS: exec approvals now respect wildcard agent allowlists (`*`). +- macOS: allow SSH agent auth when no identity file is set. (#1384) Thanks @ameno-. +- Gateway: prevent multiple gateways from sharing the same config/state at once (singleton lock). +- UI: remove the chat stop button and keep the composer aligned to the bottom edge. +- Typing: start instant typing indicators at run start so DMs and mentions show immediately. +- Configure: restrict the model allowlist picker to OAuth-compatible Anthropic models and preselect Opus 4.5. +- Configure: seed model fallbacks from the allowlist selection when multiple models are chosen. +- Model picker: list the full catalog when no model allowlist is configured. +- Discord: honor wildcard channel configs via shared match helpers. (#1334) Thanks @pvoo. +- BlueBubbles: resolve short message IDs safely and expose full IDs in templates. (#1387) Thanks @tyler6204. +- Infra: preserve fetch helper methods when wrapping abort signals. (#1387) +- macOS: default distribution packaging to universal binaries. (#1396) Thanks @JustYannicc. + +## 2026.1.20 + +### Changes +- Control UI: add copy-as-markdown with error feedback. (#1345) https://docs.clawd.bot/web/control-ui +- Control UI: drop the legacy list view. (#1345) https://docs.clawd.bot/web/control-ui +- TUI: add syntax highlighting for code blocks. (#1200) https://docs.clawd.bot/tui +- TUI: session picker shows derived titles, fuzzy search, relative times, and last message preview. (#1271) https://docs.clawd.bot/tui +- TUI: add a searchable model picker for quicker model selection. (#1198) https://docs.clawd.bot/tui +- TUI: add input history (up/down) for submitted messages. (#1348) https://docs.clawd.bot/tui +- ACP: add `clawdbot acp` for IDE integrations. https://docs.clawd.bot/cli/acp +- ACP: add `clawdbot acp client` interactive harness for debugging. https://docs.clawd.bot/cli/acp +- Skills: add download installs with OS-filtered options. https://docs.clawd.bot/tools/skills +- Skills: add the local sherpa-onnx-tts skill. https://docs.clawd.bot/tools/skills +- Memory: add hybrid BM25 + vector search (FTS5) with weighted merging and fallback. https://docs.clawd.bot/concepts/memory +- Memory: add SQLite embedding cache to speed up reindexing and frequent updates. https://docs.clawd.bot/concepts/memory +- Memory: add OpenAI batch indexing for embeddings when configured. https://docs.clawd.bot/concepts/memory +- Memory: enable OpenAI batch indexing by default for OpenAI embeddings. https://docs.clawd.bot/concepts/memory +- Memory: allow parallel OpenAI batch indexing jobs (default concurrency: 2). https://docs.clawd.bot/concepts/memory +- Memory: render progress immediately, color batch statuses in verbose logs, and poll OpenAI batch status every 2s by default. https://docs.clawd.bot/concepts/memory +- Memory: add `--verbose` logging for memory status + batch indexing details. https://docs.clawd.bot/concepts/memory +- Memory: add native Gemini embeddings provider for memory search. (#1151) https://docs.clawd.bot/concepts/memory +- Browser: allow config defaults for efficient snapshots in the tool/CLI. (#1336) https://docs.clawd.bot/tools/browser +- Nostr: add the Nostr channel plugin with profile management + onboarding defaults. (#1323) https://docs.clawd.bot/channels/nostr +- Matrix: migrate to matrix-bot-sdk with E2EE support, location handling, and group allowlist upgrades. (#1298) https://docs.clawd.bot/channels/matrix +- Slack: add HTTP webhook mode via Bolt HTTP receiver. (#1143) https://docs.clawd.bot/channels/slack +- Telegram: enrich forwarded-message context with normalized origin details + legacy fallback. (#1090) https://docs.clawd.bot/channels/telegram +- Discord: fall back to `/skill` when native command limits are exceeded. (#1287) +- Discord: expose `/skill` globally. (#1287) +- Zalouser: add channel dock metadata, config schema, setup wiring, probe, and status issues. (#1219) https://docs.clawd.bot/plugins/zalouser +- Plugins: require manifest-embedded config schemas with preflight validation warnings. (#1272) https://docs.clawd.bot/plugins/manifest +- Plugins: move channel catalog metadata into plugin manifests. (#1290) https://docs.clawd.bot/plugins/manifest +- Plugins: align Nextcloud Talk policy helpers with core patterns. (#1290) https://docs.clawd.bot/plugins/manifest +- Plugins/UI: let channel plugin metadata drive UI labels/icons and cron channel options. (#1306) https://docs.clawd.bot/web/control-ui +- Agents/UI: add agent avatar support in identity config, IDENTITY.md, and the Control UI. (#1329) https://docs.clawd.bot/gateway/configuration +- Plugins: add plugin slots with a dedicated memory slot selector. https://docs.clawd.bot/plugins/agent-tools +- Plugins: ship the bundled BlueBubbles channel plugin (disabled by default). https://docs.clawd.bot/channels/bluebubbles +- Plugins: migrate bundled messaging extensions to the plugin SDK and resolve plugin-sdk imports in the loader. +- Plugins: migrate the Zalo plugin to the shared plugin SDK runtime. https://docs.clawd.bot/channels/zalo +- Plugins: migrate the Zalo Personal plugin to the shared plugin SDK runtime. https://docs.clawd.bot/plugins/zalouser +- Plugins: allow optional agent tools with explicit allowlists and add the plugin tool authoring guide. https://docs.clawd.bot/plugins/agent-tools - Plugins: auto-enable bundled channel/provider plugins when configuration is present. +- Plugins: sync plugin sources on channel switches and update npm-installed plugins during `clawdbot update`. +- Plugins: share npm plugin update logic between `clawdbot update` and `clawdbot plugins update`. + +- Gateway/API: add `/v1/responses` (OpenResponses) with item-based input + semantic streaming events. (#1229) +- Gateway/API: expand `/v1/responses` to support file/image inputs, tool_choice, usage, and output limits. (#1229) +- Usage: add `/usage cost` summaries and macOS menu cost charts. https://docs.clawd.bot/reference/api-usage-costs +- Security: warn when <=300B models run without sandboxing while web tools are enabled. https://docs.clawd.bot/cli/security +- Exec: add host/security/ask routing for gateway + node exec. https://docs.clawd.bot/tools/exec +- Exec: add `/exec` directive for per-session exec defaults (host/security/ask/node). https://docs.clawd.bot/tools/exec +- Exec approvals: migrate approvals to `~/.clawdbot/exec-approvals.json` with per-agent allowlists + skill auto-allow toggle, and add approvals UI + node exec lifecycle events. https://docs.clawd.bot/tools/exec-approvals +- Nodes: add headless node host (`clawdbot node start`) for `system.run`/`system.which`. https://docs.clawd.bot/cli/node +- Nodes: add node daemon service install/status/start/stop/restart. https://docs.clawd.bot/cli/node +- Bridge: add `skills.bins` RPC to support node host auto-allow skill bins. +- Sessions: add daily reset policy with per-type overrides and idle windows (default 4am local), preserving legacy idle-only configs. (#1146) https://docs.clawd.bot/concepts/session +- Sessions: allow `sessions_spawn` to override thinking level for sub-agent runs. https://docs.clawd.bot/tools/subagents +- Channels: unify thread/topic allowlist matching + command/mention gating helpers across core providers. https://docs.clawd.bot/concepts/groups +- Models: add Qwen Portal OAuth provider support. (#1120) https://docs.clawd.bot/providers/qwen +- Onboarding: add allowlist prompts and username-to-id resolution across core and extension channels. https://docs.clawd.bot/start/onboarding +- Docs: clarify allowlist input types and onboarding behavior for messaging channels. https://docs.clawd.bot/start/onboarding +- Docs: refresh Android node discovery docs for the Gateway WS service type. https://docs.clawd.bot/platforms/android +- Docs: surface Amazon Bedrock in provider lists and clarify Bedrock auth env vars. (#1289) https://docs.clawd.bot/bedrock +- Docs: clarify WhatsApp voice notes. https://docs.clawd.bot/channels/whatsapp +- Docs: clarify Windows WSL portproxy LAN access notes. https://docs.clawd.bot/platforms/windows +- Docs: refresh bird skill install metadata and usage notes. (#1302) https://docs.clawd.bot/tools/browser-login +- Agents: add local docs path resolution and include docs/mirror/source/community pointers in the system prompt. +- Agents: clarify node_modules read-only guidance in agent instructions. - Config: stamp last-touched metadata on write and warn if the config is newer than the running build. - macOS: hide usage section when usage is unavailable instead of showing provider errors. -- Memory: add native Gemini embeddings provider for memory search. (#1151) -- Agents: add local docs path resolution and include docs/mirror/source/community pointers in the system prompt. -- Slack: add HTTP webhook mode via Bolt HTTP receiver for Events API deployments. (#1143) — thanks @jdrhyne. +- Android: migrate node transport to the Gateway WebSocket protocol with TLS pinning support + gateway discovery naming. +- Android: send structured payloads in node events/invokes and include user-agent metadata in gateway connects. +- Android: remove legacy bridge transport code now that nodes use the gateway protocol. +- Android: bump okhttp + dnsjava to satisfy lint dependency checks. +- Build: update workspace + core/plugin deps. +- Build: use tsgo for dev/watch builds by default (opt out with `CLAWDBOT_TS_COMPILER=tsc`). +- Repo: remove the Peekaboo git submodule now that the SPM release is used. +- macOS: switch PeekabooBridge integration to the tagged Swift Package Manager release. +- macOS: stop syncing Peekaboo in postinstall. +- Swabble: use the tagged Commander Swift package release. + +### Breaking +- **BREAKING:** Reject invalid/unknown config entries and refuse to start the gateway for safety. Run `clawdbot doctor --fix` to repair, then update plugins (`clawdbot plugins update`) if you use any. ### Fixes -- Auth profiles: keep auto-pinned preference while allowing rotation on failover; user pins stay locked. (#1138) — thanks @cheeeee. +- Discovery: shorten Bonjour DNS-SD service type to `_clawdbot-gw._tcp` and update discovery clients/docs. +- Diagnostics: export OTLP logs, correct queue depth tracking, and document message-flow telemetry. +- Diagnostics: emit message-flow diagnostics across channels via shared dispatch. (#1244) +- Diagnostics: gate heartbeat/webhook logging. (#1244) +- Gateway: strip inbound envelope headers from chat history messages to keep clients clean. +- Gateway: clarify unauthorized handshake responses with token/password mismatch guidance. +- Gateway: allow mobile node client ids for iOS + Android handshake validation. (#1354) +- Gateway: clarify connect/validation errors for gateway params. (#1347) +- Gateway: preserve restart wake routing + thread replies across restarts. (#1337) +- Gateway: reschedule per-agent heartbeats on config hot reload without restarting the runner. +- Gateway: require authorized restarts for SIGUSR1 (restart/apply/update) so config gating can't be bypassed. +- Cron: auto-deliver isolated agent output to explicit targets without tool calls. (#1285) +- Agents: preserve subagent announce thread/topic routing + queued replies across channels. (#1241) +- Agents: propagate accountId into embedded runs so sub-agent announce routing honors the originating account. (#1058) +- Agents: avoid treating timeout errors with "aborted" messages as user aborts, so model fallback still runs. (#1137) - Agents: sanitize oversized image payloads before send and surface image-dimension errors. -- macOS: Doctor repairs LaunchAgent bootstrap issues for Gateway + Node when listed but not loaded. (#1166) — thanks @AlexMikhalev. -- macOS: avoid touching launchd in Remote over SSH so quitting the app no longer disables the remote gateway. (#1105) +- Sessions: fall back to session labels when listing display names. (#1124) +- Compaction: include tool failure summaries in safeguard compaction to prevent retry loops. (#1084) +- Config: log invalid config issues once per run and keep invalid-config errors stackless. +- Config: allow Perplexity as a web_search provider in config validation. (#1230) +- Config: allow custom fields under `skills.entries..config` for skill credentials/config. (#1226) +- Doctor: clarify plugin auto-enable hint text in the startup banner. +- Doctor: canonicalize legacy session keys in session stores to prevent stale metadata. (#1169) +- Docs: make docs:list fail fast with a clear error if the docs directory is missing. +- Plugins: add Nextcloud Talk manifest for plugin config validation. (#1297) +- Plugins: surface plugin load/register/config errors in gateway logs with plugin/source context. +- CLI: preserve cron delivery settings when editing message payloads. (#1322) +- CLI: keep `clawdbot logs` output resilient to broken pipes while preserving progress output. +- CLI: avoid duplicating --profile/--dev flags when formatting commands. +- CLI: centralize CLI command registration to keep fast-path routing and program wiring in sync. (#1207) +- CLI: keep banners on routed commands, restore config guarding outside fast-path routing, and tighten fast-path flag parsing while skipping console capture for extra speed. (#1195) +- CLI: skip runner rebuilds when dist is fresh. (#1231) +- CLI: add WSL2/systemd unavailable hints in daemon status/doctor output. +- Status: route native `/status` to the active agent so model selection reflects the correct profile. (#1301) +- Status: show both usage windows with reset hints when usage data is available. (#1101) +- UI: keep config form enums typed, preserve empty strings, protect sensitive defaults, and deepen config search. (#1315) +- UI: preserve ordered list numbering in chat markdown. (#1341) +- UI: allow Control UI to read gatewayUrl from URL params for remote WebSocket targets. (#1342) +- UI: prevent double-scroll in Control UI chat by locking chat layout to the viewport. (#1283) +- UI: enable shell mode for sync Windows spawns to avoid `pnpm ui:build` EINVAL. (#1212) +- TUI: keep thinking blocks ordered before content during streaming and isolate per-run assembly. (#1202) +- TUI: align custom editor initialization with the latest pi-tui API. (#1298) +- TUI: show generic empty-state text for searchable pickers. (#1201) +- TUI: highlight model search matches and stabilize search ordering. +- Configure: hide OpenRouter auto routing model from the model picker. (#1182) +- Memory: show total file counts + scan issues in `clawdbot memory status`. +- Memory: fall back to non-batch embeddings after repeated batch failures. +- Memory: apply OpenAI batch defaults even without explicit remote config. - Memory: index atomically so failed reindex preserves the previous memory database. (#1151) - Memory: avoid sqlite-vec unique constraint failures when reindexing duplicate chunk ids. (#1151) - -## 2026.1.18-3 - -### Changes -- Exec: add host/security/ask routing for gateway + node exec. -- Exec: add `/exec` directive for per-session exec defaults (host/security/ask/node). -- macOS: migrate exec approvals to `~/.clawdbot/exec-approvals.json` with per-agent allowlists and skill auto-allow toggle. -- macOS: add approvals socket UI server + node exec lifecycle events. -- Nodes: add headless node host (`clawdbot node start`) for `system.run`/`system.which`. -- Nodes: add node daemon service install/status/start/stop/restart. -- Bridge: add `skills.bins` RPC to support node host auto-allow skill bins. -- Slash commands: replace `/cost` with `/usage off|tokens|full` to control per-response usage footer; `/usage` no longer aliases `/status`. (Supersedes #1140) — thanks @Nachx639. -- Sessions: add daily reset policy with per-type overrides and idle windows (default 4am local), preserving legacy idle-only configs. (#1146) — thanks @austinm911. -- Agents: auto-inject local image references for vision models and avoid reloading history images. (#1098) — thanks @tyler6204. -- Docs: refresh exec/elevated/exec-approvals docs for the new flow. https://docs.clawd.bot/tools/exec-approvals -- Docs: add node host CLI + update exec approvals/bridge protocol docs. https://docs.clawd.bot/cli/node -- ACP: add experimental ACP support for IDE integrations (`clawdbot acp`). Thanks @visionik. -- Tools: allow `sessions_spawn` to override thinking level for sub-agent runs. -- Channels: unify thread/topic allowlist matching + command/mention gating helpers across core providers. -- Models: add Qwen Portal OAuth provider support. (#1120) — thanks @mukhtharcm. -- Memory: add `--verbose` logging for memory status + batch indexing details. -- Memory: allow parallel OpenAI batch indexing jobs (default concurrency: 2). -- macOS: add per-agent exec approvals with allowlists, skill CLI auto-allow, and settings UI. -- Docs: add exec approvals guide and link from tools index. https://docs.clawd.bot/tools/exec-approvals -- macOS: add exec-host IPC for node service `system.run` with HMAC + peer UID checks. - -### Fixes -- Exec approvals: enforce allowlist when ask is off; prefer raw command for node approvals/events. -- Tools: return a companion-app-required message when node exec is requested with no paired node. -- Streaming: emit assistant deltas for OpenAI-compatible SSE chunks. (#1147) — thanks @alauppe. -- Model fallback: treat timeout aborts as failover while preserving user aborts. (#1137) — thanks @cheeeee. - -## 2026.1.18-2 - -### Fixes -- Tests: stabilize plugin SDK resolution and embedded agent timeouts. - -## 2026.1.18-1 - -### Changes -- Tools: allow `sessions_spawn` to override thinking level for sub-agent runs. -- Channels: unify thread/topic allowlist matching + command/mention gating helpers across core providers. -- Models: add Qwen Portal OAuth provider support. (#1120) — thanks @mukhtharcm. -- Memory: add `--verbose` logging for memory status + batch indexing details. -- Memory: allow parallel OpenAI batch indexing jobs (default concurrency: 2). -- macOS: add per-agent exec approvals with allowlists, skill CLI auto-allow, and settings UI. -- Docs: add exec approvals guide and link from tools index. https://docs.clawd.bot/tools/exec-approvals - -### Fixes -- Memory: apply OpenAI batch defaults even without explicit remote config. -- macOS: bundle Textual resources in packaged app builds to avoid code block crashes. (#1006) -- Tools: return a companion-app-required message when `system.run` is requested without a supporting node. -- Discord: only emit slow listener warnings after 30s. - -## 2026.1.17-6 - -### Changes -- Plugins: add exclusive plugin slots with a dedicated memory slot selector. -- Memory: ship core memory tools + CLI as the bundled `memory-core` plugin. -- Docs: document plugin slots and memory plugin behavior. -- Plugins: add the bundled BlueBubbles channel plugin (disabled by default). -- Plugins: migrate bundled messaging extensions to the plugin SDK; resolve plugin-sdk imports in loader. -- Plugins: migrate the Zalo plugin to the shared plugin SDK runtime. -- Plugins: migrate the Zalo Personal plugin to the shared plugin SDK runtime. - -## 2026.1.17-5 - -### Changes -- Memory: add hybrid BM25 + vector search (FTS5) with weighted merging and fallback. -- Memory: add SQLite embedding cache to speed up reindexing and frequent updates. -- CLI: surface FTS + embedding cache state in `clawdbot memory status`. -- Memory: render progress immediately, color batch statuses in verbose logs, and poll OpenAI batch status every 2s by default. -- Plugins: allow optional agent tools with explicit allowlists and add plugin tool authoring guide. https://docs.clawd.bot/plugins/agent-tools -- Tools: centralize plugin tool policy helpers. -- Commands: add `/subagents info` and show sub-agent counts in `/status`. -- Docs: clarify plugin agent tool configuration. https://docs.clawd.bot/plugins/agent-tools - -### Fixes -- Voice call: include request query in Twilio webhook verification when publicUrl is set. (#864) - -## 2026.1.18-1 - -### Changes -- Tools: allow `sessions_spawn` to override thinking level for sub-agent runs. -- Channels: unify thread/topic allowlist matching + command/mention gating helpers across core providers. -- Models: add Qwen Portal OAuth provider support. (#1120) — thanks @mukhtharcm. -- Memory: add `--verbose` logging for memory status + batch indexing details. -- Memory: allow parallel OpenAI batch indexing jobs (default concurrency: 2). -- macOS: add per-agent exec approvals with allowlists, skill CLI auto-allow, and settings UI. -- Docs: add exec approvals guide and link from tools index. https://docs.clawd.bot/tools/exec-approvals - -### Fixes -- Memory: apply OpenAI batch defaults even without explicit remote config. -- macOS: bundle Textual resources in packaged app builds to avoid code block crashes. (#1006) -- Tools: return a companion-app-required message when `system.run` is requested without a supporting node. -- Discord: only emit slow listener warnings after 30s. -## 2026.1.17-3 - -### Changes -- Memory: add OpenAI Batch API indexing for embeddings when configured. -- Memory: enable OpenAI batch indexing by default for OpenAI embeddings. - -### Fixes - Memory: retry transient 5xx errors (Cloudflare) during embedding indexing. - -## 2026.1.17-2 - -### Changes - -### Fixes -- Tools: show exec elevated flag before the command and keep it outside markdown in tool summaries. - Memory: parallelize embedding indexing with rate-limit retries. - Memory: split overly long lines to keep embeddings under token limits. - Memory: skip empty chunks to avoid invalid embedding inputs. -- Sessions: fall back to session labels when listing display names. (#1124) — thanks @abdaraxus. -- Discord: inherit parent channel allowlists for thread slash commands and reactions. (#1123) — thanks @thewilloftheshadow. - -## 2026.1.17-1 - -### Changes -- Telegram: enrich forwarded message context with normalized origin details + legacy fallback. (#1090) — thanks @sleontenko. -- macOS: strip prerelease/build suffixes when parsing gateway semver patches. (#1110) — thanks @zerone0x. -- macOS: keep CLI install pinned to the full build suffix. (#1111) — thanks @artuskg. -- CLI: surface update availability in `clawdbot status`. -- CLI: add `clawdbot memory status --deep/--index` probes. -- CLI: add playful update completion quips. - -### Fixes -- Doctor: avoid re-adding WhatsApp ack reaction config when only legacy auth files exist. (#1087) — thanks @YuriNachos. -- Hooks: parse multi-line/YAML frontmatter metadata blocks (JSON5-friendly). (#1114) — thanks @sebslight. -- CLI: add WSL2/systemd unavailable hints in daemon status/doctor output. -- Windows: install gateway scheduled task as the current user; show friendly guidance instead of failing on access denied. -- Status: show both usage windows with reset hints when usage data is available. (#1101) — thanks @rhjoh. -- Memory: probe sqlite-vec availability in `clawdbot memory status`. - Memory: split embedding batches to avoid OpenAI token limits during indexing. -- Telegram: preserve hidden text_link URLs by expanding entities in inbound text. (#1118) — thanks @sleontenko. +- Memory: probe sqlite-vec availability in `clawdbot memory status`. +- Exec approvals: enforce allowlist when ask is off. +- Exec approvals: prefer raw command for node approvals/events. +- Tools: show exec elevated flag before the command and keep it outside markdown in tool summaries. +- Tools: return a companion-app-required message when node exec is requested with no paired node. +- Tools: return a companion-app-required message when `system.run` is requested without a supporting node. +- Exec: default gateway/node exec security to allowlist when unset (sandbox stays deny). +- Exec: prefer bash when fish is default shell, falling back to sh if bash is missing. (#1297) +- Exec: merge login-shell PATH for host=gateway exec while keeping daemon PATH minimal. (#1304) +- Streaming: emit assistant deltas for OpenAI-compatible SSE chunks. (#1147) +- Discord: make resolve warnings avoid raw JSON payloads on rate limits. +- Discord: process message handlers in parallel across sessions to avoid event queue blocking. (#1295) +- Discord: stop reconnecting the gateway after aborts to prevent duplicate listeners. +- Discord: only emit slow listener warnings after 30s. +- Discord: inherit parent channel allowlists for thread slash commands and reactions. (#1123) +- Telegram: honor pairing allowlists for native slash commands. +- Telegram: preserve hidden text_link URLs by expanding entities in inbound text. (#1118) +- Slack: resolve Bolt import interop for Bun + Node. (#1191) +- Web search: infer Perplexity base URL from API key source (direct vs OpenRouter). +- Web fetch: harden SSRF protection with shared hostname checks and redirect limits. (#1346) +- Browser: register AI snapshot refs for act commands. (#1282) +- Voice call: include request query in Twilio webhook verification when publicUrl is set. (#864) +- Anthropic: default API prompt caching to 1h with configurable TTL override. +- Anthropic: ignore TTL for OAuth. +- Auth profiles: keep auto-pinned preference while allowing rotation on failover. (#1138) +- Auth profiles: user pins stay locked. (#1138) +- Model catalog: avoid caching import failures, log transient discovery errors, and keep partial results. (#1332) +- Tests: stabilize Windows gateway/CLI tests by skipping sidecars, normalizing argv, and extending timeouts. +- Tests: stabilize plugin SDK resolution and embedded agent timeouts. +- Windows: install gateway scheduled task as the current user. +- Windows: show friendly guidance instead of failing on access denied. +- macOS: load menu session previews asynchronously so items populate while the menu is open. +- macOS: use label colors for session preview text so previews render in menu subviews. +- macOS: suppress usage error text in the menubar cost view. +- macOS: Doctor repairs LaunchAgent bootstrap issues for Gateway + Node when listed but not loaded. (#1166) +- macOS: avoid touching launchd in Remote over SSH so quitting the app no longer disables the remote gateway. (#1105) +- macOS: bundle Textual resources in packaged app builds to avoid code block crashes. (#1006) +- Daemon: include HOME in service environments to avoid missing HOME errors. (#1214) + +Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @NicholaiVogel, @RyanLisse, @ThePickle31, @VACInc, @Whoaa512, @YuriNachos, @aaronveklabs, @abdaraxus, @alauppe, @ameno-, @artuskg, @austinm911, @bradleypriest, @cheeeee, @dougvk, @fogboots, @gnarco, @gumadeiras, @jdrhyne, @joelklabo, @longmaba, @mukhtharcm, @odysseus0, @oscargavin, @rhjoh, @sebslight, @sibbl, @sleontenko, @steipete, @suminhthanh, @thewilloftheshadow, @tyler6204, @vignesh07, @visionik, @ysqander, @zerone0x. ## 2026.1.16-2 diff --git a/README.md b/README.md index 446c366ef..8f4411f12 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,13 @@

CI status GitHub release + DeepWiki Discord MIT License

**Clawdbot** is a *personal AI assistant* you run on your own devices. -It answers you on the channels you already use (WhatsApp, Telegram, Slack, Discord, Signal, iMessage, Microsoft Teams, WebChat), can speak and listen on macOS/iOS/Android, and can render a live Canvas you control. The Gateway is just the control plane — the product is the assistant. +It answers you on the channels you already use (WhatsApp, Telegram, Slack, Discord, Signal, iMessage, Microsoft Teams, WebChat), plus extension channels like BlueBubbles, Matrix, Zalo, and Zalo Personal. It can speak and listen on macOS/iOS/Android, and can render a live Canvas you control. The Gateway is just the control plane — the product is the assistant. If you want a personal, single-user assistant that feels local, fast, and always-on, this is it. @@ -64,12 +65,21 @@ clawdbot gateway --port 18789 --verbose # Send a message clawdbot message send --to +1234567890 --message "Hello from Clawdbot" -# Talk to the assistant (optionally deliver back to WhatsApp/Telegram/Slack/Discord/Microsoft Teams) +# Talk to the assistant (optionally deliver back to any connected channel: WhatsApp/Telegram/Slack/Discord/Signal/iMessage/BlueBubbles/Microsoft Teams/Matrix/Zalo/Zalo Personal/WebChat) clawdbot agent --message "Ship checklist" --thinking high ``` Upgrading? [Updating guide](https://docs.clawd.bot/install/updating) (and run `clawdbot doctor`). +## Development channels + +- **stable**: tagged releases (`vYYYY.M.D` or `vYYYY.M.D-`), npm dist-tag `latest`. +- **beta**: prerelease tags (`vYYYY.M.D-beta.N`), npm dist-tag `beta` (macOS app may be missing). +- **dev**: moving head of `main`, npm dist-tag `dev` (when published). + +Switch channels (git + npm): `clawdbot update --channel stable|beta|dev`. +Details: [Development channels](https://docs.clawd.bot/install/development-channels). + ## From source (development) Prefer `pnpm` for builds from source. Bun is optional for running TypeScript directly. @@ -106,7 +116,7 @@ Run `clawdbot doctor` to surface risky/misconfigured DM policies. ## Highlights - **[Local-first Gateway](https://docs.clawd.bot/gateway)** — single control plane for sessions, channels, tools, and events. -- **[Multi-channel inbox](https://docs.clawd.bot/channels)** — WhatsApp, Telegram, Slack, Discord, Signal, iMessage, Microsoft Teams, WebChat, macOS, iOS/Android. +- **[Multi-channel inbox](https://docs.clawd.bot/channels)** — WhatsApp, Telegram, Slack, Discord, Signal, iMessage, BlueBubbles, Microsoft Teams, Matrix, Zalo, Zalo Personal, WebChat, macOS, iOS/Android. - **[Multi-agent routing](https://docs.clawd.bot/gateway/configuration)** — route inbound channels/accounts/peers to isolated agents (workspaces + per-agent sessions). - **[Voice Wake](https://docs.clawd.bot/nodes/voicewake) + [Talk Mode](https://docs.clawd.bot/nodes/talk)** — always-on speech for macOS/iOS/Android with ElevenLabs. - **[Live Canvas](https://docs.clawd.bot/platforms/mac/canvas)** — agent-driven visual workspace with [A2UI](https://docs.clawd.bot/platforms/mac/canvas#canvas-a2ui). @@ -128,7 +138,7 @@ Run `clawdbot doctor` to surface risky/misconfigured DM policies. - [Media pipeline](https://docs.clawd.bot/nodes/images): images/audio/video, transcription hooks, size caps, temp file lifecycle. Audio details: [Audio](https://docs.clawd.bot/nodes/audio). ### Channels -- [Channels](https://docs.clawd.bot/channels): [WhatsApp](https://docs.clawd.bot/channels/whatsapp) (Baileys), [Telegram](https://docs.clawd.bot/channels/telegram) (grammY), [Slack](https://docs.clawd.bot/channels/slack) (Bolt), [Discord](https://docs.clawd.bot/channels/discord) (discord.js), [Signal](https://docs.clawd.bot/channels/signal) (signal-cli), [iMessage](https://docs.clawd.bot/channels/imessage) (imsg), [Microsoft Teams](https://docs.clawd.bot/channels/msteams) (Bot Framework), [WebChat](https://docs.clawd.bot/web/webchat). +- [Channels](https://docs.clawd.bot/channels): [WhatsApp](https://docs.clawd.bot/channels/whatsapp) (Baileys), [Telegram](https://docs.clawd.bot/channels/telegram) (grammY), [Slack](https://docs.clawd.bot/channels/slack) (Bolt), [Discord](https://docs.clawd.bot/channels/discord) (discord.js), [Signal](https://docs.clawd.bot/channels/signal) (signal-cli), [iMessage](https://docs.clawd.bot/channels/imessage) (imsg), [BlueBubbles](https://docs.clawd.bot/channels/bluebubbles) (extension), [Microsoft Teams](https://docs.clawd.bot/channels/msteams) (extension), [Matrix](https://docs.clawd.bot/channels/matrix) (extension), [Zalo](https://docs.clawd.bot/channels/zalo) (extension), [Zalo Personal](https://docs.clawd.bot/channels/zalouser) (extension), [WebChat](https://docs.clawd.bot/web/webchat). - [Group routing](https://docs.clawd.bot/concepts/group-messages): mention gating, reply tags, per-channel chunking and routing. Channel rules: [Channels](https://docs.clawd.bot/channels). ### Apps + nodes @@ -159,7 +169,7 @@ Run `clawdbot doctor` to surface risky/misconfigured DM policies. ## How it works (short) ``` -WhatsApp / Telegram / Slack / Discord / Signal / iMessage / Microsoft Teams / WebChat +WhatsApp / Telegram / Slack / Discord / Signal / iMessage / BlueBubbles / Microsoft Teams / Matrix / Zalo / Zalo Personal / WebChat │ ▼ ┌───────────────────────────────┐ @@ -257,12 +267,7 @@ Send these in WhatsApp/Telegram/Slack/Microsoft Teams/WebChat (group commands ar The Gateway alone delivers a great experience. All apps are optional and add extra features. -If you plan to build/run companion apps, initialize submodules first: - -```bash -git submodule update --init --recursive -./scripts/restart-mac.sh -``` +If you plan to build/run companion apps, follow the platform runbooks below. ### macOS (Clawdbot.app) (optional) @@ -466,33 +471,37 @@ by Peter Steinberger and the community. See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines, maintainers, and how to submit PRs. AI/vibe-coded PRs welcome! 🤖 -Special thanks to @andrewting19 for the Anthropic OAuth tool-name fix. - -Core contributors: -- @cpojer — Telegram onboarding UX + docs +Special thanks to [Mario Zechner](https://mariozechner.at/) for his support and for +[pi-mono](https://github.com/badlogic/pi-mono). Thanks to all clawtributors:

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

diff --git a/Swabble/Package.resolved b/Swabble/Package.resolved index 2b2b7f857..24de6ea3a 100644 --- a/Swabble/Package.resolved +++ b/Swabble/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "5d29ee82825e0764775562242cfa1ff4dc79584797dd638f76c9876545454748", + "originHash" : "c0677e232394b5f6b0191b6dbb5bae553d55264f65ae725cd03a8ffdfda9cdd3", "pins" : [ { - "identity" : "elevenlabskit", + "identity" : "commander", "kind" : "remoteSourceControl", - "location" : "https://github.com/steipete/ElevenLabsKit", + "location" : "https://github.com/steipete/Commander.git", "state" : { - "revision" : "c8679fbd37416a8780fe43be88a497ff16209e2d", - "version" : "0.1.0" + "revision" : "9e349575c8e3c6745e81fe19e5bb5efa01b078ce", + "version" : "0.2.1" } }, { diff --git a/Swabble/README.md b/Swabble/README.md index 7a5015460..bf6dc3dc8 100644 --- a/Swabble/README.md +++ b/Swabble/README.md @@ -101,8 +101,8 @@ Environment variables: - Authorization requested at first start; requires macOS 26 + new Speech.framework APIs. ## Development -- Format: `./scripts/format.sh` (uses ../peekaboo/.swiftformat if present) -- Lint: `./scripts/lint.sh` (uses ../peekaboo/.swiftlint.yml if present) +- Format: `./scripts/format.sh` (uses local `.swiftformat`) +- Lint: `./scripts/lint.sh` (uses local `.swiftlint.yml`) - Tests: `swift test` (uses swift-testing package) ## Roadmap diff --git a/Swabble/scripts/format.sh b/Swabble/scripts/format.sh index 0ce82a7fb..dd12e45bb 100755 --- a/Swabble/scripts/format.sh +++ b/Swabble/scripts/format.sh @@ -1,10 +1,5 @@ #!/bin/bash set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" -PEEKABOO_ROOT="${ROOT}/../peekaboo" -if [ -f "${PEEKABOO_ROOT}/.swiftformat" ]; then - CONFIG="${PEEKABOO_ROOT}/.swiftformat" -else - CONFIG="${ROOT}/.swiftformat" -fi +CONFIG="${ROOT}/.swiftformat" swiftformat --config "$CONFIG" "$ROOT/Sources" diff --git a/Swabble/scripts/lint.sh b/Swabble/scripts/lint.sh index 650f09176..d674628d0 100755 --- a/Swabble/scripts/lint.sh +++ b/Swabble/scripts/lint.sh @@ -1,12 +1,7 @@ #!/bin/bash set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" -PEEKABOO_ROOT="${ROOT}/../peekaboo" -if [ -f "${PEEKABOO_ROOT}/.swiftlint.yml" ]; then - CONFIG="${PEEKABOO_ROOT}/.swiftlint.yml" -else - CONFIG="$ROOT/.swiftlint.yml" -fi +CONFIG="${ROOT}/.swiftlint.yml" if ! command -v swiftlint >/dev/null; then echo "swiftlint not installed" >&2 exit 1 diff --git a/appcast.xml b/appcast.xml index 3fa448960..bed929dfb 100644 --- a/appcast.xml +++ b/appcast.xml @@ -3,273 +3,213 @@ Clawdbot - 2026.1.16-2 - Sat, 17 Jan 2026 12:46:22 +0000 + 2026.1.23 + Sat, 24 Jan 2026 13:02:18 +0000 https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml - 6273 - 2026.1.16-2 + 7750 + 2026.1.23 15.0 - Clawdbot 2026.1.16-2 + Clawdbot 2026.1.23 +

Highlights

+
    +
  • TTS: allow model-driven TTS tags by default for expressive audio replies (laughter, singing cues, etc.).
  • +

Changes

    -
  • CLI: stamp build commit into dist metadata so banners show the commit in npm installs.
  • +
  • Gateway: add /tools/invoke HTTP endpoint for direct tool calls and document it. (#1575) Thanks @vignesh07.
  • +
  • Agents: keep system prompt time zone-only and move current time to session_status for better cache hits.
  • +
  • Agents: remove redundant bash tool alias from tool registration/display. (#1571) Thanks @Takhoffman.
  • +
  • Browser: add node-host proxy auto-routing for remote gateways (configurable per gateway/node).
  • +
  • Heartbeat: add per-channel visibility controls (OK/alerts/indicator). (#1452) Thanks @dlauer.
  • +
  • Plugins: add optional llm-task JSON-only tool for workflows. (#1498) Thanks @vignesh07.
  • +
  • CLI: restart the gateway by default after clawdbot update; add --no-restart to skip it.
  • +
  • CLI: add live auth probes to clawdbot models status for per-profile verification.
  • +
  • CLI: add clawdbot system for system events + heartbeat controls; remove standalone wake.
  • +
  • Agents: add Bedrock auto-discovery defaults + config overrides. (#1553) Thanks @fal3.
  • +
  • Docs: add cron vs heartbeat decision guide (with Lobster workflow notes). (#1533) Thanks @JustYannicc.
  • +
  • Docs: clarify HEARTBEAT.md empty file skips heartbeats, missing file still runs. (#1535) Thanks @JustYannicc.
  • +
  • Markdown: add per-channel table conversion (bullets for Signal/WhatsApp, code blocks elsewhere). (#1495) Thanks @odysseus0.
  • +
  • Tlon: add Urbit channel plugin (DMs, group mentions, thread replies). (#1544) Thanks @wca4a.
  • +
  • Channels: allow per-group tool allow/deny policies across built-in + plugin channels. (#1546) Thanks @adam91holt.
  • +
  • TTS: move Telegram TTS into core with auto-replies, commands, and gateway methods. (#1559) Thanks @Glucksberg.
  • +
+

Fixes

+
    +
  • Sessions: accept non-UUID sessionIds for history/send/status while preserving agent scoping. (#1518)
  • +
  • Gateway: compare Linux process start time to avoid PID recycling lock loops; keep locks unless stale. (#1572) Thanks @steipete.
  • +
  • Messaging: mirror outbound sends into target session keys (threads + dmScope) and create session entries on send. (#1520)
  • +
  • Sessions: normalize session key casing to lowercase for consistent routing.
  • +
  • BlueBubbles: normalize group session keys for outbound mirroring. (#1520)
  • +
  • Skills: gate bird Homebrew install to macOS. (#1569) Thanks @bradleypriest.
  • +
  • Slack: honor open groupPolicy for unlisted channels in message + slash gating. (#1563) Thanks @itsjaydesu.
  • +
  • Agents: show tool error fallback when the last assistant turn only invoked tools (prevents silent stops).
  • +
  • Agents: ignore IDENTITY.md template placeholders when parsing identity to avoid placeholder replies. (#1556)
  • +
  • Agents: drop orphaned OpenAI Responses reasoning blocks on model switches. (#1562) Thanks @roshanasingh4.
  • +
  • Docker: update gateway command in docker-compose and Hetzner guide. (#1514)
  • +
  • Sessions: reject array-backed session stores to prevent silent wipes. (#1469)
  • +
  • Voice wake: auto-save wake words on blur/submit across iOS/Android and align limits with macOS.
  • +
  • UI: keep the Control UI sidebar visible while scrolling long pages. (#1515) Thanks @pookNast.
  • +
  • UI: cache Control UI markdown rendering + memoize chat text extraction to reduce Safari typing jank.
  • +
  • Tailscale: retry serve/funnel with sudo only for permission errors and keep original failure details. (#1551) Thanks @sweepies.
  • +
  • Agents: add CLI log hint to "agent failed before reply" messages. (#1550) Thanks @sweepies.
  • +
  • Discord: limit autoThread mention bypass to bot-owned threads; keep ack reactions mention-gated. (#1511) Thanks @pvoo.
  • +
  • Discord: retry rate-limited allowlist resolution + command deploy to avoid gateway crashes.
  • +
  • Mentions: ignore mentionPattern matches when another explicit mention is present in group chats (Slack/Discord/Telegram/WhatsApp).
  • +
  • Gateway: accept null optional fields in exec approval requests. (#1511) Thanks @pvoo.
  • +
  • Exec: honor tools.exec ask/security defaults for elevated approvals (avoid unwanted prompts).
  • +
  • TUI: forward unknown slash commands (for example, /context) to the Gateway.
  • +
  • TUI: include Gateway slash commands in autocomplete and /help.
  • +
  • CLI: skip usage lines in clawdbot models status when provider usage is unavailable.
  • +
  • CLI: suppress diagnostic session/run noise during auth probes.
  • +
  • CLI: hide auth probe timeout warnings from embedded runs.
  • +
  • CLI: render auth probe results as a table in clawdbot models status.
  • +
  • CLI: suppress probe-only embedded logs unless --verbose is set.
  • +
  • CLI: move auth probe errors below the table to reduce wrapping.
  • +
  • CLI: prevent ANSI color bleed when table cells wrap.
  • +
  • CLI: explain when auth profiles are excluded by auth.order in probe details.
  • +
  • CLI: drop the em dash when the banner tagline wraps to a second line.
  • +
  • CLI: inline auth probe errors in status rows to reduce wrapping.
  • +
  • Telegram: render markdown in media captions. (#1478)
  • +
  • Agents: honor enqueue overrides for embedded runs to avoid queue deadlocks in tests.
  • +
  • Agents: trigger model fallback when auth profiles are all in cooldown or unavailable. (#1522)
  • +
  • Daemon: use platform PATH delimiters when building minimal service paths.
  • +
  • Tests: skip embedded runner ordering assertion on Windows to avoid CI timeouts.
  • +
  • Linux: include env-configured user bin roots in systemd PATH and align PATH audits. (#1512) Thanks @robbyczgw-cla.
  • +
  • TUI: render Gateway slash-command replies as system output (for example, /context).
  • +
  • Media: only parse MEDIA: tags when they start the line to avoid stripping prose mentions. (#1206)
  • +
  • Media: preserve PNG alpha when possible; fall back to JPEG when still over size cap. (#1491) Thanks @robbyczgw-cla.
  • +
  • Agents: treat plugin-only tool allowlists as opt-ins; keep core tools enabled. (#1467)
  • +
  • Exec approvals: persist allowlist entry ids to keep macOS allowlist rows stable. (#1521) Thanks @ngutman.
  • +
  • MS Teams (plugin): remove .default suffix from Graph scopes to avoid double-appending. (#1507) Thanks @Evizero.
  • +
  • MS Teams (plugin): remove .default suffix from Bot Framework probe scope to avoid double-appending. (#1574) Thanks @Evizero.
  • +
  • Browser: keep extension relay tabs controllable when the extension reuses a session id after switching tabs. (#1160)
  • +
  • Agents: warn and ignore tool allowlists that only reference unknown or unloaded plugin tools. (#1566)

View full changelog

]]>
- +
- 2026.1.15 - Fri, 16 Jan 2026 10:31:53 +0000 + 2026.1.22 + Fri, 23 Jan 2026 08:58:14 +0000 https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml - 5998 - 2026.1.15 + 7530 + 2026.1.22 15.0 - Clawdbot 2026.1.15 + Clawdbot 2026.1.22 +

Changes

+
    +
  • Highlight: Compaction safeguard now uses adaptive chunking, progressive fallback, and UI status + retries. (#1466) Thanks @dlauer.
  • +
  • Providers: add Antigravity usage tracking to status output. (#1490) Thanks @patelhiren.
  • +
  • Slack: add chat-type reply threading overrides via replyToModeByChatType. (#1442) Thanks @stefangalescu.
  • +
  • BlueBubbles: add asVoice support for MP3/CAF voice memos in sendAttachment. (#1477, #1482) Thanks @Nicell.
  • +
  • Onboarding: add hatch choice (TUI/Web/Later), token explainer, background dashboard seed on macOS, and showcase link.
  • +
+

Fixes

+
    +
  • BlueBubbles: stop typing indicator on idle/no-reply. (#1439) Thanks @Nicell.
  • +
  • Message tool: keep path/filePath as-is for send; hydrate buffers only for sendAttachment. (#1444) Thanks @hopyky.
  • +
  • Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla.
  • +
  • Control UI: resolve local avatar URLs with basePath across injection + identity RPC. (#1457) Thanks @dlauer.
  • +
  • Agents: sanitize assistant history text to strip tool-call markers. (#1456) Thanks @zerone0x.
  • +
  • Discord: clarify Message Content Intent onboarding hint. (#1487) Thanks @kyleok.
  • +
  • Gateway: stop the service before uninstalling and fail if it remains loaded.
  • +
  • Agents: surface concrete API error details instead of generic AI service errors.
  • +
  • Exec: fall back to non-PTY when PTY spawn fails (EBADF). (#1484)
  • +
  • Exec approvals: allow per-segment allowlists for chained shell commands on gateway + node hosts. (#1458) Thanks @czekaj.
  • +
  • Agents: make OpenAI sessions image-sanitize-only; gate tool-id/repair sanitization by provider.
  • +
  • Doctor: honor CLAWDBOT_GATEWAY_TOKEN for auth checks and security audit token reuse. (#1448) Thanks @azade-c.
  • +
  • Agents: make tool summaries more readable and only show optional params when set.
  • +
  • Agents: honor SOUL.md guidance even when the file is nested or path-qualified. (#1434) Thanks @neooriginal.
  • +
  • Matrix (plugin): persist m.direct for resolved DMs and harden room fallback. (#1436, #1486) Thanks @sibbl.
  • +
  • CLI: prefer ~ for home paths in output.
  • +
  • Mattermost (plugin): enforce pairing/allowlist gating, keep @username targets, and clarify plugin-only docs. (#1428) Thanks @damoahdominic.
  • +
  • Agents: centralize transcript sanitization in the runner; keep tags and error turns intact.
  • +
  • Auth: skip auth profiles in cooldown during initial selection and rotation. (#1316) Thanks @odrobnik.
  • +
  • Agents/TUI: honor user-pinned auth profiles during cooldown and preserve search picker ranking. (#1432) Thanks @tobiasbischoff.
  • +
  • Docs: fix gog auth services example to include docs scope. (#1454) Thanks @zerone0x.
  • +
  • Slack: reduce WebClient retries to avoid duplicate sends. (#1481)
  • +
  • Slack: read thread replies for message reads when threadId is provided (replies-only). (#1450) Thanks @rodrigouroz.
  • +
  • macOS: prefer linked channels in gateway summary to avoid false “not linked” status.
  • +
  • macOS/tests: fix gateway summary lookup after guard unwrap; prevent browser opens during tests. (ECID-1483)
  • +
+

View full changelog

+]]>
+ +
+ + 2026.1.21 + Thu, 22 Jan 2026 12:22:35 +0000 + https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml + 7374 + 2026.1.21 + 15.0 + Clawdbot 2026.1.21

Highlights

    -
  • Plugins: add provider auth registry + clawdbot models auth login for plugin-driven OAuth/API key flows.
  • -
  • Browser: improve remote CDP/Browserless support (auth passthrough, wss upgrade, timeouts, clearer errors).
  • -
  • Heartbeat: per-agent configuration + 24h duplicate suppression. (#980) — thanks @voidserf.
  • -
  • Security: audit warns on weak model tiers; app nodes store auth tokens encrypted (Keychain/SecurePrefs).
  • +
  • Lobster optional plugin tool for typed workflows + approval gates. https://docs.clawd.bot/tools/lobster
  • +
  • Custom assistant identity + avatars in the Control UI. https://docs.clawd.bot/cli/agents https://docs.clawd.bot/web/control-ui
  • +
  • Cache optimizations: cache-ttl pruning + defaults reduce token spend on cold requests. https://docs.clawd.bot/concepts/session-pruning
  • +
  • Exec approvals + elevated ask/full modes. https://docs.clawd.bot/tools/exec-approvals https://docs.clawd.bot/tools/elevated
  • +
  • Signal typing/read receipts + MSTeams attachments. https://docs.clawd.bot/channels/signal https://docs.clawd.bot/channels/msteams
  • +
  • /models UX refresh + clawdbot update wizard. https://docs.clawd.bot/cli/models https://docs.clawd.bot/cli/update
  • +
+

Changes

+
    +
  • Highlight: Lobster optional plugin tool for typed workflows + approval gates. https://docs.clawd.bot/tools/lobster (#1152) Thanks @vignesh07.
  • +
  • Agents/UI: add identity avatar config support and Control UI avatar rendering. (#1329, #1424) Thanks @dlauer. https://docs.clawd.bot/gateway/configuration https://docs.clawd.bot/cli/agents
  • +
  • Control UI: add custom assistant identity support and per-session identity display. (#1420) Thanks @robbyczgw-cla. https://docs.clawd.bot/web/control-ui
  • +
  • CLI: add clawdbot update wizard with interactive channel selection + restart prompts, plus preflight checks before rebasing. https://docs.clawd.bot/cli/update
  • +
  • Models/Commands: add /models, improve /model listing UX, and expand clawdbot models paging. (#1398) Thanks @vignesh07. https://docs.clawd.bot/cli/models
  • +
  • CLI: move gateway service commands under clawdbot gateway, flatten node service commands under clawdbot node, and add gateway probe for reachability. https://docs.clawd.bot/cli/gateway https://docs.clawd.bot/cli/node
  • +
  • Exec: add elevated ask/full modes, tighten allowlist gating, and render approvals tables on write. https://docs.clawd.bot/tools/elevated https://docs.clawd.bot/tools/exec-approvals
  • +
  • Exec approvals: default to local host, add gateway/node targeting + target details, support wildcard agent allowlists, and tighten allowlist parsing/safe bins. https://docs.clawd.bot/cli/approvals https://docs.clawd.bot/tools/exec-approvals
  • +
  • Heartbeat: allow explicit session keys and active hours. (#1256) Thanks @zknicker. https://docs.clawd.bot/gateway/heartbeat
  • +
  • Sessions: add per-channel idle durations via sessions.channelIdleMinutes. (#1353) Thanks @cash-echo-bot.
  • +
  • Nodes: run exec-style, expose PATH in status/describe, and bootstrap PATH for node-host execution. https://docs.clawd.bot/cli/node
  • +
  • Cache: add cache.ttlPrune mode and auth-aware defaults for cache TTL behavior.
  • +
  • Queue: add per-channel debounce overrides for auto-reply. https://docs.clawd.bot/concepts/queue
  • +
  • Discord: add wildcard channel config support. (#1334) Thanks @pvoo. https://docs.clawd.bot/channels/discord
  • +
  • Signal: add typing indicators and DM read receipts via signal-cli. https://docs.clawd.bot/channels/signal
  • +
  • MSTeams: add file uploads, adaptive cards, and attachment handling improvements. (#1410) Thanks @Evizero. https://docs.clawd.bot/channels/msteams
  • +
  • Onboarding: remove the run setup-token auth option (paste setup-token or reuse CLI creds instead).
  • +
  • macOS: refresh Settings (location access in Permissions, connection mode in menu, remove CLI install UI).
  • +
  • Diagnostics: add cache trace config for debugging. (#1370) Thanks @parubets.
  • +
  • Docs: Lobster guides + org URL updates, /model allowlist troubleshooting, Gmail message search examples, gateway.mode troubleshooting, prompt injection guidance, npm prefix/node CLI notes, control UI dev gatewayUrl note, tool_use FAQ, showcase video, and sharp/node-gyp workaround. (#1427, #1220, #1405) Thanks @vignesh07, @mbelinky.

Breaking

    -
  • BREAKING: iOS minimum version is now 18.0 to support Textual markdown rendering in native chat. (#702)
  • -
  • BREAKING: Microsoft Teams is now a plugin; install @clawdbot/msteams via clawdbot plugins install @clawdbot/msteams.
  • -
-

Changes

-
    -
  • CLI: set process titles to clawdbot- for clearer process listings.
  • -
  • CLI/macOS: sync remote SSH target/identity to config and let gateway status auto-infer SSH targets (ssh-config aware).
  • -
  • Heartbeat: tighten prompt guidance + suppress duplicate alerts for 24h. (#980) — thanks @voidserf.
  • -
  • Sessions/Security: add session.dmScope for multi-user DM isolation and audit warnings. (#948) — thanks @Alphonse-arianee.
  • -
  • Plugins: add provider auth registry + clawdbot models auth login for plugin-driven OAuth/API key flows.
  • -
  • Onboarding: switch channels setup to a single-select loop with per-channel actions and disabled hints in the picker.
  • -
  • TUI: show provider/model labels for the active session and default model.
  • -
  • Heartbeat: add per-agent heartbeat configuration and multi-agent docs example.
  • -
  • UI: show gateway auth guidance + doc link on unauthorized Control UI connections.
  • -
  • Security: warn on weak model tiers (Haiku, below GPT-5, below Claude 4.5) in clawdbot security audit.
  • -
  • Apps: store node auth tokens encrypted (Keychain/SecurePrefs).
  • -
  • Daemon: share profile/state-dir resolution across service helpers and honor CLAWDBOT_STATE_DIR for Windows task scripts.
  • -
  • Docs: clarify multi-gateway rescue bot guidance. (#969) — thanks @bjesuiter.
  • -
  • Agents: add Current Date & Time system prompt section with configurable time format (auto/12/24).
  • -
  • Tools: normalize Slack/Discord message timestamps with timestampMs/timestampUtc while keeping raw provider fields.
  • -
  • macOS: add system.which for prompt-free remote skill discovery (with gateway fallback to system.run).
  • -
  • Docs: add Date & Time guide and update prompt/timezone configuration docs.
  • -
  • Messages: debounce rapid inbound messages across channels with per-connector overrides. (#971) — thanks @juanpablodlc.
  • -
  • Messages: allow media-only sends (CLI/tool) and show Telegram voice recording status for voice notes. (#957) — thanks @rdev.
  • -
  • Auth/Status: keep auth profiles sticky per session (rotate on compaction/new), surface provider usage headers in /status and clawdbot models status, and update docs.
  • -
  • CLI: add --json output for clawdbot daemon lifecycle/install commands.
  • -
  • Memory: make node-llama-cpp an optional dependency (avoid Node 25 install failures) and improve local-embeddings fallback/errors.
  • -
  • Browser: add snapshot refs=aria (Playwright aria-ref ids) for self-resolving refs across snapshotact.
  • -
  • Browser: profile="chrome" now defaults to host control and returns clearer “attach a tab” errors.
  • -
  • Browser: prefer stable Chrome for auto-detect, with Brave/Edge fallbacks and updated docs. (#983) — thanks @cpojer.
  • -
  • Browser: increase remote CDP reachability timeouts + add remoteCdpTimeoutMs/remoteCdpHandshakeTimeoutMs.
  • -
  • Browser: preserve auth/query tokens for remote CDP endpoints and pass Basic auth for CDP HTTP/WS. (#895) — thanks @mukhtharcm.
  • -
  • Telegram: add bidirectional reaction support with configurable notifications and agent guidance. (#964) — thanks @bohdanpodvirnyi.
  • -
  • Telegram: allow custom commands in the bot menu (merged with native; conflicts ignored). (#860) — thanks @nachoiacovino.
  • -
  • Discord: allow allowlisted guilds without channel lists to receive messages when groupPolicy="allowlist". — thanks @thewilloftheshadow.
  • -
  • Discord: allow emoji/sticker uploads + channel actions in config defaults. (#870) — thanks @JDIVE.
  • +
  • BREAKING: Control UI now rejects insecure HTTP without device identity by default. Use HTTPS (Tailscale Serve) or set gateway.controlUi.allowInsecureAuth: true to allow token-only auth. https://docs.clawd.bot/web/control-ui#insecure-http
  • +
  • BREAKING: Envelope and system event timestamps now default to host-local time (was UTC) so agents don’t have to constantly convert.

Fixes

    -
  • Fix: list model picker entries as provider/model pairs for explicit selection. (#970) — thanks @mcinteerj.
  • -
  • Fix: align OpenAI image-gen defaults with DALL-E 3 standard quality and document output formats. (#880) — thanks @mkbehr.
  • -
  • Fix: persist gateway.mode=local after selecting Local run mode in clawdbot configure, even if no other sections are chosen.
  • -
  • Daemon: fix profile-aware service label resolution (env-driven) and add coverage for launchd/systemd/schtasks. (#969) — thanks @bjesuiter.
  • -
  • Agents: avoid false positives when logging unsupported Google tool schema keywords.
  • -
  • Agents: skip Gemini history downgrades for google-antigravity to preserve tool calls. (#894) — thanks @mukhtharcm.
  • -
  • Status: restore usage summary line for current provider when no OAuth profiles exist.
  • -
  • Fix: guard model fallback against undefined provider/model values. (#954) — thanks @roshanasingh4.
  • -
  • Fix: refactor session store updates, add chat.inject, and harden subagent cleanup flow. (#944) — thanks @tyler6204.
  • -
  • Fix: clean up suspended CLI processes across backends. (#978) — thanks @Nachx639.
  • -
  • Fix: support MiniMax coding plan usage responses with model_remains/current_interval_* payloads.
  • -
  • Fix: suppress WhatsApp pairing replies for historical catch-up DMs on initial link. (#904)
  • -
  • Browser: extension mode recovers when only one tab is attached (stale targetId fallback).
  • -
  • Browser: fix tab not found for extension relay snapshots/actions when Playwright blocks newCDPSession (use the single available Page).
  • -
  • Browser: upgrade wswss when remote CDP uses https (fixes Browserless handshake).
  • -
  • Telegram: skip message_thread_id=1 for General topic sends while keeping typing indicators. (#848) — thanks @azade-c.
  • -
  • Fix: sanitize user-facing error text + strip tags across reply pipelines. (#975) — thanks @ThomsenDrake.
  • -
  • Fix: normalize pairing CLI aliases, allow extension channels, and harden Zalo webhook payload parsing. (#991) — thanks @longmaba.
  • -
  • Fix: allow local Tailscale Serve hostnames without treating tailnet clients as direct. (#885) — thanks @oswalpalash.
  • -
  • Fix: reset sessions after role-ordering conflicts to recover from consecutive user turns. (#998)
  • +
  • Streaming/Typing/Media: keep reply tags across streamed chunks, start typing indicators at run start, and accept MEDIA paths with spaces/tilde while preferring the message tool hint for image replies.
  • +
  • Agents/Providers: drop unsigned thinking blocks for Claude models (Google Antigravity) and enforce alphanumeric tool call ids for strict providers (Mistral/OpenRouter). (#1372) Thanks @zerone0x.
  • +
  • Exec approvals: treat main as the default agent, align node/gateway allowlist prechecks, validate resolved paths, avoid allowlist resolve races, and avoid null optional params. (#1417, #1414, #1425) Thanks @czekaj.
  • +
  • Exec/Windows: resolve Windows exec paths with extensions and handle safe-bin exe names.
  • +
  • Nodes/macOS: prompt on allowlist miss for node exec approvals, persist allowlist decisions, and flatten node invoke errors. (#1394) Thanks @ngutman.
  • +
  • Gateway: prevent multiple gateways from sharing the same config/state (singleton lock), keep auto bind loopback-first with explicit tailnet binding, and improve SSH auth handling. (#1380)
  • +
  • Control UI: remove the chat stop button, keep the composer aligned to the bottom edge, stabilize session previews, and refresh the debug panel on route-driven tab changes. (#1373) Thanks @yazinsai.
  • +
  • UI/config: export SECTION_META for config form modules. (#1418) Thanks @MaudeBot.
  • +
  • macOS: keep chat pinned during streaming replies, include Textual resources, respect wildcard exec approvals, allow SSH agent auth, and default distribution builds to universal binaries. (#1279, #1362, #1384, #1396) Thanks @ameno-, @JustYannicc.
  • +
  • BlueBubbles: resolve short message IDs safely, expose full IDs in templates, and harden short-id fetch wrappers. (#1369, #1387) Thanks @tyler6204.
  • +
  • Models/Configure: inherit session model overrides in threads/topics, map OpenCode Zen models to the correct APIs, narrow Anthropic OAuth allowlist handling, seed allowlist fallbacks, list the full catalog when no allowlist is set, and limit /model list output. (#1376, #1416)
  • +
  • Memory: prevent CLI hangs by deferring vector probes, add sqlite-vec/embedding timeouts, and make session memory indexing async.
  • +
  • Cron: cap reminder context history to 10 messages and honor contextMessages. (#1103) Thanks @mkbehr.
  • +
  • Cache: restore the 1h cache TTL option and reset the pruning window.
  • +
  • Zalo Personal: tolerate ANSI/log-prefixed JSON output from zca. (#1379) Thanks @ptn1411.
  • +
  • Browser: suppress Chrome restore prompts for managed profiles. (#1419) Thanks @jamesgroat.
  • +
  • Infra: preserve fetch helper methods/preconnect when wrapping abort signals and normalize Telegram fetch aborts.
  • +
  • Config/Doctor: avoid stack traces for invalid configs, log the config path, avoid WhatsApp config resurrection, and warn when gateway.mode is unset. (#900)
  • +
  • CLI: read Codex CLI account_id for workspace billing. (#1422) Thanks @aj47.
  • +
  • Logs/Status: align rolling log filenames with local time and report sandboxed runtime in clawdbot status. (#1343)
  • +
  • Embedded runner: persist injected history images so attachments aren’t reloaded each turn. (#1374) Thanks @Nicell.
  • +
  • Nodes/Subagents: include agent/node/gateway context in tool failure logs and ensure subagent list uses the command session.

View full changelog

]]>
- -
- - 2026.1.14-1 - Thu, 15 Jan 2026 11:14:40 +0000 - https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml - 5825 - 2026.1.14-1 - 15.0 - Clawdbot 2026.1.14-1 -

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.
  • -
  • 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.
  • -
-

Changes

-

Web Tools

-
    -
  • Tools: add web_search/web_fetch (Brave API), including helpful setup hints when the key is missing.
  • -
  • Tools: enable web_fetch by default (unless explicitly disabled in config).
  • -
  • CLI/Docs: add clawdbot configure --section web for storing Brave API keys and update onboarding tips.
  • -
-

Browser / Control UI

-
    -
  • Browser: add Chrome extension relay takeover mode (toolbar button) + clawdbot browser serve remote control + browser.controlToken.
  • -
  • Browser: ship a built-in chrome profile for extension relay and start the relay automatically when running locally.
  • -
  • Browser: default browser.defaultProfile to chrome (existing Chrome takeover mode).
  • -
  • Browser: add clawdbot browser extension install/path and copy extension path to clipboard.
  • -
  • Browser: add snapshot refs=aria (Playwright aria-ref ids) for self-resolving refs across snapshotact.
  • -
  • Browser: profile="chrome" now defaults to host control and returns clearer “attach a tab” errors.
  • -
  • Browser: extension mode recovers when only one tab is attached (stale targetId fallback).
  • -
  • Control UI: show raw any-map entries in config views; move Docs link into the left nav.
  • -
-

Plugins

-
    -
  • Plugins: add plugin HTTP hooks + loader updates to support channel plugins. (#854) — thanks @longmaba.
  • -
  • Plugins: add onboarding plugin install flow. (#854) — thanks @longmaba.
  • -
  • Channels: add Matrix plugin (external) with docs + onboarding hooks.
  • -
  • Voice Call: add Plivo provider (no SDK dependency). (#846) — thanks @vrknetha.
  • -
-

Security

-
    -
  • Security: expand clawdbot security audit checks and publish a SECURITY.md reporting policy.
  • -
  • Security: extend clawdbot security audit --fix to tighten more sensitive state paths.
  • -
  • Security: add detect-secrets CI scan and baseline guidance. (#227) — thanks @Hyaxia.
  • -
-

Onboarding / Daemon

-
    -
  • Onboarding: add a security checkpoint prompt (docs link + sandboxing hint); require --accept-risk for --non-interactive.
  • -
  • Daemon: support profile-aware service names for multi-gateway setups. (#671) — thanks @bjesuiter.
  • -
-

Auth / Usage / Config

-
    -
  • Usage: add MiniMax coding plan usage tracking.
  • -
  • Auth: label Claude Code CLI auth options. (#915) — thanks @SeanZoR.
  • -
  • Agents: add optional auth-profile copy prompt on agents add and improve auth error messaging.
  • -
  • Auth: add dynamic template variables to messages.responsePrefix. (#928) — thanks @sebslight.
  • -
  • Config: add channels..configWrites gating for channel-initiated config writes; migrate Slack channel IDs.
  • -
-

Channels

-
    -
  • Telegram: add message delete action in the message tool. (#903) — thanks @sleontenko.
  • -
  • WhatsApp: add channels.whatsapp.sendReadReceipts to disable auto read receipts. (#882) — thanks @chrisrodz.
  • -
-

Docs

-
    -
  • Docs: clarify per-agent auth stores, sandboxed skill binaries, and elevated semantics.
  • -
  • Docs: add FAQ entries for missing provider auth after adding agents and Gemini thinking signature errors.
  • -
  • Docs: expand gateway security hardening guidance and incident response checklist.
  • -
  • Docs: document DM history limits for channel DMs. (#883) — thanks @pkrmf.
  • -
  • Docs: standardize Claude Code CLI naming across docs and prompts. (follow-up to #915)
  • -
  • Docs: add per-command CLI doc pages and link them from clawdbot --help.
  • -
  • Docs: add multi-gateway guide (sidebar + nav).
  • -
-

Fixes

-

Gateway / Daemon / Sessions

-
    -
  • Gateway: forward termination signals to respawned CLI child processes to avoid orphaned systemd runs. (#933) — thanks @roshanasingh4.
  • -
  • Gateway/UI: ship session defaults in the hello snapshot so the Control UI canonicalizes main session keys (no bare main alias).
  • -
  • Agents: skip thinking/final tag stripping inside Markdown code spans. (#939) — thanks @ngutman.
  • -
  • Browser: add tests for snapshot labels/efficient query params and labeled image responses.
  • -
  • Browser: persist role snapshot refs per CDP target so snapshotact clicks work even if Playwright returns a different Page instance.
  • -
  • macOS: ensure launchd log directory exists with a test-only override. (#909) — thanks @roshanasingh4.
  • -
  • macOS: format ConnectionsStore config to satisfy SwiftFormat lint. (#852) — thanks @mneves75.
  • -
  • Packaging: run pnpm build on prepack so npm publishes include fresh dist/ output.
  • -
  • Telegram: register dock native commands with underscores to avoid BOT_COMMAND_INVALID (#929, fixes #901) — thanks @grp06.
  • -
  • Google: downgrade unsigned thinking blocks before send to avoid missing signature errors.
  • -
  • Agents: make user time zone and 24-hour time explicit in the system prompt. (#859) — thanks @CashWilliams.
  • -
  • Agents: strip downgraded tool call text without eating adjacent replies and filter thinking-tag leaks. (#905) — thanks @erikpr1994.
  • -
  • Agents: cap tool call IDs for OpenAI/OpenRouter to avoid request rejections. (#875) — thanks @j1philli.
  • -
  • Doctor: avoid re-adding WhatsApp config when only legacy ack reactions are set. (#927, fixes #900) — thanks @grp06.
  • -
  • Agents: scrub tuple items schemas for Gemini tool calls. (#926, fixes #746) — thanks @grp06.
  • -
  • Agents: stabilize sub-agent announce status from runtime outcomes and normalize Result/Notes. (#835) — thanks @roshanasingh4.
  • -
  • Apps: use canonical main session keys from gateway defaults across macOS/iOS/Android to avoid creating bare main sessions.
  • -
  • Embedded runner: suppress raw API error payloads from replies. (#924) — thanks @grp06.
  • -
  • Auth: normalize Claude Code CLI profile mode to oauth and auto-migrate config. (#855) — thanks @sebslight.
  • -
  • Daemon: clear persisted launchd disabled state before bootstrap (fixes daemon install after uninstall). (#849) — thanks @ndraiman.
  • -
  • Sessions: return deep clones (structuredClone) so cached session entries can't be mutated. (#934) — thanks @ronak-guliani.
  • -
  • Heartbeat: keep updatedAt monotonic when restoring heartbeat sessions. (#934) — thanks @ronak-guliani.
  • -
  • Agent: clear run context after CLI runs (clearAgentRunContext) to avoid runaway contexts. (#934) — thanks @ronak-guliani.
  • -
  • Gateway/Dev: ensure pnpm gateway:dev always uses the dev profile config + state (~/.clawdbot-dev).
  • -
-

CLI / Onboarding

-
    -
  • Onboarding: show web search setup at the end (not the beginning).
  • -
  • Onboarding: show daemon install/restart progress (avoid “blinking cursor”) and fix daemon install output formatting.
  • -
  • Health: colorize “not configured” provider lines for easier scanning.
  • -
-

Control UI / TUI

-
    -
  • Control UI: load cron run history on job selection and clarify empty-state messaging. (#866)
  • -
  • UI: use application-defined WebSocket close code and fix dashboard auth query items. (#918) — thanks @rahthakor.
  • -
  • UI: always apply ?token= from URL (fixes unauthorized after re-onboard).
  • -
  • Browser: add tests for snapshot labels/efficient query params and labeled image responses.
  • -
  • TUI: render picker overlays via the overlay stack so /models and /settings display. (#921) — thanks @grizzdank.
  • -
  • TUI: add a bright spinner + elapsed time in the status line for send/stream/run states.
  • -
  • TUI: show LLM error messages (rate limits, auth, etc.) instead of (no output).
  • -
-

Agents / Auth / Tools / Sandbox

-
    -
  • Agents: make user time zone and 24-hour time explicit in the system prompt. (#859) — thanks @CashWilliams.
  • -
  • Agents: strip downgraded tool call text without eating adjacent replies and filter thinking-tag leaks. (#905) — thanks @erikpr1994.
  • -
  • Agents: cap tool call IDs for OpenAI/OpenRouter to avoid request rejections. (#875) — thanks @j1philli.
  • -
  • Agents: scrub tuple items schemas for Gemini tool calls. (#926, fixes #746) — thanks @grp06.
  • -
  • Agents: stabilize sub-agent announce status from runtime outcomes and normalize Result/Notes. (#835) — thanks @roshanasingh4.
  • -
  • Auth: normalize Claude Code CLI profile mode to oauth and auto-migrate config. (#855) — thanks @sebslight.
  • -
  • Embedded runner: suppress raw API error payloads from replies. (#924) — thanks @grp06.
  • -
  • Logging: tolerate EIO from console writes to avoid gateway crashes. (#925, fixes #878) — thanks @grp06.
  • -
  • Sandbox: restore docker.binds config validation and preserve configured PATH for docker exec. (#873) — thanks @akonyer.
  • -
  • Google: downgrade unsigned thinking blocks before send to avoid missing signature errors.
  • -
-

macOS / Apps

-
    -
  • macOS: ensure launchd log directory exists with a test-only override. (#909) — thanks @roshanasingh4.
  • -
  • macOS: format ConnectionsStore config to satisfy SwiftFormat lint. (#852) — thanks @mneves75.
  • -
  • macOS: pass auth token/password to dashboard URL for authenticated access. (#918) — thanks @rahthakor.
  • -
  • macOS: reuse launchd gateway auth and skip wizard when gateway config already exists. (#917)
  • -
  • Apps: use canonical main session keys from gateway defaults across macOS/iOS/Android to avoid creating bare main sessions.
  • -
  • macOS: fix cron preview/testing payload to use channel key. (#867) — thanks @wes-davis.
  • -
  • macOS: update cron testing channel arg. (#896) — thanks @ngutman.
  • -
-

Channels / Messaging

-
    -
  • Slack: isolate thread history and avoid inheriting channel transcripts for new threads by default. (#758)
  • -
  • Slack: respect channels.slack.requireMention default when resolving channel mention gating. (#850) — thanks @evalexpr.
  • -
  • Slack: drop Socket Mode events with mismatched api_app_id/team_id. (#889) — thanks @roshanasingh4.
  • -
  • Commands: add native command argument menus across Discord/Slack/Telegram. (#936) — thanks @thewilloftheshadow.
  • -
  • Discord: isolate autoThread thread context. (#856) — thanks @davidguttman.
  • -
  • Telegram: honor channels.telegram.timeoutSeconds for grammY API requests. (#863) — thanks @Snaver.
  • -
  • Telegram: aggregate split inbound messages into one prompt (reduces “one reply per fragment”).
  • -
  • Telegram: let control commands bypass per-chat sequentialization; always allow abort triggers.
  • -
  • Telegram: split long captions into media + follow-up text messages. (#907) — thanks @jalehman.
  • -
  • Telegram: migrate group config when supergroups change chat IDs. (#906) — thanks @sleontenko.
  • -
  • Telegram: register dock native commands with underscores to avoid BOT_COMMAND_INVALID (#929, fixes #901) — thanks @grp06.
  • -
  • Messaging: unify markdown formatting + format-first chunking for Slack/Telegram/Signal. (#920) — thanks @TheSethRose.
  • -
  • iMessage: prefer handle routing for direct-message replies; include imsg RPC error details. (#935)
  • -
  • WhatsApp: fix context isolation using wrong ID (was bot's number, now conversation ID). (#911) — thanks @tristanmanchester.
  • -
  • WhatsApp: normalize user JIDs with device suffix for allowlist checks in groups. (#838) — thanks @peschee.
  • -
  • WhatsApp: harden owner command auth.
  • -
  • Auto-reply: treat trailing NO_REPLY tokens as silent replies.
  • -
-

Config / Doctor / Packaging

-
    -
  • Config: prevent partial config writes from clobbering unrelated settings (base hash guard + merge patch for connection saves).
  • -
  • Config/Doctor: remove legacy Clawdis env fallbacks and config/service migrations (Clawdbot-only).
  • -
  • Doctor: avoid re-adding WhatsApp config when only legacy ack reactions are set. (#927, fixes #900) — thanks @grp06.
  • -
  • Packaging: run pnpm build on prepack so npm publishes include fresh dist/ output.
  • -
-

View full changelog

-]]>
- +
\ No newline at end of file diff --git a/apps/android/README.md b/apps/android/README.md index 02908b82d..ca0967643 100644 --- a/apps/android/README.md +++ b/apps/android/README.md @@ -1,6 +1,6 @@ ## Clawdbot Node (Android) (internal) -Modern Android node app: connects to the **Gateway-owned bridge** (`_clawdbot-bridge._tcp`) over TCP and exposes **Canvas + Chat + Camera**. +Modern Android node app: connects to the **Gateway WebSocket** (`_clawdbot-gw._tcp`) and exposes **Canvas + Chat + Camera**. Notes: - The node keeps the connection alive via a **foreground service** (persistent notification with a Disconnect action). @@ -30,7 +30,7 @@ pnpm clawdbot gateway --port 18789 --verbose 2) In the Android app: - Open **Settings** -- Either select a discovered bridge under **Discovered Bridges**, or use **Advanced → Manual Bridge** (host + port). +- Either select a discovered gateway under **Discovered Gateways**, or use **Advanced → Manual Gateway** (host + port). 3) Approve pairing (on the gateway machine): ```bash @@ -38,7 +38,7 @@ clawdbot nodes pending clawdbot nodes approve ``` -More details: `docs/android/connect.md`. +More details: `docs/platforms/android.md`. ## Permissions diff --git a/apps/android/app/build.gradle.kts b/apps/android/app/build.gradle.kts index 7b245a0a3..d8d77ebe1 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 = 202601114 - versionName = "2026.1.11-4" + versionCode = 202601240 + versionName = "2026.1.24" } buildTypes { @@ -103,6 +103,7 @@ dependencies { implementation("androidx.security:security-crypto:1.1.0") implementation("androidx.exifinterface:exifinterface:1.4.2") + implementation("com.squareup.okhttp3:okhttp:5.3.2") // CameraX (for node.invoke camera.* parity) implementation("androidx.camera:camera-core:1.5.2") @@ -112,7 +113,7 @@ dependencies { implementation("androidx.camera:camera-view:1.5.2") // Unicast DNS-SD (Wide-Area Bonjour) for tailnet discovery domains. - implementation("dnsjava:dnsjava:3.6.3") + implementation("dnsjava:dnsjava:3.6.4") testImplementation("junit:junit:4.13.2") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2") diff --git a/apps/android/app/src/main/java/com/clawdbot/android/MainViewModel.kt b/apps/android/app/src/main/java/com/clawdbot/android/MainViewModel.kt index 021ebf587..1329f06d4 100644 --- a/apps/android/app/src/main/java/com/clawdbot/android/MainViewModel.kt +++ b/apps/android/app/src/main/java/com/clawdbot/android/MainViewModel.kt @@ -2,7 +2,7 @@ package com.clawdbot.android import android.app.Application import androidx.lifecycle.AndroidViewModel -import com.clawdbot.android.bridge.BridgeEndpoint +import com.clawdbot.android.gateway.GatewayEndpoint import com.clawdbot.android.chat.OutgoingAttachment import com.clawdbot.android.node.CameraCaptureManager import com.clawdbot.android.node.CanvasController @@ -18,7 +18,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app) { val screenRecorder: ScreenRecordManager = runtime.screenRecorder val sms: SmsManager = runtime.sms - val bridges: StateFlow> = runtime.bridges + val gateways: StateFlow> = runtime.gateways val discoveryStatusText: StateFlow = runtime.discoveryStatusText val isConnected: StateFlow = runtime.isConnected @@ -50,6 +50,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app) { val manualEnabled: StateFlow = runtime.manualEnabled val manualHost: StateFlow = runtime.manualHost val manualPort: StateFlow = runtime.manualPort + val manualTls: StateFlow = runtime.manualTls val canvasDebugStatusEnabled: StateFlow = runtime.canvasDebugStatusEnabled val chatSessionKey: StateFlow = runtime.chatSessionKey @@ -99,6 +100,10 @@ class MainViewModel(app: Application) : AndroidViewModel(app) { runtime.setManualPort(value) } + fun setManualTls(value: Boolean) { + runtime.setManualTls(value) + } + fun setCanvasDebugStatusEnabled(value: Boolean) { runtime.setCanvasDebugStatusEnabled(value) } @@ -119,11 +124,11 @@ class MainViewModel(app: Application) : AndroidViewModel(app) { runtime.setTalkEnabled(enabled) } - fun refreshBridgeHello() { - runtime.refreshBridgeHello() + fun refreshGatewayConnection() { + runtime.refreshGatewayConnection() } - fun connect(endpoint: BridgeEndpoint) { + fun connect(endpoint: GatewayEndpoint) { runtime.connect(endpoint) } diff --git a/apps/android/app/src/main/java/com/clawdbot/android/NodeRuntime.kt b/apps/android/app/src/main/java/com/clawdbot/android/NodeRuntime.kt index 83054c4e1..603e4b82b 100644 --- a/apps/android/app/src/main/java/com/clawdbot/android/NodeRuntime.kt +++ b/apps/android/app/src/main/java/com/clawdbot/android/NodeRuntime.kt @@ -12,11 +12,14 @@ import com.clawdbot.android.chat.ChatMessage import com.clawdbot.android.chat.ChatPendingToolCall import com.clawdbot.android.chat.ChatSessionEntry import com.clawdbot.android.chat.OutgoingAttachment -import com.clawdbot.android.bridge.BridgeDiscovery -import com.clawdbot.android.bridge.BridgeEndpoint -import com.clawdbot.android.bridge.BridgePairingClient -import com.clawdbot.android.bridge.BridgeSession -import com.clawdbot.android.bridge.BridgeTlsParams +import com.clawdbot.android.gateway.DeviceAuthStore +import com.clawdbot.android.gateway.DeviceIdentityStore +import com.clawdbot.android.gateway.GatewayClientInfo +import com.clawdbot.android.gateway.GatewayConnectOptions +import com.clawdbot.android.gateway.GatewayDiscovery +import com.clawdbot.android.gateway.GatewayEndpoint +import com.clawdbot.android.gateway.GatewaySession +import com.clawdbot.android.gateway.GatewayTlsParams import com.clawdbot.android.node.CameraCaptureManager import com.clawdbot.android.node.LocationCaptureManager import com.clawdbot.android.BuildConfig @@ -60,6 +63,7 @@ class NodeRuntime(context: Context) { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) val prefs = SecurePrefs(appContext) + private val deviceAuthStore = DeviceAuthStore(prefs) val canvas = CanvasController() val camera = CameraCaptureManager(appContext) val location = LocationCaptureManager(appContext) @@ -74,7 +78,7 @@ class NodeRuntime(context: Context) { context = appContext, scope = scope, onCommand = { command -> - session.sendEvent( + nodeSession.sendNodeEvent( event = "agent.request", payloadJson = buildJsonObject { @@ -103,10 +107,12 @@ class NodeRuntime(context: Context) { val talkIsSpeaking: StateFlow get() = talkMode.isSpeaking - private val discovery = BridgeDiscovery(appContext, scope = scope) - val bridges: StateFlow> = discovery.bridges + private val discovery = GatewayDiscovery(appContext, scope = scope) + val gateways: StateFlow> = discovery.gateways val discoveryStatusText: StateFlow = discovery.statusText + private val identityStore = DeviceIdentityStore(appContext) + private val _isConnected = MutableStateFlow(false) val isConnected: StateFlow = _isConnected.asStateFlow() @@ -139,52 +145,89 @@ class NodeRuntime(context: Context) { val isForeground: StateFlow = _isForeground.asStateFlow() private var lastAutoA2uiUrl: String? = null + private var operatorConnected = false + private var nodeConnected = false + private var operatorStatusText: String = "Offline" + private var nodeStatusText: String = "Offline" + private var connectedEndpoint: GatewayEndpoint? = null - private val session = - BridgeSession( + private val operatorSession = + GatewaySession( scope = scope, + identityStore = identityStore, + deviceAuthStore = deviceAuthStore, onConnected = { name, remote, mainSessionKey -> - _statusText.value = "Connected" + operatorConnected = true + operatorStatusText = "Connected" _serverName.value = name _remoteAddress.value = remote - _isConnected.value = true _seamColorArgb.value = DEFAULT_SEAM_COLOR_ARGB applyMainSessionKey(mainSessionKey) + updateStatus() scope.launch { refreshBrandingFromGateway() } scope.launch { refreshWakeWordsFromGateway() } + }, + onDisconnected = { message -> + operatorConnected = false + operatorStatusText = message + _serverName.value = null + _remoteAddress.value = null + _seamColorArgb.value = DEFAULT_SEAM_COLOR_ARGB + if (!isCanonicalMainSessionKey(_mainSessionKey.value)) { + _mainSessionKey.value = "main" + } + val mainKey = resolveMainSessionKey() + talkMode.setMainSessionKey(mainKey) + chat.applyMainSessionKey(mainKey) + chat.onDisconnected(message) + updateStatus() + }, + onEvent = { event, payloadJson -> + handleGatewayEvent(event, payloadJson) + }, + ) + + private val nodeSession = + GatewaySession( + scope = scope, + identityStore = identityStore, + deviceAuthStore = deviceAuthStore, + onConnected = { _, _, _ -> + nodeConnected = true + nodeStatusText = "Connected" + updateStatus() maybeNavigateToA2uiOnConnect() }, - onDisconnected = { message -> handleSessionDisconnected(message) }, - onEvent = { event, payloadJson -> - handleBridgeEvent(event, payloadJson) + onDisconnected = { message -> + nodeConnected = false + nodeStatusText = message + updateStatus() + showLocalCanvasOnDisconnect() }, + onEvent = { _, _ -> }, onInvoke = { req -> handleInvoke(req.command, req.paramsJson) }, onTlsFingerprint = { stableId, fingerprint -> - prefs.saveBridgeTlsFingerprint(stableId, fingerprint) + prefs.saveGatewayTlsFingerprint(stableId, fingerprint) }, ) - private val chat = ChatController(scope = scope, session = session, json = json) + private val chat: ChatController = + ChatController( + scope = scope, + session = operatorSession, + json = json, + supportsChatSubscribe = false, + ) private val talkMode: TalkModeManager by lazy { - TalkModeManager(context = appContext, scope = scope).also { it.attachSession(session) } - } - - private fun handleSessionDisconnected(message: String) { - _statusText.value = message - _serverName.value = null - _remoteAddress.value = null - _isConnected.value = false - _seamColorArgb.value = DEFAULT_SEAM_COLOR_ARGB - if (!isCanonicalMainSessionKey(_mainSessionKey.value)) { - _mainSessionKey.value = "main" - } - val mainKey = resolveMainSessionKey() - talkMode.setMainSessionKey(mainKey) - chat.applyMainSessionKey(mainKey) - chat.onDisconnected(message) - showLocalCanvasOnDisconnect() + TalkModeManager( + context = appContext, + scope = scope, + session = operatorSession, + supportsChatSubscribe = false, + isConnected = { operatorConnected }, + ) } private fun applyMainSessionKey(candidate: String?) { @@ -197,6 +240,18 @@ class NodeRuntime(context: Context) { chat.applyMainSessionKey(trimmed) } + private fun updateStatus() { + _isConnected.value = operatorConnected + _statusText.value = + when { + operatorConnected && nodeConnected -> "Connected" + operatorConnected && !nodeConnected -> "Connected (node offline)" + !operatorConnected && nodeConnected -> "Connected (operator offline)" + operatorStatusText.isNotBlank() && operatorStatusText != "Offline" -> operatorStatusText + else -> nodeStatusText + } + } + private fun resolveMainSessionKey(): String { val trimmed = _mainSessionKey.value.trim() return if (trimmed.isEmpty()) "main" else trimmed @@ -228,6 +283,7 @@ class NodeRuntime(context: Context) { val manualEnabled: StateFlow = prefs.manualEnabled val manualHost: StateFlow = prefs.manualHost val manualPort: StateFlow = prefs.manualPort + val manualTls: StateFlow = prefs.manualTls val lastDiscoveredStableId: StateFlow = prefs.lastDiscoveredStableId val canvasDebugStatusEnabled: StateFlow = prefs.canvasDebugStatusEnabled @@ -288,24 +344,21 @@ class NodeRuntime(context: Context) { } scope.launch(Dispatchers.Default) { - bridges.collect { list -> + gateways.collect { list -> if (list.isNotEmpty()) { - // Persist the last discovered bridge (best-effort UX parity with iOS). + // Persist the last discovered gateway (best-effort UX parity with iOS). prefs.setLastDiscoveredStableId(list.last().stableId) } if (didAutoConnect) return@collect if (_isConnected.value) return@collect - val token = prefs.loadBridgeToken() - if (token.isNullOrBlank()) return@collect - if (manualEnabled.value) { val host = manualHost.value.trim() val port = manualPort.value if (host.isNotEmpty() && port in 1..65535) { didAutoConnect = true - connect(BridgeEndpoint.manual(host = host, port = port)) + connect(GatewayEndpoint.manual(host = host, port = port)) } return@collect } @@ -371,6 +424,10 @@ class NodeRuntime(context: Context) { prefs.setManualPort(value) } + fun setManualTls(value: Boolean) { + prefs.setManualTls(value) + } + fun setCanvasDebugStatusEnabled(value: Boolean) { prefs.setCanvasDebugStatusEnabled(value) } @@ -429,99 +486,87 @@ class NodeRuntime(context: Context) { } } - private fun buildPairingHello(token: String?): BridgePairingClient.Hello { - val modelIdentifier = listOfNotNull(Build.MANUFACTURER, Build.MODEL) - .joinToString(" ") - .trim() - .ifEmpty { null } + private fun resolvedVersionName(): String { val versionName = BuildConfig.VERSION_NAME.trim().ifEmpty { "dev" } - val advertisedVersion = - if (BuildConfig.DEBUG && !versionName.contains("dev", ignoreCase = true)) { - "$versionName-dev" - } else { - versionName - } - return BridgePairingClient.Hello( - nodeId = instanceId.value, - displayName = displayName.value, - token = token, - platform = "Android", - version = advertisedVersion, - deviceFamily = "Android", - modelIdentifier = modelIdentifier, - caps = buildCapabilities(), - commands = buildInvokeCommands(), - ) - } - - private fun buildSessionHello(token: String?): BridgeSession.Hello { - val modelIdentifier = listOfNotNull(Build.MANUFACTURER, Build.MODEL) - .joinToString(" ") - .trim() - .ifEmpty { null } - val versionName = BuildConfig.VERSION_NAME.trim().ifEmpty { "dev" } - val advertisedVersion = - if (BuildConfig.DEBUG && !versionName.contains("dev", ignoreCase = true)) { - "$versionName-dev" - } else { - versionName - } - return BridgeSession.Hello( - nodeId = instanceId.value, - displayName = displayName.value, - token = token, - platform = "Android", - version = advertisedVersion, - deviceFamily = "Android", - modelIdentifier = modelIdentifier, - caps = buildCapabilities(), - commands = buildInvokeCommands(), - ) - } - - fun refreshBridgeHello() { - scope.launch { - if (!_isConnected.value) return@launch - val token = prefs.loadBridgeToken() - if (token.isNullOrBlank()) return@launch - session.updateHello(buildSessionHello(token)) + return if (BuildConfig.DEBUG && !versionName.contains("dev", ignoreCase = true)) { + "$versionName-dev" + } else { + versionName } } - fun connect(endpoint: BridgeEndpoint) { - scope.launch { - _statusText.value = "Connecting…" - val storedToken = prefs.loadBridgeToken() - val tls = resolveTlsParams(endpoint) - val resolved = - if (storedToken.isNullOrBlank()) { - _statusText.value = "Pairing…" - BridgePairingClient().pairAndHello( - endpoint = endpoint, - hello = buildPairingHello(token = null), - tls = tls, - onTlsFingerprint = { fingerprint -> - prefs.saveBridgeTlsFingerprint(endpoint.stableId, fingerprint) - }, - ) - } else { - BridgePairingClient.PairResult(ok = true, token = storedToken.trim()) - } + private fun resolveModelIdentifier(): String? { + return listOfNotNull(Build.MANUFACTURER, Build.MODEL) + .joinToString(" ") + .trim() + .ifEmpty { null } + } - if (!resolved.ok || resolved.token.isNullOrBlank()) { - val errorMessage = resolved.error?.trim().orEmpty().ifEmpty { "pairing required" } - _statusText.value = "Failed: $errorMessage" - return@launch - } + private fun buildUserAgent(): String { + val version = resolvedVersionName() + val release = Build.VERSION.RELEASE?.trim().orEmpty() + val releaseLabel = if (release.isEmpty()) "unknown" else release + return "ClawdbotAndroid/$version (Android $releaseLabel; SDK ${Build.VERSION.SDK_INT})" + } - val authToken = requireNotNull(resolved.token).trim() - prefs.saveBridgeToken(authToken) - session.connect( - endpoint = endpoint, - hello = buildSessionHello(token = authToken), - tls = tls, - ) - } + private fun buildClientInfo(clientId: String, clientMode: String): GatewayClientInfo { + return GatewayClientInfo( + id = clientId, + displayName = displayName.value, + version = resolvedVersionName(), + platform = "android", + mode = clientMode, + instanceId = instanceId.value, + deviceFamily = "Android", + modelIdentifier = resolveModelIdentifier(), + ) + } + + private fun buildNodeConnectOptions(): GatewayConnectOptions { + return GatewayConnectOptions( + role = "node", + scopes = emptyList(), + caps = buildCapabilities(), + commands = buildInvokeCommands(), + permissions = emptyMap(), + client = buildClientInfo(clientId = "clawdbot-android", clientMode = "node"), + userAgent = buildUserAgent(), + ) + } + + private fun buildOperatorConnectOptions(): GatewayConnectOptions { + return GatewayConnectOptions( + role = "operator", + scopes = emptyList(), + caps = emptyList(), + commands = emptyList(), + permissions = emptyMap(), + client = buildClientInfo(clientId = "clawdbot-control-ui", clientMode = "ui"), + userAgent = buildUserAgent(), + ) + } + + fun refreshGatewayConnection() { + val endpoint = connectedEndpoint ?: return + val token = prefs.loadGatewayToken() + val password = prefs.loadGatewayPassword() + val tls = resolveTlsParams(endpoint) + operatorSession.connect(endpoint, token, password, buildOperatorConnectOptions(), tls) + nodeSession.connect(endpoint, token, password, buildNodeConnectOptions(), tls) + operatorSession.reconnect() + nodeSession.reconnect() + } + + fun connect(endpoint: GatewayEndpoint) { + connectedEndpoint = endpoint + operatorStatusText = "Connecting…" + nodeStatusText = "Connecting…" + updateStatus() + val token = prefs.loadGatewayToken() + val password = prefs.loadGatewayPassword() + val tls = resolveTlsParams(endpoint) + operatorSession.connect(endpoint, token, password, buildOperatorConnectOptions(), tls) + nodeSession.connect(endpoint, token, password, buildNodeConnectOptions(), tls) } private fun hasRecordAudioPermission(): Boolean { @@ -559,20 +604,32 @@ class NodeRuntime(context: Context) { _statusText.value = "Failed: invalid manual host/port" return } - connect(BridgeEndpoint.manual(host = host, port = port)) + connect(GatewayEndpoint.manual(host = host, port = port)) } fun disconnect() { - session.disconnect() + connectedEndpoint = null + operatorSession.disconnect() + nodeSession.disconnect() } - private fun resolveTlsParams(endpoint: BridgeEndpoint): BridgeTlsParams? { - val stored = prefs.loadBridgeTlsFingerprint(endpoint.stableId) + private fun resolveTlsParams(endpoint: GatewayEndpoint): GatewayTlsParams? { + val stored = prefs.loadGatewayTlsFingerprint(endpoint.stableId) val hinted = endpoint.tlsEnabled || !endpoint.tlsFingerprintSha256.isNullOrBlank() val manual = endpoint.stableId.startsWith("manual|") + if (manual) { + if (!manualTls.value) return null + return GatewayTlsParams( + required = true, + expectedFingerprint = endpoint.tlsFingerprintSha256 ?: stored, + allowTOFU = stored == null, + stableId = endpoint.stableId, + ) + } + if (hinted) { - return BridgeTlsParams( + return GatewayTlsParams( required = true, expectedFingerprint = endpoint.tlsFingerprintSha256 ?: stored, allowTOFU = stored == null, @@ -581,7 +638,7 @@ class NodeRuntime(context: Context) { } if (!stored.isNullOrBlank()) { - return BridgeTlsParams( + return GatewayTlsParams( required = true, expectedFingerprint = stored, allowTOFU = false, @@ -589,15 +646,6 @@ class NodeRuntime(context: Context) { ) } - if (manual) { - return BridgeTlsParams( - required = false, - expectedFingerprint = null, - allowTOFU = true, - stableId = endpoint.stableId, - ) - } - return null } @@ -637,11 +685,11 @@ class NodeRuntime(context: Context) { contextJson = contextJson, ) - val connected = isConnected.value + val connected = nodeConnected var error: String? = null if (connected) { try { - session.sendEvent( + nodeSession.sendNodeEvent( event = "agent.request", payloadJson = buildJsonObject { @@ -656,7 +704,7 @@ class NodeRuntime(context: Context) { error = e.message ?: "send failed" } } else { - error = "bridge not connected" + error = "gateway not connected" } try { @@ -702,7 +750,7 @@ class NodeRuntime(context: Context) { chat.sendMessage(message = message, thinkingLevel = thinking, attachments = attachments) } - private fun handleBridgeEvent(event: String, payloadJson: String?) { + private fun handleGatewayEvent(event: String, payloadJson: String?) { if (event == "voicewake.changed") { if (payloadJson.isNullOrBlank()) return try { @@ -716,8 +764,8 @@ class NodeRuntime(context: Context) { return } - talkMode.handleBridgeEvent(event, payloadJson) - chat.handleBridgeEvent(event, payloadJson) + talkMode.handleGatewayEvent(event, payloadJson) + chat.handleGatewayEvent(event, payloadJson) } private fun applyWakeWordsFromGateway(words: List) { @@ -738,7 +786,7 @@ class NodeRuntime(context: Context) { val jsonList = snapshot.joinToString(separator = ",") { it.toJsonString() } val params = """{"triggers":[$jsonList]}""" try { - session.request("voicewake.set", params) + operatorSession.request("voicewake.set", params) } catch (_: Throwable) { // ignore } @@ -748,7 +796,7 @@ class NodeRuntime(context: Context) { private suspend fun refreshWakeWordsFromGateway() { if (!_isConnected.value) return try { - val res = session.request("voicewake.get", "{}") + val res = operatorSession.request("voicewake.get", "{}") val payload = json.parseToJsonElement(res).asObjectOrNull() ?: return val array = payload["triggers"] as? JsonArray ?: return val triggers = array.mapNotNull { it.asStringOrNull() } @@ -761,7 +809,7 @@ class NodeRuntime(context: Context) { private suspend fun refreshBrandingFromGateway() { if (!_isConnected.value) return try { - val res = session.request("config.get", "{}") + val res = operatorSession.request("config.get", "{}") val root = json.parseToJsonElement(res).asObjectOrNull() val config = root?.get("config").asObjectOrNull() val ui = config?.get("ui").asObjectOrNull() @@ -777,7 +825,7 @@ class NodeRuntime(context: Context) { } } - private suspend fun handleInvoke(command: String, paramsJson: String?): BridgeSession.InvokeResult { + private suspend fun handleInvoke(command: String, paramsJson: String?): GatewaySession.InvokeResult { if ( command.startsWith(ClawdbotCanvasCommand.NamespacePrefix) || command.startsWith(ClawdbotCanvasA2UICommand.NamespacePrefix) || @@ -785,14 +833,14 @@ class NodeRuntime(context: Context) { command.startsWith(ClawdbotScreenCommand.NamespacePrefix) ) { if (!isForeground.value) { - return BridgeSession.InvokeResult.error( + return GatewaySession.InvokeResult.error( code = "NODE_BACKGROUND_UNAVAILABLE", message = "NODE_BACKGROUND_UNAVAILABLE: canvas/camera/screen commands require foreground", ) } } if (command.startsWith(ClawdbotCameraCommand.NamespacePrefix) && !cameraEnabled.value) { - return BridgeSession.InvokeResult.error( + return GatewaySession.InvokeResult.error( code = "CAMERA_DISABLED", message = "CAMERA_DISABLED: enable Camera in Settings", ) @@ -800,7 +848,7 @@ class NodeRuntime(context: Context) { if (command.startsWith(ClawdbotLocationCommand.NamespacePrefix) && locationMode.value == LocationMode.Off ) { - return BridgeSession.InvokeResult.error( + return GatewaySession.InvokeResult.error( code = "LOCATION_DISABLED", message = "LOCATION_DISABLED: enable Location in Settings", ) @@ -810,18 +858,18 @@ class NodeRuntime(context: Context) { ClawdbotCanvasCommand.Present.rawValue -> { val url = CanvasController.parseNavigateUrl(paramsJson) canvas.navigate(url) - BridgeSession.InvokeResult.ok(null) + GatewaySession.InvokeResult.ok(null) } - ClawdbotCanvasCommand.Hide.rawValue -> BridgeSession.InvokeResult.ok(null) + ClawdbotCanvasCommand.Hide.rawValue -> GatewaySession.InvokeResult.ok(null) ClawdbotCanvasCommand.Navigate.rawValue -> { val url = CanvasController.parseNavigateUrl(paramsJson) canvas.navigate(url) - BridgeSession.InvokeResult.ok(null) + GatewaySession.InvokeResult.ok(null) } ClawdbotCanvasCommand.Eval.rawValue -> { val js = CanvasController.parseEvalJs(paramsJson) - ?: return BridgeSession.InvokeResult.error( + ?: return GatewaySession.InvokeResult.error( code = "INVALID_REQUEST", message = "INVALID_REQUEST: javaScript required", ) @@ -829,12 +877,12 @@ class NodeRuntime(context: Context) { try { canvas.eval(js) } catch (err: Throwable) { - return BridgeSession.InvokeResult.error( + return GatewaySession.InvokeResult.error( code = "NODE_BACKGROUND_UNAVAILABLE", message = "NODE_BACKGROUND_UNAVAILABLE: canvas unavailable", ) } - BridgeSession.InvokeResult.ok("""{"result":${result.toJsonString()}}""") + GatewaySession.InvokeResult.ok("""{"result":${result.toJsonString()}}""") } ClawdbotCanvasCommand.Snapshot.rawValue -> { val snapshotParams = CanvasController.parseSnapshotParams(paramsJson) @@ -846,51 +894,51 @@ class NodeRuntime(context: Context) { maxWidth = snapshotParams.maxWidth, ) } catch (err: Throwable) { - return BridgeSession.InvokeResult.error( + return GatewaySession.InvokeResult.error( code = "NODE_BACKGROUND_UNAVAILABLE", message = "NODE_BACKGROUND_UNAVAILABLE: canvas unavailable", ) } - BridgeSession.InvokeResult.ok("""{"format":"${snapshotParams.format.rawValue}","base64":"$base64"}""") + GatewaySession.InvokeResult.ok("""{"format":"${snapshotParams.format.rawValue}","base64":"$base64"}""") } ClawdbotCanvasA2UICommand.Reset.rawValue -> { val a2uiUrl = resolveA2uiHostUrl() - ?: return BridgeSession.InvokeResult.error( + ?: return GatewaySession.InvokeResult.error( code = "A2UI_HOST_NOT_CONFIGURED", message = "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host", ) val ready = ensureA2uiReady(a2uiUrl) if (!ready) { - return BridgeSession.InvokeResult.error( + return GatewaySession.InvokeResult.error( code = "A2UI_HOST_UNAVAILABLE", message = "A2UI host not reachable", ) } val res = canvas.eval(a2uiResetJS) - BridgeSession.InvokeResult.ok(res) + GatewaySession.InvokeResult.ok(res) } ClawdbotCanvasA2UICommand.Push.rawValue, ClawdbotCanvasA2UICommand.PushJSONL.rawValue -> { val messages = try { decodeA2uiMessages(command, paramsJson) } catch (err: Throwable) { - return BridgeSession.InvokeResult.error(code = "INVALID_REQUEST", message = err.message ?: "invalid A2UI payload") + return GatewaySession.InvokeResult.error(code = "INVALID_REQUEST", message = err.message ?: "invalid A2UI payload") } val a2uiUrl = resolveA2uiHostUrl() - ?: return BridgeSession.InvokeResult.error( + ?: return GatewaySession.InvokeResult.error( code = "A2UI_HOST_NOT_CONFIGURED", message = "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host", ) val ready = ensureA2uiReady(a2uiUrl) if (!ready) { - return BridgeSession.InvokeResult.error( + return GatewaySession.InvokeResult.error( code = "A2UI_HOST_UNAVAILABLE", message = "A2UI host not reachable", ) } val js = a2uiApplyMessagesJS(messages) val res = canvas.eval(js) - BridgeSession.InvokeResult.ok(res) + GatewaySession.InvokeResult.ok(res) } ClawdbotCameraCommand.Snap.rawValue -> { showCameraHud(message = "Taking photo…", kind = CameraHudKind.Photo) @@ -901,10 +949,10 @@ class NodeRuntime(context: Context) { } catch (err: Throwable) { val (code, message) = invokeErrorFromThrowable(err) showCameraHud(message = message, kind = CameraHudKind.Error, autoHideMs = 2200) - return BridgeSession.InvokeResult.error(code = code, message = message) + return GatewaySession.InvokeResult.error(code = code, message = message) } showCameraHud(message = "Photo captured", kind = CameraHudKind.Success, autoHideMs = 1600) - BridgeSession.InvokeResult.ok(res.payloadJson) + GatewaySession.InvokeResult.ok(res.payloadJson) } ClawdbotCameraCommand.Clip.rawValue -> { val includeAudio = paramsJson?.contains("\"includeAudio\":true") != false @@ -917,10 +965,10 @@ class NodeRuntime(context: Context) { } catch (err: Throwable) { val (code, message) = invokeErrorFromThrowable(err) showCameraHud(message = message, kind = CameraHudKind.Error, autoHideMs = 2400) - return BridgeSession.InvokeResult.error(code = code, message = message) + return GatewaySession.InvokeResult.error(code = code, message = message) } showCameraHud(message = "Clip captured", kind = CameraHudKind.Success, autoHideMs = 1800) - BridgeSession.InvokeResult.ok(res.payloadJson) + GatewaySession.InvokeResult.ok(res.payloadJson) } finally { if (includeAudio) externalAudioCaptureActive.value = false } @@ -928,19 +976,19 @@ class NodeRuntime(context: Context) { ClawdbotLocationCommand.Get.rawValue -> { val mode = locationMode.value if (!isForeground.value && mode != LocationMode.Always) { - return BridgeSession.InvokeResult.error( + return GatewaySession.InvokeResult.error( code = "LOCATION_BACKGROUND_UNAVAILABLE", message = "LOCATION_BACKGROUND_UNAVAILABLE: background location requires Always", ) } if (!hasFineLocationPermission() && !hasCoarseLocationPermission()) { - return BridgeSession.InvokeResult.error( + return GatewaySession.InvokeResult.error( code = "LOCATION_PERMISSION_REQUIRED", message = "LOCATION_PERMISSION_REQUIRED: grant Location permission", ) } if (!isForeground.value && mode == LocationMode.Always && !hasBackgroundLocationPermission()) { - return BridgeSession.InvokeResult.error( + return GatewaySession.InvokeResult.error( code = "LOCATION_PERMISSION_REQUIRED", message = "LOCATION_PERMISSION_REQUIRED: enable Always in system Settings", ) @@ -967,15 +1015,15 @@ class NodeRuntime(context: Context) { timeoutMs = timeoutMs, isPrecise = accuracy == "precise", ) - BridgeSession.InvokeResult.ok(payload.payloadJson) + GatewaySession.InvokeResult.ok(payload.payloadJson) } catch (err: TimeoutCancellationException) { - BridgeSession.InvokeResult.error( + GatewaySession.InvokeResult.error( code = "LOCATION_TIMEOUT", message = "LOCATION_TIMEOUT: no fix in time", ) } catch (err: Throwable) { val message = err.message ?: "LOCATION_UNAVAILABLE: no fix" - BridgeSession.InvokeResult.error(code = "LOCATION_UNAVAILABLE", message = message) + GatewaySession.InvokeResult.error(code = "LOCATION_UNAVAILABLE", message = message) } } ClawdbotScreenCommand.Record.rawValue -> { @@ -987,9 +1035,9 @@ class NodeRuntime(context: Context) { screenRecorder.record(paramsJson) } catch (err: Throwable) { val (code, message) = invokeErrorFromThrowable(err) - return BridgeSession.InvokeResult.error(code = code, message = message) + return GatewaySession.InvokeResult.error(code = code, message = message) } - BridgeSession.InvokeResult.ok(res.payloadJson) + GatewaySession.InvokeResult.ok(res.payloadJson) } finally { _screenRecordActive.value = false } @@ -997,16 +1045,16 @@ class NodeRuntime(context: Context) { ClawdbotSmsCommand.Send.rawValue -> { val res = sms.send(paramsJson) if (res.ok) { - BridgeSession.InvokeResult.ok(res.payloadJson) + GatewaySession.InvokeResult.ok(res.payloadJson) } else { val error = res.error ?: "SMS_SEND_FAILED" val idx = error.indexOf(':') val code = if (idx > 0) error.substring(0, idx).trim() else "SMS_SEND_FAILED" - BridgeSession.InvokeResult.error(code = code, message = error) + GatewaySession.InvokeResult.error(code = code, message = error) } } else -> - BridgeSession.InvokeResult.error( + GatewaySession.InvokeResult.error( code = "INVALID_REQUEST", message = "INVALID_REQUEST: unknown command", ) @@ -1062,7 +1110,9 @@ class NodeRuntime(context: Context) { } private fun resolveA2uiHostUrl(): String? { - val raw = session.currentCanvasHostUrl()?.trim().orEmpty() + val nodeRaw = nodeSession.currentCanvasHostUrl()?.trim().orEmpty() + val operatorRaw = operatorSession.currentCanvasHostUrl()?.trim().orEmpty() + val raw = if (nodeRaw.isNotBlank()) nodeRaw else operatorRaw if (raw.isBlank()) return null val base = raw.trimEnd('/') return "${base}/__clawdbot__/a2ui/?platform=android" diff --git a/apps/android/app/src/main/java/com/clawdbot/android/SecurePrefs.kt b/apps/android/app/src/main/java/com/clawdbot/android/SecurePrefs.kt index 3b02c88a5..cd6270dd5 100644 --- a/apps/android/app/src/main/java/com/clawdbot/android/SecurePrefs.kt +++ b/apps/android/app/src/main/java/com/clawdbot/android/SecurePrefs.kt @@ -58,17 +58,30 @@ class SecurePrefs(context: Context) { private val _preventSleep = MutableStateFlow(prefs.getBoolean("screen.preventSleep", true)) val preventSleep: StateFlow = _preventSleep - private val _manualEnabled = MutableStateFlow(prefs.getBoolean("bridge.manual.enabled", false)) + private val _manualEnabled = + MutableStateFlow(readBoolWithMigration("gateway.manual.enabled", "bridge.manual.enabled", false)) val manualEnabled: StateFlow = _manualEnabled - private val _manualHost = MutableStateFlow(prefs.getString("bridge.manual.host", "")!!) + private val _manualHost = + MutableStateFlow(readStringWithMigration("gateway.manual.host", "bridge.manual.host", "")) val manualHost: StateFlow = _manualHost - private val _manualPort = MutableStateFlow(prefs.getInt("bridge.manual.port", 18790)) + private val _manualPort = + MutableStateFlow(readIntWithMigration("gateway.manual.port", "bridge.manual.port", 18789)) val manualPort: StateFlow = _manualPort + private val _manualTls = + MutableStateFlow(readBoolWithMigration("gateway.manual.tls", null, true)) + val manualTls: StateFlow = _manualTls + private val _lastDiscoveredStableId = - MutableStateFlow(prefs.getString("bridge.lastDiscoveredStableId", "")!!) + MutableStateFlow( + readStringWithMigration( + "gateway.lastDiscoveredStableID", + "bridge.lastDiscoveredStableId", + "", + ), + ) val lastDiscoveredStableId: StateFlow = _lastDiscoveredStableId private val _canvasDebugStatusEnabled = @@ -86,7 +99,7 @@ class SecurePrefs(context: Context) { fun setLastDiscoveredStableId(value: String) { val trimmed = value.trim() - prefs.edit { putString("bridge.lastDiscoveredStableId", trimmed) } + prefs.edit { putString("gateway.lastDiscoveredStableID", trimmed) } _lastDiscoveredStableId.value = trimmed } @@ -117,46 +130,77 @@ class SecurePrefs(context: Context) { } fun setManualEnabled(value: Boolean) { - prefs.edit { putBoolean("bridge.manual.enabled", value) } + prefs.edit { putBoolean("gateway.manual.enabled", value) } _manualEnabled.value = value } fun setManualHost(value: String) { val trimmed = value.trim() - prefs.edit { putString("bridge.manual.host", trimmed) } + prefs.edit { putString("gateway.manual.host", trimmed) } _manualHost.value = trimmed } fun setManualPort(value: Int) { - prefs.edit { putInt("bridge.manual.port", value) } + prefs.edit { putInt("gateway.manual.port", value) } _manualPort.value = value } + fun setManualTls(value: Boolean) { + prefs.edit { putBoolean("gateway.manual.tls", value) } + _manualTls.value = value + } + fun setCanvasDebugStatusEnabled(value: Boolean) { prefs.edit { putBoolean("canvas.debugStatusEnabled", value) } _canvasDebugStatusEnabled.value = value } - fun loadBridgeToken(): String? { - val key = "bridge.token.${_instanceId.value}" - return prefs.getString(key, null) + fun loadGatewayToken(): String? { + val key = "gateway.token.${_instanceId.value}" + val stored = prefs.getString(key, null)?.trim() + if (!stored.isNullOrEmpty()) return stored + val legacy = prefs.getString("bridge.token.${_instanceId.value}", null)?.trim() + return legacy?.takeIf { it.isNotEmpty() } } - fun saveBridgeToken(token: String) { - val key = "bridge.token.${_instanceId.value}" + fun saveGatewayToken(token: String) { + val key = "gateway.token.${_instanceId.value}" prefs.edit { putString(key, token.trim()) } } - fun loadBridgeTlsFingerprint(stableId: String): String? { - val key = "bridge.tls.$stableId" + fun loadGatewayPassword(): String? { + val key = "gateway.password.${_instanceId.value}" + val stored = prefs.getString(key, null)?.trim() + return stored?.takeIf { it.isNotEmpty() } + } + + fun saveGatewayPassword(password: String) { + val key = "gateway.password.${_instanceId.value}" + prefs.edit { putString(key, password.trim()) } + } + + fun loadGatewayTlsFingerprint(stableId: String): String? { + val key = "gateway.tls.$stableId" return prefs.getString(key, null)?.trim()?.takeIf { it.isNotEmpty() } } - fun saveBridgeTlsFingerprint(stableId: String, fingerprint: String) { - val key = "bridge.tls.$stableId" + fun saveGatewayTlsFingerprint(stableId: String, fingerprint: String) { + val key = "gateway.tls.$stableId" prefs.edit { putString(key, fingerprint.trim()) } } + fun getString(key: String): String? { + return prefs.getString(key, null) + } + + fun putString(key: String, value: String) { + prefs.edit { putString(key, value) } + } + + fun remove(key: String) { + prefs.edit { remove(key) } + } + private fun loadOrCreateInstanceId(): String { val existing = prefs.getString("node.instanceId", null)?.trim() if (!existing.isNullOrBlank()) return existing @@ -225,4 +269,40 @@ class SecurePrefs(context: Context) { defaultWakeWords } } + + private fun readBoolWithMigration(newKey: String, oldKey: String?, defaultValue: Boolean): Boolean { + if (prefs.contains(newKey)) { + return prefs.getBoolean(newKey, defaultValue) + } + if (oldKey != null && prefs.contains(oldKey)) { + val value = prefs.getBoolean(oldKey, defaultValue) + prefs.edit { putBoolean(newKey, value) } + return value + } + return defaultValue + } + + private fun readStringWithMigration(newKey: String, oldKey: String?, defaultValue: String): String { + if (prefs.contains(newKey)) { + return prefs.getString(newKey, defaultValue) ?: defaultValue + } + if (oldKey != null && prefs.contains(oldKey)) { + val value = prefs.getString(oldKey, defaultValue) ?: defaultValue + prefs.edit { putString(newKey, value) } + return value + } + return defaultValue + } + + private fun readIntWithMigration(newKey: String, oldKey: String?, defaultValue: Int): Int { + if (prefs.contains(newKey)) { + return prefs.getInt(newKey, defaultValue) + } + if (oldKey != null && prefs.contains(oldKey)) { + val value = prefs.getInt(oldKey, defaultValue) + prefs.edit { putInt(newKey, value) } + return value + } + return defaultValue + } } diff --git a/apps/android/app/src/main/java/com/clawdbot/android/WakeWords.kt b/apps/android/app/src/main/java/com/clawdbot/android/WakeWords.kt index 855a0de7c..d54ed1e08 100644 --- a/apps/android/app/src/main/java/com/clawdbot/android/WakeWords.kt +++ b/apps/android/app/src/main/java/com/clawdbot/android/WakeWords.kt @@ -8,10 +8,14 @@ object WakeWords { return input.split(",").map { it.trim() }.filter { it.isNotEmpty() } } + fun parseIfChanged(input: String, current: List): List? { + val parsed = parseCommaSeparated(input) + return if (parsed == current) null else parsed + } + fun sanitize(words: List, defaults: List): List { val cleaned = words.map { it.trim() }.filter { it.isNotEmpty() }.take(maxWords).map { it.take(maxWordLength) } return cleaned.ifEmpty { defaults } } } - diff --git a/apps/android/app/src/main/java/com/clawdbot/android/bridge/BridgePairingClient.kt b/apps/android/app/src/main/java/com/clawdbot/android/bridge/BridgePairingClient.kt deleted file mode 100644 index 00ecbd25e..000000000 --- a/apps/android/app/src/main/java/com/clawdbot/android/bridge/BridgePairingClient.kt +++ /dev/null @@ -1,158 +0,0 @@ -package com.clawdbot.android.bridge - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.JsonNull -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.buildJsonObject -import java.io.BufferedReader -import java.io.BufferedWriter -import java.io.InputStreamReader -import java.io.OutputStreamWriter -import java.net.InetSocketAddress - -class BridgePairingClient { - private val json = Json { ignoreUnknownKeys = true } - - data class Hello( - val nodeId: String, - val displayName: String?, - val token: String?, - val platform: String?, - val version: String?, - val deviceFamily: String?, - val modelIdentifier: String?, - val caps: List?, - val commands: List?, - ) - - data class PairResult(val ok: Boolean, val token: String?, val error: String? = null) - - suspend fun pairAndHello( - endpoint: BridgeEndpoint, - hello: Hello, - tls: BridgeTlsParams? = null, - onTlsFingerprint: ((String) -> Unit)? = null, - ): PairResult = - withContext(Dispatchers.IO) { - if (tls != null) { - try { - return@withContext pairAndHelloWithTls(endpoint, hello, tls, onTlsFingerprint) - } catch (e: Exception) { - if (tls.required) throw e - } - } - pairAndHelloWithTls(endpoint, hello, null, null) - } - - private fun pairAndHelloWithTls( - endpoint: BridgeEndpoint, - hello: Hello, - tls: BridgeTlsParams?, - onTlsFingerprint: ((String) -> Unit)?, - ): PairResult { - val socket = - createBridgeSocket(tls) { fingerprint -> - onTlsFingerprint?.invoke(fingerprint) - } - socket.tcpNoDelay = true - try { - socket.connect(InetSocketAddress(endpoint.host, endpoint.port), 8_000) - socket.soTimeout = 60_000 - startTlsHandshakeIfNeeded(socket) - - val reader = BufferedReader(InputStreamReader(socket.getInputStream(), Charsets.UTF_8)) - val writer = BufferedWriter(OutputStreamWriter(socket.getOutputStream(), Charsets.UTF_8)) - - fun send(line: String) { - writer.write(line) - writer.write("\n") - writer.flush() - } - - fun sendJson(obj: JsonObject) = send(obj.toString()) - - sendJson( - buildJsonObject { - put("type", JsonPrimitive("hello")) - put("nodeId", JsonPrimitive(hello.nodeId)) - hello.displayName?.let { put("displayName", JsonPrimitive(it)) } - hello.token?.let { put("token", JsonPrimitive(it)) } - hello.platform?.let { put("platform", JsonPrimitive(it)) } - hello.version?.let { put("version", JsonPrimitive(it)) } - hello.deviceFamily?.let { put("deviceFamily", JsonPrimitive(it)) } - hello.modelIdentifier?.let { put("modelIdentifier", JsonPrimitive(it)) } - hello.caps?.let { put("caps", JsonArray(it.map(::JsonPrimitive))) } - hello.commands?.let { put("commands", JsonArray(it.map(::JsonPrimitive))) } - }, - ) - - val firstObj = json.parseToJsonElement(reader.readLine()).asObjectOrNull() - ?: return PairResult(ok = false, token = null, error = "unexpected bridge response") - return when (firstObj["type"].asStringOrNull()) { - "hello-ok" -> PairResult(ok = true, token = hello.token) - "error" -> { - val code = firstObj["code"].asStringOrNull() ?: "UNAVAILABLE" - val message = firstObj["message"].asStringOrNull() ?: "pairing required" - if (code != "NOT_PAIRED" && code != "UNAUTHORIZED") { - return PairResult(ok = false, token = null, error = "$code: $message") - } - - sendJson( - buildJsonObject { - put("type", JsonPrimitive("pair-request")) - put("nodeId", JsonPrimitive(hello.nodeId)) - hello.displayName?.let { put("displayName", JsonPrimitive(it)) } - hello.platform?.let { put("platform", JsonPrimitive(it)) } - hello.version?.let { put("version", JsonPrimitive(it)) } - hello.deviceFamily?.let { put("deviceFamily", JsonPrimitive(it)) } - hello.modelIdentifier?.let { put("modelIdentifier", JsonPrimitive(it)) } - hello.caps?.let { put("caps", JsonArray(it.map(::JsonPrimitive))) } - hello.commands?.let { put("commands", JsonArray(it.map(::JsonPrimitive))) } - }, - ) - - while (true) { - val nextLine = reader.readLine() ?: break - val next = json.parseToJsonElement(nextLine).asObjectOrNull() ?: continue - when (next["type"].asStringOrNull()) { - "pair-ok" -> { - val token = next["token"].asStringOrNull() - return PairResult(ok = !token.isNullOrBlank(), token = token) - } - "error" -> { - val c = next["code"].asStringOrNull() ?: "UNAVAILABLE" - val m = next["message"].asStringOrNull() ?: "pairing failed" - return PairResult(ok = false, token = null, error = "$c: $m") - } - } - } - PairResult(ok = false, token = null, error = "pairing failed") - } - else -> PairResult(ok = false, token = null, error = "unexpected bridge response") - } - } catch (e: Exception) { - val message = e.message?.trim().orEmpty().ifEmpty { "gateway unreachable" } - return PairResult(ok = false, token = null, error = message) - } finally { - try { - socket.close() - } catch (_: Throwable) { - // ignore - } - } - } -} - -private fun JsonElement?.asObjectOrNull(): JsonObject? = this as? JsonObject - -private fun JsonElement?.asStringOrNull(): String? = - when (this) { - is JsonNull -> null - is JsonPrimitive -> content - else -> null - } diff --git a/apps/android/app/src/main/java/com/clawdbot/android/bridge/BridgeSession.kt b/apps/android/app/src/main/java/com/clawdbot/android/bridge/BridgeSession.kt deleted file mode 100644 index 768ec9128..000000000 --- a/apps/android/app/src/main/java/com/clawdbot/android/bridge/BridgeSession.kt +++ /dev/null @@ -1,398 +0,0 @@ -package com.clawdbot.android.bridge - -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancelAndJoin -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import com.clawdbot.android.BuildConfig -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonNull -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.buildJsonObject -import java.io.BufferedReader -import java.io.BufferedWriter -import java.io.InputStreamReader -import java.io.OutputStreamWriter -import java.net.InetSocketAddress -import java.net.URI -import java.net.Socket -import java.util.UUID -import java.util.concurrent.ConcurrentHashMap - -class BridgeSession( - private val scope: CoroutineScope, - private val onConnected: (serverName: String, remoteAddress: String?, mainSessionKey: String?) -> Unit, - private val onDisconnected: (message: String) -> Unit, - private val onEvent: (event: String, payloadJson: String?) -> Unit, - private val onInvoke: suspend (InvokeRequest) -> InvokeResult, - private val onTlsFingerprint: ((stableId: String, fingerprint: String) -> Unit)? = null, -) { - data class Hello( - val nodeId: String, - val displayName: String?, - val token: String?, - val platform: String?, - val version: String?, - val deviceFamily: String?, - val modelIdentifier: String?, - val caps: List?, - val commands: List?, - ) - - data class InvokeRequest(val id: String, val command: String, val paramsJson: String?) - - data class InvokeResult(val ok: Boolean, val payloadJson: String?, val error: ErrorShape?) { - companion object { - fun ok(payloadJson: String?) = InvokeResult(ok = true, payloadJson = payloadJson, error = null) - fun error(code: String, message: String) = - InvokeResult(ok = false, payloadJson = null, error = ErrorShape(code = code, message = message)) - } - } - - data class ErrorShape(val code: String, val message: String) - - private val json = Json { ignoreUnknownKeys = true } - private val writeLock = Mutex() - private val pending = ConcurrentHashMap>() - @Volatile private var canvasHostUrl: String? = null - @Volatile private var mainSessionKey: String? = null - - private data class DesiredConnection( - val endpoint: BridgeEndpoint, - val hello: Hello, - val tls: BridgeTlsParams?, - ) - - private var desired: DesiredConnection? = null - private var job: Job? = null - - fun connect(endpoint: BridgeEndpoint, hello: Hello, tls: BridgeTlsParams? = null) { - desired = DesiredConnection(endpoint, hello, tls) - if (job == null) { - job = scope.launch(Dispatchers.IO) { runLoop() } - } - } - - suspend fun updateHello(hello: Hello) { - val target = desired ?: return - desired = target.copy(hello = hello) - val conn = currentConnection ?: return - conn.sendJson(buildHelloJson(hello)) - } - - fun disconnect() { - desired = null - // Unblock connectOnce() read loop. Coroutine cancellation alone won't interrupt BufferedReader.readLine(). - currentConnection?.closeQuietly() - scope.launch(Dispatchers.IO) { - job?.cancelAndJoin() - job = null - canvasHostUrl = null - mainSessionKey = null - onDisconnected("Offline") - } - } - - fun currentCanvasHostUrl(): String? = canvasHostUrl - fun currentMainSessionKey(): String? = mainSessionKey - - suspend fun sendEvent(event: String, payloadJson: String?) { - val conn = currentConnection ?: return - conn.sendJson( - buildJsonObject { - put("type", JsonPrimitive("event")) - put("event", JsonPrimitive(event)) - if (payloadJson != null) put("payloadJSON", JsonPrimitive(payloadJson)) else put("payloadJSON", JsonNull) - }, - ) - } - - suspend fun request(method: String, paramsJson: String?): String { - val conn = currentConnection ?: throw IllegalStateException("not connected") - val id = UUID.randomUUID().toString() - val deferred = CompletableDeferred() - pending[id] = deferred - conn.sendJson( - buildJsonObject { - put("type", JsonPrimitive("req")) - put("id", JsonPrimitive(id)) - put("method", JsonPrimitive(method)) - if (paramsJson != null) put("paramsJSON", JsonPrimitive(paramsJson)) else put("paramsJSON", JsonNull) - }, - ) - val res = deferred.await() - if (res.ok) return res.payloadJson ?: "" - val err = res.error - throw IllegalStateException("${err?.code ?: "UNAVAILABLE"}: ${err?.message ?: "request failed"}") - } - - private data class RpcResponse(val id: String, val ok: Boolean, val payloadJson: String?, val error: ErrorShape?) - - private class Connection(private val socket: Socket, private val reader: BufferedReader, private val writer: BufferedWriter, private val writeLock: Mutex) { - val remoteAddress: String? = - socket.inetAddress?.hostAddress?.takeIf { it.isNotBlank() }?.let { "${it}:${socket.port}" } - - suspend fun sendJson(obj: JsonObject) { - writeLock.withLock { - writer.write(obj.toString()) - writer.write("\n") - writer.flush() - } - } - - fun closeQuietly() { - try { - socket.close() - } catch (_: Throwable) { - // ignore - } - } - } - - @Volatile private var currentConnection: Connection? = null - - private suspend fun runLoop() { - var attempt = 0 - while (scope.isActive) { - val target = desired - if (target == null) { - currentConnection?.closeQuietly() - currentConnection = null - delay(250) - continue - } - - val (endpoint, hello, tls) = target - try { - onDisconnected(if (attempt == 0) "Connecting…" else "Reconnecting…") - connectOnce(endpoint, hello, tls) - attempt = 0 - } catch (err: Throwable) { - attempt += 1 - onDisconnected("Bridge error: ${err.message ?: err::class.java.simpleName}") - val sleepMs = minOf(8_000L, (350.0 * Math.pow(1.7, attempt.toDouble())).toLong()) - delay(sleepMs) - } - } - } - - private fun invokeErrorFromThrowable(err: Throwable): InvokeResult { - val msg = err.message?.trim().takeIf { !it.isNullOrEmpty() } ?: err::class.java.simpleName - val parts = msg.split(":", limit = 2) - if (parts.size == 2) { - val code = parts[0].trim() - val rest = parts[1].trim() - if (code.isNotEmpty() && code.all { it.isUpperCase() || it == '_' }) { - return InvokeResult.error(code = code, message = rest.ifEmpty { msg }) - } - } - return InvokeResult.error(code = "UNAVAILABLE", message = msg) - } - - private suspend fun connectOnce(endpoint: BridgeEndpoint, hello: Hello, tls: BridgeTlsParams?) = - withContext(Dispatchers.IO) { - if (tls != null) { - try { - connectWithSocket(endpoint, hello, tls) - return@withContext - } catch (err: Throwable) { - if (tls.required) throw err - } - } - connectWithSocket(endpoint, hello, null) - } - - private suspend fun connectWithSocket(endpoint: BridgeEndpoint, hello: Hello, tls: BridgeTlsParams?) { - val socket = - createBridgeSocket(tls) { fingerprint -> - onTlsFingerprint?.invoke(tls?.stableId ?: endpoint.stableId, fingerprint) - } - socket.tcpNoDelay = true - socket.connect(InetSocketAddress(endpoint.host, endpoint.port), 8_000) - socket.soTimeout = 0 - startTlsHandshakeIfNeeded(socket) - - val reader = BufferedReader(InputStreamReader(socket.getInputStream(), Charsets.UTF_8)) - val writer = BufferedWriter(OutputStreamWriter(socket.getOutputStream(), Charsets.UTF_8)) - - val conn = Connection(socket, reader, writer, writeLock) - currentConnection = conn - - try { - conn.sendJson(buildHelloJson(hello)) - - val firstLine = reader.readLine() ?: throw IllegalStateException("bridge closed connection") - val first = json.parseToJsonElement(firstLine).asObjectOrNull() - ?: throw IllegalStateException("unexpected bridge response") - when (first["type"].asStringOrNull()) { - "hello-ok" -> { - val name = first["serverName"].asStringOrNull() ?: "Bridge" - val rawCanvasUrl = first["canvasHostUrl"].asStringOrNull()?.trim()?.ifEmpty { null } - val rawMainSessionKey = first["mainSessionKey"].asStringOrNull()?.trim()?.ifEmpty { null } - canvasHostUrl = normalizeCanvasHostUrl(rawCanvasUrl, endpoint) - mainSessionKey = rawMainSessionKey - if (BuildConfig.DEBUG) { - // Local JVM unit tests use android.jar stubs; Log.d can throw "not mocked". - runCatching { - android.util.Log.d( - "ClawdbotBridge", - "canvasHostUrl resolved=${canvasHostUrl ?: "none"} (raw=${rawCanvasUrl ?: "none"})", - ) - } - } - onConnected(name, conn.remoteAddress, rawMainSessionKey) - } - "error" -> { - val code = first["code"].asStringOrNull() ?: "UNAVAILABLE" - val msg = first["message"].asStringOrNull() ?: "connect failed" - throw IllegalStateException("$code: $msg") - } - else -> throw IllegalStateException("unexpected bridge response") - } - - while (scope.isActive) { - val line = reader.readLine() ?: break - val frame = json.parseToJsonElement(line).asObjectOrNull() ?: continue - when (frame["type"].asStringOrNull()) { - "event" -> { - val event = frame["event"].asStringOrNull() ?: continue - val payload = frame["payloadJSON"].asStringOrNull() - onEvent(event, payload) - } - "ping" -> { - val id = frame["id"].asStringOrNull() ?: "" - conn.sendJson(buildJsonObject { put("type", JsonPrimitive("pong")); put("id", JsonPrimitive(id)) }) - } - "res" -> { - val id = frame["id"].asStringOrNull() ?: continue - val ok = frame["ok"].asBooleanOrNull() ?: false - val payloadJson = frame["payloadJSON"].asStringOrNull() - val error = - frame["error"]?.let { - val obj = it.asObjectOrNull() ?: return@let null - val code = obj["code"].asStringOrNull() ?: "UNAVAILABLE" - val msg = obj["message"].asStringOrNull() ?: "request failed" - ErrorShape(code, msg) - } - pending.remove(id)?.complete(RpcResponse(id, ok, payloadJson, error)) - } - "invoke" -> { - val id = frame["id"].asStringOrNull() ?: continue - val command = frame["command"].asStringOrNull() ?: "" - val params = frame["paramsJSON"].asStringOrNull() - val result = - try { - onInvoke(InvokeRequest(id, command, params)) - } catch (err: Throwable) { - invokeErrorFromThrowable(err) - } - conn.sendJson( - buildJsonObject { - put("type", JsonPrimitive("invoke-res")) - put("id", JsonPrimitive(id)) - put("ok", JsonPrimitive(result.ok)) - if (result.payloadJson != null) put("payloadJSON", JsonPrimitive(result.payloadJson)) - if (result.error != null) { - put( - "error", - buildJsonObject { - put("code", JsonPrimitive(result.error.code)) - put("message", JsonPrimitive(result.error.message)) - }, - ) - } - }, - ) - } - "invoke-res" -> { - // gateway->node only (ignore) - } - } - } - } finally { - currentConnection = null - for ((_, waiter) in pending) { - waiter.cancel() - } - pending.clear() - conn.closeQuietly() - } - } - - private fun buildHelloJson(hello: Hello): JsonObject = - buildJsonObject { - put("type", JsonPrimitive("hello")) - put("nodeId", JsonPrimitive(hello.nodeId)) - hello.displayName?.let { put("displayName", JsonPrimitive(it)) } - hello.token?.let { put("token", JsonPrimitive(it)) } - hello.platform?.let { put("platform", JsonPrimitive(it)) } - hello.version?.let { put("version", JsonPrimitive(it)) } - hello.deviceFamily?.let { put("deviceFamily", JsonPrimitive(it)) } - hello.modelIdentifier?.let { put("modelIdentifier", JsonPrimitive(it)) } - hello.caps?.let { put("caps", JsonArray(it.map(::JsonPrimitive))) } - hello.commands?.let { put("commands", JsonArray(it.map(::JsonPrimitive))) } - } - - private fun normalizeCanvasHostUrl(raw: String?, endpoint: BridgeEndpoint): String? { - val trimmed = raw?.trim().orEmpty() - val parsed = trimmed.takeIf { it.isNotBlank() }?.let { runCatching { URI(it) }.getOrNull() } - val host = parsed?.host?.trim().orEmpty() - val port = parsed?.port ?: -1 - val scheme = parsed?.scheme?.trim().orEmpty().ifBlank { "http" } - - if (trimmed.isNotBlank() && !isLoopbackHost(host)) { - return trimmed - } - - val fallbackHost = - endpoint.tailnetDns?.trim().takeIf { !it.isNullOrEmpty() } - ?: endpoint.lanHost?.trim().takeIf { !it.isNullOrEmpty() } - ?: endpoint.host.trim() - if (fallbackHost.isEmpty()) return trimmed.ifBlank { null } - - val fallbackPort = endpoint.canvasPort ?: if (port > 0) port else 18793 - val formattedHost = if (fallbackHost.contains(":")) "[${fallbackHost}]" else fallbackHost - return "$scheme://$formattedHost:$fallbackPort" - } - - private fun isLoopbackHost(raw: String?): Boolean { - val host = raw?.trim()?.lowercase().orEmpty() - if (host.isEmpty()) return false - if (host == "localhost") return true - if (host == "::1") return true - if (host == "0.0.0.0" || host == "::") return true - return host.startsWith("127.") - } -} - -private fun JsonElement?.asObjectOrNull(): JsonObject? = this as? JsonObject - -private fun JsonElement?.asStringOrNull(): String? = - when (this) { - is JsonNull -> null - is JsonPrimitive -> content - else -> null - } - -private fun JsonElement?.asBooleanOrNull(): Boolean? = - when (this) { - is JsonPrimitive -> { - val c = content.trim() - when { - c.equals("true", ignoreCase = true) -> true - c.equals("false", ignoreCase = true) -> false - else -> null - } - } - else -> null - } diff --git a/apps/android/app/src/main/java/com/clawdbot/android/chat/ChatController.kt b/apps/android/app/src/main/java/com/clawdbot/android/chat/ChatController.kt index 794bd9edf..a8e64048c 100644 --- a/apps/android/app/src/main/java/com/clawdbot/android/chat/ChatController.kt +++ b/apps/android/app/src/main/java/com/clawdbot/android/chat/ChatController.kt @@ -1,6 +1,6 @@ package com.clawdbot.android.chat -import com.clawdbot.android.bridge.BridgeSession +import com.clawdbot.android.gateway.GatewaySession import java.util.UUID import java.util.concurrent.ConcurrentHashMap import kotlinx.coroutines.CoroutineScope @@ -20,8 +20,9 @@ import kotlinx.serialization.json.buildJsonObject class ChatController( private val scope: CoroutineScope, - private val session: BridgeSession, + private val session: GatewaySession, private val json: Json, + private val supportsChatSubscribe: Boolean, ) { private val _sessionKey = MutableStateFlow("main") val sessionKey: StateFlow = _sessionKey.asStateFlow() @@ -224,7 +225,7 @@ class ChatController( } } - fun handleBridgeEvent(event: String, payloadJson: String?) { + fun handleGatewayEvent(event: String, payloadJson: String?) { when (event) { "tick" -> { scope.launch { pollHealthIfNeeded(force = false) } @@ -259,10 +260,12 @@ class ChatController( val key = _sessionKey.value try { - try { - session.sendEvent("chat.subscribe", """{"sessionKey":"$key"}""") - } catch (_: Throwable) { - // best-effort + if (supportsChatSubscribe) { + try { + session.sendNodeEvent("chat.subscribe", """{"sessionKey":"$key"}""") + } catch (_: Throwable) { + // best-effort + } } val historyJson = session.request("chat.history", """{"sessionKey":"$key"}""") diff --git a/apps/android/app/src/main/java/com/clawdbot/android/bridge/BonjourEscapes.kt b/apps/android/app/src/main/java/com/clawdbot/android/gateway/BonjourEscapes.kt similarity index 96% rename from apps/android/app/src/main/java/com/clawdbot/android/bridge/BonjourEscapes.kt rename to apps/android/app/src/main/java/com/clawdbot/android/gateway/BonjourEscapes.kt index 9334572fd..c05d41b4b 100644 --- a/apps/android/app/src/main/java/com/clawdbot/android/bridge/BonjourEscapes.kt +++ b/apps/android/app/src/main/java/com/clawdbot/android/gateway/BonjourEscapes.kt @@ -1,4 +1,4 @@ -package com.clawdbot.android.bridge +package com.clawdbot.android.gateway object BonjourEscapes { fun decode(input: String): String { diff --git a/apps/android/app/src/main/java/com/clawdbot/android/gateway/DeviceAuthStore.kt b/apps/android/app/src/main/java/com/clawdbot/android/gateway/DeviceAuthStore.kt new file mode 100644 index 000000000..88643d8d7 --- /dev/null +++ b/apps/android/app/src/main/java/com/clawdbot/android/gateway/DeviceAuthStore.kt @@ -0,0 +1,26 @@ +package com.clawdbot.android.gateway + +import com.clawdbot.android.SecurePrefs + +class DeviceAuthStore(private val prefs: SecurePrefs) { + fun loadToken(deviceId: String, role: String): String? { + val key = tokenKey(deviceId, role) + return prefs.getString(key)?.trim()?.takeIf { it.isNotEmpty() } + } + + fun saveToken(deviceId: String, role: String, token: String) { + val key = tokenKey(deviceId, role) + prefs.putString(key, token.trim()) + } + + fun clearToken(deviceId: String, role: String) { + val key = tokenKey(deviceId, role) + prefs.remove(key) + } + + private fun tokenKey(deviceId: String, role: String): String { + val normalizedDevice = deviceId.trim().lowercase() + val normalizedRole = role.trim().lowercase() + return "gateway.deviceToken.$normalizedDevice.$normalizedRole" + } +} diff --git a/apps/android/app/src/main/java/com/clawdbot/android/gateway/DeviceIdentityStore.kt b/apps/android/app/src/main/java/com/clawdbot/android/gateway/DeviceIdentityStore.kt new file mode 100644 index 000000000..72500b750 --- /dev/null +++ b/apps/android/app/src/main/java/com/clawdbot/android/gateway/DeviceIdentityStore.kt @@ -0,0 +1,146 @@ +package com.clawdbot.android.gateway + +import android.content.Context +import android.util.Base64 +import java.io.File +import java.security.KeyFactory +import java.security.KeyPairGenerator +import java.security.MessageDigest +import java.security.Signature +import java.security.spec.PKCS8EncodedKeySpec +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +@Serializable +data class DeviceIdentity( + val deviceId: String, + val publicKeyRawBase64: String, + val privateKeyPkcs8Base64: String, + val createdAtMs: Long, +) + +class DeviceIdentityStore(context: Context) { + private val json = Json { ignoreUnknownKeys = true } + private val identityFile = File(context.filesDir, "clawdbot/identity/device.json") + + @Synchronized + fun loadOrCreate(): DeviceIdentity { + val existing = load() + if (existing != null) { + val derived = deriveDeviceId(existing.publicKeyRawBase64) + if (derived != null && derived != existing.deviceId) { + val updated = existing.copy(deviceId = derived) + save(updated) + return updated + } + return existing + } + val fresh = generate() + save(fresh) + return fresh + } + + fun signPayload(payload: String, identity: DeviceIdentity): String? { + return try { + val privateKeyBytes = Base64.decode(identity.privateKeyPkcs8Base64, Base64.DEFAULT) + val keySpec = PKCS8EncodedKeySpec(privateKeyBytes) + val keyFactory = KeyFactory.getInstance("Ed25519") + val privateKey = keyFactory.generatePrivate(keySpec) + val signature = Signature.getInstance("Ed25519") + signature.initSign(privateKey) + signature.update(payload.toByteArray(Charsets.UTF_8)) + base64UrlEncode(signature.sign()) + } catch (_: Throwable) { + null + } + } + + fun publicKeyBase64Url(identity: DeviceIdentity): String? { + return try { + val raw = Base64.decode(identity.publicKeyRawBase64, Base64.DEFAULT) + base64UrlEncode(raw) + } catch (_: Throwable) { + null + } + } + + private fun load(): DeviceIdentity? { + return try { + if (!identityFile.exists()) return null + val raw = identityFile.readText(Charsets.UTF_8) + val decoded = json.decodeFromString(DeviceIdentity.serializer(), raw) + if (decoded.deviceId.isBlank() || + decoded.publicKeyRawBase64.isBlank() || + decoded.privateKeyPkcs8Base64.isBlank() + ) { + null + } else { + decoded + } + } catch (_: Throwable) { + null + } + } + + private fun save(identity: DeviceIdentity) { + try { + identityFile.parentFile?.mkdirs() + val encoded = json.encodeToString(DeviceIdentity.serializer(), identity) + identityFile.writeText(encoded, Charsets.UTF_8) + } catch (_: Throwable) { + // best-effort only + } + } + + private fun generate(): DeviceIdentity { + val keyPair = KeyPairGenerator.getInstance("Ed25519").generateKeyPair() + val spki = keyPair.public.encoded + val rawPublic = stripSpkiPrefix(spki) + val deviceId = sha256Hex(rawPublic) + val privateKey = keyPair.private.encoded + return DeviceIdentity( + deviceId = deviceId, + publicKeyRawBase64 = Base64.encodeToString(rawPublic, Base64.NO_WRAP), + privateKeyPkcs8Base64 = Base64.encodeToString(privateKey, Base64.NO_WRAP), + createdAtMs = System.currentTimeMillis(), + ) + } + + private fun deriveDeviceId(publicKeyRawBase64: String): String? { + return try { + val raw = Base64.decode(publicKeyRawBase64, Base64.DEFAULT) + sha256Hex(raw) + } catch (_: Throwable) { + null + } + } + + private fun stripSpkiPrefix(spki: ByteArray): ByteArray { + if (spki.size == ED25519_SPKI_PREFIX.size + 32 && + spki.copyOfRange(0, ED25519_SPKI_PREFIX.size).contentEquals(ED25519_SPKI_PREFIX) + ) { + return spki.copyOfRange(ED25519_SPKI_PREFIX.size, spki.size) + } + return spki + } + + private fun sha256Hex(data: ByteArray): String { + val digest = MessageDigest.getInstance("SHA-256").digest(data) + val out = StringBuilder(digest.size * 2) + for (byte in digest) { + out.append(String.format("%02x", byte)) + } + return out.toString() + } + + private fun base64UrlEncode(data: ByteArray): String { + return Base64.encodeToString(data, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING) + } + + companion object { + private val ED25519_SPKI_PREFIX = + byteArrayOf( + 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00, + ) + } +} diff --git a/apps/android/app/src/main/java/com/clawdbot/android/bridge/BridgeDiscovery.kt b/apps/android/app/src/main/java/com/clawdbot/android/gateway/GatewayDiscovery.kt similarity index 93% rename from apps/android/app/src/main/java/com/clawdbot/android/bridge/BridgeDiscovery.kt rename to apps/android/app/src/main/java/com/clawdbot/android/gateway/GatewayDiscovery.kt index d619200bf..121a95485 100644 --- a/apps/android/app/src/main/java/com/clawdbot/android/bridge/BridgeDiscovery.kt +++ b/apps/android/app/src/main/java/com/clawdbot/android/gateway/GatewayDiscovery.kt @@ -1,4 +1,4 @@ -package com.clawdbot.android.bridge +package com.clawdbot.android.gateway import android.content.Context import android.net.ConnectivityManager @@ -44,21 +44,21 @@ import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @Suppress("DEPRECATION") -class BridgeDiscovery( +class GatewayDiscovery( context: Context, private val scope: CoroutineScope, ) { private val nsd = context.getSystemService(NsdManager::class.java) private val connectivity = context.getSystemService(ConnectivityManager::class.java) private val dns = DnsResolver.getInstance() - private val serviceType = "_clawdbot-bridge._tcp." + private val serviceType = "_clawdbot-gw._tcp." private val wideAreaDomain = "clawdbot.internal." - private val logTag = "Clawdbot/BridgeDiscovery" + private val logTag = "Clawdbot/GatewayDiscovery" - private val localById = ConcurrentHashMap() - private val unicastById = ConcurrentHashMap() - private val _bridges = MutableStateFlow>(emptyList()) - val bridges: StateFlow> = _bridges.asStateFlow() + private val localById = ConcurrentHashMap() + private val unicastById = ConcurrentHashMap() + private val _gateways = MutableStateFlow>(emptyList()) + val gateways: StateFlow> = _gateways.asStateFlow() private val _statusText = MutableStateFlow("Searching…") val statusText: StateFlow = _statusText.asStateFlow() @@ -77,7 +77,7 @@ class BridgeDiscovery( override fun onDiscoveryStopped(serviceType: String) {} override fun onServiceFound(serviceInfo: NsdServiceInfo) { - if (serviceInfo.serviceType != this@BridgeDiscovery.serviceType) return + if (serviceInfo.serviceType != this@GatewayDiscovery.serviceType) return resolve(serviceInfo) } @@ -141,13 +141,12 @@ class BridgeDiscovery( val lanHost = txt(resolved, "lanHost") val tailnetDns = txt(resolved, "tailnetDns") val gatewayPort = txtInt(resolved, "gatewayPort") - val bridgePort = txtInt(resolved, "bridgePort") val canvasPort = txtInt(resolved, "canvasPort") - val tlsEnabled = txtBool(resolved, "bridgeTls") - val tlsFingerprint = txt(resolved, "bridgeTlsSha256") + val tlsEnabled = txtBool(resolved, "gatewayTls") + val tlsFingerprint = txt(resolved, "gatewayTlsSha256") val id = stableId(serviceName, "local.") localById[id] = - BridgeEndpoint( + GatewayEndpoint( stableId = id, name = displayName, host = host, @@ -155,7 +154,6 @@ class BridgeDiscovery( lanHost = lanHost, tailnetDns = tailnetDns, gatewayPort = gatewayPort, - bridgePort = bridgePort, canvasPort = canvasPort, tlsEnabled = tlsEnabled, tlsFingerprintSha256 = tlsFingerprint, @@ -167,7 +165,7 @@ class BridgeDiscovery( } private fun publish() { - _bridges.value = + _gateways.value = (localById.values + unicastById.values).sortedBy { it.name.lowercase() } _statusText.value = buildStatusText() } @@ -186,7 +184,7 @@ class BridgeDiscovery( } return when { - localCount == 0 && wideRcode == null -> "Searching for bridges…" + localCount == 0 && wideRcode == null -> "Searching for gateways…" localCount == 0 -> "$wide" else -> "Local: $localCount • $wide" } @@ -223,7 +221,7 @@ class BridgeDiscovery( val ptrMsg = lookupUnicastMessage(ptrName, Type.PTR) ?: return val ptrRecords = records(ptrMsg, Section.ANSWER).mapNotNull { it as? PTRRecord } - val next = LinkedHashMap() + val next = LinkedHashMap() for (ptr in ptrRecords) { val instanceFqdn = ptr.target.toString() val srv = @@ -259,13 +257,12 @@ class BridgeDiscovery( val lanHost = txtValue(txt, "lanHost") val tailnetDns = txtValue(txt, "tailnetDns") val gatewayPort = txtIntValue(txt, "gatewayPort") - val bridgePort = txtIntValue(txt, "bridgePort") val canvasPort = txtIntValue(txt, "canvasPort") - val tlsEnabled = txtBoolValue(txt, "bridgeTls") - val tlsFingerprint = txtValue(txt, "bridgeTlsSha256") + val tlsEnabled = txtBoolValue(txt, "gatewayTls") + val tlsFingerprint = txtValue(txt, "gatewayTlsSha256") val id = stableId(instanceName, domain) next[id] = - BridgeEndpoint( + GatewayEndpoint( stableId = id, name = displayName, host = host, @@ -273,7 +270,6 @@ class BridgeDiscovery( lanHost = lanHost, tailnetDns = tailnetDns, gatewayPort = gatewayPort, - bridgePort = bridgePort, canvasPort = canvasPort, tlsEnabled = tlsEnabled, tlsFingerprintSha256 = tlsFingerprint, diff --git a/apps/android/app/src/main/java/com/clawdbot/android/bridge/BridgeEndpoint.kt b/apps/android/app/src/main/java/com/clawdbot/android/gateway/GatewayEndpoint.kt similarity index 68% rename from apps/android/app/src/main/java/com/clawdbot/android/bridge/BridgeEndpoint.kt rename to apps/android/app/src/main/java/com/clawdbot/android/gateway/GatewayEndpoint.kt index c86352d76..ab8aeacc9 100644 --- a/apps/android/app/src/main/java/com/clawdbot/android/bridge/BridgeEndpoint.kt +++ b/apps/android/app/src/main/java/com/clawdbot/android/gateway/GatewayEndpoint.kt @@ -1,6 +1,6 @@ -package com.clawdbot.android.bridge +package com.clawdbot.android.gateway -data class BridgeEndpoint( +data class GatewayEndpoint( val stableId: String, val name: String, val host: String, @@ -8,15 +8,14 @@ data class BridgeEndpoint( val lanHost: String? = null, val tailnetDns: String? = null, val gatewayPort: Int? = null, - val bridgePort: Int? = null, val canvasPort: Int? = null, val tlsEnabled: Boolean = false, val tlsFingerprintSha256: String? = null, ) { companion object { - fun manual(host: String, port: Int): BridgeEndpoint = - BridgeEndpoint( - stableId = "manual|$host|$port", + fun manual(host: String, port: Int): GatewayEndpoint = + GatewayEndpoint( + stableId = "manual|${host.lowercase()}|$port", name = "$host:$port", host = host, port = port, diff --git a/apps/android/app/src/main/java/com/clawdbot/android/gateway/GatewayProtocol.kt b/apps/android/app/src/main/java/com/clawdbot/android/gateway/GatewayProtocol.kt new file mode 100644 index 000000000..4873de122 --- /dev/null +++ b/apps/android/app/src/main/java/com/clawdbot/android/gateway/GatewayProtocol.kt @@ -0,0 +1,3 @@ +package com.clawdbot.android.gateway + +const val GATEWAY_PROTOCOL_VERSION = 3 diff --git a/apps/android/app/src/main/java/com/clawdbot/android/gateway/GatewaySession.kt b/apps/android/app/src/main/java/com/clawdbot/android/gateway/GatewaySession.kt new file mode 100644 index 000000000..ddd249a8e --- /dev/null +++ b/apps/android/app/src/main/java/com/clawdbot/android/gateway/GatewaySession.kt @@ -0,0 +1,683 @@ +package com.clawdbot.android.gateway + +import android.util.Log +import java.util.Locale +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicBoolean +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonObject +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okhttp3.WebSocket +import okhttp3.WebSocketListener + +data class GatewayClientInfo( + val id: String, + val displayName: String?, + val version: String, + val platform: String, + val mode: String, + val instanceId: String?, + val deviceFamily: String?, + val modelIdentifier: String?, +) + +data class GatewayConnectOptions( + val role: String, + val scopes: List, + val caps: List, + val commands: List, + val permissions: Map, + val client: GatewayClientInfo, + val userAgent: String? = null, +) + +class GatewaySession( + private val scope: CoroutineScope, + private val identityStore: DeviceIdentityStore, + private val deviceAuthStore: DeviceAuthStore, + private val onConnected: (serverName: String?, remoteAddress: String?, mainSessionKey: String?) -> Unit, + private val onDisconnected: (message: String) -> Unit, + private val onEvent: (event: String, payloadJson: String?) -> Unit, + private val onInvoke: (suspend (InvokeRequest) -> InvokeResult)? = null, + private val onTlsFingerprint: ((stableId: String, fingerprint: String) -> Unit)? = null, +) { + data class InvokeRequest( + val id: String, + val nodeId: String, + val command: String, + val paramsJson: String?, + val timeoutMs: Long?, + ) + + data class InvokeResult(val ok: Boolean, val payloadJson: String?, val error: ErrorShape?) { + companion object { + fun ok(payloadJson: String?) = InvokeResult(ok = true, payloadJson = payloadJson, error = null) + fun error(code: String, message: String) = + InvokeResult(ok = false, payloadJson = null, error = ErrorShape(code = code, message = message)) + } + } + + data class ErrorShape(val code: String, val message: String) + + private val json = Json { ignoreUnknownKeys = true } + private val writeLock = Mutex() + private val pending = ConcurrentHashMap>() + + @Volatile private var canvasHostUrl: String? = null + @Volatile private var mainSessionKey: String? = null + + private data class DesiredConnection( + val endpoint: GatewayEndpoint, + val token: String?, + val password: String?, + val options: GatewayConnectOptions, + val tls: GatewayTlsParams?, + ) + + private var desired: DesiredConnection? = null + private var job: Job? = null + @Volatile private var currentConnection: Connection? = null + + fun connect( + endpoint: GatewayEndpoint, + token: String?, + password: String?, + options: GatewayConnectOptions, + tls: GatewayTlsParams? = null, + ) { + desired = DesiredConnection(endpoint, token, password, options, tls) + if (job == null) { + job = scope.launch(Dispatchers.IO) { runLoop() } + } + } + + fun disconnect() { + desired = null + currentConnection?.closeQuietly() + scope.launch(Dispatchers.IO) { + job?.cancelAndJoin() + job = null + canvasHostUrl = null + mainSessionKey = null + onDisconnected("Offline") + } + } + + fun reconnect() { + currentConnection?.closeQuietly() + } + + fun currentCanvasHostUrl(): String? = canvasHostUrl + fun currentMainSessionKey(): String? = mainSessionKey + + suspend fun sendNodeEvent(event: String, payloadJson: String?) { + val conn = currentConnection ?: return + val parsedPayload = payloadJson?.let { parseJsonOrNull(it) } + val params = + buildJsonObject { + put("event", JsonPrimitive(event)) + if (parsedPayload != null) { + put("payload", parsedPayload) + } else if (payloadJson != null) { + put("payloadJSON", JsonPrimitive(payloadJson)) + } else { + put("payloadJSON", JsonNull) + } + } + try { + conn.request("node.event", params, timeoutMs = 8_000) + } catch (err: Throwable) { + Log.w("ClawdbotGateway", "node.event failed: ${err.message ?: err::class.java.simpleName}") + } + } + + suspend fun request(method: String, paramsJson: String?, timeoutMs: Long = 15_000): String { + val conn = currentConnection ?: throw IllegalStateException("not connected") + val params = + if (paramsJson.isNullOrBlank()) { + null + } else { + json.parseToJsonElement(paramsJson) + } + val res = conn.request(method, params, timeoutMs) + if (res.ok) return res.payloadJson ?: "" + val err = res.error + throw IllegalStateException("${err?.code ?: "UNAVAILABLE"}: ${err?.message ?: "request failed"}") + } + + private data class RpcResponse(val id: String, val ok: Boolean, val payloadJson: String?, val error: ErrorShape?) + + private inner class Connection( + private val endpoint: GatewayEndpoint, + private val token: String?, + private val password: String?, + private val options: GatewayConnectOptions, + private val tls: GatewayTlsParams?, + ) { + private val connectDeferred = CompletableDeferred() + private val closedDeferred = CompletableDeferred() + private val isClosed = AtomicBoolean(false) + private val connectNonceDeferred = CompletableDeferred() + private val client: OkHttpClient = buildClient() + private var socket: WebSocket? = null + private val loggerTag = "ClawdbotGateway" + + val remoteAddress: String = + if (endpoint.host.contains(":")) { + "[${endpoint.host}]:${endpoint.port}" + } else { + "${endpoint.host}:${endpoint.port}" + } + + suspend fun connect() { + val scheme = if (tls != null) "wss" else "ws" + val url = "$scheme://${endpoint.host}:${endpoint.port}" + val request = Request.Builder().url(url).build() + socket = client.newWebSocket(request, Listener()) + try { + connectDeferred.await() + } catch (err: Throwable) { + throw err + } + } + + suspend fun request(method: String, params: JsonElement?, timeoutMs: Long): RpcResponse { + val id = UUID.randomUUID().toString() + val deferred = CompletableDeferred() + pending[id] = deferred + val frame = + buildJsonObject { + put("type", JsonPrimitive("req")) + put("id", JsonPrimitive(id)) + put("method", JsonPrimitive(method)) + if (params != null) put("params", params) + } + sendJson(frame) + return try { + withTimeout(timeoutMs) { deferred.await() } + } catch (err: TimeoutCancellationException) { + pending.remove(id) + throw IllegalStateException("request timeout") + } + } + + suspend fun sendJson(obj: JsonObject) { + val jsonString = obj.toString() + writeLock.withLock { + socket?.send(jsonString) + } + } + + suspend fun awaitClose() = closedDeferred.await() + + fun closeQuietly() { + if (isClosed.compareAndSet(false, true)) { + socket?.close(1000, "bye") + socket = null + closedDeferred.complete(Unit) + } + } + + private fun buildClient(): OkHttpClient { + val builder = OkHttpClient.Builder() + val tlsConfig = buildGatewayTlsConfig(tls) { fingerprint -> + onTlsFingerprint?.invoke(tls?.stableId ?: endpoint.stableId, fingerprint) + } + if (tlsConfig != null) { + builder.sslSocketFactory(tlsConfig.sslSocketFactory, tlsConfig.trustManager) + builder.hostnameVerifier(tlsConfig.hostnameVerifier) + } + return builder.build() + } + + private inner class Listener : WebSocketListener() { + override fun onOpen(webSocket: WebSocket, response: Response) { + scope.launch { + try { + val nonce = awaitConnectNonce() + sendConnect(nonce) + } catch (err: Throwable) { + connectDeferred.completeExceptionally(err) + closeQuietly() + } + } + } + + override fun onMessage(webSocket: WebSocket, text: String) { + scope.launch { handleMessage(text) } + } + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + if (!connectDeferred.isCompleted) { + connectDeferred.completeExceptionally(t) + } + if (isClosed.compareAndSet(false, true)) { + failPending() + closedDeferred.complete(Unit) + onDisconnected("Gateway error: ${t.message ?: t::class.java.simpleName}") + } + } + + override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { + if (!connectDeferred.isCompleted) { + connectDeferred.completeExceptionally(IllegalStateException("Gateway closed: $reason")) + } + if (isClosed.compareAndSet(false, true)) { + failPending() + closedDeferred.complete(Unit) + onDisconnected("Gateway closed: $reason") + } + } + } + + private suspend fun sendConnect(connectNonce: String?) { + val identity = identityStore.loadOrCreate() + val storedToken = deviceAuthStore.loadToken(identity.deviceId, options.role) + val trimmedToken = token?.trim().orEmpty() + val authToken = if (storedToken.isNullOrBlank()) trimmedToken else storedToken + val canFallbackToShared = !storedToken.isNullOrBlank() && trimmedToken.isNotBlank() + val payload = buildConnectParams(identity, connectNonce, authToken, password?.trim()) + val res = request("connect", payload, timeoutMs = 8_000) + if (!res.ok) { + val msg = res.error?.message ?: "connect failed" + if (canFallbackToShared) { + deviceAuthStore.clearToken(identity.deviceId, options.role) + } + throw IllegalStateException(msg) + } + val payloadJson = res.payloadJson ?: throw IllegalStateException("connect failed: missing payload") + val obj = json.parseToJsonElement(payloadJson).asObjectOrNull() ?: throw IllegalStateException("connect failed") + val serverName = obj["server"].asObjectOrNull()?.get("host").asStringOrNull() + val authObj = obj["auth"].asObjectOrNull() + val deviceToken = authObj?.get("deviceToken").asStringOrNull() + val authRole = authObj?.get("role").asStringOrNull() ?: options.role + if (!deviceToken.isNullOrBlank()) { + deviceAuthStore.saveToken(identity.deviceId, authRole, deviceToken) + } + val rawCanvas = obj["canvasHostUrl"].asStringOrNull() + canvasHostUrl = normalizeCanvasHostUrl(rawCanvas, endpoint) + val sessionDefaults = + obj["snapshot"].asObjectOrNull() + ?.get("sessionDefaults").asObjectOrNull() + mainSessionKey = sessionDefaults?.get("mainSessionKey").asStringOrNull() + onConnected(serverName, remoteAddress, mainSessionKey) + connectDeferred.complete(Unit) + } + + private fun buildConnectParams( + identity: DeviceIdentity, + connectNonce: String?, + authToken: String, + authPassword: String?, + ): JsonObject { + val client = options.client + val locale = Locale.getDefault().toLanguageTag() + val clientObj = + buildJsonObject { + put("id", JsonPrimitive(client.id)) + client.displayName?.let { put("displayName", JsonPrimitive(it)) } + put("version", JsonPrimitive(client.version)) + put("platform", JsonPrimitive(client.platform)) + put("mode", JsonPrimitive(client.mode)) + client.instanceId?.let { put("instanceId", JsonPrimitive(it)) } + client.deviceFamily?.let { put("deviceFamily", JsonPrimitive(it)) } + client.modelIdentifier?.let { put("modelIdentifier", JsonPrimitive(it)) } + } + + val password = authPassword?.trim().orEmpty() + val authJson = + when { + authToken.isNotEmpty() -> + buildJsonObject { + put("token", JsonPrimitive(authToken)) + } + password.isNotEmpty() -> + buildJsonObject { + put("password", JsonPrimitive(password)) + } + else -> null + } + + val signedAtMs = System.currentTimeMillis() + val payload = + buildDeviceAuthPayload( + deviceId = identity.deviceId, + clientId = client.id, + clientMode = client.mode, + role = options.role, + scopes = options.scopes, + signedAtMs = signedAtMs, + token = if (authToken.isNotEmpty()) authToken else null, + nonce = connectNonce, + ) + val signature = identityStore.signPayload(payload, identity) + val publicKey = identityStore.publicKeyBase64Url(identity) + val deviceJson = + if (!signature.isNullOrBlank() && !publicKey.isNullOrBlank()) { + buildJsonObject { + put("id", JsonPrimitive(identity.deviceId)) + put("publicKey", JsonPrimitive(publicKey)) + put("signature", JsonPrimitive(signature)) + put("signedAt", JsonPrimitive(signedAtMs)) + if (!connectNonce.isNullOrBlank()) { + put("nonce", JsonPrimitive(connectNonce)) + } + } + } else { + null + } + + return buildJsonObject { + put("minProtocol", JsonPrimitive(GATEWAY_PROTOCOL_VERSION)) + put("maxProtocol", JsonPrimitive(GATEWAY_PROTOCOL_VERSION)) + put("client", clientObj) + if (options.caps.isNotEmpty()) put("caps", JsonArray(options.caps.map(::JsonPrimitive))) + if (options.commands.isNotEmpty()) put("commands", JsonArray(options.commands.map(::JsonPrimitive))) + if (options.permissions.isNotEmpty()) { + put( + "permissions", + buildJsonObject { + options.permissions.forEach { (key, value) -> + put(key, JsonPrimitive(value)) + } + }, + ) + } + put("role", JsonPrimitive(options.role)) + if (options.scopes.isNotEmpty()) put("scopes", JsonArray(options.scopes.map(::JsonPrimitive))) + authJson?.let { put("auth", it) } + deviceJson?.let { put("device", it) } + put("locale", JsonPrimitive(locale)) + options.userAgent?.trim()?.takeIf { it.isNotEmpty() }?.let { + put("userAgent", JsonPrimitive(it)) + } + } + } + + private suspend fun handleMessage(text: String) { + val frame = json.parseToJsonElement(text).asObjectOrNull() ?: return + when (frame["type"].asStringOrNull()) { + "res" -> handleResponse(frame) + "event" -> handleEvent(frame) + } + } + + private fun handleResponse(frame: JsonObject) { + val id = frame["id"].asStringOrNull() ?: return + val ok = frame["ok"].asBooleanOrNull() ?: false + val payloadJson = frame["payload"]?.let { payload -> payload.toString() } + val error = + frame["error"]?.asObjectOrNull()?.let { obj -> + val code = obj["code"].asStringOrNull() ?: "UNAVAILABLE" + val msg = obj["message"].asStringOrNull() ?: "request failed" + ErrorShape(code, msg) + } + pending.remove(id)?.complete(RpcResponse(id, ok, payloadJson, error)) + } + + private fun handleEvent(frame: JsonObject) { + val event = frame["event"].asStringOrNull() ?: return + val payloadJson = + frame["payload"]?.let { it.toString() } ?: frame["payloadJSON"].asStringOrNull() + if (event == "connect.challenge") { + val nonce = extractConnectNonce(payloadJson) + if (!connectNonceDeferred.isCompleted) { + connectNonceDeferred.complete(nonce) + } + return + } + if (event == "node.invoke.request" && payloadJson != null && onInvoke != null) { + handleInvokeEvent(payloadJson) + return + } + onEvent(event, payloadJson) + } + + private suspend fun awaitConnectNonce(): String? { + if (isLoopbackHost(endpoint.host)) return null + return try { + withTimeout(2_000) { connectNonceDeferred.await() } + } catch (_: Throwable) { + null + } + } + + private fun extractConnectNonce(payloadJson: String?): String? { + if (payloadJson.isNullOrBlank()) return null + val obj = parseJsonOrNull(payloadJson)?.asObjectOrNull() ?: return null + return obj["nonce"].asStringOrNull() + } + + private fun handleInvokeEvent(payloadJson: String) { + val payload = + try { + json.parseToJsonElement(payloadJson).asObjectOrNull() + } catch (_: Throwable) { + null + } ?: return + val id = payload["id"].asStringOrNull() ?: return + val nodeId = payload["nodeId"].asStringOrNull() ?: return + val command = payload["command"].asStringOrNull() ?: return + val params = + payload["paramsJSON"].asStringOrNull() + ?: payload["params"]?.let { value -> if (value is JsonNull) null else value.toString() } + val timeoutMs = payload["timeoutMs"].asLongOrNull() + scope.launch { + val result = + try { + onInvoke?.invoke(InvokeRequest(id, nodeId, command, params, timeoutMs)) + ?: InvokeResult.error("UNAVAILABLE", "invoke handler missing") + } catch (err: Throwable) { + invokeErrorFromThrowable(err) + } + sendInvokeResult(id, nodeId, result) + } + } + + private suspend fun sendInvokeResult(id: String, nodeId: String, result: InvokeResult) { + val parsedPayload = result.payloadJson?.let { parseJsonOrNull(it) } + val params = + buildJsonObject { + put("id", JsonPrimitive(id)) + put("nodeId", JsonPrimitive(nodeId)) + put("ok", JsonPrimitive(result.ok)) + if (parsedPayload != null) { + put("payload", parsedPayload) + } else if (result.payloadJson != null) { + put("payloadJSON", JsonPrimitive(result.payloadJson)) + } + result.error?.let { err -> + put( + "error", + buildJsonObject { + put("code", JsonPrimitive(err.code)) + put("message", JsonPrimitive(err.message)) + }, + ) + } + } + try { + request("node.invoke.result", params, timeoutMs = 15_000) + } catch (err: Throwable) { + Log.w(loggerTag, "node.invoke.result failed: ${err.message ?: err::class.java.simpleName}") + } + } + + private fun invokeErrorFromThrowable(err: Throwable): InvokeResult { + val msg = err.message?.trim().takeIf { !it.isNullOrEmpty() } ?: err::class.java.simpleName + val parts = msg.split(":", limit = 2) + if (parts.size == 2) { + val code = parts[0].trim() + val rest = parts[1].trim() + if (code.isNotEmpty() && code.all { it.isUpperCase() || it == '_' }) { + return InvokeResult.error(code = code, message = rest.ifEmpty { msg }) + } + } + return InvokeResult.error(code = "UNAVAILABLE", message = msg) + } + + private fun failPending() { + for ((_, waiter) in pending) { + waiter.cancel() + } + pending.clear() + } + } + + private suspend fun runLoop() { + var attempt = 0 + while (scope.isActive) { + val target = desired + if (target == null) { + currentConnection?.closeQuietly() + currentConnection = null + delay(250) + continue + } + + try { + onDisconnected(if (attempt == 0) "Connecting…" else "Reconnecting…") + connectOnce(target) + attempt = 0 + } catch (err: Throwable) { + attempt += 1 + onDisconnected("Gateway error: ${err.message ?: err::class.java.simpleName}") + val sleepMs = minOf(8_000L, (350.0 * Math.pow(1.7, attempt.toDouble())).toLong()) + delay(sleepMs) + } + } + } + + private suspend fun connectOnce(target: DesiredConnection) = withContext(Dispatchers.IO) { + val conn = Connection(target.endpoint, target.token, target.password, target.options, target.tls) + currentConnection = conn + try { + conn.connect() + conn.awaitClose() + } finally { + currentConnection = null + canvasHostUrl = null + mainSessionKey = null + } + } + + private fun buildDeviceAuthPayload( + deviceId: String, + clientId: String, + clientMode: String, + role: String, + scopes: List, + signedAtMs: Long, + token: String?, + nonce: String?, + ): String { + val scopeString = scopes.joinToString(",") + val authToken = token.orEmpty() + val version = if (nonce.isNullOrBlank()) "v1" else "v2" + val parts = + mutableListOf( + version, + deviceId, + clientId, + clientMode, + role, + scopeString, + signedAtMs.toString(), + authToken, + ) + if (!nonce.isNullOrBlank()) { + parts.add(nonce) + } + return parts.joinToString("|") + } + + private fun normalizeCanvasHostUrl(raw: String?, endpoint: GatewayEndpoint): String? { + val trimmed = raw?.trim().orEmpty() + val parsed = trimmed.takeIf { it.isNotBlank() }?.let { runCatching { java.net.URI(it) }.getOrNull() } + val host = parsed?.host?.trim().orEmpty() + val port = parsed?.port ?: -1 + val scheme = parsed?.scheme?.trim().orEmpty().ifBlank { "http" } + + if (trimmed.isNotBlank() && !isLoopbackHost(host)) { + return trimmed + } + + val fallbackHost = + endpoint.tailnetDns?.trim().takeIf { !it.isNullOrEmpty() } + ?: endpoint.lanHost?.trim().takeIf { !it.isNullOrEmpty() } + ?: endpoint.host.trim() + if (fallbackHost.isEmpty()) return trimmed.ifBlank { null } + + val fallbackPort = endpoint.canvasPort ?: if (port > 0) port else 18793 + val formattedHost = if (fallbackHost.contains(":")) "[${fallbackHost}]" else fallbackHost + return "$scheme://$formattedHost:$fallbackPort" + } + + private fun isLoopbackHost(raw: String?): Boolean { + val host = raw?.trim()?.lowercase().orEmpty() + if (host.isEmpty()) return false + if (host == "localhost") return true + if (host == "::1") return true + if (host == "0.0.0.0" || host == "::") return true + return host.startsWith("127.") + } +} + +private fun JsonElement?.asObjectOrNull(): JsonObject? = this as? JsonObject + +private fun JsonElement?.asStringOrNull(): String? = + when (this) { + is JsonNull -> null + is JsonPrimitive -> content + else -> null + } + +private fun JsonElement?.asBooleanOrNull(): Boolean? = + when (this) { + is JsonPrimitive -> { + val c = content.trim() + when { + c.equals("true", ignoreCase = true) -> true + c.equals("false", ignoreCase = true) -> false + else -> null + } + } + else -> null + } + +private fun JsonElement?.asLongOrNull(): Long? = + when (this) { + is JsonPrimitive -> content.toLongOrNull() + else -> null + } + +private fun parseJsonOrNull(payload: String): JsonElement? { + val trimmed = payload.trim() + if (trimmed.isEmpty()) return null + return try { + Json.parseToJsonElement(trimmed) + } catch (_: Throwable) { + null + } +} diff --git a/apps/android/app/src/main/java/com/clawdbot/android/bridge/BridgeTls.kt b/apps/android/app/src/main/java/com/clawdbot/android/gateway/GatewayTls.kt similarity index 71% rename from apps/android/app/src/main/java/com/clawdbot/android/bridge/BridgeTls.kt rename to apps/android/app/src/main/java/com/clawdbot/android/gateway/GatewayTls.kt index 1a3afd148..bcca51583 100644 --- a/apps/android/app/src/main/java/com/clawdbot/android/bridge/BridgeTls.kt +++ b/apps/android/app/src/main/java/com/clawdbot/android/gateway/GatewayTls.kt @@ -1,25 +1,34 @@ -package com.clawdbot.android.bridge +package com.clawdbot.android.gateway import android.annotation.SuppressLint -import java.net.Socket import java.security.MessageDigest import java.security.SecureRandom import java.security.cert.CertificateException import java.security.cert.X509Certificate +import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLContext -import javax.net.ssl.SSLSocket +import javax.net.ssl.SSLSocketFactory import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager -data class BridgeTlsParams( +data class GatewayTlsParams( val required: Boolean, val expectedFingerprint: String?, val allowTOFU: Boolean, val stableId: String, ) -fun createBridgeSocket(params: BridgeTlsParams?, onStore: ((String) -> Unit)? = null): Socket { - if (params == null) return Socket() +data class GatewayTlsConfig( + val sslSocketFactory: SSLSocketFactory, + val trustManager: X509TrustManager, + val hostnameVerifier: HostnameVerifier, +) + +fun buildGatewayTlsConfig( + params: GatewayTlsParams?, + onStore: ((String) -> Unit)? = null, +): GatewayTlsConfig? { + if (params == null) return null val expected = params.expectedFingerprint?.let(::normalizeFingerprint) val defaultTrust = defaultTrustManager() @SuppressLint("CustomX509TrustManager") @@ -34,7 +43,7 @@ fun createBridgeSocket(params: BridgeTlsParams?, onStore: ((String) -> Unit)? = val fingerprint = sha256Hex(chain[0].encoded) if (expected != null) { if (fingerprint != expected) { - throw CertificateException("bridge TLS fingerprint mismatch") + throw CertificateException("gateway TLS fingerprint mismatch") } return } @@ -50,13 +59,11 @@ fun createBridgeSocket(params: BridgeTlsParams?, onStore: ((String) -> Unit)? = val context = SSLContext.getInstance("TLS") context.init(null, arrayOf(trustManager), SecureRandom()) - return context.socketFactory.createSocket() -} - -fun startTlsHandshakeIfNeeded(socket: Socket) { - if (socket is SSLSocket) { - socket.startHandshake() - } + return GatewayTlsConfig( + sslSocketFactory = context.socketFactory, + trustManager = trustManager, + hostnameVerifier = HostnameVerifier { _, _ -> true }, + ) } private fun defaultTrustManager(): X509TrustManager { @@ -77,5 +84,7 @@ private fun sha256Hex(data: ByteArray): String { } private fun normalizeFingerprint(raw: String): String { - return raw.lowercase().filter { it in '0'..'9' || it in 'a'..'f' } + val stripped = raw.trim() + .replace(Regex("^sha-?256\\s*:?\\s*", RegexOption.IGNORE_CASE), "") + return stripped.lowercase().filter { it in '0'..'9' || it in 'a'..'f' } } diff --git a/apps/android/app/src/main/java/com/clawdbot/android/ui/RootScreen.kt b/apps/android/app/src/main/java/com/clawdbot/android/ui/RootScreen.kt index 10b74926a..96d2543a7 100644 --- a/apps/android/app/src/main/java/com/clawdbot/android/ui/RootScreen.kt +++ b/apps/android/app/src/main/java/com/clawdbot/android/ui/RootScreen.kt @@ -118,7 +118,7 @@ fun RootScreen(viewModel: MainViewModel) { contentDescription = "Approval pending", ) } - // Avoid duplicating the primary bridge status ("Connecting…") in the activity slot. + // Avoid duplicating the primary gateway status ("Connecting…") in the activity slot. if (screenRecordActive) { return@remember StatusActivity( @@ -179,14 +179,14 @@ fun RootScreen(viewModel: MainViewModel) { null } - val bridgeState = + val gatewayState = remember(serverName, statusText) { when { - serverName != null -> BridgeState.Connected + serverName != null -> GatewayState.Connected statusText.contains("connecting", ignoreCase = true) || - statusText.contains("reconnecting", ignoreCase = true) -> BridgeState.Connecting - statusText.contains("error", ignoreCase = true) -> BridgeState.Error - else -> BridgeState.Disconnected + statusText.contains("reconnecting", ignoreCase = true) -> GatewayState.Connecting + statusText.contains("error", ignoreCase = true) -> GatewayState.Error + else -> GatewayState.Disconnected } } @@ -206,7 +206,7 @@ fun RootScreen(viewModel: MainViewModel) { // Keep the overlay buttons above the WebView canvas (AndroidView), otherwise they may not receive touches. Popup(alignment = Alignment.TopStart, properties = PopupProperties(focusable = false)) { StatusPill( - bridge = bridgeState, + gateway = gatewayState, voiceEnabled = voiceEnabled, activity = activity, onClick = { sheet = Sheet.Settings }, diff --git a/apps/android/app/src/main/java/com/clawdbot/android/ui/SettingsSheet.kt b/apps/android/app/src/main/java/com/clawdbot/android/ui/SettingsSheet.kt index a140f3344..e3a9b3ecb 100644 --- a/apps/android/app/src/main/java/com/clawdbot/android/ui/SettingsSheet.kt +++ b/apps/android/app/src/main/java/com/clawdbot/android/ui/SettingsSheet.kt @@ -28,6 +28,8 @@ import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ExpandLess import androidx.compose.material.icons.filled.ExpandMore @@ -48,7 +50,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat @@ -57,6 +63,7 @@ import com.clawdbot.android.LocationMode import com.clawdbot.android.MainViewModel import com.clawdbot.android.NodeForegroundService import com.clawdbot.android.VoiceWakeMode +import com.clawdbot.android.WakeWords @Composable fun SettingsSheet(viewModel: MainViewModel) { @@ -74,16 +81,19 @@ fun SettingsSheet(viewModel: MainViewModel) { val manualEnabled by viewModel.manualEnabled.collectAsState() val manualHost by viewModel.manualHost.collectAsState() val manualPort by viewModel.manualPort.collectAsState() + val manualTls by viewModel.manualTls.collectAsState() val canvasDebugStatusEnabled by viewModel.canvasDebugStatusEnabled.collectAsState() val statusText by viewModel.statusText.collectAsState() val serverName by viewModel.serverName.collectAsState() val remoteAddress by viewModel.remoteAddress.collectAsState() - val bridges by viewModel.bridges.collectAsState() + val gateways by viewModel.gateways.collectAsState() val discoveryStatusText by viewModel.discoveryStatusText.collectAsState() val listState = rememberLazyListState() val (wakeWordsText, setWakeWordsText) = remember { mutableStateOf("") } val (advancedExpanded, setAdvancedExpanded) = remember { mutableStateOf(false) } + val focusManager = LocalFocusManager.current + var wakeWordsHadFocus by remember { mutableStateOf(false) } val deviceModel = remember { listOfNotNull(Build.MANUFACTURER, Build.MODEL) @@ -102,6 +112,12 @@ fun SettingsSheet(viewModel: MainViewModel) { } LaunchedEffect(wakeWords) { setWakeWordsText(wakeWords.joinToString(", ")) } + val commitWakeWords = { + val parsed = WakeWords.parseIfChanged(wakeWordsText, wakeWords) + if (parsed != null) { + viewModel.setWakeWords(parsed) + } + } val permissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { perms -> @@ -163,7 +179,7 @@ fun SettingsSheet(viewModel: MainViewModel) { val smsPermissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> smsPermissionGranted = granted - viewModel.refreshBridgeHello() + viewModel.refreshGatewayConnection() } fun setCameraEnabledChecked(checked: Boolean) { @@ -223,20 +239,20 @@ fun SettingsSheet(viewModel: MainViewModel) { } } - val visibleBridges = + val visibleGateways = if (isConnected && remoteAddress != null) { - bridges.filterNot { "${it.host}:${it.port}" == remoteAddress } + gateways.filterNot { "${it.host}:${it.port}" == remoteAddress } } else { - bridges + gateways } - val bridgeDiscoveryFooterText = - if (visibleBridges.isEmpty()) { + val gatewayDiscoveryFooterText = + if (visibleGateways.isEmpty()) { discoveryStatusText } else if (isConnected) { - "Discovery active • ${visibleBridges.size} other bridge${if (visibleBridges.size == 1) "" else "s"} found" + "Discovery active • ${visibleGateways.size} other gateway${if (visibleGateways.size == 1) "" else "s"} found" } else { - "Discovery active • ${visibleBridges.size} bridge${if (visibleBridges.size == 1) "" else "s"} found" + "Discovery active • ${visibleGateways.size} gateway${if (visibleGateways.size == 1) "" else "s"} found" } LazyColumn( @@ -250,7 +266,7 @@ fun SettingsSheet(viewModel: MainViewModel) { contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(6.dp), ) { - // Order parity: Node → Bridge → Voice → Camera → Messaging → Location → Screen. + // Order parity: Node → Gateway → Voice → Camera → Messaging → Location → Screen. item { Text("Node", style = MaterialTheme.typography.titleSmall) } item { OutlinedTextField( @@ -266,8 +282,8 @@ fun SettingsSheet(viewModel: MainViewModel) { item { HorizontalDivider() } - // Bridge - item { Text("Bridge", style = MaterialTheme.typography.titleSmall) } + // Gateway + item { Text("Gateway", style = MaterialTheme.typography.titleSmall) } item { ListItem(headlineContent = { Text("Status") }, supportingContent = { Text(statusText) }) } if (serverName != null) { item { ListItem(headlineContent = { Text("Server") }, supportingContent = { Text(serverName!!) }) } @@ -291,31 +307,30 @@ fun SettingsSheet(viewModel: MainViewModel) { item { HorizontalDivider() } - if (!isConnected || visibleBridges.isNotEmpty()) { + if (!isConnected || visibleGateways.isNotEmpty()) { item { Text( - if (isConnected) "Other Bridges" else "Discovered Bridges", + if (isConnected) "Other Gateways" else "Discovered Gateways", style = MaterialTheme.typography.titleSmall, ) } - if (!isConnected && visibleBridges.isEmpty()) { - item { Text("No bridges found yet.", color = MaterialTheme.colorScheme.onSurfaceVariant) } + if (!isConnected && visibleGateways.isEmpty()) { + item { Text("No gateways found yet.", color = MaterialTheme.colorScheme.onSurfaceVariant) } } else { - items(items = visibleBridges, key = { it.stableId }) { bridge -> + items(items = visibleGateways, key = { it.stableId }) { gateway -> val detailLines = buildList { - add("IP: ${bridge.host}:${bridge.port}") - bridge.lanHost?.let { add("LAN: $it") } - bridge.tailnetDns?.let { add("Tailnet: $it") } - if (bridge.gatewayPort != null || bridge.bridgePort != null || bridge.canvasPort != null) { - val gw = bridge.gatewayPort?.toString() ?: "—" - val br = (bridge.bridgePort ?: bridge.port).toString() - val canvas = bridge.canvasPort?.toString() ?: "—" - add("Ports: gw $gw · bridge $br · canvas $canvas") + add("IP: ${gateway.host}:${gateway.port}") + gateway.lanHost?.let { add("LAN: $it") } + gateway.tailnetDns?.let { add("Tailnet: $it") } + if (gateway.gatewayPort != null || gateway.canvasPort != null) { + val gw = (gateway.gatewayPort ?: gateway.port).toString() + val canvas = gateway.canvasPort?.toString() ?: "—" + add("Ports: gw $gw · canvas $canvas") } } ListItem( - headlineContent = { Text(bridge.name) }, + headlineContent = { Text(gateway.name) }, supportingContent = { Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { detailLines.forEach { line -> @@ -327,7 +342,7 @@ fun SettingsSheet(viewModel: MainViewModel) { Button( onClick = { NodeForegroundService.start(context) - viewModel.connect(bridge) + viewModel.connect(gateway) }, ) { Text("Connect") @@ -338,7 +353,7 @@ fun SettingsSheet(viewModel: MainViewModel) { } item { Text( - bridgeDiscoveryFooterText, + gatewayDiscoveryFooterText, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, style = MaterialTheme.typography.labelMedium, @@ -352,7 +367,7 @@ fun SettingsSheet(viewModel: MainViewModel) { item { ListItem( headlineContent = { Text("Advanced") }, - supportingContent = { Text("Manual bridge connection") }, + supportingContent = { Text("Manual gateway connection") }, trailingContent = { Icon( imageVector = if (advancedExpanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore, @@ -369,7 +384,7 @@ fun SettingsSheet(viewModel: MainViewModel) { AnimatedVisibility(visible = advancedExpanded) { Column(verticalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.fillMaxWidth()) { ListItem( - headlineContent = { Text("Use Manual Bridge") }, + headlineContent = { Text("Use Manual Gateway") }, supportingContent = { Text("Use this when discovery is blocked.") }, trailingContent = { Switch(checked = manualEnabled, onCheckedChange = viewModel::setManualEnabled) }, ) @@ -388,6 +403,12 @@ fun SettingsSheet(viewModel: MainViewModel) { modifier = Modifier.fillMaxWidth(), enabled = manualEnabled, ) + ListItem( + headlineContent = { Text("Require TLS") }, + supportingContent = { Text("Pin the gateway certificate on first connect.") }, + trailingContent = { Switch(checked = manualTls, onCheckedChange = viewModel::setManualTls, enabled = manualEnabled) }, + modifier = Modifier.alpha(if (manualEnabled) 1f else 0.5f), + ) val hostOk = manualHost.trim().isNotEmpty() val portOk = manualPort in 1..65535 @@ -474,29 +495,31 @@ fun SettingsSheet(viewModel: MainViewModel) { value = wakeWordsText, onValueChange = setWakeWordsText, label = { Text("Wake Words (comma-separated)") }, - modifier = Modifier.fillMaxWidth(), + modifier = + Modifier.fillMaxWidth().onFocusChanged { focusState -> + if (focusState.isFocused) { + wakeWordsHadFocus = true + } else if (wakeWordsHadFocus) { + wakeWordsHadFocus = false + commitWakeWords() + } + }, singleLine = true, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = + KeyboardActions( + onDone = { + commitWakeWords() + focusManager.clearFocus() + }, + ), ) } - item { - Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { - Button( - onClick = { - val parsed = com.clawdbot.android.WakeWords.parseCommaSeparated(wakeWordsText) - viewModel.setWakeWords(parsed) - }, - enabled = isConnected, - ) { - Text("Save + Sync") - } - - Button(onClick = viewModel::resetWakeWordsDefaults) { Text("Reset defaults") } - } - } + item { Button(onClick = viewModel::resetWakeWordsDefaults) { Text("Reset defaults") } } item { Text( if (isConnected) { - "Any node can edit wake words. Changes sync via the gateway bridge." + "Any node can edit wake words. Changes sync via the gateway." } else { "Connect to a gateway to sync wake words globally." }, @@ -511,7 +534,7 @@ fun SettingsSheet(viewModel: MainViewModel) { item { ListItem( headlineContent = { Text("Allow Camera") }, - supportingContent = { Text("Allows the bridge to request photos or short video clips (foreground only).") }, + supportingContent = { Text("Allows the gateway to request photos or short video clips (foreground only).") }, trailingContent = { Switch(checked = cameraEnabled, onCheckedChange = ::setCameraEnabledChecked) }, ) } @@ -538,7 +561,7 @@ fun SettingsSheet(viewModel: MainViewModel) { supportingContent = { Text( if (smsPermissionAvailable) { - "Allow the bridge to send SMS from this device." + "Allow the gateway to send SMS from this device." } else { "SMS requires a device with telephony hardware." }, diff --git a/apps/android/app/src/main/java/com/clawdbot/android/ui/StatusPill.kt b/apps/android/app/src/main/java/com/clawdbot/android/ui/StatusPill.kt index 669d448ad..564d96b52 100644 --- a/apps/android/app/src/main/java/com/clawdbot/android/ui/StatusPill.kt +++ b/apps/android/app/src/main/java/com/clawdbot/android/ui/StatusPill.kt @@ -26,7 +26,7 @@ import androidx.compose.ui.unit.dp @Composable fun StatusPill( - bridge: BridgeState, + gateway: GatewayState, voiceEnabled: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, @@ -49,11 +49,11 @@ fun StatusPill( Surface( modifier = Modifier.size(9.dp), shape = CircleShape, - color = bridge.color, + color = gateway.color, ) {} Text( - text = bridge.title, + text = gateway.title, style = MaterialTheme.typography.labelLarge, ) } @@ -106,7 +106,7 @@ data class StatusActivity( val tint: Color? = null, ) -enum class BridgeState(val title: String, val color: Color) { +enum class GatewayState(val title: String, val color: Color) { Connected("Connected", Color(0xFF2ECC71)), Connecting("Connecting…", Color(0xFFF1C40F)), Error("Error", Color(0xFFE74C3C)), diff --git a/apps/android/app/src/main/java/com/clawdbot/android/voice/TalkModeManager.kt b/apps/android/app/src/main/java/com/clawdbot/android/voice/TalkModeManager.kt index 919a0ce3c..41f98140d 100644 --- a/apps/android/app/src/main/java/com/clawdbot/android/voice/TalkModeManager.kt +++ b/apps/android/app/src/main/java/com/clawdbot/android/voice/TalkModeManager.kt @@ -20,7 +20,7 @@ import android.speech.tts.TextToSpeech import android.speech.tts.UtteranceProgressListener import android.util.Log import androidx.core.content.ContextCompat -import com.clawdbot.android.bridge.BridgeSession +import com.clawdbot.android.gateway.GatewaySession import com.clawdbot.android.isCanonicalMainSessionKey import com.clawdbot.android.normalizeMainKey import java.net.HttpURLConnection @@ -46,6 +46,9 @@ import kotlin.math.max class TalkModeManager( private val context: Context, private val scope: CoroutineScope, + private val session: GatewaySession, + private val supportsChatSubscribe: Boolean, + private val isConnected: () -> Boolean, ) { companion object { private const val tag = "TalkMode" @@ -99,7 +102,6 @@ class TalkModeManager( private var modelOverrideActive = false private var mainSessionKey: String = "main" - private var session: BridgeSession? = null private var pendingRunId: String? = null private var pendingFinal: CompletableDeferred? = null private var chatSubscribedSessionKey: String? = null @@ -112,11 +114,6 @@ class TalkModeManager( private var systemTtsPending: CompletableDeferred? = null private var systemTtsPendingId: String? = null - fun attachSession(session: BridgeSession) { - this.session = session - chatSubscribedSessionKey = null - } - fun setMainSessionKey(sessionKey: String?) { val trimmed = sessionKey?.trim().orEmpty() if (trimmed.isEmpty()) return @@ -136,7 +133,7 @@ class TalkModeManager( } } - fun handleBridgeEvent(event: String, payloadJson: String?) { + fun handleGatewayEvent(event: String, payloadJson: String?) { if (event != "chat") return if (payloadJson.isNullOrBlank()) return val pending = pendingRunId ?: return @@ -306,25 +303,24 @@ class TalkModeManager( reloadConfig() val prompt = buildPrompt(transcript) - val bridge = session - if (bridge == null) { - _statusText.value = "Bridge not connected" - Log.w(tag, "finalize: bridge not connected") + if (!isConnected()) { + _statusText.value = "Gateway not connected" + Log.w(tag, "finalize: gateway not connected") start() return } try { val startedAt = System.currentTimeMillis().toDouble() / 1000.0 - subscribeChatIfNeeded(bridge = bridge, sessionKey = mainSessionKey) + subscribeChatIfNeeded(session = session, sessionKey = mainSessionKey) Log.d(tag, "chat.send start sessionKey=${mainSessionKey.ifBlank { "main" }} chars=${prompt.length}") - val runId = sendChat(prompt, bridge) + val runId = sendChat(prompt, session) Log.d(tag, "chat.send ok runId=$runId") val ok = waitForChatFinal(runId) if (!ok) { Log.w(tag, "chat final timeout runId=$runId; attempting history fallback") } - val assistant = waitForAssistantText(bridge, startedAt, if (ok) 12_000 else 25_000) + val assistant = waitForAssistantText(session, startedAt, if (ok) 12_000 else 25_000) if (assistant.isNullOrBlank()) { _statusText.value = "No reply" Log.w(tag, "assistant text timeout runId=$runId") @@ -343,12 +339,13 @@ class TalkModeManager( } } - private suspend fun subscribeChatIfNeeded(bridge: BridgeSession, sessionKey: String) { + private suspend fun subscribeChatIfNeeded(session: GatewaySession, sessionKey: String) { + if (!supportsChatSubscribe) return val key = sessionKey.trim() if (key.isEmpty()) return if (chatSubscribedSessionKey == key) return try { - bridge.sendEvent("chat.subscribe", """{"sessionKey":"$key"}""") + session.sendNodeEvent("chat.subscribe", """{"sessionKey":"$key"}""") chatSubscribedSessionKey = key Log.d(tag, "chat.subscribe ok sessionKey=$key") } catch (err: Throwable) { @@ -370,7 +367,7 @@ class TalkModeManager( return lines.joinToString("\n") } - private suspend fun sendChat(message: String, bridge: BridgeSession): String { + private suspend fun sendChat(message: String, session: GatewaySession): String { val runId = UUID.randomUUID().toString() val params = buildJsonObject { @@ -380,7 +377,7 @@ class TalkModeManager( put("timeoutMs", JsonPrimitive(30_000)) put("idempotencyKey", JsonPrimitive(runId)) } - val res = bridge.request("chat.send", params.toString()) + val res = session.request("chat.send", params.toString()) val parsed = parseRunId(res) ?: runId if (parsed != runId) { pendingRunId = parsed @@ -411,13 +408,13 @@ class TalkModeManager( } private suspend fun waitForAssistantText( - bridge: BridgeSession, + session: GatewaySession, sinceSeconds: Double, timeoutMs: Long, ): String? { val deadline = SystemClock.elapsedRealtime() + timeoutMs while (SystemClock.elapsedRealtime() < deadline) { - val text = fetchLatestAssistantText(bridge, sinceSeconds) + val text = fetchLatestAssistantText(session, sinceSeconds) if (!text.isNullOrBlank()) return text delay(300) } @@ -425,11 +422,11 @@ class TalkModeManager( } private suspend fun fetchLatestAssistantText( - bridge: BridgeSession, + session: GatewaySession, sinceSeconds: Double? = null, ): String? { val key = mainSessionKey.ifBlank { "main" } - val res = bridge.request("chat.history", "{\"sessionKey\":\"$key\"}") + val res = session.request("chat.history", "{\"sessionKey\":\"$key\"}") val root = json.parseToJsonElement(res).asObjectOrNull() ?: return null val messages = root["messages"] as? JsonArray ?: return null for (item in messages.reversed()) { @@ -813,12 +810,11 @@ class TalkModeManager( } private suspend fun reloadConfig() { - val bridge = session ?: return val envVoice = System.getenv("ELEVENLABS_VOICE_ID")?.trim() val sagVoice = System.getenv("SAG_VOICE_ID")?.trim() val envKey = System.getenv("ELEVENLABS_API_KEY")?.trim() try { - val res = bridge.request("config.get", "{}") + val res = session.request("config.get", "{}") val root = json.parseToJsonElement(res).asObjectOrNull() val config = root?.get("config").asObjectOrNull() val talk = config?.get("talk").asObjectOrNull() diff --git a/apps/android/app/src/test/java/com/clawdbot/android/WakeWordsTest.kt b/apps/android/app/src/test/java/com/clawdbot/android/WakeWordsTest.kt index 1d61383e8..9363e810c 100644 --- a/apps/android/app/src/test/java/com/clawdbot/android/WakeWordsTest.kt +++ b/apps/android/app/src/test/java/com/clawdbot/android/WakeWordsTest.kt @@ -1,6 +1,7 @@ package com.clawdbot.android import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull import org.junit.Test class WakeWordsTest { @@ -32,5 +33,18 @@ class WakeWordsTest { assertEquals("w1", sanitized.first()) assertEquals("w${WakeWords.maxWords}", sanitized.last()) } -} + @Test + fun parseIfChangedSkipsWhenUnchanged() { + val current = listOf("clawd", "claude") + val parsed = WakeWords.parseIfChanged(" clawd , claude ", current) + assertNull(parsed) + } + + @Test + fun parseIfChangedReturnsUpdatedList() { + val current = listOf("clawd") + val parsed = WakeWords.parseIfChanged(" clawd , jarvis ", current) + assertEquals(listOf("clawd", "jarvis"), parsed) + } +} diff --git a/apps/android/app/src/test/java/com/clawdbot/android/bridge/BridgeEndpointKotestTest.kt b/apps/android/app/src/test/java/com/clawdbot/android/bridge/BridgeEndpointKotestTest.kt deleted file mode 100644 index 524267650..000000000 --- a/apps/android/app/src/test/java/com/clawdbot/android/bridge/BridgeEndpointKotestTest.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.clawdbot.android.bridge - -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe - -class BridgeEndpointKotestTest : StringSpec({ - "manual endpoint builds stable id + name" { - val endpoint = BridgeEndpoint.manual("10.0.0.5", 18790) - endpoint.stableId shouldBe "manual|10.0.0.5|18790" - endpoint.name shouldBe "10.0.0.5:18790" - endpoint.host shouldBe "10.0.0.5" - endpoint.port shouldBe 18790 - } -}) diff --git a/apps/android/app/src/test/java/com/clawdbot/android/bridge/BridgePairingClientTest.kt b/apps/android/app/src/test/java/com/clawdbot/android/bridge/BridgePairingClientTest.kt deleted file mode 100644 index 7fa6fa6ac..000000000 --- a/apps/android/app/src/test/java/com/clawdbot/android/bridge/BridgePairingClientTest.kt +++ /dev/null @@ -1,108 +0,0 @@ -package com.clawdbot.android.bridge - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Test -import java.io.BufferedReader -import java.io.BufferedWriter -import java.io.InputStreamReader -import java.io.OutputStreamWriter -import java.net.ServerSocket - -class BridgePairingClientTest { - @Test - fun helloOkReturnsExistingToken() = runBlocking { - val serverSocket = ServerSocket(0) - val port = serverSocket.localPort - - val server = - async(Dispatchers.IO) { - serverSocket.use { ss -> - val sock = ss.accept() - sock.use { s -> - val reader = BufferedReader(InputStreamReader(s.getInputStream(), Charsets.UTF_8)) - val writer = BufferedWriter(OutputStreamWriter(s.getOutputStream(), Charsets.UTF_8)) - - val hello = reader.readLine() - assertTrue(hello.contains("\"type\":\"hello\"")) - writer.write("""{"type":"hello-ok","serverName":"Test Bridge"}""") - writer.write("\n") - writer.flush() - } - } - } - - val client = BridgePairingClient() - val res = - client.pairAndHello( - endpoint = BridgeEndpoint.manual(host = "127.0.0.1", port = port), - hello = - BridgePairingClient.Hello( - nodeId = "node-1", - displayName = "Android Node", - token = "token-123", - platform = "Android", - version = "test", - deviceFamily = "Android", - modelIdentifier = "SM-X000", - caps = null, - commands = null, - ), - ) - assertTrue(res.ok) - assertEquals("token-123", res.token) - server.await() - } - - @Test - fun notPairedTriggersPairRequestAndReturnsToken() = runBlocking { - val serverSocket = ServerSocket(0) - val port = serverSocket.localPort - - val server = - async(Dispatchers.IO) { - serverSocket.use { ss -> - val sock = ss.accept() - sock.use { s -> - val reader = BufferedReader(InputStreamReader(s.getInputStream(), Charsets.UTF_8)) - val writer = BufferedWriter(OutputStreamWriter(s.getOutputStream(), Charsets.UTF_8)) - - reader.readLine() // hello - writer.write("""{"type":"error","code":"NOT_PAIRED","message":"not paired"}""") - writer.write("\n") - writer.flush() - - val pairReq = reader.readLine() - assertTrue(pairReq.contains("\"type\":\"pair-request\"")) - writer.write("""{"type":"pair-ok","token":"new-token"}""") - writer.write("\n") - writer.flush() - } - } - } - - val client = BridgePairingClient() - val res = - client.pairAndHello( - endpoint = BridgeEndpoint.manual(host = "127.0.0.1", port = port), - hello = - BridgePairingClient.Hello( - nodeId = "node-1", - displayName = "Android Node", - token = null, - platform = "Android", - version = "test", - deviceFamily = "Android", - modelIdentifier = "SM-X000", - caps = null, - commands = null, - ), - ) - assertTrue(res.ok) - assertEquals("new-token", res.token) - server.await() - } -} diff --git a/apps/android/app/src/test/java/com/clawdbot/android/bridge/BridgeSessionTest.kt b/apps/android/app/src/test/java/com/clawdbot/android/bridge/BridgeSessionTest.kt deleted file mode 100644 index 4310228e2..000000000 --- a/apps/android/app/src/test/java/com/clawdbot/android/bridge/BridgeSessionTest.kt +++ /dev/null @@ -1,307 +0,0 @@ -package com.clawdbot.android.bridge - -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.async -import kotlinx.coroutines.cancel -import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Test -import java.io.BufferedReader -import java.io.BufferedWriter -import java.io.InputStreamReader -import java.io.OutputStreamWriter -import java.net.ServerSocket -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit - -class BridgeSessionTest { - @Test - fun requestReturnsPayloadJson() = runBlocking { - val serverSocket = ServerSocket(0) - val port = serverSocket.localPort - - val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - val connected = CompletableDeferred() - - val session = - BridgeSession( - scope = scope, - onConnected = { _, _, _ -> connected.complete(Unit) }, - onDisconnected = { /* ignore */ }, - onEvent = { _, _ -> /* ignore */ }, - onInvoke = { BridgeSession.InvokeResult.ok(null) }, - ) - - val server = - async(Dispatchers.IO) { - serverSocket.use { ss -> - val sock = ss.accept() - sock.use { s -> - val reader = BufferedReader(InputStreamReader(s.getInputStream(), Charsets.UTF_8)) - val writer = BufferedWriter(OutputStreamWriter(s.getOutputStream(), Charsets.UTF_8)) - - val hello = reader.readLine() - assertTrue(hello.contains("\"type\":\"hello\"")) - writer.write("""{"type":"hello-ok","serverName":"Test Bridge","canvasHostUrl":"http://127.0.0.1:18789"}""") - writer.write("\n") - writer.flush() - - val req = reader.readLine() - assertTrue(req.contains("\"type\":\"req\"")) - val id = extractJsonString(req, "id") - writer.write("""{"type":"res","id":"$id","ok":true,"payloadJSON":"{\"value\":123}"}""") - writer.write("\n") - writer.flush() - } - } - } - - session.connect( - endpoint = BridgeEndpoint.manual(host = "127.0.0.1", port = port), - hello = - BridgeSession.Hello( - nodeId = "node-1", - displayName = "Android Node", - token = null, - platform = "Android", - version = "test", - deviceFamily = null, - modelIdentifier = null, - caps = null, - commands = null, - ), - ) - - connected.await() - assertEquals("http://127.0.0.1:18789", session.currentCanvasHostUrl()) - val payload = session.request(method = "health", paramsJson = null) - assertEquals("""{"value":123}""", payload) - server.await() - - session.disconnect() - scope.cancel() - } - - @Test - fun requestThrowsOnErrorResponse() = runBlocking { - val serverSocket = ServerSocket(0) - val port = serverSocket.localPort - - val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - val connected = CompletableDeferred() - - val session = - BridgeSession( - scope = scope, - onConnected = { _, _, _ -> connected.complete(Unit) }, - onDisconnected = { /* ignore */ }, - onEvent = { _, _ -> /* ignore */ }, - onInvoke = { BridgeSession.InvokeResult.ok(null) }, - ) - - val server = - async(Dispatchers.IO) { - serverSocket.use { ss -> - val sock = ss.accept() - sock.use { s -> - val reader = BufferedReader(InputStreamReader(s.getInputStream(), Charsets.UTF_8)) - val writer = BufferedWriter(OutputStreamWriter(s.getOutputStream(), Charsets.UTF_8)) - - reader.readLine() // hello - writer.write("""{"type":"hello-ok","serverName":"Test Bridge"}""") - writer.write("\n") - writer.flush() - - val req = reader.readLine() - val id = extractJsonString(req, "id") - writer.write( - """{"type":"res","id":"$id","ok":false,"error":{"code":"FORBIDDEN","message":"nope"}}""", - ) - writer.write("\n") - writer.flush() - } - } - } - - session.connect( - endpoint = BridgeEndpoint.manual(host = "127.0.0.1", port = port), - hello = - BridgeSession.Hello( - nodeId = "node-1", - displayName = "Android Node", - token = null, - platform = "Android", - version = "test", - deviceFamily = null, - modelIdentifier = null, - caps = null, - commands = null, - ), - ) - connected.await() - - try { - session.request(method = "chat.history", paramsJson = """{"sessionKey":"main"}""") - throw AssertionError("expected request() to throw") - } catch (e: IllegalStateException) { - assertTrue(e.message?.contains("FORBIDDEN: nope") == true) - } - server.await() - - session.disconnect() - scope.cancel() - } - - @Test - fun invokeResReturnsErrorWhenHandlerThrows() = runBlocking { - val serverSocket = ServerSocket(0) - val port = serverSocket.localPort - - val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - val connected = CompletableDeferred() - - val session = - BridgeSession( - scope = scope, - onConnected = { _, _, _ -> connected.complete(Unit) }, - onDisconnected = { /* ignore */ }, - onEvent = { _, _ -> /* ignore */ }, - onInvoke = { throw IllegalStateException("FOO_BAR: boom") }, - ) - - val invokeResLine = CompletableDeferred() - val server = - async(Dispatchers.IO) { - serverSocket.use { ss -> - val sock = ss.accept() - sock.use { s -> - val reader = BufferedReader(InputStreamReader(s.getInputStream(), Charsets.UTF_8)) - val writer = BufferedWriter(OutputStreamWriter(s.getOutputStream(), Charsets.UTF_8)) - - reader.readLine() // hello - writer.write("""{"type":"hello-ok","serverName":"Test Bridge"}""") - writer.write("\n") - writer.flush() - - // Ask the node to invoke something; handler will throw. - writer.write("""{"type":"invoke","id":"i1","command":"canvas.snapshot","paramsJSON":null}""") - writer.write("\n") - writer.flush() - - val res = reader.readLine() - invokeResLine.complete(res) - } - } - } - - session.connect( - endpoint = BridgeEndpoint.manual(host = "127.0.0.1", port = port), - hello = - BridgeSession.Hello( - nodeId = "node-1", - displayName = "Android Node", - token = null, - platform = "Android", - version = "test", - deviceFamily = null, - modelIdentifier = null, - caps = null, - commands = null, - ), - ) - connected.await() - - // Give the reader loop time to process. - val line = invokeResLine.await() - assertTrue(line.contains("\"type\":\"invoke-res\"")) - assertTrue(line.contains("\"ok\":false")) - assertTrue(line.contains("\"code\":\"FOO_BAR\"")) - assertTrue(line.contains("\"message\":\"boom\"")) - server.await() - - session.disconnect() - scope.cancel() - } - - @Test(timeout = 12_000) - fun reconnectsAfterBridgeClosesDuringHello() = runBlocking { - val serverSocket = ServerSocket(0) - val port = serverSocket.localPort - - val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - val connected = CountDownLatch(1) - val connectionsSeen = CountDownLatch(2) - - val session = - BridgeSession( - scope = scope, - onConnected = { _, _, _ -> connected.countDown() }, - onDisconnected = { /* ignore */ }, - onEvent = { _, _ -> /* ignore */ }, - onInvoke = { BridgeSession.InvokeResult.ok(null) }, - ) - - val server = - async(Dispatchers.IO) { - serverSocket.use { ss -> - // First connection: read hello, then close (no response). - val sock1 = ss.accept() - sock1.use { s -> - val reader = BufferedReader(InputStreamReader(s.getInputStream(), Charsets.UTF_8)) - reader.readLine() // hello - connectionsSeen.countDown() - } - - // Second connection: complete hello. - val sock2 = ss.accept() - sock2.use { s -> - val reader = BufferedReader(InputStreamReader(s.getInputStream(), Charsets.UTF_8)) - val writer = BufferedWriter(OutputStreamWriter(s.getOutputStream(), Charsets.UTF_8)) - reader.readLine() // hello - writer.write("""{"type":"hello-ok","serverName":"Test Bridge"}""") - writer.write("\n") - writer.flush() - connectionsSeen.countDown() - Thread.sleep(200) - } - } - } - - session.connect( - endpoint = BridgeEndpoint.manual(host = "127.0.0.1", port = port), - hello = - BridgeSession.Hello( - nodeId = "node-1", - displayName = "Android Node", - token = null, - platform = "Android", - version = "test", - deviceFamily = null, - modelIdentifier = null, - caps = null, - commands = null, - ), - ) - - assertTrue("expected two connection attempts", connectionsSeen.await(8, TimeUnit.SECONDS)) - assertTrue("expected session to connect", connected.await(8, TimeUnit.SECONDS)) - - session.disconnect() - scope.cancel() - server.await() - } -} - -private fun extractJsonString(raw: String, key: String): String { - val needle = "\"$key\":\"" - val start = raw.indexOf(needle) - if (start < 0) throw IllegalArgumentException("missing key $key in $raw") - val from = start + needle.length - val end = raw.indexOf('"', from) - if (end < 0) throw IllegalArgumentException("unterminated string for $key in $raw") - return raw.substring(from, end) -} diff --git a/apps/android/app/src/test/java/com/clawdbot/android/bridge/BonjourEscapesTest.kt b/apps/android/app/src/test/java/com/clawdbot/android/gateway/BonjourEscapesTest.kt similarity index 93% rename from apps/android/app/src/test/java/com/clawdbot/android/bridge/BonjourEscapesTest.kt rename to apps/android/app/src/test/java/com/clawdbot/android/gateway/BonjourEscapesTest.kt index 5abf4519e..8952ed6cf 100644 --- a/apps/android/app/src/test/java/com/clawdbot/android/bridge/BonjourEscapesTest.kt +++ b/apps/android/app/src/test/java/com/clawdbot/android/gateway/BonjourEscapesTest.kt @@ -1,4 +1,4 @@ -package com.clawdbot.android.bridge +package com.clawdbot.android.gateway import org.junit.Assert.assertEquals import org.junit.Test diff --git a/apps/ios/Sources/Bridge/BridgeClient.swift b/apps/ios/Sources/Bridge/BridgeClient.swift deleted file mode 100644 index f880e6896..000000000 --- a/apps/ios/Sources/Bridge/BridgeClient.swift +++ /dev/null @@ -1,244 +0,0 @@ -import ClawdbotKit -import Foundation -import Network - -actor BridgeClient { - private let encoder = JSONEncoder() - private let decoder = JSONDecoder() - private var lineBuffer = Data() - - func pairAndHello( - endpoint: NWEndpoint, - hello: BridgeHello, - tls: BridgeTLSParams? = nil, - onStatus: (@Sendable (String) -> Void)? = nil) async throws -> String - { - do { - return try await self.pairAndHelloOnce( - endpoint: endpoint, - hello: hello, - tls: tls, - onStatus: onStatus) - } catch { - if let tls, !tls.required { - return try await self.pairAndHelloOnce( - endpoint: endpoint, - hello: hello, - tls: nil, - onStatus: onStatus) - } - throw error - } - } - - private func pairAndHelloOnce( - endpoint: NWEndpoint, - hello: BridgeHello, - tls: BridgeTLSParams?, - onStatus: (@Sendable (String) -> Void)? = nil) async throws -> String - { - self.lineBuffer = Data() - let params = self.makeParameters(tls: tls) - let connection = NWConnection(to: endpoint, using: params) - let queue = DispatchQueue(label: "com.clawdbot.ios.bridge-client") - defer { connection.cancel() } - try await self.withTimeout(seconds: 8, purpose: "connect") { - try await self.startAndWaitForReady(connection, queue: queue) - } - - onStatus?("Authenticating…") - try await self.send(hello, over: connection) - - let first = try await self.withTimeout(seconds: 10, purpose: "hello") { () -> ReceivedFrame in - guard let frame = try await self.receiveFrame(over: connection) else { - throw NSError(domain: "Bridge", code: 0, userInfo: [ - NSLocalizedDescriptionKey: "Bridge closed connection during hello", - ]) - } - return frame - } - - switch first.base.type { - case "hello-ok": - // We only return a token if we have one; callers should treat empty as "no token yet". - return hello.token ?? "" - - case "error": - let err = try self.decoder.decode(BridgeErrorFrame.self, from: first.data) - if err.code != "NOT_PAIRED", err.code != "UNAUTHORIZED" { - throw NSError(domain: "Bridge", code: 1, userInfo: [ - NSLocalizedDescriptionKey: "\(err.code): \(err.message)", - ]) - } - - onStatus?("Requesting approval…") - try await self.send( - BridgePairRequest( - nodeId: hello.nodeId, - displayName: hello.displayName, - platform: hello.platform, - version: hello.version, - deviceFamily: hello.deviceFamily, - modelIdentifier: hello.modelIdentifier, - caps: hello.caps, - commands: hello.commands), - over: connection) - - onStatus?("Waiting for approval…") - let ok = try await self.withTimeout(seconds: 60, purpose: "pairing approval") { - while let next = try await self.receiveFrame(over: connection) { - switch next.base.type { - case "pair-ok": - return try self.decoder.decode(BridgePairOk.self, from: next.data) - case "error": - let e = try self.decoder.decode(BridgeErrorFrame.self, from: next.data) - throw NSError(domain: "Bridge", code: 2, userInfo: [ - NSLocalizedDescriptionKey: "\(e.code): \(e.message)", - ]) - default: - continue - } - } - throw NSError(domain: "Bridge", code: 3, userInfo: [ - NSLocalizedDescriptionKey: "Pairing failed: bridge closed connection", - ]) - } - - return ok.token - - default: - throw NSError(domain: "Bridge", code: 0, userInfo: [ - NSLocalizedDescriptionKey: "Unexpected bridge response", - ]) - } - } - - private func send(_ obj: some Encodable, over connection: NWConnection) async throws { - let data = try self.encoder.encode(obj) - var line = Data() - line.append(data) - line.append(0x0A) - try await withCheckedThrowingContinuation(isolation: nil) { (cont: CheckedContinuation) in - connection.send(content: line, completion: .contentProcessed { err in - if let err { cont.resume(throwing: err) } else { cont.resume(returning: ()) } - }) - } - } - - private struct ReceivedFrame { - var base: BridgeBaseFrame - var data: Data - } - - private func receiveFrame(over connection: NWConnection) async throws -> ReceivedFrame? { - guard let lineData = try await self.receiveLineData(over: connection) else { - return nil - } - let base = try self.decoder.decode(BridgeBaseFrame.self, from: lineData) - return ReceivedFrame(base: base, data: lineData) - } - - private func receiveChunk(over connection: NWConnection) async throws -> Data { - try await withCheckedThrowingContinuation(isolation: nil) { (cont: CheckedContinuation) in - connection.receive(minimumIncompleteLength: 1, maximumLength: 64 * 1024) { data, _, isComplete, error in - if let error { - cont.resume(throwing: error) - return - } - if isComplete { - cont.resume(returning: Data()) - return - } - cont.resume(returning: data ?? Data()) - } - } - } - - private func receiveLineData(over connection: NWConnection) async throws -> Data? { - while true { - if let idx = self.lineBuffer.firstIndex(of: 0x0A) { - let line = self.lineBuffer.prefix(upTo: idx) - self.lineBuffer.removeSubrange(...idx) - return Data(line) - } - - let chunk = try await self.receiveChunk(over: connection) - if chunk.isEmpty { return nil } - self.lineBuffer.append(chunk) - } - } - - private func makeParameters(tls: BridgeTLSParams?) -> NWParameters { - if let tlsOptions = makeBridgeTLSOptions(tls) { - let tcpOptions = NWProtocolTCP.Options() - let params = NWParameters(tls: tlsOptions, tcp: tcpOptions) - params.includePeerToPeer = true - return params - } - let params = NWParameters.tcp - params.includePeerToPeer = true - return params - } - - private struct TimeoutError: LocalizedError, Sendable { - var purpose: String - var seconds: Int - - var errorDescription: String? { - if self.purpose == "pairing approval" { - return - "Timed out waiting for approval (\(self.seconds)s). " + - "Approve the node on your gateway and try again." - } - return "Timed out during \(self.purpose) (\(self.seconds)s)." - } - } - - private func withTimeout( - seconds: Int, - purpose: String, - _ op: @escaping @Sendable () async throws -> T) async throws -> T - { - try await AsyncTimeout.withTimeout( - seconds: Double(seconds), - onTimeout: { TimeoutError(purpose: purpose, seconds: seconds) }, - operation: op) - } - - private func startAndWaitForReady(_ connection: NWConnection, queue: DispatchQueue) async throws { - try await withCheckedThrowingContinuation(isolation: nil) { (cont: CheckedContinuation) in - final class ResumeFlag: @unchecked Sendable { - private let lock = NSLock() - private var value = false - - func trySet() -> Bool { - self.lock.lock() - defer { self.lock.unlock() } - if self.value { return false } - self.value = true - return true - } - } - let didResume = ResumeFlag() - connection.stateUpdateHandler = { state in - switch state { - case .ready: - if didResume.trySet() { cont.resume(returning: ()) } - case let .failed(err): - if didResume.trySet() { cont.resume(throwing: err) } - case let .waiting(err): - if didResume.trySet() { cont.resume(throwing: err) } - case .cancelled: - if didResume.trySet() { - cont.resume(throwing: NSError(domain: "Bridge", code: 50, userInfo: [ - NSLocalizedDescriptionKey: "Connection cancelled", - ])) - } - default: - break - } - } - connection.start(queue: queue) - } - } -} diff --git a/apps/ios/Sources/Bridge/BridgeEndpointID.swift b/apps/ios/Sources/Bridge/BridgeEndpointID.swift deleted file mode 100644 index 40cb9fee1..000000000 --- a/apps/ios/Sources/Bridge/BridgeEndpointID.swift +++ /dev/null @@ -1,26 +0,0 @@ -import ClawdbotKit -import Foundation -import Network - -enum BridgeEndpointID { - static func stableID(_ endpoint: NWEndpoint) -> String { - switch endpoint { - case let .service(name, type, domain, _): - // Keep this stable across encode/decode differences (e.g. `\032` for spaces). - let normalizedName = Self.normalizeServiceNameForID(name) - return "\(type)|\(domain)|\(normalizedName)" - default: - return String(describing: endpoint) - } - } - - static func prettyDescription(_ endpoint: NWEndpoint) -> String { - BonjourEscapes.decode(String(describing: endpoint)) - } - - private static func normalizeServiceNameForID(_ rawName: String) -> String { - let decoded = BonjourEscapes.decode(rawName) - let normalized = decoded.split(whereSeparator: \.isWhitespace).joined(separator: " ") - return normalized.trimmingCharacters(in: .whitespacesAndNewlines) - } -} diff --git a/apps/ios/Sources/Bridge/BridgeSession.swift b/apps/ios/Sources/Bridge/BridgeSession.swift deleted file mode 100644 index adc3e0c00..000000000 --- a/apps/ios/Sources/Bridge/BridgeSession.swift +++ /dev/null @@ -1,422 +0,0 @@ -import ClawdbotKit -import Foundation -import Network - -actor BridgeSession { - private struct TimeoutError: LocalizedError { - var message: String - var errorDescription: String? { self.message } - } - - enum State: Sendable, Equatable { - case idle - case connecting - case connected(serverName: String) - case failed(message: String) - } - - private let encoder = JSONEncoder() - private let decoder = JSONDecoder() - - private var connection: NWConnection? - private var queue: DispatchQueue? - private var buffer = Data() - private var pendingRPC: [String: CheckedContinuation] = [:] - private var serverEventSubscribers: [UUID: AsyncStream.Continuation] = [:] - - private(set) var state: State = .idle - private var canvasHostUrl: String? - private var mainSessionKey: String? - - func currentCanvasHostUrl() -> String? { - self.canvasHostUrl - } - - func currentRemoteAddress() -> String? { - guard let endpoint = self.connection?.currentPath?.remoteEndpoint else { return nil } - return Self.prettyRemoteEndpoint(endpoint) - } - - private static func prettyRemoteEndpoint(_ endpoint: NWEndpoint) -> String? { - switch endpoint { - case let .hostPort(host, port): - let hostString = Self.prettyHostString(host) - if hostString.contains(":") { - return "[\(hostString)]:\(port)" - } - return "\(hostString):\(port)" - default: - return String(describing: endpoint) - } - } - - private static func prettyHostString(_ host: NWEndpoint.Host) -> String { - var hostString = String(describing: host) - hostString = hostString.replacingOccurrences(of: "::ffff:", with: "") - - guard let percentIndex = hostString.firstIndex(of: "%") else { return hostString } - - let prefix = hostString[.. Void)? = nil, - onInvoke: @escaping @Sendable (BridgeInvokeRequest) async -> BridgeInvokeResponse) - async throws - { - await self.disconnect() - self.state = .connecting - do { - try await self.connectOnce( - endpoint: endpoint, - hello: hello, - tls: tls, - onConnected: onConnected, - onInvoke: onInvoke) - } catch { - if let tls, !tls.required { - try await self.connectOnce( - endpoint: endpoint, - hello: hello, - tls: nil, - onConnected: onConnected, - onInvoke: onInvoke) - return - } - throw error - } - } - - private func connectOnce( - endpoint: NWEndpoint, - hello: BridgeHello, - tls: BridgeTLSParams?, - onConnected: (@Sendable (String, String?) async -> Void)?, - onInvoke: @escaping @Sendable (BridgeInvokeRequest) async -> BridgeInvokeResponse) async throws - { - let params = self.makeParameters(tls: tls) - let connection = NWConnection(to: endpoint, using: params) - let queue = DispatchQueue(label: "com.clawdbot.ios.bridge-session") - self.connection = connection - self.queue = queue - - let stateStream = Self.makeStateStream(for: connection) - connection.start(queue: queue) - - try await Self.waitForReady(stateStream, timeoutSeconds: 6) - - try await Self.withTimeout(seconds: 6) { - try await self.send(hello) - } - - guard let line = try await Self.withTimeout(seconds: 6, operation: { - try await self.receiveLine() - }), - let data = line.data(using: .utf8), - let base = try? self.decoder.decode(BridgeBaseFrame.self, from: data) - else { - await self.disconnect() - throw NSError(domain: "Bridge", code: 1, userInfo: [ - NSLocalizedDescriptionKey: "Unexpected bridge response", - ]) - } - - if base.type == "hello-ok" { - let ok = try self.decoder.decode(BridgeHelloOk.self, from: data) - self.state = .connected(serverName: ok.serverName) - self.canvasHostUrl = ok.canvasHostUrl?.trimmingCharacters(in: .whitespacesAndNewlines) - let mainKey = ok.mainSessionKey?.trimmingCharacters(in: .whitespacesAndNewlines) - self.mainSessionKey = (mainKey?.isEmpty == false) ? mainKey : nil - await onConnected?(ok.serverName, self.mainSessionKey) - } else if base.type == "error" { - let err = try self.decoder.decode(BridgeErrorFrame.self, from: data) - self.state = .failed(message: "\(err.code): \(err.message)") - await self.disconnect() - throw NSError(domain: "Bridge", code: 2, userInfo: [ - NSLocalizedDescriptionKey: "\(err.code): \(err.message)", - ]) - } else { - self.state = .failed(message: "Unexpected bridge response") - await self.disconnect() - throw NSError(domain: "Bridge", code: 3, userInfo: [ - NSLocalizedDescriptionKey: "Unexpected bridge response", - ]) - } - - while true { - guard let next = try await self.receiveLine() else { break } - guard let nextData = next.data(using: .utf8) else { continue } - guard let nextBase = try? self.decoder.decode(BridgeBaseFrame.self, from: nextData) else { continue } - - switch nextBase.type { - case "res": - let res = try self.decoder.decode(BridgeRPCResponse.self, from: nextData) - if let cont = self.pendingRPC.removeValue(forKey: res.id) { - cont.resume(returning: res) - } - - case "event": - let evt = try self.decoder.decode(BridgeEventFrame.self, from: nextData) - self.broadcastServerEvent(evt) - - case "ping": - let ping = try self.decoder.decode(BridgePing.self, from: nextData) - try await self.send(BridgePong(type: "pong", id: ping.id)) - - case "invoke": - let req = try self.decoder.decode(BridgeInvokeRequest.self, from: nextData) - let res = await onInvoke(req) - try await self.send(res) - - default: - continue - } - } - - await self.disconnect() - } - - func sendEvent(event: String, payloadJSON: String?) async throws { - try await self.send(BridgeEventFrame(type: "event", event: event, payloadJSON: payloadJSON)) - } - - func request(method: String, paramsJSON: String?, timeoutSeconds: Int = 15) async throws -> Data { - guard self.connection != nil else { - throw NSError(domain: "Bridge", code: 11, userInfo: [ - NSLocalizedDescriptionKey: "not connected", - ]) - } - - let id = UUID().uuidString - let req = BridgeRPCRequest(type: "req", id: id, method: method, paramsJSON: paramsJSON) - - let timeoutTask = Task { - try await Task.sleep(nanoseconds: UInt64(timeoutSeconds) * 1_000_000_000) - await self.timeoutRPC(id: id) - } - defer { timeoutTask.cancel() } - - let res: BridgeRPCResponse = try await withCheckedThrowingContinuation { cont in - Task { [weak self] in - guard let self else { return } - await self.beginRPC(id: id, request: req, continuation: cont) - } - } - - if res.ok { - let payload = res.payloadJSON ?? "" - guard let data = payload.data(using: .utf8) else { - throw NSError(domain: "Bridge", code: 12, userInfo: [ - NSLocalizedDescriptionKey: "Bridge response not UTF-8", - ]) - } - return data - } - - let code = res.error?.code ?? "UNAVAILABLE" - let message = res.error?.message ?? "request failed" - throw NSError(domain: "Bridge", code: 13, userInfo: [ - NSLocalizedDescriptionKey: "\(code): \(message)", - ]) - } - - func subscribeServerEvents(bufferingNewest: Int = 200) -> AsyncStream { - let id = UUID() - let session = self - return AsyncStream(bufferingPolicy: .bufferingNewest(bufferingNewest)) { continuation in - self.serverEventSubscribers[id] = continuation - continuation.onTermination = { @Sendable _ in - Task { await session.removeServerEventSubscriber(id) } - } - } - } - - func disconnect() async { - self.connection?.cancel() - self.connection = nil - self.queue = nil - self.buffer = Data() - self.canvasHostUrl = nil - self.mainSessionKey = nil - - let pending = self.pendingRPC.values - self.pendingRPC.removeAll() - for cont in pending { - cont.resume(throwing: NSError(domain: "Bridge", code: 14, userInfo: [ - NSLocalizedDescriptionKey: "UNAVAILABLE: connection closed", - ])) - } - - for (_, cont) in self.serverEventSubscribers { - cont.finish() - } - self.serverEventSubscribers.removeAll() - - self.state = .idle - } - - func currentMainSessionKey() -> String? { - self.mainSessionKey - } - - private func beginRPC( - id: String, - request: BridgeRPCRequest, - continuation: CheckedContinuation) async - { - self.pendingRPC[id] = continuation - do { - try await self.send(request) - } catch { - await self.failRPC(id: id, error: error) - } - } - - private func makeParameters(tls: BridgeTLSParams?) -> NWParameters { - if let tlsOptions = makeBridgeTLSOptions(tls) { - let tcpOptions = NWProtocolTCP.Options() - let params = NWParameters(tls: tlsOptions, tcp: tcpOptions) - params.includePeerToPeer = true - return params - } - let params = NWParameters.tcp - params.includePeerToPeer = true - return params - } - - private func timeoutRPC(id: String) async { - guard let cont = self.pendingRPC.removeValue(forKey: id) else { return } - cont.resume(throwing: NSError(domain: "Bridge", code: 15, userInfo: [ - NSLocalizedDescriptionKey: "UNAVAILABLE: request timeout", - ])) - } - - private func failRPC(id: String, error: Error) async { - guard let cont = self.pendingRPC.removeValue(forKey: id) else { return } - cont.resume(throwing: error) - } - - private func broadcastServerEvent(_ evt: BridgeEventFrame) { - for (_, cont) in self.serverEventSubscribers { - cont.yield(evt) - } - } - - private func removeServerEventSubscriber(_ id: UUID) { - self.serverEventSubscribers[id] = nil - } - - private func send(_ obj: some Encodable) async throws { - guard let connection = self.connection else { - throw NSError(domain: "Bridge", code: 10, userInfo: [ - NSLocalizedDescriptionKey: "not connected", - ]) - } - let data = try self.encoder.encode(obj) - var line = Data() - line.append(data) - line.append(0x0A) - try await withCheckedThrowingContinuation(isolation: nil) { (cont: CheckedContinuation) in - connection.send(content: line, completion: .contentProcessed { err in - if let err { cont.resume(throwing: err) } else { cont.resume(returning: ()) } - }) - } - } - - private func receiveLine() async throws -> String? { - while true { - if let idx = self.buffer.firstIndex(of: 0x0A) { - let lineData = self.buffer.prefix(upTo: idx) - self.buffer.removeSubrange(...idx) - return String(data: lineData, encoding: .utf8) - } - - let chunk = try await self.receiveChunk() - if chunk.isEmpty { return nil } - self.buffer.append(chunk) - } - } - - private func receiveChunk() async throws -> Data { - guard let connection = self.connection else { return Data() } - return try await withCheckedThrowingContinuation(isolation: nil) { (cont: CheckedContinuation) in - connection.receive(minimumIncompleteLength: 1, maximumLength: 64 * 1024) { data, _, isComplete, error in - if let error { - cont.resume(throwing: error) - return - } - if isComplete { - cont.resume(returning: Data()) - return - } - cont.resume(returning: data ?? Data()) - } - } - } - - private static func withTimeout( - seconds: Double, - operation: @escaping @Sendable () async throws -> T) async throws -> T - { - try await AsyncTimeout.withTimeout( - seconds: seconds, - onTimeout: { TimeoutError(message: "UNAVAILABLE: connection timeout") }, - operation: operation) - } - - private static func makeStateStream(for connection: NWConnection) -> AsyncStream { - AsyncStream { continuation in - continuation.onTermination = { @Sendable _ in - connection.stateUpdateHandler = nil - } - - connection.stateUpdateHandler = { state in - continuation.yield(state) - switch state { - case .ready, .cancelled, .failed, .waiting: - continuation.finish() - case .setup, .preparing: - break - @unknown default: - break - } - } - } - } - - private static func waitForReady( - _ stateStream: AsyncStream, - timeoutSeconds: Double) async throws - { - try await self.withTimeout(seconds: timeoutSeconds) { - for await state in stateStream { - switch state { - case .ready: - return - case let .failed(error): - throw error - case let .waiting(error): - throw error - case .cancelled: - throw TimeoutError(message: "UNAVAILABLE: connection cancelled") - case .setup, .preparing: - break - @unknown default: - break - } - } - - throw TimeoutError(message: "UNAVAILABLE: connection ended") - } - } -} diff --git a/apps/ios/Sources/Bridge/BridgeSettingsStore.swift b/apps/ios/Sources/Bridge/BridgeSettingsStore.swift deleted file mode 100644 index 7d0766235..000000000 --- a/apps/ios/Sources/Bridge/BridgeSettingsStore.swift +++ /dev/null @@ -1,112 +0,0 @@ -import Foundation - -enum BridgeSettingsStore { - private static let bridgeService = "com.clawdbot.bridge" - private static let nodeService = "com.clawdbot.node" - - private static let instanceIdDefaultsKey = "node.instanceId" - private static let preferredBridgeStableIDDefaultsKey = "bridge.preferredStableID" - private static let lastDiscoveredBridgeStableIDDefaultsKey = "bridge.lastDiscoveredStableID" - - private static let instanceIdAccount = "instanceId" - private static let preferredBridgeStableIDAccount = "preferredStableID" - private static let lastDiscoveredBridgeStableIDAccount = "lastDiscoveredStableID" - - static func bootstrapPersistence() { - self.ensureStableInstanceID() - self.ensurePreferredBridgeStableID() - self.ensureLastDiscoveredBridgeStableID() - } - - static func loadStableInstanceID() -> String? { - KeychainStore.loadString(service: self.nodeService, account: self.instanceIdAccount)? - .trimmingCharacters(in: .whitespacesAndNewlines) - } - - static func saveStableInstanceID(_ instanceId: String) { - _ = KeychainStore.saveString(instanceId, service: self.nodeService, account: self.instanceIdAccount) - } - - static func loadPreferredBridgeStableID() -> String? { - KeychainStore.loadString(service: self.bridgeService, account: self.preferredBridgeStableIDAccount)? - .trimmingCharacters(in: .whitespacesAndNewlines) - } - - static func savePreferredBridgeStableID(_ stableID: String) { - _ = KeychainStore.saveString( - stableID, - service: self.bridgeService, - account: self.preferredBridgeStableIDAccount) - } - - static func loadLastDiscoveredBridgeStableID() -> String? { - KeychainStore.loadString(service: self.bridgeService, account: self.lastDiscoveredBridgeStableIDAccount)? - .trimmingCharacters(in: .whitespacesAndNewlines) - } - - static func saveLastDiscoveredBridgeStableID(_ stableID: String) { - _ = KeychainStore.saveString( - stableID, - service: self.bridgeService, - account: self.lastDiscoveredBridgeStableIDAccount) - } - - private static func ensureStableInstanceID() { - let defaults = UserDefaults.standard - - if let existing = defaults.string(forKey: self.instanceIdDefaultsKey)? - .trimmingCharacters(in: .whitespacesAndNewlines), - !existing.isEmpty - { - if self.loadStableInstanceID() == nil { - self.saveStableInstanceID(existing) - } - return - } - - if let stored = self.loadStableInstanceID(), !stored.isEmpty { - defaults.set(stored, forKey: self.instanceIdDefaultsKey) - return - } - - let fresh = UUID().uuidString - self.saveStableInstanceID(fresh) - defaults.set(fresh, forKey: self.instanceIdDefaultsKey) - } - - private static func ensurePreferredBridgeStableID() { - let defaults = UserDefaults.standard - - if let existing = defaults.string(forKey: self.preferredBridgeStableIDDefaultsKey)? - .trimmingCharacters(in: .whitespacesAndNewlines), - !existing.isEmpty - { - if self.loadPreferredBridgeStableID() == nil { - self.savePreferredBridgeStableID(existing) - } - return - } - - if let stored = self.loadPreferredBridgeStableID(), !stored.isEmpty { - defaults.set(stored, forKey: self.preferredBridgeStableIDDefaultsKey) - } - } - - private static func ensureLastDiscoveredBridgeStableID() { - let defaults = UserDefaults.standard - - if let existing = defaults.string(forKey: self.lastDiscoveredBridgeStableIDDefaultsKey)? - .trimmingCharacters(in: .whitespacesAndNewlines), - !existing.isEmpty - { - if self.loadLastDiscoveredBridgeStableID() == nil { - self.saveLastDiscoveredBridgeStableID(existing) - } - return - } - - if let stored = self.loadLastDiscoveredBridgeStableID(), !stored.isEmpty { - defaults.set(stored, forKey: self.lastDiscoveredBridgeStableIDDefaultsKey) - } - } -} diff --git a/apps/ios/Sources/Bridge/BridgeTLS.swift b/apps/ios/Sources/Bridge/BridgeTLS.swift deleted file mode 100644 index 5ede00d3f..000000000 --- a/apps/ios/Sources/Bridge/BridgeTLS.swift +++ /dev/null @@ -1,66 +0,0 @@ -import CryptoKit -import Foundation -import Network -import Security - -struct BridgeTLSParams: Sendable { - let required: Bool - let expectedFingerprint: String? - let allowTOFU: Bool - let storeKey: String? -} - -enum BridgeTLSStore { - private static let service = "com.clawdbot.bridge.tls" - - static func loadFingerprint(stableID: String) -> String? { - KeychainStore.loadString(service: service, account: stableID)?.trimmingCharacters(in: .whitespacesAndNewlines) - } - - static func saveFingerprint(_ value: String, stableID: String) { - _ = KeychainStore.saveString(value, service: service, account: stableID) - } -} - -func makeBridgeTLSOptions(_ params: BridgeTLSParams?) -> NWProtocolTLS.Options? { - guard let params else { return nil } - let options = NWProtocolTLS.Options() - let expected = params.expectedFingerprint.map(normalizeBridgeFingerprint) - let allowTOFU = params.allowTOFU - let storeKey = params.storeKey - - sec_protocol_options_set_verify_block( - options.securityProtocolOptions, - { _, trust, complete in - let trustRef = sec_trust_copy_ref(trust).takeRetainedValue() - if let chain = SecTrustCopyCertificateChain(trustRef) as? [SecCertificate], - let cert = chain.first - { - let data = SecCertificateCopyData(cert) as Data - let fingerprint = sha256Hex(data) - if let expected { - complete(fingerprint == expected) - return - } - if allowTOFU { - if let storeKey { BridgeTLSStore.saveFingerprint(fingerprint, stableID: storeKey) } - complete(true) - return - } - } - let ok = SecTrustEvaluateWithError(trustRef, nil) - complete(ok) - }, - DispatchQueue(label: "com.clawdbot.bridge.tls.verify")) - - return options -} - -private func sha256Hex(_ data: Data) -> String { - let digest = SHA256.hash(data: data) - return digest.map { String(format: "%02x", $0) }.joined() -} - -private func normalizeBridgeFingerprint(_ raw: String) -> String { - raw.lowercased().filter { $0.isHexDigit } -} diff --git a/apps/ios/Sources/Camera/CameraController.swift b/apps/ios/Sources/Camera/CameraController.swift index e2ea47e05..b33f40a00 100644 --- a/apps/ios/Sources/Camera/CameraController.swift +++ b/apps/ios/Sources/Camera/CameraController.swift @@ -44,7 +44,7 @@ actor CameraController { { let facing = params.facing ?? .front let format = params.format ?? .jpg - // Default to a reasonable max width to keep bridge payload sizes manageable. + // Default to a reasonable max width to keep gateway payload sizes manageable. // If you need the full-res photo, explicitly request a larger maxWidth. let maxWidth = params.maxWidth.flatMap { $0 > 0 ? $0 : nil } ?? 1600 let quality = Self.clampQuality(params.quality) @@ -160,14 +160,14 @@ actor CameraController { defer { session.stopRunning() } await Self.warmUpCaptureSession() - let movURL = FileManager.default.temporaryDirectory + let movURL = FileManager().temporaryDirectory .appendingPathComponent("clawdbot-camera-\(UUID().uuidString).mov") - let mp4URL = FileManager.default.temporaryDirectory + let mp4URL = FileManager().temporaryDirectory .appendingPathComponent("clawdbot-camera-\(UUID().uuidString).mp4") defer { - try? FileManager.default.removeItem(at: movURL) - try? FileManager.default.removeItem(at: mp4URL) + try? FileManager().removeItem(at: movURL) + try? FileManager().removeItem(at: mp4URL) } var delegate: MovieFileDelegate? @@ -270,7 +270,7 @@ actor CameraController { nonisolated static func clampDurationMs(_ ms: Int?) -> Int { let v = ms ?? 3000 - // Keep clips short by default; avoid huge base64 payloads on the bridge. + // Keep clips short by default; avoid huge base64 payloads on the gateway. return min(60000, max(250, v)) } diff --git a/apps/ios/Sources/Chat/ChatSheet.swift b/apps/ios/Sources/Chat/ChatSheet.swift index 0db033238..c0e5593ff 100644 --- a/apps/ios/Sources/Chat/ChatSheet.swift +++ b/apps/ios/Sources/Chat/ChatSheet.swift @@ -1,4 +1,5 @@ import ClawdbotChatUI +import ClawdbotKit import SwiftUI struct ChatSheet: View { @@ -6,8 +7,8 @@ struct ChatSheet: View { @State private var viewModel: ClawdbotChatViewModel private let userAccent: Color? - init(bridge: BridgeSession, sessionKey: String, userAccent: Color? = nil) { - let transport = IOSBridgeChatTransport(bridge: bridge) + init(gateway: GatewayNodeSession, sessionKey: String, userAccent: Color? = nil) { + let transport = IOSGatewayChatTransport(gateway: gateway) self._viewModel = State( initialValue: ClawdbotChatViewModel( sessionKey: sessionKey, diff --git a/apps/ios/Sources/Chat/IOSBridgeChatTransport.swift b/apps/ios/Sources/Chat/IOSGatewayChatTransport.swift similarity index 66% rename from apps/ios/Sources/Chat/IOSBridgeChatTransport.swift rename to apps/ios/Sources/Chat/IOSGatewayChatTransport.swift index f6d3cc4a0..f7ee4aa79 100644 --- a/apps/ios/Sources/Chat/IOSBridgeChatTransport.swift +++ b/apps/ios/Sources/Chat/IOSGatewayChatTransport.swift @@ -1,12 +1,13 @@ import ClawdbotChatUI import ClawdbotKit +import ClawdbotProtocol import Foundation -struct IOSBridgeChatTransport: ClawdbotChatTransport, Sendable { - private let bridge: BridgeSession +struct IOSGatewayChatTransport: ClawdbotChatTransport, Sendable { + private let gateway: GatewayNodeSession - init(bridge: BridgeSession) { - self.bridge = bridge + init(gateway: GatewayNodeSession) { + self.gateway = gateway } func abortRun(sessionKey: String, runId: String) async throws { @@ -16,7 +17,7 @@ struct IOSBridgeChatTransport: ClawdbotChatTransport, Sendable { } let data = try JSONEncoder().encode(Params(sessionKey: sessionKey, runId: runId)) let json = String(data: data, encoding: .utf8) - _ = try await self.bridge.request(method: "chat.abort", paramsJSON: json, timeoutSeconds: 10) + _ = try await self.gateway.request(method: "chat.abort", paramsJSON: json, timeoutSeconds: 10) } func listSessions(limit: Int?) async throws -> ClawdbotChatSessionsListResponse { @@ -27,7 +28,7 @@ struct IOSBridgeChatTransport: ClawdbotChatTransport, Sendable { } let data = try JSONEncoder().encode(Params(includeGlobal: true, includeUnknown: false, limit: limit)) let json = String(data: data, encoding: .utf8) - let res = try await self.bridge.request(method: "sessions.list", paramsJSON: json, timeoutSeconds: 15) + let res = try await self.gateway.request(method: "sessions.list", paramsJSON: json, timeoutSeconds: 15) return try JSONDecoder().decode(ClawdbotChatSessionsListResponse.self, from: res) } @@ -35,14 +36,14 @@ struct IOSBridgeChatTransport: ClawdbotChatTransport, Sendable { struct Subscribe: Codable { var sessionKey: String } let data = try JSONEncoder().encode(Subscribe(sessionKey: sessionKey)) let json = String(data: data, encoding: .utf8) - try await self.bridge.sendEvent(event: "chat.subscribe", payloadJSON: json) + await self.gateway.sendEvent(event: "chat.subscribe", payloadJSON: json) } func requestHistory(sessionKey: String) async throws -> ClawdbotChatHistoryPayload { struct Params: Codable { var sessionKey: String } let data = try JSONEncoder().encode(Params(sessionKey: sessionKey)) let json = String(data: data, encoding: .utf8) - let res = try await self.bridge.request(method: "chat.history", paramsJSON: json, timeoutSeconds: 15) + let res = try await self.gateway.request(method: "chat.history", paramsJSON: json, timeoutSeconds: 15) return try JSONDecoder().decode(ClawdbotChatHistoryPayload.self, from: res) } @@ -71,20 +72,20 @@ struct IOSBridgeChatTransport: ClawdbotChatTransport, Sendable { idempotencyKey: idempotencyKey) let data = try JSONEncoder().encode(params) let json = String(data: data, encoding: .utf8) - let res = try await self.bridge.request(method: "chat.send", paramsJSON: json, timeoutSeconds: 35) + let res = try await self.gateway.request(method: "chat.send", paramsJSON: json, timeoutSeconds: 35) return try JSONDecoder().decode(ClawdbotChatSendResponse.self, from: res) } func requestHealth(timeoutMs: Int) async throws -> Bool { let seconds = max(1, Int(ceil(Double(timeoutMs) / 1000.0))) - let res = try await self.bridge.request(method: "health", paramsJSON: nil, timeoutSeconds: seconds) + let res = try await self.gateway.request(method: "health", paramsJSON: nil, timeoutSeconds: seconds) return (try? JSONDecoder().decode(ClawdbotGatewayHealthOK.self, from: res))?.ok ?? true } func events() -> AsyncStream { AsyncStream { continuation in let task = Task { - let stream = await self.bridge.subscribeServerEvents() + let stream = await self.gateway.subscribeServerEvents() for await evt in stream { if Task.isCancelled { return } switch evt.event { @@ -93,18 +94,26 @@ struct IOSBridgeChatTransport: ClawdbotChatTransport, Sendable { case "seqGap": continuation.yield(.seqGap) case "health": - guard let json = evt.payloadJSON, let data = json.data(using: .utf8) else { break } - let ok = (try? JSONDecoder().decode(ClawdbotGatewayHealthOK.self, from: data))?.ok ?? true + guard let payload = evt.payload else { break } + let ok = (try? GatewayPayloadDecoding.decode( + payload, + as: ClawdbotGatewayHealthOK.self))?.ok ?? true continuation.yield(.health(ok: ok)) case "chat": - guard let json = evt.payloadJSON, let data = json.data(using: .utf8) else { break } - if let payload = try? JSONDecoder().decode(ClawdbotChatEventPayload.self, from: data) { - continuation.yield(.chat(payload)) + guard let payload = evt.payload else { break } + if let chatPayload = try? GatewayPayloadDecoding.decode( + payload, + as: ClawdbotChatEventPayload.self) + { + continuation.yield(.chat(chatPayload)) } case "agent": - guard let json = evt.payloadJSON, let data = json.data(using: .utf8) else { break } - if let payload = try? JSONDecoder().decode(ClawdbotAgentEventPayload.self, from: data) { - continuation.yield(.agent(payload)) + guard let payload = evt.payload else { break } + if let agentPayload = try? GatewayPayloadDecoding.decode( + payload, + as: ClawdbotAgentEventPayload.self) + { + continuation.yield(.agent(agentPayload)) } default: break diff --git a/apps/ios/Sources/ClawdbotApp.swift b/apps/ios/Sources/ClawdbotApp.swift index c29572e30..3ed8933b6 100644 --- a/apps/ios/Sources/ClawdbotApp.swift +++ b/apps/ios/Sources/ClawdbotApp.swift @@ -3,14 +3,14 @@ import SwiftUI @main struct ClawdbotApp: App { @State private var appModel: NodeAppModel - @State private var bridgeController: BridgeConnectionController + @State private var gatewayController: GatewayConnectionController @Environment(\.scenePhase) private var scenePhase init() { - BridgeSettingsStore.bootstrapPersistence() + GatewaySettingsStore.bootstrapPersistence() let appModel = NodeAppModel() _appModel = State(initialValue: appModel) - _bridgeController = State(initialValue: BridgeConnectionController(appModel: appModel)) + _gatewayController = State(initialValue: GatewayConnectionController(appModel: appModel)) } var body: some Scene { @@ -18,13 +18,13 @@ struct ClawdbotApp: App { RootCanvas() .environment(self.appModel) .environment(self.appModel.voiceWake) - .environment(self.bridgeController) + .environment(self.gatewayController) .onOpenURL { url in Task { await self.appModel.handleDeepLink(url: url) } } .onChange(of: self.scenePhase) { _, newValue in self.appModel.setScenePhase(newValue) - self.bridgeController.setScenePhase(newValue) + self.gatewayController.setScenePhase(newValue) } } } diff --git a/apps/ios/Sources/Bridge/BridgeConnectionController.swift b/apps/ios/Sources/Gateway/GatewayConnectionController.swift similarity index 52% rename from apps/ios/Sources/Bridge/BridgeConnectionController.swift rename to apps/ios/Sources/Gateway/GatewayConnectionController.swift index 01a6c6c0a..0f1cd02cf 100644 --- a/apps/ios/Sources/Bridge/BridgeConnectionController.swift +++ b/apps/ios/Sources/Gateway/GatewayConnectionController.swift @@ -6,40 +6,23 @@ import Observation import SwiftUI import UIKit -protocol BridgePairingClient: Sendable { - func pairAndHello( - endpoint: NWEndpoint, - hello: BridgeHello, - tls: BridgeTLSParams?, - onStatus: (@Sendable (String) -> Void)?) async throws -> String -} - -extension BridgeClient: BridgePairingClient {} - @MainActor @Observable -final class BridgeConnectionController { - private(set) var bridges: [BridgeDiscoveryModel.DiscoveredBridge] = [] +final class GatewayConnectionController { + private(set) var gateways: [GatewayDiscoveryModel.DiscoveredGateway] = [] private(set) var discoveryStatusText: String = "Idle" - private(set) var discoveryDebugLog: [BridgeDiscoveryModel.DebugLogEntry] = [] + private(set) var discoveryDebugLog: [GatewayDiscoveryModel.DebugLogEntry] = [] - private let discovery = BridgeDiscoveryModel() + private let discovery = GatewayDiscoveryModel() private weak var appModel: NodeAppModel? private var didAutoConnect = false - private let bridgeClientFactory: @Sendable () -> any BridgePairingClient - - init( - appModel: NodeAppModel, - startDiscovery: Bool = true, - bridgeClientFactory: @escaping @Sendable () -> any BridgePairingClient = { BridgeClient() }) - { + init(appModel: NodeAppModel, startDiscovery: Bool = true) { self.appModel = appModel - self.bridgeClientFactory = bridgeClientFactory - BridgeSettingsStore.bootstrapPersistence() + GatewaySettingsStore.bootstrapPersistence() let defaults = UserDefaults.standard - self.discovery.setDebugLoggingEnabled(defaults.bool(forKey: "bridge.discovery.debugLogs")) + self.discovery.setDebugLoggingEnabled(defaults.bool(forKey: "gateway.discovery.debugLogs")) self.updateFromDiscovery() self.observeDiscovery() @@ -64,18 +47,61 @@ final class BridgeConnectionController { } } + func connect(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) async { + let instanceId = UserDefaults.standard.string(forKey: "node.instanceId")? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + let token = GatewaySettingsStore.loadGatewayToken(instanceId: instanceId) + let password = GatewaySettingsStore.loadGatewayPassword(instanceId: instanceId) + guard let host = self.resolveGatewayHost(gateway) else { return } + let port = gateway.gatewayPort ?? 18789 + let tlsParams = self.resolveDiscoveredTLSParams(gateway: gateway) + guard let url = self.buildGatewayURL( + host: host, + port: port, + useTLS: tlsParams?.required == true) + else { return } + self.didAutoConnect = true + self.startAutoConnect( + url: url, + gatewayStableID: gateway.stableID, + tls: tlsParams, + token: token, + password: password) + } + + func connectManual(host: String, port: Int, useTLS: Bool) async { + let instanceId = UserDefaults.standard.string(forKey: "node.instanceId")? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + let token = GatewaySettingsStore.loadGatewayToken(instanceId: instanceId) + let password = GatewaySettingsStore.loadGatewayPassword(instanceId: instanceId) + let stableID = self.manualStableID(host: host, port: port) + let tlsParams = self.resolveManualTLSParams(stableID: stableID, tlsEnabled: useTLS) + guard let url = self.buildGatewayURL( + host: host, + port: port, + useTLS: tlsParams?.required == true) + else { return } + self.didAutoConnect = true + self.startAutoConnect( + url: url, + gatewayStableID: stableID, + tls: tlsParams, + token: token, + password: password) + } + private func updateFromDiscovery() { - let newBridges = self.discovery.bridges - self.bridges = newBridges + let newGateways = self.discovery.gateways + self.gateways = newGateways self.discoveryStatusText = self.discovery.statusText self.discoveryDebugLog = self.discovery.debugLog - self.updateLastDiscoveredBridge(from: newBridges) + self.updateLastDiscoveredGateway(from: newGateways) self.maybeAutoConnect() } private func observeDiscovery() { withObservationTracking { - _ = self.discovery.bridges + _ = self.discovery.gateways _ = self.discovery.statusText _ = self.discovery.debugLog } onChange: { [weak self] in @@ -90,181 +116,176 @@ final class BridgeConnectionController { private func maybeAutoConnect() { guard !self.didAutoConnect else { return } guard let appModel = self.appModel else { return } - guard appModel.bridgeServerName == nil else { return } + guard appModel.gatewayServerName == nil else { return } let defaults = UserDefaults.standard - let manualEnabled = defaults.bool(forKey: "bridge.manual.enabled") + let manualEnabled = defaults.bool(forKey: "gateway.manual.enabled") let instanceId = defaults.string(forKey: "node.instanceId")? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" guard !instanceId.isEmpty else { return } - let token = KeychainStore.loadString( - service: "com.clawdbot.bridge", - account: self.keychainAccount(instanceId: instanceId))? - .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - guard !token.isEmpty else { return } + let token = GatewaySettingsStore.loadGatewayToken(instanceId: instanceId) + let password = GatewaySettingsStore.loadGatewayPassword(instanceId: instanceId) if manualEnabled { - let manualHost = defaults.string(forKey: "bridge.manual.host")? + let manualHost = defaults.string(forKey: "gateway.manual.host")? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" guard !manualHost.isEmpty else { return } - let manualPort = defaults.integer(forKey: "bridge.manual.port") - let resolvedPort = manualPort > 0 ? manualPort : 18790 - guard let port = NWEndpoint.Port(rawValue: UInt16(resolvedPort)) else { return } + let manualPort = defaults.integer(forKey: "gateway.manual.port") + let resolvedPort = manualPort > 0 ? manualPort : 18789 + let manualTLS = defaults.bool(forKey: "gateway.manual.tls") + + let stableID = self.manualStableID(host: manualHost, port: resolvedPort) + let tlsParams = self.resolveManualTLSParams(stableID: stableID, tlsEnabled: manualTLS) + + guard let url = self.buildGatewayURL( + host: manualHost, + port: resolvedPort, + useTLS: tlsParams?.required == true) + else { return } self.didAutoConnect = true - let endpoint = NWEndpoint.hostPort(host: NWEndpoint.Host(manualHost), port: port) - let stableID = BridgeEndpointID.stableID(endpoint) - let tlsParams = self.resolveManualTLSParams(stableID: stableID) self.startAutoConnect( - endpoint: endpoint, - bridgeStableID: stableID, + url: url, + gatewayStableID: stableID, tls: tlsParams, token: token, - instanceId: instanceId) + password: password) return } - let preferredStableID = defaults.string(forKey: "bridge.preferredStableID")? + let preferredStableID = defaults.string(forKey: "gateway.preferredStableID")? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - let lastDiscoveredStableID = defaults.string(forKey: "bridge.lastDiscoveredStableID")? + let lastDiscoveredStableID = defaults.string(forKey: "gateway.lastDiscoveredStableID")? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" let candidates = [preferredStableID, lastDiscoveredStableID].filter { !$0.isEmpty } guard let targetStableID = candidates.first(where: { id in - self.bridges.contains(where: { $0.stableID == id }) + self.gateways.contains(where: { $0.stableID == id }) }) else { return } - guard let target = self.bridges.first(where: { $0.stableID == targetStableID }) else { return } + guard let target = self.gateways.first(where: { $0.stableID == targetStableID }) else { return } + guard let host = self.resolveGatewayHost(target) else { return } + let port = target.gatewayPort ?? 18789 + let tlsParams = self.resolveDiscoveredTLSParams(gateway: target) + guard let url = self.buildGatewayURL(host: host, port: port, useTLS: tlsParams?.required == true) + else { return } - let tlsParams = self.resolveDiscoveredTLSParams(bridge: target) self.didAutoConnect = true self.startAutoConnect( - endpoint: target.endpoint, - bridgeStableID: target.stableID, + url: url, + gatewayStableID: target.stableID, tls: tlsParams, token: token, - instanceId: instanceId) + password: password) } - private func updateLastDiscoveredBridge(from bridges: [BridgeDiscoveryModel.DiscoveredBridge]) { + private func updateLastDiscoveredGateway(from gateways: [GatewayDiscoveryModel.DiscoveredGateway]) { let defaults = UserDefaults.standard - let preferred = defaults.string(forKey: "bridge.preferredStableID")? + let preferred = defaults.string(forKey: "gateway.preferredStableID")? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - let existingLast = defaults.string(forKey: "bridge.lastDiscoveredStableID")? + let existingLast = defaults.string(forKey: "gateway.lastDiscoveredStableID")? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" // Avoid overriding user intent (preferred/lastDiscovered are also set on manual Connect). guard preferred.isEmpty, existingLast.isEmpty else { return } - guard let first = bridges.first else { return } + guard let first = gateways.first else { return } - defaults.set(first.stableID, forKey: "bridge.lastDiscoveredStableID") - BridgeSettingsStore.saveLastDiscoveredBridgeStableID(first.stableID) - } - - private func makeHello(token: String) -> BridgeHello { - let defaults = UserDefaults.standard - let nodeId = defaults.string(forKey: "node.instanceId") ?? "ios-node" - let displayName = self.resolvedDisplayName(defaults: defaults) - - return BridgeHello( - nodeId: nodeId, - displayName: displayName, - token: token, - platform: self.platformString(), - version: self.appVersion(), - deviceFamily: self.deviceFamily(), - modelIdentifier: self.modelIdentifier(), - caps: self.currentCaps(), - commands: self.currentCommands()) - } - - private func keychainAccount(instanceId: String) -> String { - "bridge-token.\(instanceId)" + defaults.set(first.stableID, forKey: "gateway.lastDiscoveredStableID") + GatewaySettingsStore.saveLastDiscoveredGatewayStableID(first.stableID) } private func startAutoConnect( - endpoint: NWEndpoint, - bridgeStableID: String, - tls: BridgeTLSParams?, - token: String, - instanceId: String) + url: URL, + gatewayStableID: String, + tls: GatewayTLSParams?, + token: String?, + password: String?) { guard let appModel else { return } + let connectOptions = self.makeConnectOptions() + Task { [weak self] in guard let self else { return } - do { - let hello = self.makeHello(token: token) - let refreshed = try await self.bridgeClientFactory().pairAndHello( - endpoint: endpoint, - hello: hello, - tls: tls, - onStatus: { status in - Task { @MainActor in - appModel.bridgeStatusText = status - } - }) - let resolvedToken = refreshed.isEmpty ? token : refreshed - if !refreshed.isEmpty, refreshed != token { - _ = KeychainStore.saveString( - refreshed, - service: "com.clawdbot.bridge", - account: self.keychainAccount(instanceId: instanceId)) - } - appModel.connectToBridge( - endpoint: endpoint, - bridgeStableID: bridgeStableID, - tls: tls, - hello: self.makeHello(token: resolvedToken)) - } catch { - await MainActor.run { - appModel.bridgeStatusText = "Bridge error: \(error.localizedDescription)" - } + await MainActor.run { + appModel.gatewayStatusText = "Connecting…" } + appModel.connectToGateway( + url: url, + gatewayStableID: gatewayStableID, + tls: tls, + token: token, + password: password, + connectOptions: connectOptions) } } - private func resolveDiscoveredTLSParams( - bridge: BridgeDiscoveryModel.DiscoveredBridge) -> BridgeTLSParams? - { - let stableID = bridge.stableID - let stored = BridgeTLSStore.loadFingerprint(stableID: stableID) + private func resolveDiscoveredTLSParams(gateway: GatewayDiscoveryModel.DiscoveredGateway) -> GatewayTLSParams? { + let stableID = gateway.stableID + let stored = GatewayTLSStore.loadFingerprint(stableID: stableID) - if bridge.tlsEnabled || bridge.tlsFingerprintSha256 != nil { - return BridgeTLSParams( + if gateway.tlsEnabled || gateway.tlsFingerprintSha256 != nil || stored != nil { + return GatewayTLSParams( required: true, - expectedFingerprint: bridge.tlsFingerprintSha256 ?? stored, + expectedFingerprint: gateway.tlsFingerprintSha256 ?? stored, allowTOFU: stored == nil, storeKey: stableID) } - if let stored { - return BridgeTLSParams( - required: true, - expectedFingerprint: stored, - allowTOFU: false, - storeKey: stableID) - } - return nil } - private func resolveManualTLSParams(stableID: String) -> BridgeTLSParams? { - if let stored = BridgeTLSStore.loadFingerprint(stableID: stableID) { - return BridgeTLSParams( + private func resolveManualTLSParams(stableID: String, tlsEnabled: Bool) -> GatewayTLSParams? { + let stored = GatewayTLSStore.loadFingerprint(stableID: stableID) + if tlsEnabled || stored != nil { + return GatewayTLSParams( required: true, expectedFingerprint: stored, - allowTOFU: false, + allowTOFU: stored == nil, storeKey: stableID) } - return BridgeTLSParams( - required: false, - expectedFingerprint: nil, - allowTOFU: true, - storeKey: stableID) + return nil + } + + private func resolveGatewayHost(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) -> String? { + if let lanHost = gateway.lanHost?.trimmingCharacters(in: .whitespacesAndNewlines), !lanHost.isEmpty { + return lanHost + } + if let tailnet = gateway.tailnetDns?.trimmingCharacters(in: .whitespacesAndNewlines), !tailnet.isEmpty { + return tailnet + } + return nil + } + + private func buildGatewayURL(host: String, port: Int, useTLS: Bool) -> URL? { + let scheme = useTLS ? "wss" : "ws" + var components = URLComponents() + components.scheme = scheme + components.host = host + components.port = port + return components.url + } + + private func manualStableID(host: String, port: Int) -> String { + "manual|\(host.lowercased())|\(port)" + } + + private func makeConnectOptions() -> GatewayConnectOptions { + let defaults = UserDefaults.standard + let displayName = self.resolvedDisplayName(defaults: defaults) + + return GatewayConnectOptions( + role: "node", + scopes: [], + caps: self.currentCaps(), + commands: self.currentCommands(), + permissions: [:], + clientId: "clawdbot-ios", + clientMode: "node", + clientDisplayName: displayName) } private func resolvedDisplayName(defaults: UserDefaults) -> String { @@ -313,6 +334,11 @@ final class BridgeConnectionController { ClawdbotCanvasA2UICommand.pushJSONL.rawValue, ClawdbotCanvasA2UICommand.reset.rawValue, ClawdbotScreenCommand.record.rawValue, + ClawdbotSystemCommand.notify.rawValue, + ClawdbotSystemCommand.which.rawValue, + ClawdbotSystemCommand.run.rawValue, + ClawdbotSystemCommand.execApprovalsGet.rawValue, + ClawdbotSystemCommand.execApprovalsSet.rawValue, ] let caps = Set(self.currentCaps()) @@ -368,11 +394,7 @@ final class BridgeConnectionController { } #if DEBUG -extension BridgeConnectionController { - func _test_makeHello(token: String) -> BridgeHello { - self.makeHello(token: token) - } - +extension GatewayConnectionController { func _test_resolvedDisplayName(defaults: UserDefaults) -> String { self.resolvedDisplayName(defaults: defaults) } @@ -401,8 +423,8 @@ extension BridgeConnectionController { self.appVersion() } - func _test_setBridges(_ bridges: [BridgeDiscoveryModel.DiscoveredBridge]) { - self.bridges = bridges + func _test_setGateways(_ gateways: [GatewayDiscoveryModel.DiscoveredGateway]) { + self.gateways = gateways } func _test_triggerAutoConnect() { diff --git a/apps/ios/Sources/Bridge/BridgeDiscoveryDebugLogView.swift b/apps/ios/Sources/Gateway/GatewayDiscoveryDebugLogView.swift similarity index 79% rename from apps/ios/Sources/Bridge/BridgeDiscoveryDebugLogView.swift rename to apps/ios/Sources/Gateway/GatewayDiscoveryDebugLogView.swift index 26a946213..830722540 100644 --- a/apps/ios/Sources/Bridge/BridgeDiscoveryDebugLogView.swift +++ b/apps/ios/Sources/Gateway/GatewayDiscoveryDebugLogView.swift @@ -1,9 +1,9 @@ import SwiftUI import UIKit -struct BridgeDiscoveryDebugLogView: View { - @Environment(BridgeConnectionController.self) private var bridgeController - @AppStorage("bridge.discovery.debugLogs") private var debugLogsEnabled: Bool = false +struct GatewayDiscoveryDebugLogView: View { + @Environment(GatewayConnectionController.self) private var gatewayController + @AppStorage("gateway.discovery.debugLogs") private var debugLogsEnabled: Bool = false var body: some View { List { @@ -12,11 +12,11 @@ struct BridgeDiscoveryDebugLogView: View { .foregroundStyle(.secondary) } - if self.bridgeController.discoveryDebugLog.isEmpty { + if self.gatewayController.discoveryDebugLog.isEmpty { Text("No log entries yet.") .foregroundStyle(.secondary) } else { - ForEach(self.bridgeController.discoveryDebugLog) { entry in + ForEach(self.gatewayController.discoveryDebugLog) { entry in VStack(alignment: .leading, spacing: 2) { Text(Self.formatTime(entry.ts)) .font(.caption) @@ -35,13 +35,13 @@ struct BridgeDiscoveryDebugLogView: View { Button("Copy") { UIPasteboard.general.string = self.formattedLog() } - .disabled(self.bridgeController.discoveryDebugLog.isEmpty) + .disabled(self.gatewayController.discoveryDebugLog.isEmpty) } } } private func formattedLog() -> String { - self.bridgeController.discoveryDebugLog + self.gatewayController.discoveryDebugLog .map { "\(Self.formatISO($0.ts)) \($0.message)" } .joined(separator: "\n") } diff --git a/apps/ios/Sources/Bridge/BridgeDiscoveryModel.swift b/apps/ios/Sources/Gateway/GatewayDiscoveryModel.swift similarity index 86% rename from apps/ios/Sources/Bridge/BridgeDiscoveryModel.swift rename to apps/ios/Sources/Gateway/GatewayDiscoveryModel.swift index ad7d010bd..a40a62397 100644 --- a/apps/ios/Sources/Bridge/BridgeDiscoveryModel.swift +++ b/apps/ios/Sources/Gateway/GatewayDiscoveryModel.swift @@ -5,14 +5,14 @@ import Observation @MainActor @Observable -final class BridgeDiscoveryModel { +final class GatewayDiscoveryModel { struct DebugLogEntry: Identifiable, Equatable { var id = UUID() var ts: Date var message: String } - struct DiscoveredBridge: Identifiable, Equatable { + struct DiscoveredGateway: Identifiable, Equatable { var id: String { self.stableID } var name: String var endpoint: NWEndpoint @@ -21,19 +21,18 @@ final class BridgeDiscoveryModel { var lanHost: String? var tailnetDns: String? var gatewayPort: Int? - var bridgePort: Int? var canvasPort: Int? var tlsEnabled: Bool var tlsFingerprintSha256: String? var cliPath: String? } - var bridges: [DiscoveredBridge] = [] + var gateways: [DiscoveredGateway] = [] var statusText: String = "Idle" private(set) var debugLog: [DebugLogEntry] = [] private var browsers: [String: NWBrowser] = [:] - private var bridgesByDomain: [String: [DiscoveredBridge]] = [:] + private var gatewaysByDomain: [String: [DiscoveredGateway]] = [:] private var statesByDomain: [String: NWBrowser.State] = [:] private var debugLoggingEnabled = false private var lastStableIDs = Set() @@ -45,7 +44,7 @@ final class BridgeDiscoveryModel { self.debugLog = [] } else if !wasEnabled { self.appendDebugLog("debug logging enabled") - self.appendDebugLog("snapshot: status=\(self.statusText) bridges=\(self.bridges.count)") + self.appendDebugLog("snapshot: status=\(self.statusText) gateways=\(self.gateways.count)") } } @@ -53,11 +52,11 @@ final class BridgeDiscoveryModel { if !self.browsers.isEmpty { return } self.appendDebugLog("start()") - for domain in ClawdbotBonjour.bridgeServiceDomains { + for domain in ClawdbotBonjour.gatewayServiceDomains { let params = NWParameters.tcp params.includePeerToPeer = true let browser = NWBrowser( - for: .bonjour(type: ClawdbotBonjour.bridgeServiceType, domain: domain), + for: .bonjour(type: ClawdbotBonjour.gatewayServiceType, domain: domain), using: params) browser.stateUpdateHandler = { [weak self] state in @@ -72,7 +71,7 @@ final class BridgeDiscoveryModel { browser.browseResultsChangedHandler = { [weak self] results, _ in Task { @MainActor in guard let self else { return } - self.bridgesByDomain[domain] = results.compactMap { result -> DiscoveredBridge? in + self.gatewaysByDomain[domain] = results.compactMap { result -> DiscoveredGateway? in switch result.endpoint { case let .service(name, _, _, _): let decodedName = BonjourEscapes.decode(name) @@ -82,18 +81,17 @@ final class BridgeDiscoveryModel { .map(Self.prettifyInstanceName) .flatMap { $0.isEmpty ? nil : $0 } let prettyName = prettyAdvertised ?? Self.prettifyInstanceName(decodedName) - return DiscoveredBridge( + return DiscoveredGateway( name: prettyName, endpoint: result.endpoint, - stableID: BridgeEndpointID.stableID(result.endpoint), - debugID: BridgeEndpointID.prettyDescription(result.endpoint), + stableID: GatewayEndpointID.stableID(result.endpoint), + debugID: GatewayEndpointID.prettyDescription(result.endpoint), lanHost: Self.txtValue(txt, key: "lanHost"), tailnetDns: Self.txtValue(txt, key: "tailnetDns"), gatewayPort: Self.txtIntValue(txt, key: "gatewayPort"), - bridgePort: Self.txtIntValue(txt, key: "bridgePort"), canvasPort: Self.txtIntValue(txt, key: "canvasPort"), - tlsEnabled: Self.txtBoolValue(txt, key: "bridgeTls"), - tlsFingerprintSha256: Self.txtValue(txt, key: "bridgeTlsSha256"), + tlsEnabled: Self.txtBoolValue(txt, key: "gatewayTls"), + tlsFingerprintSha256: Self.txtValue(txt, key: "gatewayTlsSha256"), cliPath: Self.txtValue(txt, key: "cliPath")) default: return nil @@ -101,12 +99,12 @@ final class BridgeDiscoveryModel { } .sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } - self.recomputeBridges() + self.recomputeGateways() } } self.browsers[domain] = browser - browser.start(queue: DispatchQueue(label: "com.clawdbot.ios.bridge-discovery.\(domain)")) + browser.start(queue: DispatchQueue(label: "com.clawdbot.ios.gateway-discovery.\(domain)")) } } @@ -116,14 +114,14 @@ final class BridgeDiscoveryModel { browser.cancel() } self.browsers = [:] - self.bridgesByDomain = [:] + self.gatewaysByDomain = [:] self.statesByDomain = [:] - self.bridges = [] + self.gateways = [] self.statusText = "Stopped" } - private func recomputeBridges() { - let next = self.bridgesByDomain.values + private func recomputeGateways() { + let next = self.gatewaysByDomain.values .flatMap(\.self) .sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } @@ -134,7 +132,7 @@ final class BridgeDiscoveryModel { self.appendDebugLog("results: total=\(next.count) added=\(added.count) removed=\(removed.count)") } self.lastStableIDs = nextIDs - self.bridges = next + self.gateways = next } private func updateStatusText() { diff --git a/apps/ios/Sources/Gateway/GatewaySettingsStore.swift b/apps/ios/Sources/Gateway/GatewaySettingsStore.swift new file mode 100644 index 000000000..52ada8d80 --- /dev/null +++ b/apps/ios/Sources/Gateway/GatewaySettingsStore.swift @@ -0,0 +1,226 @@ +import Foundation + +enum GatewaySettingsStore { + private static let gatewayService = "com.clawdbot.gateway" + private static let legacyBridgeService = "com.clawdbot.bridge" + private static let nodeService = "com.clawdbot.node" + + private static let instanceIdDefaultsKey = "node.instanceId" + private static let preferredGatewayStableIDDefaultsKey = "gateway.preferredStableID" + private static let lastDiscoveredGatewayStableIDDefaultsKey = "gateway.lastDiscoveredStableID" + private static let manualEnabledDefaultsKey = "gateway.manual.enabled" + private static let manualHostDefaultsKey = "gateway.manual.host" + private static let manualPortDefaultsKey = "gateway.manual.port" + private static let manualTlsDefaultsKey = "gateway.manual.tls" + private static let discoveryDebugLogsDefaultsKey = "gateway.discovery.debugLogs" + + private static let legacyPreferredBridgeStableIDDefaultsKey = "bridge.preferredStableID" + private static let legacyLastDiscoveredBridgeStableIDDefaultsKey = "bridge.lastDiscoveredStableID" + private static let legacyManualEnabledDefaultsKey = "bridge.manual.enabled" + private static let legacyManualHostDefaultsKey = "bridge.manual.host" + private static let legacyManualPortDefaultsKey = "bridge.manual.port" + private static let legacyDiscoveryDebugLogsDefaultsKey = "bridge.discovery.debugLogs" + + private static let instanceIdAccount = "instanceId" + private static let preferredGatewayStableIDAccount = "preferredStableID" + private static let lastDiscoveredGatewayStableIDAccount = "lastDiscoveredStableID" + + static func bootstrapPersistence() { + self.ensureStableInstanceID() + self.ensurePreferredGatewayStableID() + self.ensureLastDiscoveredGatewayStableID() + self.migrateLegacyDefaults() + } + + static func loadStableInstanceID() -> String? { + KeychainStore.loadString(service: self.nodeService, account: self.instanceIdAccount)? + .trimmingCharacters(in: .whitespacesAndNewlines) + } + + static func saveStableInstanceID(_ instanceId: String) { + _ = KeychainStore.saveString(instanceId, service: self.nodeService, account: self.instanceIdAccount) + } + + static func loadPreferredGatewayStableID() -> String? { + KeychainStore.loadString(service: self.gatewayService, account: self.preferredGatewayStableIDAccount)? + .trimmingCharacters(in: .whitespacesAndNewlines) + } + + static func savePreferredGatewayStableID(_ stableID: String) { + _ = KeychainStore.saveString( + stableID, + service: self.gatewayService, + account: self.preferredGatewayStableIDAccount) + } + + static func loadLastDiscoveredGatewayStableID() -> String? { + KeychainStore.loadString(service: self.gatewayService, account: self.lastDiscoveredGatewayStableIDAccount)? + .trimmingCharacters(in: .whitespacesAndNewlines) + } + + static func saveLastDiscoveredGatewayStableID(_ stableID: String) { + _ = KeychainStore.saveString( + stableID, + service: self.gatewayService, + account: self.lastDiscoveredGatewayStableIDAccount) + } + + static func loadGatewayToken(instanceId: String) -> String? { + let account = self.gatewayTokenAccount(instanceId: instanceId) + let token = KeychainStore.loadString(service: self.gatewayService, account: account)? + .trimmingCharacters(in: .whitespacesAndNewlines) + if token?.isEmpty == false { return token } + + let legacyAccount = self.legacyBridgeTokenAccount(instanceId: instanceId) + let legacy = KeychainStore.loadString(service: self.legacyBridgeService, account: legacyAccount)? + .trimmingCharacters(in: .whitespacesAndNewlines) + if let legacy, !legacy.isEmpty { + _ = KeychainStore.saveString(legacy, service: self.gatewayService, account: account) + return legacy + } + return nil + } + + static func saveGatewayToken(_ token: String, instanceId: String) { + _ = KeychainStore.saveString( + token, + service: self.gatewayService, + account: self.gatewayTokenAccount(instanceId: instanceId)) + } + + static func loadGatewayPassword(instanceId: String) -> String? { + KeychainStore.loadString( + service: self.gatewayService, + account: self.gatewayPasswordAccount(instanceId: instanceId))? + .trimmingCharacters(in: .whitespacesAndNewlines) + } + + static func saveGatewayPassword(_ password: String, instanceId: String) { + _ = KeychainStore.saveString( + password, + service: self.gatewayService, + account: self.gatewayPasswordAccount(instanceId: instanceId)) + } + + private static func gatewayTokenAccount(instanceId: String) -> String { + "gateway-token.\(instanceId)" + } + + private static func legacyBridgeTokenAccount(instanceId: String) -> String { + "bridge-token.\(instanceId)" + } + + private static func gatewayPasswordAccount(instanceId: String) -> String { + "gateway-password.\(instanceId)" + } + + private static func ensureStableInstanceID() { + let defaults = UserDefaults.standard + + if let existing = defaults.string(forKey: self.instanceIdDefaultsKey)? + .trimmingCharacters(in: .whitespacesAndNewlines), + !existing.isEmpty + { + if self.loadStableInstanceID() == nil { + self.saveStableInstanceID(existing) + } + return + } + + if let stored = self.loadStableInstanceID(), !stored.isEmpty { + defaults.set(stored, forKey: self.instanceIdDefaultsKey) + return + } + + let fresh = UUID().uuidString + self.saveStableInstanceID(fresh) + defaults.set(fresh, forKey: self.instanceIdDefaultsKey) + } + + private static func ensurePreferredGatewayStableID() { + let defaults = UserDefaults.standard + + if let existing = defaults.string(forKey: self.preferredGatewayStableIDDefaultsKey)? + .trimmingCharacters(in: .whitespacesAndNewlines), + !existing.isEmpty + { + if self.loadPreferredGatewayStableID() == nil { + self.savePreferredGatewayStableID(existing) + } + return + } + + if let stored = self.loadPreferredGatewayStableID(), !stored.isEmpty { + defaults.set(stored, forKey: self.preferredGatewayStableIDDefaultsKey) + } + } + + private static func ensureLastDiscoveredGatewayStableID() { + let defaults = UserDefaults.standard + + if let existing = defaults.string(forKey: self.lastDiscoveredGatewayStableIDDefaultsKey)? + .trimmingCharacters(in: .whitespacesAndNewlines), + !existing.isEmpty + { + if self.loadLastDiscoveredGatewayStableID() == nil { + self.saveLastDiscoveredGatewayStableID(existing) + } + return + } + + if let stored = self.loadLastDiscoveredGatewayStableID(), !stored.isEmpty { + defaults.set(stored, forKey: self.lastDiscoveredGatewayStableIDDefaultsKey) + } + } + + private static func migrateLegacyDefaults() { + let defaults = UserDefaults.standard + + if defaults.string(forKey: self.preferredGatewayStableIDDefaultsKey)?.isEmpty != false, + let legacy = defaults.string(forKey: self.legacyPreferredBridgeStableIDDefaultsKey), + !legacy.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + { + defaults.set(legacy, forKey: self.preferredGatewayStableIDDefaultsKey) + self.savePreferredGatewayStableID(legacy) + } + + if defaults.string(forKey: self.lastDiscoveredGatewayStableIDDefaultsKey)?.isEmpty != false, + let legacy = defaults.string(forKey: self.legacyLastDiscoveredBridgeStableIDDefaultsKey), + !legacy.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + { + defaults.set(legacy, forKey: self.lastDiscoveredGatewayStableIDDefaultsKey) + self.saveLastDiscoveredGatewayStableID(legacy) + } + + if defaults.object(forKey: self.manualEnabledDefaultsKey) == nil, + defaults.object(forKey: self.legacyManualEnabledDefaultsKey) != nil + { + defaults.set( + defaults.bool(forKey: self.legacyManualEnabledDefaultsKey), + forKey: self.manualEnabledDefaultsKey) + } + + if defaults.string(forKey: self.manualHostDefaultsKey)?.isEmpty != false, + let legacy = defaults.string(forKey: self.legacyManualHostDefaultsKey), + !legacy.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + { + defaults.set(legacy, forKey: self.manualHostDefaultsKey) + } + + if defaults.integer(forKey: self.manualPortDefaultsKey) == 0, + defaults.integer(forKey: self.legacyManualPortDefaultsKey) > 0 + { + defaults.set( + defaults.integer(forKey: self.legacyManualPortDefaultsKey), + forKey: self.manualPortDefaultsKey) + } + + if defaults.object(forKey: self.discoveryDebugLogsDefaultsKey) == nil, + defaults.object(forKey: self.legacyDiscoveryDebugLogsDefaultsKey) != nil + { + defaults.set( + defaults.bool(forKey: self.legacyDiscoveryDebugLogsDefaultsKey), + forKey: self.discoveryDebugLogsDefaultsKey) + } + } +} diff --git a/apps/ios/Sources/Bridge/KeychainStore.swift b/apps/ios/Sources/Gateway/KeychainStore.swift similarity index 100% rename from apps/ios/Sources/Bridge/KeychainStore.swift rename to apps/ios/Sources/Gateway/KeychainStore.swift diff --git a/apps/ios/Sources/Info.plist b/apps/ios/Sources/Info.plist index a75e27741..9dd7a0315 100644 --- a/apps/ios/Sources/Info.plist +++ b/apps/ios/Sources/Info.plist @@ -19,9 +19,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2026.1.11-4 + 2026.1.24 CFBundleVersion - 202601113 + 20260124 NSAppTransportSecurity NSAllowsArbitraryLoadsInWebContent @@ -29,12 +29,12 @@ NSBonjourServices - _clawdbot-bridge._tcp + _clawdbot-gw._tcp NSCameraUsageDescription - Clawdbot can capture photos or short video clips when requested via the bridge. + Clawdbot can capture photos or short video clips when requested via the gateway. NSLocalNetworkUsageDescription - Clawdbot discovers and connects to your Clawdbot bridge on the local network. + Clawdbot discovers and connects to your Clawdbot gateway on the local network. NSLocationAlwaysAndWhenInUseUsageDescription Clawdbot can share your location in the background when you enable Always. NSLocationWhenInUseUsageDescription diff --git a/apps/ios/Sources/Model/NodeAppModel.swift b/apps/ios/Sources/Model/NodeAppModel.swift index 258d79f4a..2830f17d7 100644 --- a/apps/ios/Sources/Model/NodeAppModel.swift +++ b/apps/ios/Sources/Model/NodeAppModel.swift @@ -18,15 +18,15 @@ final class NodeAppModel { let screen = ScreenController() let camera = CameraController() private let screenRecorder = ScreenRecordService() - var bridgeStatusText: String = "Offline" - var bridgeServerName: String? - var bridgeRemoteAddress: String? - var connectedBridgeID: String? + var gatewayStatusText: String = "Offline" + var gatewayServerName: String? + var gatewayRemoteAddress: String? + var connectedGatewayID: String? var seamColorHex: String? var mainSessionKey: String = "main" - private let bridge = BridgeSession() - private var bridgeTask: Task? + private let gateway = GatewayNodeSession() + private var gatewayTask: Task? private var voiceWakeSyncTask: Task? @ObservationIgnored private var cameraHUDDismissTask: Task? let voiceWake = VoiceWakeManager() @@ -34,7 +34,8 @@ final class NodeAppModel { private let locationService = LocationService() private var lastAutoA2uiURL: String? - var bridgeSession: BridgeSession { self.bridge } + private var gatewayConnected = false + var gatewaySession: GatewayNodeSession { self.gateway } var cameraHUDText: String? var cameraHUDKind: CameraHUDKind? @@ -54,7 +55,7 @@ final class NodeAppModel { let enabled = UserDefaults.standard.bool(forKey: "voiceWake.enabled") self.voiceWake.setEnabled(enabled) - self.talkMode.attachBridge(self.bridge) + self.talkMode.attachGateway(self.gateway) let talkEnabled = UserDefaults.standard.bool(forKey: "talk.enabled") self.talkMode.setEnabled(talkEnabled) @@ -120,9 +121,9 @@ final class NodeAppModel { let ok: Bool var errorText: String? - if await !self.isBridgeConnected() { + if await !self.isGatewayConnected() { ok = false - errorText = "bridge not connected" + errorText = "gateway not connected" } else { do { try await self.sendAgentRequest(link: AgentDeepLink( @@ -150,7 +151,7 @@ final class NodeAppModel { } private func resolveA2UIHostURL() async -> String? { - guard let raw = await self.bridge.currentCanvasHostUrl() else { return nil } + guard let raw = await self.gateway.currentCanvasHostUrl() else { return nil } let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty, let base = URL(string: trimmed) else { return nil } return base.appendingPathComponent("__clawdbot__/a2ui/").absoluteString + "?platform=ios" @@ -202,56 +203,70 @@ final class NodeAppModel { } } - func connectToBridge( - endpoint: NWEndpoint, - bridgeStableID: String, - tls: BridgeTLSParams?, - hello: BridgeHello) + func connectToGateway( + url: URL, + gatewayStableID: String, + tls: GatewayTLSParams?, + token: String?, + password: String?, + connectOptions: GatewayConnectOptions) { - self.bridgeTask?.cancel() - self.bridgeServerName = nil - self.bridgeRemoteAddress = nil - let id = bridgeStableID.trimmingCharacters(in: .whitespacesAndNewlines) - self.connectedBridgeID = id.isEmpty ? BridgeEndpointID.stableID(endpoint) : id + self.gatewayTask?.cancel() + self.gatewayServerName = nil + self.gatewayRemoteAddress = nil + let id = gatewayStableID.trimmingCharacters(in: .whitespacesAndNewlines) + self.connectedGatewayID = id.isEmpty ? url.absoluteString : id + self.gatewayConnected = false self.voiceWakeSyncTask?.cancel() self.voiceWakeSyncTask = nil + let sessionBox = tls.map { WebSocketSessionBox(session: GatewayTLSPinningSession(params: $0)) } - self.bridgeTask = Task { + self.gatewayTask = Task { var attempt = 0 while !Task.isCancelled { await MainActor.run { if attempt == 0 { - self.bridgeStatusText = "Connecting…" + self.gatewayStatusText = "Connecting…" } else { - self.bridgeStatusText = "Reconnecting…" + self.gatewayStatusText = "Reconnecting…" } - self.bridgeServerName = nil - self.bridgeRemoteAddress = nil + self.gatewayServerName = nil + self.gatewayRemoteAddress = nil } do { - try await self.bridge.connect( - endpoint: endpoint, - hello: hello, - tls: tls, - onConnected: { [weak self] serverName, mainSessionKey in + try await self.gateway.connect( + url: url, + token: token, + password: password, + connectOptions: connectOptions, + sessionBox: sessionBox, + onConnected: { [weak self] in guard let self else { return } await MainActor.run { - self.bridgeStatusText = "Connected" - self.bridgeServerName = serverName + self.gatewayStatusText = "Connected" + self.gatewayServerName = url.host ?? "gateway" + self.gatewayConnected = true } - await MainActor.run { - self.applyMainSessionKey(mainSessionKey) - } - if let addr = await self.bridge.currentRemoteAddress() { + if let addr = await self.gateway.currentRemoteAddress() { await MainActor.run { - self.bridgeRemoteAddress = addr + self.gatewayRemoteAddress = addr } } await self.refreshBrandingFromGateway() await self.startVoiceWakeSync() await self.showA2UIOnConnectIfNeeded() }, + onDisconnected: { [weak self] reason in + guard let self else { return } + await MainActor.run { + self.gatewayStatusText = "Disconnected" + self.gatewayRemoteAddress = nil + self.gatewayConnected = false + self.showLocalCanvasOnDisconnect() + self.gatewayStatusText = "Disconnected: \(reason)" + } + }, onInvoke: { [weak self] req in guard let self else { return BridgeInvokeResponse( @@ -265,19 +280,16 @@ final class NodeAppModel { }) if Task.isCancelled { break } - await MainActor.run { - self.showLocalCanvasOnDisconnect() - } - attempt += 1 - let sleepSeconds = min(6.0, 0.35 * pow(1.7, Double(attempt))) - try? await Task.sleep(nanoseconds: UInt64(sleepSeconds * 1_000_000_000)) + attempt = 0 + try? await Task.sleep(nanoseconds: 1_000_000_000) } catch { if Task.isCancelled { break } attempt += 1 await MainActor.run { - self.bridgeStatusText = "Bridge error: \(error.localizedDescription)" - self.bridgeServerName = nil - self.bridgeRemoteAddress = nil + self.gatewayStatusText = "Gateway error: \(error.localizedDescription)" + self.gatewayServerName = nil + self.gatewayRemoteAddress = nil + self.gatewayConnected = false self.showLocalCanvasOnDisconnect() } let sleepSeconds = min(8.0, 0.5 * pow(1.7, Double(attempt))) @@ -286,10 +298,11 @@ final class NodeAppModel { } await MainActor.run { - self.bridgeStatusText = "Offline" - self.bridgeServerName = nil - self.bridgeRemoteAddress = nil - self.connectedBridgeID = nil + self.gatewayStatusText = "Offline" + self.gatewayServerName = nil + self.gatewayRemoteAddress = nil + self.connectedGatewayID = nil + self.gatewayConnected = false self.seamColorHex = nil if !SessionKey.isCanonicalMainSessionKey(self.mainSessionKey) { self.mainSessionKey = "main" @@ -300,16 +313,17 @@ final class NodeAppModel { } } - func disconnectBridge() { - self.bridgeTask?.cancel() - self.bridgeTask = nil + func disconnectGateway() { + self.gatewayTask?.cancel() + self.gatewayTask = nil self.voiceWakeSyncTask?.cancel() self.voiceWakeSyncTask = nil - Task { await self.bridge.disconnect() } - self.bridgeStatusText = "Offline" - self.bridgeServerName = nil - self.bridgeRemoteAddress = nil - self.connectedBridgeID = nil + Task { await self.gateway.disconnect() } + self.gatewayStatusText = "Offline" + self.gatewayServerName = nil + self.gatewayRemoteAddress = nil + self.connectedGatewayID = nil + self.gatewayConnected = false self.seamColorHex = nil if !SessionKey.isCanonicalMainSessionKey(self.mainSessionKey) { self.mainSessionKey = "main" @@ -347,7 +361,7 @@ final class NodeAppModel { private func refreshBrandingFromGateway() async { do { - let res = try await self.bridge.request(method: "config.get", paramsJSON: "{}", timeoutSeconds: 8) + let res = try await self.gateway.request(method: "config.get", paramsJSON: "{}", timeoutSeconds: 8) guard let json = try JSONSerialization.jsonObject(with: res) as? [String: Any] else { return } guard let config = json["config"] as? [String: Any] else { return } let ui = config["ui"] as? [String: Any] @@ -378,7 +392,7 @@ final class NodeAppModel { else { return } do { - _ = try await self.bridge.request(method: "voicewake.set", paramsJSON: json, timeoutSeconds: 12) + _ = try await self.gateway.request(method: "voicewake.set", paramsJSON: json, timeoutSeconds: 12) } catch { // Best-effort only. } @@ -391,12 +405,14 @@ final class NodeAppModel { await self.refreshWakeWordsFromGateway() - let stream = await self.bridge.subscribeServerEvents(bufferingNewest: 200) + let stream = await self.gateway.subscribeServerEvents(bufferingNewest: 200) for await evt in stream { if Task.isCancelled { return } guard evt.event == "voicewake.changed" else { continue } - guard let payloadJSON = evt.payloadJSON else { continue } - guard let triggers = VoiceWakePreferences.decodeGatewayTriggers(from: payloadJSON) else { continue } + guard let payload = evt.payload else { continue } + struct Payload: Decodable { var triggers: [String] } + guard let decoded = try? GatewayPayloadDecoding.decode(payload, as: Payload.self) else { continue } + let triggers = VoiceWakePreferences.sanitizeTriggerWords(decoded.triggers) VoiceWakePreferences.saveTriggerWords(triggers) } } @@ -404,7 +420,7 @@ final class NodeAppModel { private func refreshWakeWordsFromGateway() async { do { - let data = try await self.bridge.request(method: "voicewake.get", paramsJSON: "{}", timeoutSeconds: 8) + let data = try await self.gateway.request(method: "voicewake.get", paramsJSON: "{}", timeoutSeconds: 8) guard let triggers = VoiceWakePreferences.decodeGatewayTriggers(from: data) else { return } VoiceWakePreferences.saveTriggerWords(triggers) } catch { @@ -413,6 +429,11 @@ final class NodeAppModel { } func sendVoiceTranscript(text: String, sessionKey: String?) async throws { + if await !self.isGatewayConnected() { + throw NSError(domain: "Gateway", code: 10, userInfo: [ + NSLocalizedDescriptionKey: "Gateway not connected", + ]) + } struct Payload: Codable { var text: String var sessionKey: String? @@ -424,7 +445,7 @@ final class NodeAppModel { NSLocalizedDescriptionKey: "Failed to encode voice transcript payload as UTF-8", ]) } - try await self.bridge.sendEvent(event: "voice.transcript", payloadJSON: json) + await self.gateway.sendEvent(event: "voice.transcript", payloadJSON: json) } func handleDeepLink(url: URL) async { @@ -445,8 +466,8 @@ final class NodeAppModel { return } - guard await self.isBridgeConnected() else { - self.screen.errorText = "Bridge not connected (cannot forward deep link)." + guard await self.isGatewayConnected() else { + self.screen.errorText = "Gateway not connected (cannot forward deep link)." return } @@ -465,7 +486,7 @@ final class NodeAppModel { ]) } - // iOS bridge forwards to the gateway; no local auth prompts here. + // iOS gateway forwards to the gateway; no local auth prompts here. // (Key-based unattended auth is handled on macOS for clawdbot:// links.) let data = try JSONEncoder().encode(link) guard let json = String(bytes: data, encoding: .utf8) else { @@ -473,12 +494,11 @@ final class NodeAppModel { NSLocalizedDescriptionKey: "Failed to encode agent request payload as UTF-8", ]) } - try await self.bridge.sendEvent(event: "agent.request", payloadJSON: json) + await self.gateway.sendEvent(event: "agent.request", payloadJSON: json) } - private func isBridgeConnected() async -> Bool { - if case .connected = await self.bridge.state { return true } - return false + private func isGatewayConnected() async -> Bool { + self.gatewayConnected } private func handleInvoke(_ req: BridgeInvokeRequest) async -> BridgeInvokeResponse { @@ -817,7 +837,7 @@ final class NodeAppModel { fps: params.fps, includeAudio: params.includeAudio, outPath: nil) - defer { try? FileManager.default.removeItem(atPath: path) } + defer { try? FileManager().removeItem(atPath: path) } let data = try Data(contentsOf: URL(fileURLWithPath: path)) struct Payload: Codable { var format: String @@ -837,26 +857,29 @@ final class NodeAppModel { return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload) } - private func locationMode() -> ClawdbotLocationMode { +} + +private extension NodeAppModel { + func locationMode() -> ClawdbotLocationMode { let raw = UserDefaults.standard.string(forKey: "location.enabledMode") ?? "off" return ClawdbotLocationMode(rawValue: raw) ?? .off } - private func isLocationPreciseEnabled() -> Bool { + func isLocationPreciseEnabled() -> Bool { if UserDefaults.standard.object(forKey: "location.preciseEnabled") == nil { return true } return UserDefaults.standard.bool(forKey: "location.preciseEnabled") } - private static func decodeParams(_ type: T.Type, from json: String?) throws -> T { + static func decodeParams(_ type: T.Type, from json: String?) throws -> T { guard let json, let data = json.data(using: .utf8) else { - throw NSError(domain: "Bridge", code: 20, userInfo: [ + throw NSError(domain: "Gateway", code: 20, userInfo: [ NSLocalizedDescriptionKey: "INVALID_REQUEST: paramsJSON required", ]) } return try JSONDecoder().decode(type, from: data) } - private static func encodePayload(_ obj: some Encodable) throws -> String { + static func encodePayload(_ obj: some Encodable) throws -> String { let data = try JSONEncoder().encode(obj) guard let json = String(bytes: data, encoding: .utf8) else { throw NSError(domain: "NodeAppModel", code: 21, userInfo: [ @@ -866,17 +889,17 @@ final class NodeAppModel { return json } - private func isCameraEnabled() -> Bool { + func isCameraEnabled() -> Bool { // Default-on: if the key doesn't exist yet, treat it as enabled. if UserDefaults.standard.object(forKey: "camera.enabled") == nil { return true } return UserDefaults.standard.bool(forKey: "camera.enabled") } - private func triggerCameraFlash() { + func triggerCameraFlash() { self.cameraFlashNonce &+= 1 } - private func showCameraHUD(text: String, kind: CameraHUDKind, autoHideSeconds: Double? = nil) { + func showCameraHUD(text: String, kind: CameraHUDKind, autoHideSeconds: Double? = nil) { self.cameraHUDDismissTask?.cancel() withAnimation(.spring(response: 0.25, dampingFraction: 0.85)) { diff --git a/apps/ios/Sources/RootCanvas.swift b/apps/ios/Sources/RootCanvas.swift index bd3fefc52..93cb81627 100644 --- a/apps/ios/Sources/RootCanvas.swift +++ b/apps/ios/Sources/RootCanvas.swift @@ -29,7 +29,7 @@ struct RootCanvas: View { ZStack { CanvasContent( systemColorScheme: self.systemColorScheme, - bridgeStatus: self.bridgeStatus, + gatewayStatus: self.gatewayStatus, voiceWakeEnabled: self.voiceWakeEnabled, voiceWakeToastText: self.voiceWakeToastText, cameraHUDText: self.appModel.cameraHUDText, @@ -52,7 +52,7 @@ struct RootCanvas: View { SettingsTab() case .chat: ChatSheet( - bridge: self.appModel.bridgeSession, + gateway: self.appModel.gatewaySession, sessionKey: self.appModel.mainSessionKey, userAccent: self.appModel.seamColor) } @@ -62,9 +62,9 @@ struct RootCanvas: View { .onChange(of: self.scenePhase) { _, _ in self.updateIdleTimer() } .onAppear { self.updateCanvasDebugStatus() } .onChange(of: self.canvasDebugStatusEnabled) { _, _ in self.updateCanvasDebugStatus() } - .onChange(of: self.appModel.bridgeStatusText) { _, _ in self.updateCanvasDebugStatus() } - .onChange(of: self.appModel.bridgeServerName) { _, _ in self.updateCanvasDebugStatus() } - .onChange(of: self.appModel.bridgeRemoteAddress) { _, _ in self.updateCanvasDebugStatus() } + .onChange(of: self.appModel.gatewayStatusText) { _, _ in self.updateCanvasDebugStatus() } + .onChange(of: self.appModel.gatewayServerName) { _, _ in self.updateCanvasDebugStatus() } + .onChange(of: self.appModel.gatewayRemoteAddress) { _, _ in self.updateCanvasDebugStatus() } .onChange(of: self.voiceWake.lastTriggeredCommand) { _, newValue in guard let newValue else { return } let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines) @@ -91,10 +91,10 @@ struct RootCanvas: View { } } - private var bridgeStatus: StatusPill.BridgeState { - if self.appModel.bridgeServerName != nil { return .connected } + private var gatewayStatus: StatusPill.GatewayState { + if self.appModel.gatewayServerName != nil { return .connected } - let text = self.appModel.bridgeStatusText.trimmingCharacters(in: .whitespacesAndNewlines) + let text = self.appModel.gatewayStatusText.trimmingCharacters(in: .whitespacesAndNewlines) if text.localizedCaseInsensitiveContains("connecting") || text.localizedCaseInsensitiveContains("reconnecting") { @@ -115,8 +115,8 @@ struct RootCanvas: View { private func updateCanvasDebugStatus() { self.appModel.screen.setDebugStatusEnabled(self.canvasDebugStatusEnabled) guard self.canvasDebugStatusEnabled else { return } - let title = self.appModel.bridgeStatusText.trimmingCharacters(in: .whitespacesAndNewlines) - let subtitle = self.appModel.bridgeServerName ?? self.appModel.bridgeRemoteAddress + let title = self.appModel.gatewayStatusText.trimmingCharacters(in: .whitespacesAndNewlines) + let subtitle = self.appModel.gatewayServerName ?? self.appModel.gatewayRemoteAddress self.appModel.screen.updateDebugStatus(title: title, subtitle: subtitle) } } @@ -126,7 +126,7 @@ private struct CanvasContent: View { @AppStorage("talk.enabled") private var talkEnabled: Bool = false @AppStorage("talk.button.enabled") private var talkButtonEnabled: Bool = true var systemColorScheme: ColorScheme - var bridgeStatus: StatusPill.BridgeState + var gatewayStatus: StatusPill.GatewayState var voiceWakeEnabled: Bool var voiceWakeToastText: String? var cameraHUDText: String? @@ -177,7 +177,7 @@ private struct CanvasContent: View { } .overlay(alignment: .topLeading) { StatusPill( - bridge: self.bridgeStatus, + gateway: self.gatewayStatus, voiceWakeEnabled: self.voiceWakeEnabled, activity: self.statusActivity, brighten: self.brightenButtons, @@ -208,15 +208,15 @@ private struct CanvasContent: View { tint: .orange) } - let bridgeStatus = self.appModel.bridgeStatusText.trimmingCharacters(in: .whitespacesAndNewlines) - let bridgeLower = bridgeStatus.lowercased() - if bridgeLower.contains("repair") { + let gatewayStatus = self.appModel.gatewayStatusText.trimmingCharacters(in: .whitespacesAndNewlines) + let gatewayLower = gatewayStatus.lowercased() + if gatewayLower.contains("repair") { return StatusPill.Activity(title: "Repairing…", systemImage: "wrench.and.screwdriver", tint: .orange) } - if bridgeLower.contains("approval") || bridgeLower.contains("pairing") { + if gatewayLower.contains("approval") || gatewayLower.contains("pairing") { return StatusPill.Activity(title: "Approval pending", systemImage: "person.crop.circle.badge.clock") } - // Avoid duplicating the primary bridge status ("Connecting…") in the activity slot. + // Avoid duplicating the primary gateway status ("Connecting…") in the activity slot. if self.appModel.screenRecordActive { return StatusPill.Activity(title: "Recording screen…", systemImage: "record.circle.fill", tint: .red) diff --git a/apps/ios/Sources/RootTabs.swift b/apps/ios/Sources/RootTabs.swift index e76d357a0..f7b3fd822 100644 --- a/apps/ios/Sources/RootTabs.swift +++ b/apps/ios/Sources/RootTabs.swift @@ -24,7 +24,7 @@ struct RootTabs: View { } .overlay(alignment: .topLeading) { StatusPill( - bridge: self.bridgeStatus, + gateway: self.gatewayStatus, voiceWakeEnabled: self.voiceWakeEnabled, activity: self.statusActivity, onTap: { self.selectedTab = 2 }) @@ -64,10 +64,10 @@ struct RootTabs: View { } } - private var bridgeStatus: StatusPill.BridgeState { - if self.appModel.bridgeServerName != nil { return .connected } + private var gatewayStatus: StatusPill.GatewayState { + if self.appModel.gatewayServerName != nil { return .connected } - let text = self.appModel.bridgeStatusText.trimmingCharacters(in: .whitespacesAndNewlines) + let text = self.appModel.gatewayStatusText.trimmingCharacters(in: .whitespacesAndNewlines) if text.localizedCaseInsensitiveContains("connecting") || text.localizedCaseInsensitiveContains("reconnecting") { @@ -90,15 +90,15 @@ struct RootTabs: View { tint: .orange) } - let bridgeStatus = self.appModel.bridgeStatusText.trimmingCharacters(in: .whitespacesAndNewlines) - let bridgeLower = bridgeStatus.lowercased() - if bridgeLower.contains("repair") { + let gatewayStatus = self.appModel.gatewayStatusText.trimmingCharacters(in: .whitespacesAndNewlines) + let gatewayLower = gatewayStatus.lowercased() + if gatewayLower.contains("repair") { return StatusPill.Activity(title: "Repairing…", systemImage: "wrench.and.screwdriver", tint: .orange) } - if bridgeLower.contains("approval") || bridgeLower.contains("pairing") { + if gatewayLower.contains("approval") || gatewayLower.contains("pairing") { return StatusPill.Activity(title: "Approval pending", systemImage: "person.crop.circle.badge.clock") } - // Avoid duplicating the primary bridge status ("Connecting…") in the activity slot. + // Avoid duplicating the primary gateway status ("Connecting…") in the activity slot. if self.appModel.screenRecordActive { return StatusPill.Activity(title: "Recording screen…", systemImage: "record.circle.fill", tint: .red) diff --git a/apps/ios/Sources/Screen/ScreenRecordService.swift b/apps/ios/Sources/Screen/ScreenRecordService.swift index e31762e51..6bccae5f9 100644 --- a/apps/ios/Sources/Screen/ScreenRecordService.swift +++ b/apps/ios/Sources/Screen/ScreenRecordService.swift @@ -91,7 +91,7 @@ final class ScreenRecordService: @unchecked Sendable { let includeAudio = includeAudio ?? true let outURL = self.makeOutputURL(outPath: outPath) - try? FileManager.default.removeItem(at: outURL) + try? FileManager().removeItem(at: outURL) return RecordConfig( durationMs: durationMs, @@ -104,7 +104,7 @@ final class ScreenRecordService: @unchecked Sendable { if let outPath, !outPath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return URL(fileURLWithPath: outPath) } - return FileManager.default.temporaryDirectory + return FileManager().temporaryDirectory .appendingPathComponent("clawdbot-screen-record-\(UUID().uuidString).mp4") } diff --git a/apps/ios/Sources/Settings/SettingsTab.swift b/apps/ios/Sources/Settings/SettingsTab.swift index bc474ff00..431761617 100644 --- a/apps/ios/Sources/Settings/SettingsTab.swift +++ b/apps/ios/Sources/Settings/SettingsTab.swift @@ -15,7 +15,7 @@ extension ConnectStatusStore: @unchecked Sendable {} struct SettingsTab: View { @Environment(NodeAppModel.self) private var appModel: NodeAppModel @Environment(VoiceWakeManager.self) private var voiceWake: VoiceWakeManager - @Environment(BridgeConnectionController.self) private var bridgeController: BridgeConnectionController + @Environment(GatewayConnectionController.self) private var gatewayController: GatewayConnectionController @Environment(\.dismiss) private var dismiss @AppStorage("node.displayName") private var displayName: String = "iOS Node" @AppStorage("node.instanceId") private var instanceId: String = UUID().uuidString @@ -26,17 +26,20 @@ struct SettingsTab: View { @AppStorage("location.enabledMode") private var locationEnabledModeRaw: String = ClawdbotLocationMode.off.rawValue @AppStorage("location.preciseEnabled") private var locationPreciseEnabled: Bool = true @AppStorage("screen.preventSleep") private var preventSleep: Bool = true - @AppStorage("bridge.preferredStableID") private var preferredBridgeStableID: String = "" - @AppStorage("bridge.lastDiscoveredStableID") private var lastDiscoveredBridgeStableID: String = "" - @AppStorage("bridge.manual.enabled") private var manualBridgeEnabled: Bool = false - @AppStorage("bridge.manual.host") private var manualBridgeHost: String = "" - @AppStorage("bridge.manual.port") private var manualBridgePort: Int = 18790 - @AppStorage("bridge.discovery.debugLogs") private var discoveryDebugLogsEnabled: Bool = false + @AppStorage("gateway.preferredStableID") private var preferredGatewayStableID: String = "" + @AppStorage("gateway.lastDiscoveredStableID") private var lastDiscoveredGatewayStableID: String = "" + @AppStorage("gateway.manual.enabled") private var manualGatewayEnabled: Bool = false + @AppStorage("gateway.manual.host") private var manualGatewayHost: String = "" + @AppStorage("gateway.manual.port") private var manualGatewayPort: Int = 18789 + @AppStorage("gateway.manual.tls") private var manualGatewayTLS: Bool = true + @AppStorage("gateway.discovery.debugLogs") private var discoveryDebugLogsEnabled: Bool = false @AppStorage("canvas.debugStatusEnabled") private var canvasDebugStatusEnabled: Bool = false @State private var connectStatus = ConnectStatusStore() - @State private var connectingBridgeID: String? + @State private var connectingGatewayID: String? @State private var localIPAddress: String? @State private var lastLocationModeRaw: String = ClawdbotLocationMode.off.rawValue + @State private var gatewayToken: String = "" + @State private var gatewayPassword: String = "" var body: some View { NavigationStack { @@ -61,12 +64,12 @@ struct SettingsTab: View { LabeledContent("Model", value: self.modelIdentifier()) } - Section("Bridge") { - LabeledContent("Discovery", value: self.bridgeController.discoveryStatusText) - LabeledContent("Status", value: self.appModel.bridgeStatusText) - if let serverName = self.appModel.bridgeServerName { + Section("Gateway") { + LabeledContent("Discovery", value: self.gatewayController.discoveryStatusText) + LabeledContent("Status", value: self.appModel.gatewayStatusText) + if let serverName = self.appModel.gatewayServerName { LabeledContent("Server", value: serverName) - if let addr = self.appModel.bridgeRemoteAddress { + if let addr = self.appModel.gatewayRemoteAddress { let parts = Self.parseHostPort(from: addr) let urlString = Self.httpURLString(host: parts?.host, port: parts?.port, fallback: addr) LabeledContent("Address") { @@ -96,12 +99,12 @@ struct SettingsTab: View { } Button("Disconnect", role: .destructive) { - self.appModel.disconnectBridge() + self.appModel.disconnectGateway() } - self.bridgeList(showing: .availableOnly) + self.gatewayList(showing: .availableOnly) } else { - self.bridgeList(showing: .all) + self.gatewayList(showing: .all) } if let text = self.connectStatus.text { @@ -111,19 +114,21 @@ struct SettingsTab: View { } DisclosureGroup("Advanced") { - Toggle("Use Manual Bridge", isOn: self.$manualBridgeEnabled) + Toggle("Use Manual Gateway", isOn: self.$manualGatewayEnabled) - TextField("Host", text: self.$manualBridgeHost) + TextField("Host", text: self.$manualGatewayHost) .textInputAutocapitalization(.never) .autocorrectionDisabled() - TextField("Port", value: self.$manualBridgePort, format: .number) + TextField("Port", value: self.$manualGatewayPort, format: .number) .keyboardType(.numberPad) + Toggle("Use TLS", isOn: self.$manualGatewayTLS) + Button { Task { await self.connectManual() } } label: { - if self.connectingBridgeID == "manual" { + if self.connectingGatewayID == "manual" { HStack(spacing: 8) { ProgressView() .progressViewStyle(.circular) @@ -133,26 +138,32 @@ struct SettingsTab: View { Text("Connect (Manual)") } } - .disabled(self.connectingBridgeID != nil || self.manualBridgeHost + .disabled(self.connectingGatewayID != nil || self.manualGatewayHost .trimmingCharacters(in: .whitespacesAndNewlines) - .isEmpty || self.manualBridgePort <= 0 || self.manualBridgePort > 65535) + .isEmpty || self.manualGatewayPort <= 0 || self.manualGatewayPort > 65535) Text( "Use this when mDNS/Bonjour discovery is blocked. " - + "The bridge runs on the gateway (default port 18790).") + + "The gateway WebSocket listens on port 18789 by default.") .font(.footnote) .foregroundStyle(.secondary) Toggle("Discovery Debug Logs", isOn: self.$discoveryDebugLogsEnabled) .onChange(of: self.discoveryDebugLogsEnabled) { _, newValue in - self.bridgeController.setDiscoveryDebugLoggingEnabled(newValue) + self.gatewayController.setDiscoveryDebugLoggingEnabled(newValue) } NavigationLink("Discovery Logs") { - BridgeDiscoveryDebugLogView() + GatewayDiscoveryDebugLogView() } Toggle("Debug Canvas Status", isOn: self.$canvasDebugStatusEnabled) + + TextField("Gateway Token", text: self.$gatewayToken) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + + SecureField("Gateway Password", text: self.$gatewayPassword) } } @@ -179,7 +190,7 @@ struct SettingsTab: View { Section("Camera") { Toggle("Allow Camera", isOn: self.$cameraEnabled) - Text("Allows the bridge to request photos or short video clips (foreground only).") + Text("Allows the gateway to request photos or short video clips (foreground only).") .font(.footnote) .foregroundStyle(.secondary) } @@ -221,13 +232,30 @@ struct SettingsTab: View { .onAppear { self.localIPAddress = Self.primaryIPv4Address() self.lastLocationModeRaw = self.locationEnabledModeRaw + let trimmedInstanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines) + if !trimmedInstanceId.isEmpty { + self.gatewayToken = GatewaySettingsStore.loadGatewayToken(instanceId: trimmedInstanceId) ?? "" + self.gatewayPassword = GatewaySettingsStore.loadGatewayPassword(instanceId: trimmedInstanceId) ?? "" + } } - .onChange(of: self.preferredBridgeStableID) { _, newValue in + .onChange(of: self.preferredGatewayStableID) { _, newValue in let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return } - BridgeSettingsStore.savePreferredBridgeStableID(trimmed) + GatewaySettingsStore.savePreferredGatewayStableID(trimmed) } - .onChange(of: self.appModel.bridgeServerName) { _, _ in + .onChange(of: self.gatewayToken) { _, newValue in + let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines) + let instanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines) + guard !instanceId.isEmpty else { return } + GatewaySettingsStore.saveGatewayToken(trimmed, instanceId: instanceId) + } + .onChange(of: self.gatewayPassword) { _, newValue in + let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines) + let instanceId = self.instanceId.trimmingCharacters(in: .whitespacesAndNewlines) + guard !instanceId.isEmpty else { return } + GatewaySettingsStore.saveGatewayPassword(trimmed, instanceId: instanceId) + } + .onChange(of: self.appModel.gatewayServerName) { _, _ in self.connectStatus.text = nil } .onChange(of: self.locationEnabledModeRaw) { _, newValue in @@ -248,14 +276,14 @@ struct SettingsTab: View { } @ViewBuilder - private func bridgeList(showing: BridgeListMode) -> some View { - if self.bridgeController.bridges.isEmpty { - Text("No bridges found yet.") + private func gatewayList(showing: GatewayListMode) -> some View { + if self.gatewayController.gateways.isEmpty { + Text("No gateways found yet.") .foregroundStyle(.secondary) } else { - let connectedID = self.appModel.connectedBridgeID - let rows = self.bridgeController.bridges.filter { bridge in - let isConnected = bridge.stableID == connectedID + let connectedID = self.appModel.connectedGatewayID + let rows = self.gatewayController.gateways.filter { gateway in + let isConnected = gateway.stableID == connectedID switch showing { case .all: return true @@ -265,14 +293,14 @@ struct SettingsTab: View { } if rows.isEmpty, showing == .availableOnly { - Text("No other bridges found.") + Text("No other gateways found.") .foregroundStyle(.secondary) } else { - ForEach(rows) { bridge in + ForEach(rows) { gateway in HStack { VStack(alignment: .leading, spacing: 2) { - Text(bridge.name) - let detailLines = self.bridgeDetailLines(bridge) + Text(gateway.name) + let detailLines = self.gatewayDetailLines(gateway) ForEach(detailLines, id: \.self) { line in Text(line) .font(.footnote) @@ -282,31 +310,27 @@ struct SettingsTab: View { Spacer() Button { - Task { await self.connect(bridge) } + Task { await self.connect(gateway) } } label: { - if self.connectingBridgeID == bridge.id { + if self.connectingGatewayID == gateway.id { ProgressView() .progressViewStyle(.circular) } else { Text("Connect") } } - .disabled(self.connectingBridgeID != nil) + .disabled(self.connectingGatewayID != nil) } } } } } - private enum BridgeListMode: Equatable { + private enum GatewayListMode: Equatable { case all case availableOnly } - private func keychainAccount() -> String { - "bridge-token.\(self.instanceId)" - } - private func platformString() -> String { let v = ProcessInfo.processInfo.operatingSystemVersion return "iOS \(v.majorVersion).\(v.minorVersion).\(v.patchVersion)" @@ -341,228 +365,37 @@ struct SettingsTab: View { return trimmed.isEmpty ? "unknown" : trimmed } - private func currentCaps() -> [String] { - var caps = [ClawdbotCapability.canvas.rawValue, ClawdbotCapability.screen.rawValue] + private func connect(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) async { + self.connectingGatewayID = gateway.id + self.manualGatewayEnabled = false + self.preferredGatewayStableID = gateway.stableID + GatewaySettingsStore.savePreferredGatewayStableID(gateway.stableID) + self.lastDiscoveredGatewayStableID = gateway.stableID + GatewaySettingsStore.saveLastDiscoveredGatewayStableID(gateway.stableID) + defer { self.connectingGatewayID = nil } - let cameraEnabled = - UserDefaults.standard.object(forKey: "camera.enabled") == nil - ? true - : UserDefaults.standard.bool(forKey: "camera.enabled") - if cameraEnabled { caps.append(ClawdbotCapability.camera.rawValue) } - - let voiceWakeEnabled = UserDefaults.standard.bool(forKey: VoiceWakePreferences.enabledKey) - if voiceWakeEnabled { caps.append(ClawdbotCapability.voiceWake.rawValue) } - - return caps - } - - private func currentCommands() -> [String] { - var commands: [String] = [ - ClawdbotCanvasCommand.present.rawValue, - ClawdbotCanvasCommand.hide.rawValue, - ClawdbotCanvasCommand.navigate.rawValue, - ClawdbotCanvasCommand.evalJS.rawValue, - ClawdbotCanvasCommand.snapshot.rawValue, - ClawdbotCanvasA2UICommand.push.rawValue, - ClawdbotCanvasA2UICommand.pushJSONL.rawValue, - ClawdbotCanvasA2UICommand.reset.rawValue, - ClawdbotScreenCommand.record.rawValue, - ] - - let caps = Set(self.currentCaps()) - if caps.contains(ClawdbotCapability.camera.rawValue) { - commands.append(ClawdbotCameraCommand.list.rawValue) - commands.append(ClawdbotCameraCommand.snap.rawValue) - commands.append(ClawdbotCameraCommand.clip.rawValue) - } - - return commands - } - - private func connect(_ bridge: BridgeDiscoveryModel.DiscoveredBridge) async { - self.connectingBridgeID = bridge.id - self.manualBridgeEnabled = false - self.preferredBridgeStableID = bridge.stableID - BridgeSettingsStore.savePreferredBridgeStableID(bridge.stableID) - self.lastDiscoveredBridgeStableID = bridge.stableID - BridgeSettingsStore.saveLastDiscoveredBridgeStableID(bridge.stableID) - defer { self.connectingBridgeID = nil } - - do { - let statusStore = self.connectStatus - let existing = KeychainStore.loadString( - service: "com.clawdbot.bridge", - account: self.keychainAccount()) - let existingToken = (existing?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false) ? - existing : - nil - - let hello = BridgeHello( - nodeId: self.instanceId, - displayName: self.displayName, - token: existingToken, - platform: self.platformString(), - version: self.appVersion(), - deviceFamily: self.deviceFamily(), - modelIdentifier: self.modelIdentifier(), - caps: self.currentCaps(), - commands: self.currentCommands()) - let tlsParams = self.resolveDiscoveredTLSParams(bridge: bridge) - let token = try await BridgeClient().pairAndHello( - endpoint: bridge.endpoint, - hello: hello, - tls: tlsParams, - onStatus: { status in - Task { @MainActor in - statusStore.text = status - } - }) - - if !token.isEmpty, token != existingToken { - _ = KeychainStore.saveString( - token, - service: "com.clawdbot.bridge", - account: self.keychainAccount()) - } - - self.appModel.connectToBridge( - endpoint: bridge.endpoint, - bridgeStableID: bridge.stableID, - tls: tlsParams, - hello: BridgeHello( - nodeId: self.instanceId, - displayName: self.displayName, - token: token, - platform: self.platformString(), - version: self.appVersion(), - deviceFamily: self.deviceFamily(), - modelIdentifier: self.modelIdentifier(), - caps: self.currentCaps(), - commands: self.currentCommands())) - - } catch { - self.connectStatus.text = "Failed: \(error.localizedDescription)" - } + await self.gatewayController.connect(gateway) } private func connectManual() async { - let host = self.manualBridgeHost.trimmingCharacters(in: .whitespacesAndNewlines) + let host = self.manualGatewayHost.trimmingCharacters(in: .whitespacesAndNewlines) guard !host.isEmpty else { self.connectStatus.text = "Failed: host required" return } - guard self.manualBridgePort > 0, self.manualBridgePort <= 65535 else { - self.connectStatus.text = "Failed: invalid port" - return - } - guard let port = NWEndpoint.Port(rawValue: UInt16(self.manualBridgePort)) else { + guard self.manualGatewayPort > 0, self.manualGatewayPort <= 65535 else { self.connectStatus.text = "Failed: invalid port" return } - self.connectingBridgeID = "manual" - self.manualBridgeEnabled = true - defer { self.connectingBridgeID = nil } + self.connectingGatewayID = "manual" + self.manualGatewayEnabled = true + defer { self.connectingGatewayID = nil } - let endpoint: NWEndpoint = .hostPort(host: NWEndpoint.Host(host), port: port) - let stableID = BridgeEndpointID.stableID(endpoint) - let tlsParams = self.resolveManualTLSParams(stableID: stableID) - - do { - let statusStore = self.connectStatus - let existing = KeychainStore.loadString( - service: "com.clawdbot.bridge", - account: self.keychainAccount()) - let existingToken = (existing?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false) ? - existing : - nil - - let hello = BridgeHello( - nodeId: self.instanceId, - displayName: self.displayName, - token: existingToken, - platform: self.platformString(), - version: self.appVersion(), - deviceFamily: self.deviceFamily(), - modelIdentifier: self.modelIdentifier(), - caps: self.currentCaps(), - commands: self.currentCommands()) - let token = try await BridgeClient().pairAndHello( - endpoint: endpoint, - hello: hello, - tls: tlsParams, - onStatus: { status in - Task { @MainActor in - statusStore.text = status - } - }) - - if !token.isEmpty, token != existingToken { - _ = KeychainStore.saveString( - token, - service: "com.clawdbot.bridge", - account: self.keychainAccount()) - } - - self.appModel.connectToBridge( - endpoint: endpoint, - bridgeStableID: stableID, - tls: tlsParams, - hello: BridgeHello( - nodeId: self.instanceId, - displayName: self.displayName, - token: token, - platform: self.platformString(), - version: self.appVersion(), - deviceFamily: self.deviceFamily(), - modelIdentifier: self.modelIdentifier(), - caps: self.currentCaps(), - commands: self.currentCommands())) - - } catch { - self.connectStatus.text = "Failed: \(error.localizedDescription)" - } - } - - private func resolveDiscoveredTLSParams( - bridge: BridgeDiscoveryModel.DiscoveredBridge) -> BridgeTLSParams? - { - let stableID = bridge.stableID - let stored = BridgeTLSStore.loadFingerprint(stableID: stableID) - - if bridge.tlsEnabled || bridge.tlsFingerprintSha256 != nil { - return BridgeTLSParams( - required: true, - expectedFingerprint: bridge.tlsFingerprintSha256 ?? stored, - allowTOFU: stored == nil, - storeKey: stableID) - } - - if let stored { - return BridgeTLSParams( - required: true, - expectedFingerprint: stored, - allowTOFU: false, - storeKey: stableID) - } - - return nil - } - - private func resolveManualTLSParams(stableID: String) -> BridgeTLSParams? { - if let stored = BridgeTLSStore.loadFingerprint(stableID: stableID) { - return BridgeTLSParams( - required: true, - expectedFingerprint: stored, - allowTOFU: false, - storeKey: stableID) - } - - return BridgeTLSParams( - required: false, - expectedFingerprint: nil, - allowTOFU: true, - storeKey: stableID) + await self.gatewayController.connectManual( + host: host, + port: self.manualGatewayPort, + useTLS: self.manualGatewayTLS) } private static func primaryIPv4Address() -> String? { @@ -611,23 +444,21 @@ struct SettingsTab: View { SettingsNetworkingHelpers.httpURLString(host: host, port: port, fallback: fallback) } - private func bridgeDetailLines(_ bridge: BridgeDiscoveryModel.DiscoveredBridge) -> [String] { + private func gatewayDetailLines(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) -> [String] { var lines: [String] = [] - if let lanHost = bridge.lanHost { lines.append("LAN: \(lanHost)") } - if let tailnet = bridge.tailnetDns { lines.append("Tailnet: \(tailnet)") } + if let lanHost = gateway.lanHost { lines.append("LAN: \(lanHost)") } + if let tailnet = gateway.tailnetDns { lines.append("Tailnet: \(tailnet)") } - let gatewayPort = bridge.gatewayPort - let bridgePort = bridge.bridgePort - let canvasPort = bridge.canvasPort - if gatewayPort != nil || bridgePort != nil || canvasPort != nil { + let gatewayPort = gateway.gatewayPort + let canvasPort = gateway.canvasPort + if gatewayPort != nil || canvasPort != nil { let gw = gatewayPort.map(String.init) ?? "—" - let br = bridgePort.map(String.init) ?? "—" let canvas = canvasPort.map(String.init) ?? "—" - lines.append("Ports: gw \(gw) · bridge \(br) · canvas \(canvas)") + lines.append("Ports: gateway \(gw) · canvas \(canvas)") } if lines.isEmpty { - lines.append(bridge.debugID) + lines.append(gateway.debugID) } return lines diff --git a/apps/ios/Sources/Settings/VoiceWakeWordsSettingsView.swift b/apps/ios/Sources/Settings/VoiceWakeWordsSettingsView.swift index 405444534..5aef87b0c 100644 --- a/apps/ios/Sources/Settings/VoiceWakeWordsSettingsView.swift +++ b/apps/ios/Sources/Settings/VoiceWakeWordsSettingsView.swift @@ -1,8 +1,10 @@ import SwiftUI +import Combine struct VoiceWakeWordsSettingsView: View { @Environment(NodeAppModel.self) private var appModel @State private var triggerWords: [String] = VoiceWakePreferences.loadTriggerWords() + @FocusState private var focusedTriggerIndex: Int? @State private var syncTask: Task? var body: some View { @@ -12,6 +14,10 @@ struct VoiceWakeWordsSettingsView: View { TextField("Wake word", text: self.binding(for: index)) .textInputAutocapitalization(.never) .autocorrectionDisabled() + .focused(self.$focusedTriggerIndex, equals: index) + .onSubmit { + self.commitTriggerWords() + } } .onDelete(perform: self.removeWords) @@ -39,17 +45,18 @@ struct VoiceWakeWordsSettingsView: View { .onAppear { if self.triggerWords.isEmpty { self.triggerWords = VoiceWakePreferences.defaultTriggerWords + self.commitTriggerWords() } } - .onChange(of: self.triggerWords) { _, newValue in - // Keep local voice wake responsive even if bridge isn't connected yet. - VoiceWakePreferences.saveTriggerWords(newValue) - - let snapshot = VoiceWakePreferences.sanitizeTriggerWords(newValue) - self.syncTask?.cancel() - self.syncTask = Task { [snapshot, weak appModel = self.appModel] in - try? await Task.sleep(nanoseconds: 650_000_000) - await appModel?.setGlobalWakeWords(snapshot) + .onChange(of: self.focusedTriggerIndex) { oldValue, newValue in + guard oldValue != nil, oldValue != newValue else { return } + self.commitTriggerWords() + } + .onReceive(NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification)) { _ in + guard self.focusedTriggerIndex == nil else { return } + let updated = VoiceWakePreferences.loadTriggerWords() + if updated != self.triggerWords { + self.triggerWords = updated } } } @@ -63,6 +70,7 @@ struct VoiceWakeWordsSettingsView: View { if self.triggerWords.isEmpty { self.triggerWords = VoiceWakePreferences.defaultTriggerWords } + self.commitTriggerWords() } private func binding(for index: Int) -> Binding { @@ -76,4 +84,15 @@ struct VoiceWakeWordsSettingsView: View { self.triggerWords[index] = newValue }) } + + private func commitTriggerWords() { + VoiceWakePreferences.saveTriggerWords(self.triggerWords) + + let snapshot = VoiceWakePreferences.sanitizeTriggerWords(self.triggerWords) + self.syncTask?.cancel() + self.syncTask = Task { [snapshot, weak appModel = self.appModel] in + try? await Task.sleep(nanoseconds: 650_000_000) + await appModel?.setGlobalWakeWords(snapshot) + } + } } diff --git a/apps/ios/Sources/Status/StatusPill.swift b/apps/ios/Sources/Status/StatusPill.swift index 1e30ad16d..cd81c011b 100644 --- a/apps/ios/Sources/Status/StatusPill.swift +++ b/apps/ios/Sources/Status/StatusPill.swift @@ -3,7 +3,7 @@ import SwiftUI struct StatusPill: View { @Environment(\.scenePhase) private var scenePhase - enum BridgeState: Equatable { + enum GatewayState: Equatable { case connected case connecting case error @@ -34,7 +34,7 @@ struct StatusPill: View { var tint: Color? } - var bridge: BridgeState + var gateway: GatewayState var voiceWakeEnabled: Bool var activity: Activity? var brighten: Bool = false @@ -47,12 +47,12 @@ struct StatusPill: View { HStack(spacing: 10) { HStack(spacing: 8) { Circle() - .fill(self.bridge.color) + .fill(self.gateway.color) .frame(width: 9, height: 9) - .scaleEffect(self.bridge == .connecting ? (self.pulse ? 1.15 : 0.85) : 1.0) - .opacity(self.bridge == .connecting ? (self.pulse ? 1.0 : 0.6) : 1.0) + .scaleEffect(self.gateway == .connecting ? (self.pulse ? 1.15 : 0.85) : 1.0) + .opacity(self.gateway == .connecting ? (self.pulse ? 1.0 : 0.6) : 1.0) - Text(self.bridge.title) + Text(self.gateway.title) .font(.system(size: 13, weight: .semibold)) .foregroundStyle(.primary) } @@ -95,26 +95,26 @@ struct StatusPill: View { .buttonStyle(.plain) .accessibilityLabel("Status") .accessibilityValue(self.accessibilityValue) - .onAppear { self.updatePulse(for: self.bridge, scenePhase: self.scenePhase) } + .onAppear { self.updatePulse(for: self.gateway, scenePhase: self.scenePhase) } .onDisappear { self.pulse = false } - .onChange(of: self.bridge) { _, newValue in + .onChange(of: self.gateway) { _, newValue in self.updatePulse(for: newValue, scenePhase: self.scenePhase) } .onChange(of: self.scenePhase) { _, newValue in - self.updatePulse(for: self.bridge, scenePhase: newValue) + self.updatePulse(for: self.gateway, scenePhase: newValue) } .animation(.easeInOut(duration: 0.18), value: self.activity?.title) } private var accessibilityValue: String { if let activity { - return "\(self.bridge.title), \(activity.title)" + return "\(self.gateway.title), \(activity.title)" } - return "\(self.bridge.title), Voice Wake \(self.voiceWakeEnabled ? "enabled" : "disabled")" + return "\(self.gateway.title), Voice Wake \(self.voiceWakeEnabled ? "enabled" : "disabled")" } - private func updatePulse(for bridge: BridgeState, scenePhase: ScenePhase) { - guard bridge == .connecting, scenePhase == .active else { + private func updatePulse(for gateway: GatewayState, scenePhase: ScenePhase) { + guard gateway == .connecting, scenePhase == .active else { withAnimation(.easeOut(duration: 0.2)) { self.pulse = false } return } diff --git a/apps/ios/Sources/Voice/TalkModeManager.swift b/apps/ios/Sources/Voice/TalkModeManager.swift index 9c0e1303d..16ae245eb 100644 --- a/apps/ios/Sources/Voice/TalkModeManager.swift +++ b/apps/ios/Sources/Voice/TalkModeManager.swift @@ -1,5 +1,6 @@ import AVFAudio import ClawdbotKit +import ClawdbotProtocol import Foundation import Observation import OSLog @@ -42,15 +43,15 @@ final class TalkModeManager: NSObject { var pcmPlayer: PCMStreamingAudioPlaying = PCMStreamingAudioPlayer.shared var mp3Player: StreamingAudioPlaying = StreamingAudioPlayer.shared - private var bridge: BridgeSession? + private var gateway: GatewayNodeSession? private let silenceWindow: TimeInterval = 0.7 private var chatSubscribedSessionKeys = Set() private let logger = Logger(subsystem: "com.clawdbot", category: "TalkMode") - func attachBridge(_ bridge: BridgeSession) { - self.bridge = bridge + func attachGateway(_ gateway: GatewayNodeSession) { + self.gateway = gateway } func updateMainSessionKey(_ sessionKey: String?) { @@ -131,6 +132,12 @@ final class TalkModeManager: NSObject { } private func startRecognition() throws { + #if targetEnvironment(simulator) + throw NSError(domain: "TalkMode", code: 2, userInfo: [ + NSLocalizedDescriptionKey: "Talk mode is not supported on the iOS simulator", + ]) + #endif + self.stopRecognition() self.speechRecognizer = SFSpeechRecognizer() guard let recognizer = self.speechRecognizer else { @@ -145,6 +152,11 @@ final class TalkModeManager: NSObject { let input = self.audioEngine.inputNode let format = input.outputFormat(forBus: 0) + guard format.sampleRate > 0, format.channelCount > 0 else { + throw NSError(domain: "TalkMode", code: 3, userInfo: [ + NSLocalizedDescriptionKey: "Invalid audio input format", + ]) + } input.removeTap(onBus: 0) let tapBlock = Self.makeAudioTapAppendCallback(request: request) input.installTap(onBus: 0, bufferSize: 2048, format: format, block: tapBlock) @@ -232,9 +244,9 @@ final class TalkModeManager: NSObject { await self.reloadConfig() let prompt = self.buildPrompt(transcript: transcript) - guard let bridge else { - self.statusText = "Bridge not connected" - self.logger.warning("finalize: bridge not connected") + guard let gateway else { + self.statusText = "Gateway not connected" + self.logger.warning("finalize: gateway not connected") await self.start() return } @@ -245,9 +257,9 @@ final class TalkModeManager: NSObject { await self.subscribeChatIfNeeded(sessionKey: sessionKey) self.logger.info( "chat.send start sessionKey=\(sessionKey, privacy: .public) chars=\(prompt.count, privacy: .public)") - let runId = try await self.sendChat(prompt, bridge: bridge) + let runId = try await self.sendChat(prompt, gateway: gateway) self.logger.info("chat.send ok runId=\(runId, privacy: .public)") - let completion = await self.waitForChatCompletion(runId: runId, bridge: bridge, timeoutSeconds: 120) + let completion = await self.waitForChatCompletion(runId: runId, gateway: gateway, timeoutSeconds: 120) if completion == .timeout { self.logger.warning( "chat completion timeout runId=\(runId, privacy: .public); attempting history fallback") @@ -264,7 +276,7 @@ final class TalkModeManager: NSObject { } guard let assistantText = try await self.waitForAssistantText( - bridge: bridge, + gateway: gateway, since: startedAt, timeoutSeconds: completion == .final ? 12 : 25) else { @@ -286,31 +298,22 @@ final class TalkModeManager: NSObject { private func subscribeChatIfNeeded(sessionKey: String) async { let key = sessionKey.trimmingCharacters(in: .whitespacesAndNewlines) guard !key.isEmpty else { return } - guard let bridge else { return } + guard let gateway else { return } guard !self.chatSubscribedSessionKeys.contains(key) else { return } - do { - let payload = "{\"sessionKey\":\"\(key)\"}" - try await bridge.sendEvent(event: "chat.subscribe", payloadJSON: payload) - self.chatSubscribedSessionKeys.insert(key) - self.logger.info("chat.subscribe ok sessionKey=\(key, privacy: .public)") - } catch { - let err = error.localizedDescription - self.logger.warning("chat.subscribe failed key=\(key, privacy: .public) err=\(err, privacy: .public)") - } + let payload = "{\"sessionKey\":\"\(key)\"}" + await gateway.sendEvent(event: "chat.subscribe", payloadJSON: payload) + self.chatSubscribedSessionKeys.insert(key) + self.logger.info("chat.subscribe ok sessionKey=\(key, privacy: .public)") } private func unsubscribeAllChats() async { - guard let bridge else { return } + guard let gateway else { return } let keys = self.chatSubscribedSessionKeys self.chatSubscribedSessionKeys.removeAll() for key in keys { - do { - let payload = "{\"sessionKey\":\"\(key)\"}" - try await bridge.sendEvent(event: "chat.unsubscribe", payloadJSON: payload) - } catch { - // ignore - } + let payload = "{\"sessionKey\":\"\(key)\"}" + await gateway.sendEvent(event: "chat.unsubscribe", payloadJSON: payload) } } @@ -336,7 +339,7 @@ final class TalkModeManager: NSObject { } } - private func sendChat(_ message: String, bridge: BridgeSession) async throws -> String { + private func sendChat(_ message: String, gateway: GatewayNodeSession) async throws -> String { struct SendResponse: Decodable { let runId: String } let payload: [String: Any] = [ "sessionKey": self.mainSessionKey, @@ -352,26 +355,27 @@ final class TalkModeManager: NSObject { code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to encode chat payload"]) } - let res = try await bridge.request(method: "chat.send", paramsJSON: json, timeoutSeconds: 30) + let res = try await gateway.request(method: "chat.send", paramsJSON: json, timeoutSeconds: 30) let decoded = try JSONDecoder().decode(SendResponse.self, from: res) return decoded.runId } private func waitForChatCompletion( runId: String, - bridge: BridgeSession, + gateway: GatewayNodeSession, timeoutSeconds: Int = 120) async -> ChatCompletionState { - let stream = await bridge.subscribeServerEvents(bufferingNewest: 200) + let stream = await gateway.subscribeServerEvents(bufferingNewest: 200) return await withTaskGroup(of: ChatCompletionState.self) { group in group.addTask { [runId] in for await evt in stream { if Task.isCancelled { return .timeout } - guard evt.event == "chat", let payload = evt.payloadJSON else { continue } - guard let data = payload.data(using: .utf8) else { continue } - guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { continue } - if (json["runId"] as? String) != runId { continue } - if let state = json["state"] as? String { + guard evt.event == "chat", let payload = evt.payload else { continue } + guard let chatEvent = try? GatewayPayloadDecoding.decode(payload, as: ChatEvent.self) else { + continue + } + guard chatEvent.runid == runId else { continue } + if let state = chatEvent.state.value as? String { switch state { case "final": return .final case "aborted": return .aborted @@ -393,13 +397,13 @@ final class TalkModeManager: NSObject { } private func waitForAssistantText( - bridge: BridgeSession, + gateway: GatewayNodeSession, since: Double, timeoutSeconds: Int) async throws -> String? { let deadline = Date().addingTimeInterval(TimeInterval(timeoutSeconds)) while Date() < deadline { - if let text = try await self.fetchLatestAssistantText(bridge: bridge, since: since) { + if let text = try await self.fetchLatestAssistantText(gateway: gateway, since: since) { return text } try? await Task.sleep(nanoseconds: 300_000_000) @@ -407,8 +411,8 @@ final class TalkModeManager: NSObject { return nil } - private func fetchLatestAssistantText(bridge: BridgeSession, since: Double? = nil) async throws -> String? { - let res = try await bridge.request( + private func fetchLatestAssistantText(gateway: GatewayNodeSession, since: Double? = nil) async throws -> String? { + let res = try await gateway.request( method: "chat.history", paramsJSON: "{\"sessionKey\":\"\(self.mainSessionKey)\"}", timeoutSeconds: 15) @@ -649,9 +653,9 @@ final class TalkModeManager: NSObject { } private func reloadConfig() async { - guard let bridge else { return } + guard let gateway else { return } do { - let res = try await bridge.request(method: "config.get", paramsJSON: "{}", timeoutSeconds: 8) + let res = try await gateway.request(method: "config.get", paramsJSON: "{}", timeoutSeconds: 8) guard let json = try JSONSerialization.jsonObject(with: res) as? [String: Any] else { return } guard let config = json["config"] as? [String: Any] else { return } let talk = config["talk"] as? [String: Any] diff --git a/apps/ios/Sources/Voice/VoiceWakePreferences.swift b/apps/ios/Sources/Voice/VoiceWakePreferences.swift index 96f46518e..4c75c22a6 100644 --- a/apps/ios/Sources/Voice/VoiceWakePreferences.swift +++ b/apps/ios/Sources/Voice/VoiceWakePreferences.swift @@ -6,6 +6,8 @@ enum VoiceWakePreferences { // Keep defaults aligned with the mac app. static let defaultTriggerWords: [String] = ["clawd", "claude"] + static let maxWords = 32 + static let maxWordLength = 64 static func decodeGatewayTriggers(from payloadJSON: String) -> [String]? { guard let data = payloadJSON.data(using: .utf8) else { return nil } @@ -30,6 +32,8 @@ enum VoiceWakePreferences { let cleaned = words .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { !$0.isEmpty } + .prefix(Self.maxWords) + .map { String($0.prefix(Self.maxWordLength)) } return cleaned.isEmpty ? Self.defaultTriggerWords : cleaned } diff --git a/apps/ios/SwiftSources.input.xcfilelist b/apps/ios/SwiftSources.input.xcfilelist index 0598b0e4e..70d0f39d6 100644 --- a/apps/ios/SwiftSources.input.xcfilelist +++ b/apps/ios/SwiftSources.input.xcfilelist @@ -1,15 +1,13 @@ -Sources/Bridge/BridgeClient.swift -Sources/Bridge/BridgeConnectionController.swift -Sources/Bridge/BridgeDiscoveryDebugLogView.swift -Sources/Bridge/BridgeDiscoveryModel.swift -Sources/Bridge/BridgeEndpointID.swift -Sources/Bridge/BridgeSession.swift -Sources/Bridge/BridgeSettingsStore.swift -Sources/Bridge/KeychainStore.swift +Sources/Gateway/GatewayConnectionController.swift +Sources/Gateway/GatewayDiscoveryDebugLogView.swift +Sources/Gateway/GatewayDiscoveryModel.swift +Sources/Gateway/GatewaySettingsStore.swift +Sources/Gateway/KeychainStore.swift Sources/Camera/CameraController.swift Sources/Chat/ChatSheet.swift -Sources/Chat/IOSBridgeChatTransport.swift +Sources/Chat/IOSGatewayChatTransport.swift Sources/ClawdbotApp.swift +Sources/Location/LocationService.swift Sources/Model/NodeAppModel.swift Sources/RootCanvas.swift Sources/RootTabs.swift @@ -17,6 +15,7 @@ Sources/Screen/ScreenController.swift Sources/Screen/ScreenRecordService.swift Sources/Screen/ScreenTab.swift Sources/Screen/ScreenWebView.swift +Sources/SessionKey.swift Sources/Settings/SettingsNetworkingHelpers.swift Sources/Settings/SettingsTab.swift Sources/Settings/VoiceWakeWordsSettingsView.swift diff --git a/apps/ios/Tests/BridgeClientTests.swift b/apps/ios/Tests/BridgeClientTests.swift deleted file mode 100644 index 21869b1cf..000000000 --- a/apps/ios/Tests/BridgeClientTests.swift +++ /dev/null @@ -1,196 +0,0 @@ -import ClawdbotKit -import Foundation -import Network -import Testing -@testable import Clawdbot - -@Suite struct BridgeClientTests { - private final class LineServer: @unchecked Sendable { - private let queue = DispatchQueue(label: "com.clawdbot.tests.bridge-client-server") - private let listener: NWListener - private var connection: NWConnection? - private var buffer = Data() - - init() throws { - self.listener = try NWListener(using: .tcp, on: .any) - } - - func start() async throws -> NWEndpoint.Port { - try await withCheckedThrowingContinuation(isolation: nil) { cont in - self.listener.stateUpdateHandler = { state in - switch state { - case .ready: - if let port = self.listener.port { - cont.resume(returning: port) - } else { - cont.resume( - throwing: NSError(domain: "LineServer", code: 1, userInfo: [ - NSLocalizedDescriptionKey: "listener missing port", - ])) - } - case let .failed(err): - cont.resume(throwing: err) - default: - break - } - } - - self.listener.newConnectionHandler = { [weak self] conn in - guard let self else { return } - self.connection = conn - conn.start(queue: self.queue) - } - - self.listener.start(queue: self.queue) - } - } - - func stop() { - self.connection?.cancel() - self.connection = nil - self.listener.cancel() - } - - func waitForConnection(timeoutMs: Int = 2000) async throws -> NWConnection { - let deadline = Date().addingTimeInterval(Double(timeoutMs) / 1000.0) - while Date() < deadline { - if let connection = self.connection { return connection } - try await Task.sleep(nanoseconds: 10_000_000) - } - throw NSError(domain: "LineServer", code: 2, userInfo: [ - NSLocalizedDescriptionKey: "timed out waiting for connection", - ]) - } - - func receiveLine(timeoutMs: Int = 2000) async throws -> Data? { - let connection = try await self.waitForConnection(timeoutMs: timeoutMs) - let deadline = Date().addingTimeInterval(Double(timeoutMs) / 1000.0) - - while Date() < deadline { - if let idx = self.buffer.firstIndex(of: 0x0A) { - let line = self.buffer.prefix(upTo: idx) - self.buffer.removeSubrange(...idx) - return Data(line) - } - - let chunk = try await withCheckedThrowingContinuation(isolation: nil) { (cont: CheckedContinuation< - Data, - Error, - >) in - connection - .receive(minimumIncompleteLength: 1, maximumLength: 64 * 1024) { data, _, isComplete, error in - if let error { - cont.resume(throwing: error) - return - } - if isComplete { - cont.resume(returning: Data()) - return - } - cont.resume(returning: data ?? Data()) - } - } - - if chunk.isEmpty { return nil } - self.buffer.append(chunk) - } - - throw NSError(domain: "LineServer", code: 3, userInfo: [ - NSLocalizedDescriptionKey: "timed out waiting for line", - ]) - } - - func sendLine(_ line: String) async throws { - let connection = try await self.waitForConnection() - var data = Data(line.utf8) - data.append(0x0A) - try await withCheckedThrowingContinuation(isolation: nil) { (cont: CheckedContinuation) in - connection.send(content: data, completion: .contentProcessed { err in - if let err { cont.resume(throwing: err) } else { cont.resume(returning: ()) } - }) - } - } - } - - @Test func helloOkReturnsExistingToken() async throws { - let server = try LineServer() - let port = try await server.start() - defer { server.stop() } - - let serverTask = Task { - let line = try await server.receiveLine() - #expect(line != nil) - _ = try JSONDecoder().decode(BridgeHello.self, from: line ?? Data()) - try await server.sendLine(#"{"type":"hello-ok","serverName":"Test Gateway"}"#) - } - defer { serverTask.cancel() } - - let client = BridgeClient() - let token = try await client.pairAndHello( - endpoint: .hostPort(host: NWEndpoint.Host("127.0.0.1"), port: port), - hello: BridgeHello( - nodeId: "ios-node", - displayName: "iOS", - token: "existing-token", - platform: "ios", - version: "1"), - onStatus: nil) - - #expect(token == "existing-token") - _ = try await serverTask.value - } - - @Test func notPairedTriggersPairRequestAndReturnsToken() async throws { - let server = try LineServer() - let port = try await server.start() - defer { server.stop() } - - let serverTask = Task { - let helloLine = try await server.receiveLine() - #expect(helloLine != nil) - _ = try JSONDecoder().decode(BridgeHello.self, from: helloLine ?? Data()) - try await server.sendLine(#"{"type":"error","code":"NOT_PAIRED","message":"not paired"}"#) - - let pairLine = try await server.receiveLine() - #expect(pairLine != nil) - _ = try JSONDecoder().decode(BridgePairRequest.self, from: pairLine ?? Data()) - try await server.sendLine(#"{"type":"pair-ok","token":"paired-token"}"#) - } - defer { serverTask.cancel() } - - let client = BridgeClient() - let token = try await client.pairAndHello( - endpoint: .hostPort(host: NWEndpoint.Host("127.0.0.1"), port: port), - hello: BridgeHello(nodeId: "ios-node", displayName: "iOS", token: nil, platform: "ios", version: "1"), - onStatus: nil) - - #expect(token == "paired-token") - _ = try await serverTask.value - } - - @Test func unexpectedErrorIsSurfaced() async { - do { - let server = try LineServer() - let port = try await server.start() - defer { server.stop() } - - let serverTask = Task { - let helloLine = try await server.receiveLine() - #expect(helloLine != nil) - _ = try JSONDecoder().decode(BridgeHello.self, from: helloLine ?? Data()) - try await server.sendLine(#"{"type":"error","code":"NOPE","message":"nope"}"#) - } - defer { serverTask.cancel() } - - let client = BridgeClient() - _ = try await client.pairAndHello( - endpoint: .hostPort(host: NWEndpoint.Host("127.0.0.1"), port: port), - hello: BridgeHello(nodeId: "ios-node", displayName: "iOS", token: nil, platform: "ios", version: "1"), - onStatus: nil) - - Issue.record("Expected pairAndHello to throw for unexpected error code") - } catch { - #expect(error.localizedDescription.contains("NOPE")) - } - } -} diff --git a/apps/ios/Tests/BridgeConnectionControllerTests.swift b/apps/ios/Tests/BridgeConnectionControllerTests.swift deleted file mode 100644 index df6198623..000000000 --- a/apps/ios/Tests/BridgeConnectionControllerTests.swift +++ /dev/null @@ -1,347 +0,0 @@ -import ClawdbotKit -import Foundation -import Network -import Testing -import UIKit -@testable import Clawdbot - -private struct KeychainEntry: Hashable { - let service: String - let account: String -} - -private let bridgeService = "com.clawdbot.bridge" -private let nodeService = "com.clawdbot.node" -private let instanceIdEntry = KeychainEntry(service: nodeService, account: "instanceId") -private let preferredBridgeEntry = KeychainEntry(service: bridgeService, account: "preferredStableID") -private let lastBridgeEntry = KeychainEntry(service: bridgeService, account: "lastDiscoveredStableID") - -private actor MockBridgePairingClient: BridgePairingClient { - private(set) var lastToken: String? - private let resultToken: String - - init(resultToken: String) { - self.resultToken = resultToken - } - - func pairAndHello( - endpoint: NWEndpoint, - hello: BridgeHello, - tls: BridgeTLSParams?, - onStatus: (@Sendable (String) -> Void)?) async throws -> String - { - self.lastToken = hello.token - onStatus?("Testing…") - return self.resultToken - } -} - -private func withUserDefaults(_ updates: [String: Any?], _ body: () throws -> T) rethrows -> T { - let defaults = UserDefaults.standard - var snapshot: [String: Any?] = [:] - for key in updates.keys { - snapshot[key] = defaults.object(forKey: key) - } - for (key, value) in updates { - if let value { - defaults.set(value, forKey: key) - } else { - defaults.removeObject(forKey: key) - } - } - defer { - for (key, value) in snapshot { - if let value { - defaults.set(value, forKey: key) - } else { - defaults.removeObject(forKey: key) - } - } - } - return try body() -} - -@MainActor -private func withUserDefaults( - _ updates: [String: Any?], - _ body: () async throws -> T) async rethrows -> T -{ - let defaults = UserDefaults.standard - var snapshot: [String: Any?] = [:] - for key in updates.keys { - snapshot[key] = defaults.object(forKey: key) - } - for (key, value) in updates { - if let value { - defaults.set(value, forKey: key) - } else { - defaults.removeObject(forKey: key) - } - } - defer { - for (key, value) in snapshot { - if let value { - defaults.set(value, forKey: key) - } else { - defaults.removeObject(forKey: key) - } - } - } - return try await body() -} - -private func withKeychainValues(_ updates: [KeychainEntry: String?], _ body: () throws -> T) rethrows -> T { - var snapshot: [KeychainEntry: String?] = [:] - for entry in updates.keys { - snapshot[entry] = KeychainStore.loadString(service: entry.service, account: entry.account) - } - for (entry, value) in updates { - if let value { - _ = KeychainStore.saveString(value, service: entry.service, account: entry.account) - } else { - _ = KeychainStore.delete(service: entry.service, account: entry.account) - } - } - defer { - for (entry, value) in snapshot { - if let value { - _ = KeychainStore.saveString(value, service: entry.service, account: entry.account) - } else { - _ = KeychainStore.delete(service: entry.service, account: entry.account) - } - } - } - return try body() -} - -@MainActor -private func withKeychainValues( - _ updates: [KeychainEntry: String?], - _ body: () async throws -> T) async rethrows -> T -{ - var snapshot: [KeychainEntry: String?] = [:] - for entry in updates.keys { - snapshot[entry] = KeychainStore.loadString(service: entry.service, account: entry.account) - } - for (entry, value) in updates { - if let value { - _ = KeychainStore.saveString(value, service: entry.service, account: entry.account) - } else { - _ = KeychainStore.delete(service: entry.service, account: entry.account) - } - } - defer { - for (entry, value) in snapshot { - if let value { - _ = KeychainStore.saveString(value, service: entry.service, account: entry.account) - } else { - _ = KeychainStore.delete(service: entry.service, account: entry.account) - } - } - } - return try await body() -} - -@Suite(.serialized) struct BridgeConnectionControllerTests { - @Test @MainActor func resolvedDisplayNameSetsDefaultWhenMissing() { - let defaults = UserDefaults.standard - let displayKey = "node.displayName" - - withKeychainValues([instanceIdEntry: nil, preferredBridgeEntry: nil, lastBridgeEntry: nil]) { - withUserDefaults([displayKey: nil, "node.instanceId": "ios-test"]) { - let appModel = NodeAppModel() - let controller = BridgeConnectionController(appModel: appModel, startDiscovery: false) - - let resolved = controller._test_resolvedDisplayName(defaults: defaults) - #expect(!resolved.isEmpty) - #expect(defaults.string(forKey: displayKey) == resolved) - } - } - } - - @Test @MainActor func resolvedDisplayNamePreservesCustomValue() { - let defaults = UserDefaults.standard - let displayKey = "node.displayName" - - withKeychainValues([instanceIdEntry: nil, preferredBridgeEntry: nil, lastBridgeEntry: nil]) { - withUserDefaults([displayKey: "My iOS Node", "node.instanceId": "ios-test"]) { - let appModel = NodeAppModel() - let controller = BridgeConnectionController(appModel: appModel, startDiscovery: false) - - let resolved = controller._test_resolvedDisplayName(defaults: defaults) - #expect(resolved == "My iOS Node") - #expect(defaults.string(forKey: displayKey) == "My iOS Node") - } - } - } - - @Test @MainActor func makeHelloBuildsCapsAndCommands() { - let voiceWakeKey = VoiceWakePreferences.enabledKey - - withKeychainValues([instanceIdEntry: nil, preferredBridgeEntry: nil, lastBridgeEntry: nil]) { - withUserDefaults([ - "node.instanceId": "ios-test", - "node.displayName": "Test Node", - "camera.enabled": false, - voiceWakeKey: true, - ]) { - let appModel = NodeAppModel() - let controller = BridgeConnectionController(appModel: appModel, startDiscovery: false) - let hello = controller._test_makeHello(token: "token-123") - - #expect(hello.nodeId == "ios-test") - #expect(hello.displayName == "Test Node") - #expect(hello.token == "token-123") - - let caps = Set(hello.caps ?? []) - #expect(caps.contains(ClawdbotCapability.canvas.rawValue)) - #expect(caps.contains(ClawdbotCapability.screen.rawValue)) - #expect(caps.contains(ClawdbotCapability.voiceWake.rawValue)) - #expect(!caps.contains(ClawdbotCapability.camera.rawValue)) - - let commands = Set(hello.commands ?? []) - #expect(commands.contains(ClawdbotCanvasCommand.present.rawValue)) - #expect(commands.contains(ClawdbotScreenCommand.record.rawValue)) - #expect(!commands.contains(ClawdbotCameraCommand.snap.rawValue)) - - #expect(!(hello.platform ?? "").isEmpty) - #expect(!(hello.deviceFamily ?? "").isEmpty) - #expect(!(hello.modelIdentifier ?? "").isEmpty) - #expect(!(hello.version ?? "").isEmpty) - } - } - } - - @Test @MainActor func makeHelloIncludesCameraCommandsWhenEnabled() { - withKeychainValues([instanceIdEntry: nil, preferredBridgeEntry: nil, lastBridgeEntry: nil]) { - withUserDefaults([ - "node.instanceId": "ios-test", - "node.displayName": "Test Node", - "camera.enabled": true, - VoiceWakePreferences.enabledKey: false, - ]) { - let appModel = NodeAppModel() - let controller = BridgeConnectionController(appModel: appModel, startDiscovery: false) - let hello = controller._test_makeHello(token: "token-456") - - let caps = Set(hello.caps ?? []) - #expect(caps.contains(ClawdbotCapability.camera.rawValue)) - - let commands = Set(hello.commands ?? []) - #expect(commands.contains(ClawdbotCameraCommand.snap.rawValue)) - #expect(commands.contains(ClawdbotCameraCommand.clip.rawValue)) - } - } - } - - @Test @MainActor func autoConnectRefreshesTokenOnUnauthorized() async { - let bridge = BridgeDiscoveryModel.DiscoveredBridge( - name: "Gateway", - endpoint: .hostPort(host: NWEndpoint.Host("127.0.0.1"), port: 18790), - stableID: "bridge-1", - debugID: "bridge-debug", - lanHost: "Mac.local", - tailnetDns: nil, - gatewayPort: 18789, - bridgePort: 18790, - canvasPort: 18793, - tlsEnabled: false, - tlsFingerprintSha256: nil, - cliPath: nil) - let mock = MockBridgePairingClient(resultToken: "new-token") - let account = "bridge-token.ios-test" - - await withKeychainValues([ - instanceIdEntry: nil, - preferredBridgeEntry: nil, - lastBridgeEntry: nil, - KeychainEntry(service: bridgeService, account: account): "old-token", - ]) { - await withUserDefaults([ - "node.instanceId": "ios-test", - "bridge.lastDiscoveredStableID": "bridge-1", - "bridge.manual.enabled": false, - ]) { - let appModel = NodeAppModel() - let controller = BridgeConnectionController( - appModel: appModel, - startDiscovery: false, - bridgeClientFactory: { mock }) - controller._test_setBridges([bridge]) - controller._test_triggerAutoConnect() - - for _ in 0..<20 { - if appModel.connectedBridgeID == bridge.stableID { break } - try? await Task.sleep(nanoseconds: 50_000_000) - } - - #expect(appModel.connectedBridgeID == bridge.stableID) - let stored = KeychainStore.loadString(service: bridgeService, account: account) - #expect(stored == "new-token") - let lastToken = await mock.lastToken - #expect(lastToken == "old-token") - } - } - } - - @Test @MainActor func autoConnectPrefersPreferredBridgeOverLastDiscovered() async { - let bridgeA = BridgeDiscoveryModel.DiscoveredBridge( - name: "Gateway A", - endpoint: .hostPort(host: NWEndpoint.Host("127.0.0.1"), port: 18790), - stableID: "bridge-1", - debugID: "bridge-a", - lanHost: "MacA.local", - tailnetDns: nil, - gatewayPort: 18789, - bridgePort: 18790, - canvasPort: 18793, - tlsEnabled: false, - tlsFingerprintSha256: nil, - cliPath: nil) - let bridgeB = BridgeDiscoveryModel.DiscoveredBridge( - name: "Gateway B", - endpoint: .hostPort(host: NWEndpoint.Host("127.0.0.1"), port: 28790), - stableID: "bridge-2", - debugID: "bridge-b", - lanHost: "MacB.local", - tailnetDns: nil, - gatewayPort: 28789, - bridgePort: 28790, - canvasPort: 28793, - tlsEnabled: false, - tlsFingerprintSha256: nil, - cliPath: nil) - - let mock = MockBridgePairingClient(resultToken: "token-ok") - let account = "bridge-token.ios-test" - - await withKeychainValues([ - instanceIdEntry: nil, - preferredBridgeEntry: nil, - lastBridgeEntry: nil, - KeychainEntry(service: bridgeService, account: account): "old-token", - ]) { - await withUserDefaults([ - "node.instanceId": "ios-test", - "bridge.preferredStableID": "bridge-2", - "bridge.lastDiscoveredStableID": "bridge-1", - "bridge.manual.enabled": false, - ]) { - let appModel = NodeAppModel() - let controller = BridgeConnectionController( - appModel: appModel, - startDiscovery: false, - bridgeClientFactory: { mock }) - controller._test_setBridges([bridgeA, bridgeB]) - controller._test_triggerAutoConnect() - - for _ in 0..<20 { - if appModel.connectedBridgeID == bridgeB.stableID { break } - try? await Task.sleep(nanoseconds: 50_000_000) - } - - #expect(appModel.connectedBridgeID == bridgeB.stableID) - } - } - } -} diff --git a/apps/ios/Tests/BridgeSessionTests.swift b/apps/ios/Tests/BridgeSessionTests.swift deleted file mode 100644 index 470441251..000000000 --- a/apps/ios/Tests/BridgeSessionTests.swift +++ /dev/null @@ -1,48 +0,0 @@ -import Foundation -import Testing -@testable import Clawdbot - -@Suite struct BridgeSessionTests { - @Test func initialStateIsIdle() async { - let session = BridgeSession() - #expect(await session.state == .idle) - } - - @Test func requestFailsWhenNotConnected() async { - let session = BridgeSession() - - do { - _ = try await session.request(method: "health", paramsJSON: nil, timeoutSeconds: 1) - Issue.record("Expected request to throw when not connected") - } catch let error as NSError { - #expect(error.domain == "Bridge") - #expect(error.code == 11) - } - } - - @Test func sendEventFailsWhenNotConnected() async { - let session = BridgeSession() - - do { - try await session.sendEvent(event: "tick", payloadJSON: nil) - Issue.record("Expected sendEvent to throw when not connected") - } catch let error as NSError { - #expect(error.domain == "Bridge") - #expect(error.code == 10) - } - } - - @Test func disconnectFinishesServerEventStreams() async throws { - let session = BridgeSession() - let stream = await session.subscribeServerEvents(bufferingNewest: 1) - - let consumer = Task { @Sendable in - for await _ in stream {} - } - - await session.disconnect() - - _ = await consumer.result - #expect(await session.state == .idle) - } -} diff --git a/apps/ios/Tests/GatewayConnectionControllerTests.swift b/apps/ios/Tests/GatewayConnectionControllerTests.swift new file mode 100644 index 000000000..3e892a7b2 --- /dev/null +++ b/apps/ios/Tests/GatewayConnectionControllerTests.swift @@ -0,0 +1,79 @@ +import ClawdbotKit +import Foundation +import Testing +import UIKit +@testable import Clawdbot + +private func withUserDefaults(_ updates: [String: Any?], _ body: () throws -> T) rethrows -> T { + let defaults = UserDefaults.standard + var snapshot: [String: Any?] = [:] + for key in updates.keys { + snapshot[key] = defaults.object(forKey: key) + } + for (key, value) in updates { + if let value { + defaults.set(value, forKey: key) + } else { + defaults.removeObject(forKey: key) + } + } + defer { + for (key, value) in snapshot { + if let value { + defaults.set(value, forKey: key) + } else { + defaults.removeObject(forKey: key) + } + } + } + return try body() +} + +@Suite(.serialized) struct GatewayConnectionControllerTests { + @Test @MainActor func resolvedDisplayNameSetsDefaultWhenMissing() { + let defaults = UserDefaults.standard + let displayKey = "node.displayName" + + withUserDefaults([displayKey: nil, "node.instanceId": "ios-test"]) { + let appModel = NodeAppModel() + let controller = GatewayConnectionController(appModel: appModel, startDiscovery: false) + + let resolved = controller._test_resolvedDisplayName(defaults: defaults) + #expect(!resolved.isEmpty) + #expect(defaults.string(forKey: displayKey) == resolved) + } + } + + @Test @MainActor func currentCapsReflectToggles() { + withUserDefaults([ + "node.instanceId": "ios-test", + "node.displayName": "Test Node", + "camera.enabled": true, + "location.enabledMode": ClawdbotLocationMode.always.rawValue, + VoiceWakePreferences.enabledKey: true, + ]) { + let appModel = NodeAppModel() + let controller = GatewayConnectionController(appModel: appModel, startDiscovery: false) + let caps = Set(controller._test_currentCaps()) + + #expect(caps.contains(ClawdbotCapability.canvas.rawValue)) + #expect(caps.contains(ClawdbotCapability.screen.rawValue)) + #expect(caps.contains(ClawdbotCapability.camera.rawValue)) + #expect(caps.contains(ClawdbotCapability.location.rawValue)) + #expect(caps.contains(ClawdbotCapability.voiceWake.rawValue)) + } + } + + @Test @MainActor func currentCommandsIncludeLocationWhenEnabled() { + withUserDefaults([ + "node.instanceId": "ios-test", + "location.enabledMode": ClawdbotLocationMode.whileUsing.rawValue, + ]) { + let appModel = NodeAppModel() + let controller = GatewayConnectionController(appModel: appModel, startDiscovery: false) + let commands = Set(controller._test_currentCommands()) + + #expect(commands.contains(ClawdbotLocationCommand.get.rawValue)) + } + } +} diff --git a/apps/ios/Tests/BridgeDiscoveryModelTests.swift b/apps/ios/Tests/GatewayDiscoveryModelTests.swift similarity index 77% rename from apps/ios/Tests/BridgeDiscoveryModelTests.swift rename to apps/ios/Tests/GatewayDiscoveryModelTests.swift index 0b9f8793a..2298647c8 100644 --- a/apps/ios/Tests/BridgeDiscoveryModelTests.swift +++ b/apps/ios/Tests/GatewayDiscoveryModelTests.swift @@ -1,9 +1,9 @@ import Testing @testable import Clawdbot -@Suite(.serialized) struct BridgeDiscoveryModelTests { +@Suite(.serialized) struct GatewayDiscoveryModelTests { @Test @MainActor func debugLoggingCapturesLifecycleAndResets() { - let model = BridgeDiscoveryModel() + let model = GatewayDiscoveryModel() #expect(model.debugLog.isEmpty) #expect(model.statusText == "Idle") @@ -13,7 +13,7 @@ import Testing model.stop() #expect(model.statusText == "Stopped") - #expect(model.bridges.isEmpty) + #expect(model.gateways.isEmpty) #expect(model.debugLog.count >= 3) model.setDebugLoggingEnabled(false) diff --git a/apps/ios/Tests/BridgeEndpointIDTests.swift b/apps/ios/Tests/GatewayEndpointIDTests.swift similarity index 60% rename from apps/ios/Tests/BridgeEndpointIDTests.swift rename to apps/ios/Tests/GatewayEndpointIDTests.swift index b1fe95cf8..37dbba5b3 100644 --- a/apps/ios/Tests/BridgeEndpointIDTests.swift +++ b/apps/ios/Tests/GatewayEndpointIDTests.swift @@ -3,30 +3,30 @@ import Network import Testing @testable import Clawdbot -@Suite struct BridgeEndpointIDTests { +@Suite struct GatewayEndpointIDTests { @Test func stableIDForServiceDecodesAndNormalizesName() { let endpoint = NWEndpoint.service( - name: "Clawdbot\\032Bridge \\032 Node\n", - type: "_clawdbot-bridge._tcp", + name: "Clawdbot\\032Gateway \\032 Node\n", + type: "_clawdbot-gw._tcp", domain: "local.", interface: nil) - #expect(BridgeEndpointID.stableID(endpoint) == "_clawdbot-bridge._tcp|local.|Clawdbot Bridge Node") + #expect(GatewayEndpointID.stableID(endpoint) == "_clawdbot-gw._tcp|local.|Clawdbot Gateway Node") } @Test func stableIDForNonServiceUsesEndpointDescription() { let endpoint = NWEndpoint.hostPort(host: NWEndpoint.Host("127.0.0.1"), port: 4242) - #expect(BridgeEndpointID.stableID(endpoint) == String(describing: endpoint)) + #expect(GatewayEndpointID.stableID(endpoint) == String(describing: endpoint)) } @Test func prettyDescriptionDecodesBonjourEscapes() { let endpoint = NWEndpoint.service( - name: "Clawdbot\\032Bridge", - type: "_clawdbot-bridge._tcp", + name: "Clawdbot\\032Gateway", + type: "_clawdbot-gw._tcp", domain: "local.", interface: nil) - let pretty = BridgeEndpointID.prettyDescription(endpoint) + let pretty = GatewayEndpointID.prettyDescription(endpoint) #expect(pretty == BonjourEscapes.decode(String(describing: endpoint))) #expect(!pretty.localizedCaseInsensitiveContains("\\032")) } diff --git a/apps/ios/Tests/BridgeSettingsStoreTests.swift b/apps/ios/Tests/GatewaySettingsStoreTests.swift similarity index 64% rename from apps/ios/Tests/BridgeSettingsStoreTests.swift rename to apps/ios/Tests/GatewaySettingsStoreTests.swift index d2840c11f..a93072ef6 100644 --- a/apps/ios/Tests/BridgeSettingsStoreTests.swift +++ b/apps/ios/Tests/GatewaySettingsStoreTests.swift @@ -7,11 +7,11 @@ private struct KeychainEntry: Hashable { let account: String } -private let bridgeService = "com.clawdbot.bridge" +private let gatewayService = "com.clawdbot.gateway" private let nodeService = "com.clawdbot.node" private let instanceIdEntry = KeychainEntry(service: nodeService, account: "instanceId") -private let preferredBridgeEntry = KeychainEntry(service: bridgeService, account: "preferredStableID") -private let lastBridgeEntry = KeychainEntry(service: bridgeService, account: "lastDiscoveredStableID") +private let preferredGatewayEntry = KeychainEntry(service: gatewayService, account: "preferredStableID") +private let lastGatewayEntry = KeychainEntry(service: gatewayService, account: "lastDiscoveredStableID") private func snapshotDefaults(_ keys: [String]) -> [String: Any?] { let defaults = UserDefaults.standard @@ -59,14 +59,14 @@ private func restoreKeychain(_ snapshot: [KeychainEntry: String?]) { applyKeychain(snapshot) } -@Suite(.serialized) struct BridgeSettingsStoreTests { +@Suite(.serialized) struct GatewaySettingsStoreTests { @Test func bootstrapCopiesDefaultsToKeychainWhenMissing() { let defaultsKeys = [ "node.instanceId", - "bridge.preferredStableID", - "bridge.lastDiscoveredStableID", + "gateway.preferredStableID", + "gateway.lastDiscoveredStableID", ] - let entries = [instanceIdEntry, preferredBridgeEntry, lastBridgeEntry] + let entries = [instanceIdEntry, preferredGatewayEntry, lastGatewayEntry] let defaultsSnapshot = snapshotDefaults(defaultsKeys) let keychainSnapshot = snapshotKeychain(entries) defer { @@ -76,29 +76,29 @@ private func restoreKeychain(_ snapshot: [KeychainEntry: String?]) { applyDefaults([ "node.instanceId": "node-test", - "bridge.preferredStableID": "preferred-test", - "bridge.lastDiscoveredStableID": "last-test", + "gateway.preferredStableID": "preferred-test", + "gateway.lastDiscoveredStableID": "last-test", ]) applyKeychain([ instanceIdEntry: nil, - preferredBridgeEntry: nil, - lastBridgeEntry: nil, + preferredGatewayEntry: nil, + lastGatewayEntry: nil, ]) - BridgeSettingsStore.bootstrapPersistence() + GatewaySettingsStore.bootstrapPersistence() #expect(KeychainStore.loadString(service: nodeService, account: "instanceId") == "node-test") - #expect(KeychainStore.loadString(service: bridgeService, account: "preferredStableID") == "preferred-test") - #expect(KeychainStore.loadString(service: bridgeService, account: "lastDiscoveredStableID") == "last-test") + #expect(KeychainStore.loadString(service: gatewayService, account: "preferredStableID") == "preferred-test") + #expect(KeychainStore.loadString(service: gatewayService, account: "lastDiscoveredStableID") == "last-test") } @Test func bootstrapCopiesKeychainToDefaultsWhenMissing() { let defaultsKeys = [ "node.instanceId", - "bridge.preferredStableID", - "bridge.lastDiscoveredStableID", + "gateway.preferredStableID", + "gateway.lastDiscoveredStableID", ] - let entries = [instanceIdEntry, preferredBridgeEntry, lastBridgeEntry] + let entries = [instanceIdEntry, preferredGatewayEntry, lastGatewayEntry] let defaultsSnapshot = snapshotDefaults(defaultsKeys) let keychainSnapshot = snapshotKeychain(entries) defer { @@ -108,20 +108,20 @@ private func restoreKeychain(_ snapshot: [KeychainEntry: String?]) { applyDefaults([ "node.instanceId": nil, - "bridge.preferredStableID": nil, - "bridge.lastDiscoveredStableID": nil, + "gateway.preferredStableID": nil, + "gateway.lastDiscoveredStableID": nil, ]) applyKeychain([ instanceIdEntry: "node-from-keychain", - preferredBridgeEntry: "preferred-from-keychain", - lastBridgeEntry: "last-from-keychain", + preferredGatewayEntry: "preferred-from-keychain", + lastGatewayEntry: "last-from-keychain", ]) - BridgeSettingsStore.bootstrapPersistence() + GatewaySettingsStore.bootstrapPersistence() let defaults = UserDefaults.standard #expect(defaults.string(forKey: "node.instanceId") == "node-from-keychain") - #expect(defaults.string(forKey: "bridge.preferredStableID") == "preferred-from-keychain") - #expect(defaults.string(forKey: "bridge.lastDiscoveredStableID") == "last-from-keychain") + #expect(defaults.string(forKey: "gateway.preferredStableID") == "preferred-from-keychain") + #expect(defaults.string(forKey: "gateway.lastDiscoveredStableID") == "last-from-keychain") } } diff --git a/apps/ios/Tests/IOSBridgeChatTransportTests.swift b/apps/ios/Tests/IOSGatewayChatTransportTests.swift similarity index 54% rename from apps/ios/Tests/IOSBridgeChatTransportTests.swift rename to apps/ios/Tests/IOSGatewayChatTransportTests.swift index 437437119..723a93146 100644 --- a/apps/ios/Tests/IOSBridgeChatTransportTests.swift +++ b/apps/ios/Tests/IOSGatewayChatTransportTests.swift @@ -1,19 +1,15 @@ +import ClawdbotKit import Testing @testable import Clawdbot -@Suite struct IOSBridgeChatTransportTests { - @Test func requestsFailFastWhenBridgeNotConnected() async { - let bridge = BridgeSession() - let transport = IOSBridgeChatTransport(bridge: bridge) - - do { - try await transport.setActiveSessionKey("node-test") - Issue.record("Expected setActiveSessionKey to throw when bridge not connected") - } catch {} +@Suite struct IOSGatewayChatTransportTests { + @Test func requestsFailFastWhenGatewayNotConnected() async { + let gateway = GatewayNodeSession() + let transport = IOSGatewayChatTransport(gateway: gateway) do { _ = try await transport.requestHistory(sessionKey: "node-test") - Issue.record("Expected requestHistory to throw when bridge not connected") + Issue.record("Expected requestHistory to throw when gateway not connected") } catch {} do { @@ -23,11 +19,12 @@ import Testing thinking: "low", idempotencyKey: "idempotency", attachments: []) - Issue.record("Expected sendMessage to throw when bridge not connected") + Issue.record("Expected sendMessage to throw when gateway not connected") } catch {} do { _ = try await transport.requestHealth(timeoutMs: 250) + Issue.record("Expected requestHealth to throw when gateway not connected") } catch {} } } diff --git a/apps/ios/Tests/Info.plist b/apps/ios/Tests/Info.plist index 0213b646b..798a77421 100644 --- a/apps/ios/Tests/Info.plist +++ b/apps/ios/Tests/Info.plist @@ -17,8 +17,8 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 2026.1.11-4 + 2026.1.24 CFBundleVersion - 202601113 + 20260124 diff --git a/apps/ios/Tests/NodeAppModelInvokeTests.swift b/apps/ios/Tests/NodeAppModelInvokeTests.swift index 436635a84..6f0d99906 100644 --- a/apps/ios/Tests/NodeAppModelInvokeTests.swift +++ b/apps/ios/Tests/NodeAppModelInvokeTests.swift @@ -159,7 +159,7 @@ private func withUserDefaults(_ updates: [String: Any?], _ body: () throws -> let appModel = NodeAppModel() let url = URL(string: "clawdbot://agent?message=hello")! await appModel.handleDeepLink(url: url) - #expect(appModel.screen.errorText?.contains("Bridge not connected") == true) + #expect(appModel.screen.errorText?.contains("Gateway not connected") == true) } @Test @MainActor func handleDeepLinkRejectsOversizedMessage() async { @@ -170,7 +170,7 @@ private func withUserDefaults(_ updates: [String: Any?], _ body: () throws -> #expect(appModel.screen.errorText?.contains("Deep link too large") == true) } - @Test @MainActor func sendVoiceTranscriptThrowsWhenBridgeOffline() async { + @Test @MainActor func sendVoiceTranscriptThrowsWhenGatewayOffline() async { let appModel = NodeAppModel() await #expect(throws: Error.self) { try await appModel.sendVoiceTranscript(text: "hello", sessionKey: "main") diff --git a/apps/ios/Tests/SwiftUIRenderSmokeTests.swift b/apps/ios/Tests/SwiftUIRenderSmokeTests.swift index 53e100448..4fe7fe88e 100644 --- a/apps/ios/Tests/SwiftUIRenderSmokeTests.swift +++ b/apps/ios/Tests/SwiftUIRenderSmokeTests.swift @@ -1,3 +1,4 @@ +import ClawdbotKit import SwiftUI import Testing import UIKit @@ -14,35 +15,35 @@ import UIKit } @Test @MainActor func statusPillConnectingBuildsAViewHierarchy() { - let root = StatusPill(bridge: .connecting, voiceWakeEnabled: true, brighten: true) {} + let root = StatusPill(gateway: .connecting, voiceWakeEnabled: true, brighten: true) {} _ = Self.host(root) } @Test @MainActor func statusPillDisconnectedBuildsAViewHierarchy() { - let root = StatusPill(bridge: .disconnected, voiceWakeEnabled: false) {} + let root = StatusPill(gateway: .disconnected, voiceWakeEnabled: false) {} _ = Self.host(root) } @Test @MainActor func settingsTabBuildsAViewHierarchy() { let appModel = NodeAppModel() - let bridgeController = BridgeConnectionController(appModel: appModel, startDiscovery: false) + let gatewayController = GatewayConnectionController(appModel: appModel, startDiscovery: false) let root = SettingsTab() .environment(appModel) .environment(appModel.voiceWake) - .environment(bridgeController) + .environment(gatewayController) _ = Self.host(root) } @Test @MainActor func rootTabsBuildAViewHierarchy() { let appModel = NodeAppModel() - let bridgeController = BridgeConnectionController(appModel: appModel, startDiscovery: false) + let gatewayController = GatewayConnectionController(appModel: appModel, startDiscovery: false) let root = RootTabs() .environment(appModel) .environment(appModel.voiceWake) - .environment(bridgeController) + .environment(gatewayController) _ = Self.host(root) } @@ -66,8 +67,8 @@ import UIKit @Test @MainActor func chatSheetBuildsAViewHierarchy() { let appModel = NodeAppModel() - let bridge = BridgeSession() - let root = ChatSheet(bridge: bridge, sessionKey: "test") + let gateway = GatewayNodeSession() + let root = ChatSheet(gateway: gateway, sessionKey: "test") .environment(appModel) .environment(appModel.voiceWake) _ = Self.host(root) diff --git a/apps/ios/Tests/VoiceWakePreferencesTests.swift b/apps/ios/Tests/VoiceWakePreferencesTests.swift index acf501654..ec4a63afa 100644 --- a/apps/ios/Tests/VoiceWakePreferencesTests.swift +++ b/apps/ios/Tests/VoiceWakePreferencesTests.swift @@ -11,6 +11,18 @@ import Testing #expect(VoiceWakePreferences.sanitizeTriggerWords(["", " "]) == VoiceWakePreferences.defaultTriggerWords) } + @Test func sanitizeTriggerWordsLimitsWordLength() { + let long = String(repeating: "x", count: VoiceWakePreferences.maxWordLength + 5) + let cleaned = VoiceWakePreferences.sanitizeTriggerWords(["ok", long]) + #expect(cleaned[1].count == VoiceWakePreferences.maxWordLength) + } + + @Test func sanitizeTriggerWordsLimitsWordCount() { + let words = (1...VoiceWakePreferences.maxWords + 3).map { "w\($0)" } + let cleaned = VoiceWakePreferences.sanitizeTriggerWords(words) + #expect(cleaned.count == VoiceWakePreferences.maxWords) + } + @Test func displayStringUsesSanitizedWords() { #expect(VoiceWakePreferences.displayString(for: ["", " "]) == "clawd, claude") } diff --git a/apps/ios/project.yml b/apps/ios/project.yml index d1b0c2fee..52faeb9d0 100644 --- a/apps/ios/project.yml +++ b/apps/ios/project.yml @@ -35,6 +35,8 @@ targets: - package: ClawdbotKit - package: ClawdbotKit product: ClawdbotChatUI + - package: ClawdbotKit + product: ClawdbotProtocol - package: Swabble product: SwabbleKit - sdk: AppIntents.framework @@ -79,19 +81,19 @@ targets: properties: CFBundleDisplayName: Clawdbot CFBundleIconName: AppIcon - CFBundleShortVersionString: "2026.1.9" - CFBundleVersion: "20260109" + CFBundleShortVersionString: "2026.1.24" + CFBundleVersion: "20260124" UILaunchScreen: {} UIApplicationSceneManifest: UIApplicationSupportsMultipleScenes: false UIBackgroundModes: - audio - NSLocalNetworkUsageDescription: Clawdbot discovers and connects to your Clawdbot bridge on the local network. + NSLocalNetworkUsageDescription: Clawdbot discovers and connects to your Clawdbot gateway on the local network. NSAppTransportSecurity: NSAllowsArbitraryLoadsInWebContent: true NSBonjourServices: - - _clawdbot-bridge._tcp - NSCameraUsageDescription: Clawdbot can capture photos or short video clips when requested via the bridge. + - _clawdbot-gw._tcp + NSCameraUsageDescription: Clawdbot can capture photos or short video clips when requested via the gateway. NSLocationWhenInUseUsageDescription: Clawdbot uses your location when you allow location sharing. NSLocationAlwaysAndWhenInUseUsageDescription: Clawdbot can share your location in the background when you enable Always. NSMicrophoneUsageDescription: Clawdbot needs microphone access for voice wake. @@ -128,5 +130,5 @@ targets: path: Tests/Info.plist properties: CFBundleDisplayName: ClawdbotTests - CFBundleShortVersionString: "2026.1.9" - CFBundleVersion: "20260109" + CFBundleShortVersionString: "2026.1.24" + CFBundleVersion: "20260124" diff --git a/apps/macos/Package.resolved b/apps/macos/Package.resolved index 248cec36e..ffc524d1c 100644 --- a/apps/macos/Package.resolved +++ b/apps/macos/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "4ed05a95fa9feada29b97f81b3194392e59a0c7b9edf24851f922bc2b72b0438", + "originHash" : "f847d54db16b371dbb1a79271d50436cdec572179b0f0cf14cfe1b75df8dfbc2", "pins" : [ { "identity" : "axorcist", diff --git a/apps/macos/Package.swift b/apps/macos/Package.swift index 3cd135e50..99ae0f991 100644 --- a/apps/macos/Package.swift +++ b/apps/macos/Package.swift @@ -12,8 +12,7 @@ let package = Package( .library(name: "ClawdbotIPC", targets: ["ClawdbotIPC"]), .library(name: "ClawdbotDiscovery", targets: ["ClawdbotDiscovery"]), .executable(name: "Clawdbot", targets: ["Clawdbot"]), - .executable(name: "clawdbot-mac-discovery", targets: ["ClawdbotDiscoveryCLI"]), - .executable(name: "clawdbot-mac-wizard", targets: ["ClawdbotWizardCLI"]), + .executable(name: "clawdbot-mac", targets: ["ClawdbotMacCLI"]), ], dependencies: [ .package(url: "https://github.com/orchetect/MenuBarExtraAccess", exact: "1.2.2"), @@ -25,13 +24,6 @@ let package = Package( .package(path: "../../Swabble"), ], targets: [ - .target( - name: "ClawdbotProtocol", - dependencies: [], - path: "Sources/ClawdbotProtocol", - swiftSettings: [ - .enableUpcomingFeature("StrictConcurrency"), - ]), .target( name: "ClawdbotIPC", dependencies: [], @@ -52,9 +44,9 @@ let package = Package( dependencies: [ "ClawdbotIPC", "ClawdbotDiscovery", - "ClawdbotProtocol", .product(name: "ClawdbotKit", package: "ClawdbotKit"), .product(name: "ClawdbotChatUI", package: "ClawdbotKit"), + .product(name: "ClawdbotProtocol", package: "ClawdbotKit"), .product(name: "SwabbleKit", package: "swabble"), .product(name: "MenuBarExtraAccess", package: "MenuBarExtraAccess"), .product(name: "Subprocess", package: "swift-subprocess"), @@ -74,20 +66,13 @@ let package = Package( .enableUpcomingFeature("StrictConcurrency"), ]), .executableTarget( - name: "ClawdbotDiscoveryCLI", + name: "ClawdbotMacCLI", dependencies: [ "ClawdbotDiscovery", + .product(name: "ClawdbotKit", package: "ClawdbotKit"), + .product(name: "ClawdbotProtocol", package: "ClawdbotKit"), ], - path: "Sources/ClawdbotDiscoveryCLI", - swiftSettings: [ - .enableUpcomingFeature("StrictConcurrency"), - ]), - .executableTarget( - name: "ClawdbotWizardCLI", - dependencies: [ - "ClawdbotProtocol", - ], - path: "Sources/ClawdbotWizardCLI", + path: "Sources/ClawdbotMacCLI", swiftSettings: [ .enableUpcomingFeature("StrictConcurrency"), ]), @@ -97,7 +82,7 @@ let package = Package( "ClawdbotIPC", "Clawdbot", "ClawdbotDiscovery", - "ClawdbotProtocol", + .product(name: "ClawdbotProtocol", package: "ClawdbotKit"), .product(name: "SwabbleKit", package: "swabble"), ], swiftSettings: [ diff --git a/apps/macos/Sources/Clawdbot/AgentEventsWindow.swift b/apps/macos/Sources/Clawdbot/AgentEventsWindow.swift index e3ccc87bc..43b60a270 100644 --- a/apps/macos/Sources/Clawdbot/AgentEventsWindow.swift +++ b/apps/macos/Sources/Clawdbot/AgentEventsWindow.swift @@ -81,7 +81,7 @@ private struct EventRow: View { return f.string(from: date) } - private func prettyJSON(_ dict: [String: AnyCodable]) -> String? { + private func prettyJSON(_ dict: [String: ClawdbotProtocol.AnyCodable]) -> String? { let normalized = dict.mapValues { $0.value } guard JSONSerialization.isValidJSONObject(normalized), let data = try? JSONSerialization.data(withJSONObject: normalized, options: [.prettyPrinted]), @@ -98,7 +98,10 @@ struct AgentEventsWindow_Previews: PreviewProvider { seq: 1, stream: "tool", ts: Date().timeIntervalSince1970 * 1000, - data: ["phase": AnyCodable("start"), "name": AnyCodable("bash")], + data: [ + "phase": ClawdbotProtocol.AnyCodable("start"), + "name": ClawdbotProtocol.AnyCodable("bash"), + ], summary: nil) AgentEventStore.shared.append(sample) return AgentEventsWindow() diff --git a/apps/macos/Sources/Clawdbot/AgentWorkspace.swift b/apps/macos/Sources/Clawdbot/AgentWorkspace.swift index 8906f2f4c..f63c1dd4d 100644 --- a/apps/macos/Sources/Clawdbot/AgentWorkspace.swift +++ b/apps/macos/Sources/Clawdbot/AgentWorkspace.swift @@ -23,7 +23,7 @@ enum AgentWorkspace { } static func displayPath(for url: URL) -> String { - let home = FileManager.default.homeDirectoryForCurrentUser.path + let home = FileManager().homeDirectoryForCurrentUser.path let path = url.path if path == home { return "~" } if path.hasPrefix(home + "/") { @@ -44,12 +44,12 @@ enum AgentWorkspace { } static func workspaceEntries(workspaceURL: URL) throws -> [String] { - let contents = try FileManager.default.contentsOfDirectory(atPath: workspaceURL.path) + let contents = try FileManager().contentsOfDirectory(atPath: workspaceURL.path) return contents.filter { !self.ignoredEntries.contains($0) } } static func isWorkspaceEmpty(workspaceURL: URL) -> Bool { - let fm = FileManager.default + let fm = FileManager() var isDir: ObjCBool = false if !fm.fileExists(atPath: workspaceURL.path, isDirectory: &isDir) { return true @@ -66,7 +66,7 @@ enum AgentWorkspace { } static func bootstrapSafety(for workspaceURL: URL) -> BootstrapSafety { - let fm = FileManager.default + let fm = FileManager() var isDir: ObjCBool = false if !fm.fileExists(atPath: workspaceURL.path, isDirectory: &isDir) { return .safe @@ -90,29 +90,29 @@ enum AgentWorkspace { static func bootstrap(workspaceURL: URL) throws -> URL { let shouldSeedBootstrap = self.isWorkspaceEmpty(workspaceURL: workspaceURL) - try FileManager.default.createDirectory(at: workspaceURL, withIntermediateDirectories: true) + try FileManager().createDirectory(at: workspaceURL, withIntermediateDirectories: true) let agentsURL = self.agentsURL(workspaceURL: workspaceURL) - if !FileManager.default.fileExists(atPath: agentsURL.path) { + if !FileManager().fileExists(atPath: agentsURL.path) { try self.defaultTemplate().write(to: agentsURL, atomically: true, encoding: .utf8) self.logger.info("Created AGENTS.md at \(agentsURL.path, privacy: .public)") } let soulURL = workspaceURL.appendingPathComponent(self.soulFilename) - if !FileManager.default.fileExists(atPath: soulURL.path) { + if !FileManager().fileExists(atPath: soulURL.path) { try self.defaultSoulTemplate().write(to: soulURL, atomically: true, encoding: .utf8) self.logger.info("Created SOUL.md at \(soulURL.path, privacy: .public)") } let identityURL = workspaceURL.appendingPathComponent(self.identityFilename) - if !FileManager.default.fileExists(atPath: identityURL.path) { + if !FileManager().fileExists(atPath: identityURL.path) { try self.defaultIdentityTemplate().write(to: identityURL, atomically: true, encoding: .utf8) self.logger.info("Created IDENTITY.md at \(identityURL.path, privacy: .public)") } let userURL = workspaceURL.appendingPathComponent(self.userFilename) - if !FileManager.default.fileExists(atPath: userURL.path) { + if !FileManager().fileExists(atPath: userURL.path) { try self.defaultUserTemplate().write(to: userURL, atomically: true, encoding: .utf8) self.logger.info("Created USER.md at \(userURL.path, privacy: .public)") } let bootstrapURL = workspaceURL.appendingPathComponent(self.bootstrapFilename) - if shouldSeedBootstrap, !FileManager.default.fileExists(atPath: bootstrapURL.path) { + if shouldSeedBootstrap, !FileManager().fileExists(atPath: bootstrapURL.path) { try self.defaultBootstrapTemplate().write(to: bootstrapURL, atomically: true, encoding: .utf8) self.logger.info("Created BOOTSTRAP.md at \(bootstrapURL.path, privacy: .public)") } @@ -120,7 +120,7 @@ enum AgentWorkspace { } static func needsBootstrap(workspaceURL: URL) -> Bool { - let fm = FileManager.default + let fm = FileManager() var isDir: ObjCBool = false if !fm.fileExists(atPath: workspaceURL.path, isDirectory: &isDir) { return true @@ -305,7 +305,7 @@ enum AgentWorkspace { if let dev = self.devTemplateURL(named: named) { urls.append(dev) } - let cwd = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) + let cwd = URL(fileURLWithPath: FileManager().currentDirectoryPath) urls.append(cwd.appendingPathComponent("docs") .appendingPathComponent(self.templateDirname) .appendingPathComponent(named)) diff --git a/apps/macos/Sources/Clawdbot/AnthropicAuthControls.swift b/apps/macos/Sources/Clawdbot/AnthropicAuthControls.swift index 42106e50d..0f5f1d61c 100644 --- a/apps/macos/Sources/Clawdbot/AnthropicAuthControls.swift +++ b/apps/macos/Sources/Clawdbot/AnthropicAuthControls.swift @@ -45,7 +45,7 @@ struct AnthropicAuthControls: View { NSWorkspace.shared.activateFileViewerSelecting([ClawdbotOAuthStore.oauthURL()]) } .buttonStyle(.bordered) - .disabled(!FileManager.default.fileExists(atPath: ClawdbotOAuthStore.oauthURL().path)) + .disabled(!FileManager().fileExists(atPath: ClawdbotOAuthStore.oauthURL().path)) Button("Refresh") { self.refresh() diff --git a/apps/macos/Sources/Clawdbot/AnthropicOAuth.swift b/apps/macos/Sources/Clawdbot/AnthropicOAuth.swift index 09113324a..4ea7f7fb9 100644 --- a/apps/macos/Sources/Clawdbot/AnthropicOAuth.swift +++ b/apps/macos/Sources/Clawdbot/AnthropicOAuth.swift @@ -234,7 +234,7 @@ enum ClawdbotOAuthStore { return URL(fileURLWithPath: expanded, isDirectory: true) } - return FileManager.default.homeDirectoryForCurrentUser + return FileManager().homeDirectoryForCurrentUser .appendingPathComponent(".clawdbot", isDirectory: true) .appendingPathComponent("credentials", isDirectory: true) } @@ -253,7 +253,7 @@ enum ClawdbotOAuthStore { urls.append(URL(fileURLWithPath: expanded, isDirectory: true).appendingPathComponent(self.oauthFilename)) } - let home = FileManager.default.homeDirectoryForCurrentUser + let home = FileManager().homeDirectoryForCurrentUser urls.append(home.appendingPathComponent(".pi/agent/\(self.oauthFilename)")) urls.append(home.appendingPathComponent(".claude/\(self.oauthFilename)")) urls.append(home.appendingPathComponent(".config/claude/\(self.oauthFilename)")) @@ -270,10 +270,10 @@ enum ClawdbotOAuthStore { static func importLegacyAnthropicOAuthIfNeeded() -> URL? { let dest = self.oauthURL() - guard !FileManager.default.fileExists(atPath: dest.path) else { return nil } + guard !FileManager().fileExists(atPath: dest.path) else { return nil } for url in self.legacyOAuthURLs() { - guard FileManager.default.fileExists(atPath: url.path) else { continue } + guard FileManager().fileExists(atPath: url.path) else { continue } guard self.anthropicOAuthStatus(at: url).isConnected else { continue } guard let storage = self.loadStorage(at: url) else { continue } do { @@ -296,7 +296,7 @@ enum ClawdbotOAuthStore { } static func anthropicOAuthStatus(at url: URL) -> AnthropicOAuthStatus { - guard FileManager.default.fileExists(atPath: url.path) else { return .missingFile } + guard FileManager().fileExists(atPath: url.path) else { return .missingFile } guard let data = try? Data(contentsOf: url) else { return .unreadableFile } guard let json = try? JSONSerialization.jsonObject(with: data, options: []) else { return .invalidJSON } @@ -360,7 +360,7 @@ enum ClawdbotOAuthStore { private static func saveStorage(_ storage: [String: Any]) throws { let dir = self.oauthDir() - try FileManager.default.createDirectory( + try FileManager().createDirectory( at: dir, withIntermediateDirectories: true, attributes: [.posixPermissions: 0o700]) @@ -370,7 +370,7 @@ enum ClawdbotOAuthStore { withJSONObject: storage, options: [.prettyPrinted, .sortedKeys]) try data.write(to: url, options: [.atomic]) - try FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: url.path) + try FileManager().setAttributes([.posixPermissions: 0o600], ofItemAtPath: url.path) } } diff --git a/apps/macos/Sources/Clawdbot/AnyCodable+Helpers.swift b/apps/macos/Sources/Clawdbot/AnyCodable+Helpers.swift index bada8e26a..831076040 100644 --- a/apps/macos/Sources/Clawdbot/AnyCodable+Helpers.swift +++ b/apps/macos/Sources/Clawdbot/AnyCodable+Helpers.swift @@ -1,6 +1,11 @@ +import ClawdbotKit import ClawdbotProtocol import Foundation +// Prefer the ClawdbotKit wrapper to keep gateway request payloads consistent. +typealias AnyCodable = ClawdbotKit.AnyCodable +typealias InstanceIdentity = ClawdbotKit.InstanceIdentity + extension AnyCodable { var stringValue: String? { self.value as? String } var boolValue: Bool? { self.value as? Bool } @@ -20,3 +25,23 @@ extension AnyCodable { } } } + +extension ClawdbotProtocol.AnyCodable { + var stringValue: String? { self.value as? String } + var boolValue: Bool? { self.value as? Bool } + var intValue: Int? { self.value as? Int } + var doubleValue: Double? { self.value as? Double } + var dictionaryValue: [String: ClawdbotProtocol.AnyCodable]? { self.value as? [String: ClawdbotProtocol.AnyCodable] } + var arrayValue: [ClawdbotProtocol.AnyCodable]? { self.value as? [ClawdbotProtocol.AnyCodable] } + + var foundationValue: Any { + switch self.value { + case let dict as [String: ClawdbotProtocol.AnyCodable]: + dict.mapValues { $0.foundationValue } + case let array as [ClawdbotProtocol.AnyCodable]: + array.map(\.foundationValue) + default: + self.value + } + } +} diff --git a/apps/macos/Sources/Clawdbot/Bridge/BridgeConnectionHandler.swift b/apps/macos/Sources/Clawdbot/Bridge/BridgeConnectionHandler.swift deleted file mode 100644 index ea38c3ee1..000000000 --- a/apps/macos/Sources/Clawdbot/Bridge/BridgeConnectionHandler.swift +++ /dev/null @@ -1,462 +0,0 @@ -import ClawdbotKit -import Foundation -import Network -import OSLog - -struct BridgeNodeInfo: Sendable { - var nodeId: String - var displayName: String? - var platform: String? - var version: String? - var coreVersion: String? - var uiVersion: String? - var deviceFamily: String? - var modelIdentifier: String? - var remoteAddress: String? - var caps: [String]? -} - -actor BridgeConnectionHandler { - private let connection: NWConnection - private let logger: Logger - private let decoder = JSONDecoder() - private let encoder = JSONEncoder() - private let queue = DispatchQueue(label: "com.clawdbot.bridge.connection") - - private var buffer = Data() - private var isAuthenticated = false - private var nodeId: String? - private var pendingInvokes: [String: CheckedContinuation] = [:] - private var isClosed = false - - init(connection: NWConnection, logger: Logger) { - self.connection = connection - self.logger = logger - } - - enum AuthResult: Sendable { - case ok - case notPaired - case unauthorized - case error(code: String, message: String) - } - - enum PairResult: Sendable { - case ok(token: String) - case rejected - case error(code: String, message: String) - } - - private struct FrameContext: Sendable { - var serverName: String - var resolveAuth: @Sendable (BridgeHello) async -> AuthResult - var handlePair: @Sendable (BridgePairRequest) async -> PairResult - var onAuthenticated: (@Sendable (BridgeNodeInfo) async -> Void)? - var onEvent: (@Sendable (String, BridgeEventFrame) async -> Void)? - var onRequest: (@Sendable (String, BridgeRPCRequest) async -> BridgeRPCResponse)? - } - - func run( - resolveAuth: @escaping @Sendable (BridgeHello) async -> AuthResult, - handlePair: @escaping @Sendable (BridgePairRequest) async -> PairResult, - onAuthenticated: (@Sendable (BridgeNodeInfo) async -> Void)? = nil, - onDisconnected: (@Sendable (String) async -> Void)? = nil, - onEvent: (@Sendable (String, BridgeEventFrame) async -> Void)? = nil, - onRequest: (@Sendable (String, BridgeRPCRequest) async -> BridgeRPCResponse)? = nil) async - { - self.configureStateLogging() - self.connection.start(queue: self.queue) - - let context = FrameContext( - serverName: Host.current().localizedName ?? ProcessInfo.processInfo.hostName, - resolveAuth: resolveAuth, - handlePair: handlePair, - onAuthenticated: onAuthenticated, - onEvent: onEvent, - onRequest: onRequest) - - while true { - do { - guard let line = try await self.receiveLine() else { break } - guard let data = line.data(using: .utf8) else { continue } - let base = try self.decoder.decode(BridgeBaseFrame.self, from: data) - try await self.handleFrame( - baseType: base.type, - data: data, - context: context) - } catch { - await self.sendError(code: "INVALID_REQUEST", message: error.localizedDescription) - } - } - - await self.close(with: onDisconnected) - } - - private func configureStateLogging() { - self.connection.stateUpdateHandler = { [logger] state in - switch state { - case .ready: - logger.debug("bridge conn ready") - case let .failed(err): - logger.error("bridge conn failed: \(err.localizedDescription, privacy: .public)") - default: - break - } - } - } - - private func handleFrame( - baseType: String, - data: Data, - context: FrameContext) async throws - { - switch baseType { - case "hello": - await self.handleHelloFrame( - data: data, - context: context) - case "pair-request": - await self.handlePairRequestFrame( - data: data, - context: context) - case "event": - await self.handleEventFrame(data: data, onEvent: context.onEvent) - case "req": - try await self.handleRPCRequestFrame(data: data, onRequest: context.onRequest) - case "ping": - try await self.handlePingFrame(data: data) - case "invoke-res": - await self.handleInvokeResponseFrame(data: data) - default: - await self.sendError(code: "INVALID_REQUEST", message: "unknown type") - } - } - - private func handleHelloFrame( - data: Data, - context: FrameContext) async - { - do { - let hello = try self.decoder.decode(BridgeHello.self, from: data) - let nodeId = hello.nodeId.trimmingCharacters(in: .whitespacesAndNewlines) - self.nodeId = nodeId - let result = await context.resolveAuth(hello) - await self.handleAuthResult(result, serverName: context.serverName) - if case .ok = result { - await context.onAuthenticated?( - BridgeNodeInfo( - nodeId: nodeId, - displayName: hello.displayName, - platform: hello.platform, - version: hello.version, - coreVersion: hello.coreVersion, - uiVersion: hello.uiVersion, - deviceFamily: hello.deviceFamily, - modelIdentifier: hello.modelIdentifier, - remoteAddress: self.remoteAddressString(), - caps: hello.caps)) - } - } catch { - await self.sendError(code: "INVALID_REQUEST", message: error.localizedDescription) - } - } - - private func handlePairRequestFrame( - data: Data, - context: FrameContext) async - { - do { - let req = try self.decoder.decode(BridgePairRequest.self, from: data) - let nodeId = req.nodeId.trimmingCharacters(in: .whitespacesAndNewlines) - self.nodeId = nodeId - let enriched = BridgePairRequest( - type: req.type, - nodeId: nodeId, - displayName: req.displayName, - platform: req.platform, - version: req.version, - coreVersion: req.coreVersion, - uiVersion: req.uiVersion, - deviceFamily: req.deviceFamily, - modelIdentifier: req.modelIdentifier, - caps: req.caps, - commands: req.commands, - remoteAddress: self.remoteAddressString(), - silent: req.silent) - let result = await context.handlePair(enriched) - await self.handlePairResult(result, serverName: context.serverName) - if case .ok = result { - await context.onAuthenticated?( - BridgeNodeInfo( - nodeId: nodeId, - displayName: enriched.displayName, - platform: enriched.platform, - version: enriched.version, - coreVersion: enriched.coreVersion, - uiVersion: enriched.uiVersion, - deviceFamily: enriched.deviceFamily, - modelIdentifier: enriched.modelIdentifier, - remoteAddress: enriched.remoteAddress, - caps: enriched.caps)) - } - } catch { - await self.sendError(code: "INVALID_REQUEST", message: error.localizedDescription) - } - } - - private func handleEventFrame( - data: Data, - onEvent: (@Sendable (String, BridgeEventFrame) async -> Void)?) async - { - guard self.isAuthenticated, let nodeId = self.nodeId else { - await self.sendError(code: "UNAUTHORIZED", message: "not authenticated") - return - } - do { - let evt = try self.decoder.decode(BridgeEventFrame.self, from: data) - await onEvent?(nodeId, evt) - } catch { - await self.sendError(code: "INVALID_REQUEST", message: error.localizedDescription) - } - } - - private func handleRPCRequestFrame( - data: Data, - onRequest: (@Sendable (String, BridgeRPCRequest) async -> BridgeRPCResponse)?) async throws - { - let req = try self.decoder.decode(BridgeRPCRequest.self, from: data) - guard self.isAuthenticated, let nodeId = self.nodeId else { - try await self.send( - BridgeRPCResponse( - id: req.id, - ok: false, - error: BridgeRPCError(code: "UNAUTHORIZED", message: "not authenticated"))) - return - } - - if let onRequest { - let res = await onRequest(nodeId, req) - try await self.send(res) - } else { - try await self.send( - BridgeRPCResponse( - id: req.id, - ok: false, - error: BridgeRPCError(code: "UNAVAILABLE", message: "RPC not supported"))) - } - } - - private func handlePingFrame(data: Data) async throws { - guard self.isAuthenticated else { - await self.sendError(code: "UNAUTHORIZED", message: "not authenticated") - return - } - let ping = try self.decoder.decode(BridgePing.self, from: data) - try await self.send(BridgePong(type: "pong", id: ping.id)) - } - - private func handleInvokeResponseFrame(data: Data) async { - guard self.isAuthenticated else { - await self.sendError(code: "UNAUTHORIZED", message: "not authenticated") - return - } - do { - let res = try self.decoder.decode(BridgeInvokeResponse.self, from: data) - if let cont = self.pendingInvokes.removeValue(forKey: res.id) { - cont.resume(returning: res) - } - } catch { - await self.sendError(code: "INVALID_REQUEST", message: error.localizedDescription) - } - } - - private func remoteAddressString() -> String? { - switch self.connection.endpoint { - case let .hostPort(host: host, port: _): - let value = String(describing: host) - return value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : value - default: - return nil - } - } - - func remoteAddress() -> String? { - self.remoteAddressString() - } - - private func handlePairResult(_ result: PairResult, serverName: String) async { - switch result { - case let .ok(token): - do { - try await self.send(BridgePairOk(type: "pair-ok", token: token)) - self.isAuthenticated = true - let mainSessionKey = await GatewayConnection.shared.mainSessionKey() - try await self.send( - BridgeHelloOk( - type: "hello-ok", - serverName: serverName, - mainSessionKey: mainSessionKey)) - } catch { - self.logger.error("bridge send pair-ok failed: \(error.localizedDescription, privacy: .public)") - } - case .rejected: - await self.sendError(code: "UNAUTHORIZED", message: "pairing rejected") - case let .error(code, message): - await self.sendError(code: code, message: message) - } - } - - private func handleAuthResult(_ result: AuthResult, serverName: String) async { - switch result { - case .ok: - self.isAuthenticated = true - do { - let mainSessionKey = await GatewayConnection.shared.mainSessionKey() - try await self.send( - BridgeHelloOk( - type: "hello-ok", - serverName: serverName, - mainSessionKey: mainSessionKey)) - } catch { - self.logger.error("bridge send hello-ok failed: \(error.localizedDescription, privacy: .public)") - } - case .notPaired: - await self.sendError(code: "NOT_PAIRED", message: "pairing required") - case .unauthorized: - await self.sendError(code: "UNAUTHORIZED", message: "invalid token") - case let .error(code, message): - await self.sendError(code: code, message: message) - } - } - - private func sendError(code: String, message: String) async { - do { - try await self.send(BridgeErrorFrame(type: "error", code: code, message: message)) - } catch { - self.logger.error("bridge send error failed: \(error.localizedDescription, privacy: .public)") - } - } - - func invoke(command: String, paramsJSON: String?) async throws -> BridgeInvokeResponse { - guard self.isAuthenticated else { - throw NSError(domain: "Bridge", code: 1, userInfo: [ - NSLocalizedDescriptionKey: "UNAUTHORIZED: not authenticated", - ]) - } - let id = UUID().uuidString - let req = BridgeInvokeRequest(type: "invoke", id: id, command: command, paramsJSON: paramsJSON) - - let timeoutTask = Task { - try await Task.sleep(nanoseconds: 15 * 1_000_000_000) - await self.timeoutInvoke(id: id) - } - defer { timeoutTask.cancel() } - - return try await withCheckedThrowingContinuation { cont in - Task { [weak self] in - guard let self else { return } - await self.beginInvoke(id: id, request: req, continuation: cont) - } - } - } - - private func beginInvoke( - id: String, - request: BridgeInvokeRequest, - continuation: CheckedContinuation) async - { - self.pendingInvokes[id] = continuation - do { - try await self.send(request) - } catch { - await self.failInvoke(id: id, error: error) - } - } - - private func timeoutInvoke(id: String) async { - guard let cont = self.pendingInvokes.removeValue(forKey: id) else { return } - cont.resume(throwing: NSError(domain: "Bridge", code: 3, userInfo: [ - NSLocalizedDescriptionKey: "UNAVAILABLE: invoke timeout", - ])) - } - - private func failInvoke(id: String, error: Error) async { - guard let cont = self.pendingInvokes.removeValue(forKey: id) else { return } - cont.resume(throwing: error) - } - - private func send(_ obj: some Encodable) async throws { - let data = try self.encoder.encode(obj) - var line = Data() - line.append(data) - line.append(0x0A) // \n - let _: Void = try await withCheckedThrowingContinuation { cont in - self.connection.send(content: line, completion: .contentProcessed { err in - if let err { - cont.resume(throwing: err) - } else { - cont.resume(returning: ()) - } - }) - } - } - - func sendServerEvent(event: String, payloadJSON: String?) async { - guard self.isAuthenticated else { return } - do { - try await self.send(BridgeEventFrame(type: "event", event: event, payloadJSON: payloadJSON)) - } catch { - self.logger.error("bridge send event failed: \(error.localizedDescription, privacy: .public)") - } - } - - private func receiveLine() async throws -> String? { - while true { - if let idx = self.buffer.firstIndex(of: 0x0A) { - let lineData = self.buffer.prefix(upTo: idx) - self.buffer.removeSubrange(...idx) - return String(data: lineData, encoding: .utf8) - } - - let chunk = try await self.receiveChunk() - if chunk.isEmpty { return nil } - self.buffer.append(chunk) - } - } - - private func receiveChunk() async throws -> Data { - try await withCheckedThrowingContinuation { cont in - self.connection - .receive(minimumIncompleteLength: 1, maximumLength: 64 * 1024) { data, _, isComplete, error in - if let error { - cont.resume(throwing: error) - return - } - if isComplete { - cont.resume(returning: Data()) - return - } - cont.resume(returning: data ?? Data()) - } - } - } - - private func close(with onDisconnected: (@Sendable (String) async -> Void)? = nil) async { - if self.isClosed { return } - self.isClosed = true - - let nodeId = self.nodeId - let pending = self.pendingInvokes.values - self.pendingInvokes.removeAll() - for cont in pending { - cont.resume(throwing: NSError(domain: "Bridge", code: 4, userInfo: [ - NSLocalizedDescriptionKey: "UNAVAILABLE: connection closed", - ])) - } - - self.connection.cancel() - if let nodeId { - await onDisconnected?(nodeId) - } - } -} diff --git a/apps/macos/Sources/Clawdbot/Bridge/BridgeServer.swift b/apps/macos/Sources/Clawdbot/Bridge/BridgeServer.swift deleted file mode 100644 index f45dcae20..000000000 --- a/apps/macos/Sources/Clawdbot/Bridge/BridgeServer.swift +++ /dev/null @@ -1,542 +0,0 @@ -import AppKit -import ClawdbotKit -import ClawdbotProtocol -import Foundation -import Network -import OSLog - -actor BridgeServer { - static let shared = BridgeServer() - - private let logger = Logger(subsystem: "com.clawdbot", category: "bridge") - private var listener: NWListener? - private var isRunning = false - private var store: PairedNodesStore? - private var connections: [String: BridgeConnectionHandler] = [:] - private var nodeInfoById: [String: BridgeNodeInfo] = [:] - private var presenceTasks: [String: Task] = [:] - private var chatSubscriptions: [String: Set] = [:] - private var gatewayPushTask: Task? - - func start() async { - if self.isRunning { return } - self.isRunning = true - - do { - let storeURL = try Self.defaultStoreURL() - let store = PairedNodesStore(fileURL: storeURL) - await store.load() - self.store = store - - let params = NWParameters.tcp - params.includePeerToPeer = true - let listener = try NWListener(using: params, on: .any) - - listener.newConnectionHandler = { [weak self] connection in - guard let self else { return } - Task { await self.handle(connection: connection) } - } - - listener.stateUpdateHandler = { [weak self] state in - guard let self else { return } - Task { await self.handleListenerState(state) } - } - - listener.start(queue: DispatchQueue(label: "com.clawdbot.bridge")) - self.listener = listener - } catch { - self.logger.error("bridge start failed: \(error.localizedDescription, privacy: .public)") - self.isRunning = false - } - } - - func stop() async { - self.isRunning = false - self.listener?.cancel() - self.listener = nil - } - - private func handleListenerState(_ state: NWListener.State) { - switch state { - case .ready: - self.logger.info("bridge listening") - case let .failed(err): - self.logger.error("bridge listener failed: \(err.localizedDescription, privacy: .public)") - case .cancelled: - self.logger.info("bridge listener cancelled") - case .waiting: - self.logger.info("bridge listener waiting") - case .setup: - break - @unknown default: - break - } - } - - private func handle(connection: NWConnection) async { - let handler = BridgeConnectionHandler(connection: connection, logger: self.logger) - await handler.run( - resolveAuth: { [weak self] hello in - await self?.authorize(hello: hello) ?? .error(code: "UNAVAILABLE", message: "bridge unavailable") - }, - handlePair: { [weak self] request in - await self?.pair(request: request) ?? .error(code: "UNAVAILABLE", message: "bridge unavailable") - }, - onAuthenticated: { [weak self] node in - await self?.registerConnection(handler: handler, node: node) - }, - onDisconnected: { [weak self] nodeId in - await self?.unregisterConnection(nodeId: nodeId) - }, - onEvent: { [weak self] nodeId, evt in - await self?.handleEvent(nodeId: nodeId, evt: evt) - }, - onRequest: { [weak self] nodeId, req in - await self?.handleRequest(nodeId: nodeId, req: req) - ?? BridgeRPCResponse( - id: req.id, - ok: false, - error: BridgeRPCError(code: "UNAVAILABLE", message: "bridge unavailable")) - }) - } - - func invoke(nodeId: String, command: String, paramsJSON: String?) async throws -> BridgeInvokeResponse { - guard let handler = self.connections[nodeId] else { - throw NSError(domain: "Bridge", code: 10, userInfo: [ - NSLocalizedDescriptionKey: "UNAVAILABLE: node not connected", - ]) - } - return try await handler.invoke(command: command, paramsJSON: paramsJSON) - } - - func connectedNodeIds() -> [String] { - Array(self.connections.keys).sorted() - } - - func connectedNodes() -> [BridgeNodeInfo] { - self.nodeInfoById.values.sorted { a, b in - (a.displayName ?? a.nodeId) < (b.displayName ?? b.nodeId) - } - } - - func pairedNodes() async -> [PairedNode] { - guard let store = self.store else { return [] } - return await store.all() - } - - private func registerConnection(handler: BridgeConnectionHandler, node: BridgeNodeInfo) async { - self.connections[node.nodeId] = handler - self.nodeInfoById[node.nodeId] = node - await self.beaconPresence(nodeId: node.nodeId, reason: "connect") - self.startPresenceTask(nodeId: node.nodeId) - self.ensureGatewayPushTask() - } - - private func unregisterConnection(nodeId: String) async { - await self.beaconPresence(nodeId: nodeId, reason: "disconnect") - self.stopPresenceTask(nodeId: nodeId) - self.connections.removeValue(forKey: nodeId) - self.nodeInfoById.removeValue(forKey: nodeId) - self.chatSubscriptions[nodeId] = nil - self.stopGatewayPushTaskIfIdle() - } - - private struct VoiceTranscriptPayload: Codable, Sendable { - var text: String - var sessionKey: String? - } - - private func handleEvent(nodeId: String, evt: BridgeEventFrame) async { - switch evt.event { - case "chat.subscribe": - guard let json = evt.payloadJSON, let data = json.data(using: .utf8) else { return } - struct Subscribe: Codable { var sessionKey: String } - guard let payload = try? JSONDecoder().decode(Subscribe.self, from: data) else { return } - let key = payload.sessionKey.trimmingCharacters(in: .whitespacesAndNewlines) - guard !key.isEmpty else { return } - var set = self.chatSubscriptions[nodeId] ?? Set() - set.insert(key) - self.chatSubscriptions[nodeId] = set - - case "chat.unsubscribe": - guard let json = evt.payloadJSON, let data = json.data(using: .utf8) else { return } - struct Unsubscribe: Codable { var sessionKey: String } - guard let payload = try? JSONDecoder().decode(Unsubscribe.self, from: data) else { return } - let key = payload.sessionKey.trimmingCharacters(in: .whitespacesAndNewlines) - guard !key.isEmpty else { return } - var set = self.chatSubscriptions[nodeId] ?? Set() - set.remove(key) - self.chatSubscriptions[nodeId] = set.isEmpty ? nil : set - - case "voice.transcript": - guard let json = evt.payloadJSON, let data = json.data(using: .utf8) else { - return - } - guard let payload = try? JSONDecoder().decode(VoiceTranscriptPayload.self, from: data) else { - return - } - let text = payload.text.trimmingCharacters(in: .whitespacesAndNewlines) - guard !text.isEmpty else { return } - - let sessionKey = payload.sessionKey?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty - ?? "main" - - _ = await GatewayConnection.shared.sendAgent(GatewayAgentInvocation( - message: text, - sessionKey: sessionKey, - thinking: "low", - deliver: false, - to: nil, - channel: .last)) - - case "agent.request": - guard let json = evt.payloadJSON, let data = json.data(using: .utf8) else { - return - } - guard let link = try? JSONDecoder().decode(AgentDeepLink.self, from: data) else { - return - } - - let message = link.message.trimmingCharacters(in: .whitespacesAndNewlines) - guard !message.isEmpty else { return } - guard message.count <= 20000 else { return } - - let sessionKey = link.sessionKey?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty - ?? "node-\(nodeId)" - let thinking = link.thinking?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty - let to = link.to?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty - let channel = GatewayAgentChannel(raw: link.channel) - - _ = await GatewayConnection.shared.sendAgent(GatewayAgentInvocation( - message: message, - sessionKey: sessionKey, - thinking: thinking, - deliver: link.deliver, - to: to, - channel: channel)) - - default: - break - } - } - - private func handleRequest(nodeId: String, req: BridgeRPCRequest) async -> BridgeRPCResponse { - let allowed: Set = ["chat.history", "chat.send", "health"] - guard allowed.contains(req.method) else { - return BridgeRPCResponse( - id: req.id, - ok: false, - error: BridgeRPCError(code: "FORBIDDEN", message: "Method not allowed")) - } - - let params: [String: ClawdbotProtocol.AnyCodable]? - if let json = req.paramsJSON?.trimmingCharacters(in: .whitespacesAndNewlines), !json.isEmpty { - guard let data = json.data(using: .utf8) else { - return BridgeRPCResponse( - id: req.id, - ok: false, - error: BridgeRPCError(code: "INVALID_REQUEST", message: "paramsJSON not UTF-8")) - } - do { - params = try JSONDecoder().decode([String: ClawdbotProtocol.AnyCodable].self, from: data) - } catch { - return BridgeRPCResponse( - id: req.id, - ok: false, - error: BridgeRPCError(code: "INVALID_REQUEST", message: error.localizedDescription)) - } - } else { - params = nil - } - - do { - let data = try await GatewayConnection.shared.request(method: req.method, params: params, timeoutMs: 30000) - guard let json = String(data: data, encoding: .utf8) else { - return BridgeRPCResponse( - id: req.id, - ok: false, - error: BridgeRPCError(code: "UNAVAILABLE", message: "Response not UTF-8")) - } - return BridgeRPCResponse(id: req.id, ok: true, payloadJSON: json) - } catch { - return BridgeRPCResponse( - id: req.id, - ok: false, - error: BridgeRPCError(code: "UNAVAILABLE", message: error.localizedDescription)) - } - } - - private func ensureGatewayPushTask() { - if self.gatewayPushTask != nil { return } - self.gatewayPushTask = Task { [weak self] in - guard let self else { return } - do { - try await GatewayConnection.shared.refresh() - } catch { - // We'll still forward events once the gateway comes up. - } - let stream = await GatewayConnection.shared.subscribe() - for await push in stream { - if Task.isCancelled { return } - await self.forwardGatewayPush(push) - } - } - } - - private func stopGatewayPushTaskIfIdle() { - guard self.connections.isEmpty else { return } - self.gatewayPushTask?.cancel() - self.gatewayPushTask = nil - } - - private func forwardGatewayPush(_ push: GatewayPush) async { - let subscribedNodes = self.chatSubscriptions.keys.filter { self.connections[$0] != nil } - guard !subscribedNodes.isEmpty else { return } - - switch push { - case let .snapshot(hello): - let payloadJSON = (try? JSONEncoder().encode(hello.snapshot.health)) - .flatMap { String(data: $0, encoding: .utf8) } - for nodeId in subscribedNodes { - await self.connections[nodeId]?.sendServerEvent(event: "health", payloadJSON: payloadJSON) - } - case let .event(evt): - switch evt.event { - case "health": - guard let payload = evt.payload else { return } - let payloadJSON = (try? JSONEncoder().encode(payload)) - .flatMap { String(data: $0, encoding: .utf8) } - for nodeId in subscribedNodes { - await self.connections[nodeId]?.sendServerEvent(event: "health", payloadJSON: payloadJSON) - } - case "tick": - for nodeId in subscribedNodes { - await self.connections[nodeId]?.sendServerEvent(event: "tick", payloadJSON: nil) - } - case "chat": - guard let payload = evt.payload else { return } - let payloadData = try? JSONEncoder().encode(payload) - let payloadJSON = payloadData.flatMap { String(data: $0, encoding: .utf8) } - - struct MinimalChat: Codable { var sessionKey: String } - let sessionKey = payloadData.flatMap { try? JSONDecoder().decode(MinimalChat.self, from: $0) }? - .sessionKey - if let sessionKey { - for nodeId in subscribedNodes { - guard self.chatSubscriptions[nodeId]?.contains(sessionKey) == true else { continue } - await self.connections[nodeId]?.sendServerEvent(event: "chat", payloadJSON: payloadJSON) - } - } else { - for nodeId in subscribedNodes { - await self.connections[nodeId]?.sendServerEvent(event: "chat", payloadJSON: payloadJSON) - } - } - default: - break - } - case .seqGap: - for nodeId in subscribedNodes { - await self.connections[nodeId]?.sendServerEvent(event: "seqGap", payloadJSON: nil) - } - } - } - - private func beaconPresence(nodeId: String, reason: String) async { - let paired = await self.store?.find(nodeId: nodeId) - let host = paired?.displayName?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty - ?? nodeId - let version = paired?.version?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty - let platform = paired?.platform?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty - let ip = await self.connections[nodeId]?.remoteAddress() - - var tags: [String] = ["node", "ios"] - if let platform { tags.append(platform) } - - let summary = [ - "Node: \(host)\(ip.map { " (\($0))" } ?? "")", - platform.map { "platform \($0)" }, - version.map { "app \($0)" }, - "mode node", - "reason \(reason)", - ].compactMap(\.self).joined(separator: " · ") - - var params: [String: ClawdbotProtocol.AnyCodable] = [ - "text": ClawdbotProtocol.AnyCodable(summary), - "instanceId": ClawdbotProtocol.AnyCodable(nodeId), - "host": ClawdbotProtocol.AnyCodable(host), - "mode": ClawdbotProtocol.AnyCodable("node"), - "reason": ClawdbotProtocol.AnyCodable(reason), - "tags": ClawdbotProtocol.AnyCodable(tags), - ] - if let ip { params["ip"] = ClawdbotProtocol.AnyCodable(ip) } - if let version { params["version"] = ClawdbotProtocol.AnyCodable(version) } - await GatewayConnection.shared.sendSystemEvent(params) - } - - private func startPresenceTask(nodeId: String) { - self.presenceTasks[nodeId]?.cancel() - self.presenceTasks[nodeId] = Task.detached { [weak self] in - while !Task.isCancelled { - try? await Task.sleep(nanoseconds: 180 * 1_000_000_000) - if Task.isCancelled { return } - await self?.beaconPresence(nodeId: nodeId, reason: "periodic") - } - } - } - - private func stopPresenceTask(nodeId: String) { - self.presenceTasks[nodeId]?.cancel() - self.presenceTasks.removeValue(forKey: nodeId) - } - - private func authorize(hello: BridgeHello) async -> BridgeConnectionHandler.AuthResult { - let nodeId = hello.nodeId.trimmingCharacters(in: .whitespacesAndNewlines) - if nodeId.isEmpty { - return .error(code: "INVALID_REQUEST", message: "nodeId required") - } - guard let store = self.store else { - return .error(code: "UNAVAILABLE", message: "store unavailable") - } - guard let paired = await store.find(nodeId: nodeId) else { - return .notPaired - } - guard let token = hello.token, token == paired.token else { - return .unauthorized - } - - do { - var updated = paired - let name = hello.displayName?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty - let platform = hello.platform?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty - let version = hello.version?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty - let deviceFamily = hello.deviceFamily?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty - let modelIdentifier = hello.modelIdentifier?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty - - if updated.displayName != name { updated.displayName = name } - if updated.platform != platform { updated.platform = platform } - if updated.version != version { updated.version = version } - if updated.deviceFamily != deviceFamily { updated.deviceFamily = deviceFamily } - if updated.modelIdentifier != modelIdentifier { updated.modelIdentifier = modelIdentifier } - - if updated != paired { - try await store.upsert(updated) - } else { - try await store.touchSeen(nodeId: nodeId) - } - } catch { - // ignore - } - return .ok - } - - private func pair(request: BridgePairRequest) async -> BridgeConnectionHandler.PairResult { - let nodeId = request.nodeId.trimmingCharacters(in: .whitespacesAndNewlines) - if nodeId.isEmpty { - return .error(code: "INVALID_REQUEST", message: "nodeId required") - } - guard let store = self.store else { - return .error(code: "UNAVAILABLE", message: "store unavailable") - } - let existing = await store.find(nodeId: nodeId) - - let approved = await BridgePairingApprover.approve(request: request, isRepair: existing != nil) - if !approved { - return .rejected - } - - let token = UUID().uuidString.replacingOccurrences(of: "-", with: "") - let nowMs = Int(Date().timeIntervalSince1970 * 1000) - let node = PairedNode( - nodeId: nodeId, - displayName: request.displayName, - platform: request.platform, - version: request.version, - deviceFamily: request.deviceFamily, - modelIdentifier: request.modelIdentifier, - token: token, - createdAtMs: nowMs, - lastSeenAtMs: nowMs) - do { - try await store.upsert(node) - return .ok(token: token) - } catch { - return .error(code: "UNAVAILABLE", message: "failed to persist pairing") - } - } - - private static func defaultStoreURL() throws -> URL { - let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first - guard let base else { - throw NSError( - domain: "Bridge", - code: 1, - userInfo: [NSLocalizedDescriptionKey: "Application Support unavailable"]) - } - return base - .appendingPathComponent("Clawdbot", isDirectory: true) - .appendingPathComponent("bridge", isDirectory: true) - .appendingPathComponent("paired-nodes.json", isDirectory: false) - } -} - -@MainActor -enum BridgePairingApprover { - static func approve(request: BridgePairRequest, isRepair: Bool) async -> Bool { - await withCheckedContinuation { cont in - let name = request.displayName ?? request.nodeId - let remote = request.remoteAddress?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty - let alert = NSAlert() - alert.messageText = isRepair ? "Re-pair Clawdbot Node?" : "Pair Clawdbot Node?" - alert.informativeText = """ - Node: \(name) - IP: \(remote ?? "unknown") - Platform: \(request.platform ?? "unknown") - Version: \(request.version ?? "unknown") - """ - alert.addButton(withTitle: "Approve") - alert.addButton(withTitle: "Reject") - if #available(macOS 11.0, *), alert.buttons.indices.contains(1) { - alert.buttons[1].hasDestructiveAction = true - } - let resp = alert.runModal() - cont.resume(returning: resp == .alertFirstButtonReturn) - } - } -} - -#if DEBUG -extension BridgeServer { - func exerciseForTesting() async { - let conn = NWConnection(to: .hostPort(host: "127.0.0.1", port: 22), using: .tcp) - let handler = BridgeConnectionHandler(connection: conn, logger: self.logger) - self.connections["node-1"] = handler - self.nodeInfoById["node-1"] = BridgeNodeInfo( - nodeId: "node-1", - displayName: "Node One", - platform: "macOS", - version: "1.0.0", - deviceFamily: "Mac", - modelIdentifier: "MacBookPro18,1", - remoteAddress: "127.0.0.1", - caps: ["chat", "voice"]) - - _ = self.connectedNodeIds() - _ = self.connectedNodes() - - self.handleListenerState(.ready) - self.handleListenerState(.failed(NWError.posix(.ECONNREFUSED))) - self.handleListenerState(.waiting(NWError.posix(.ETIMEDOUT))) - self.handleListenerState(.cancelled) - self.handleListenerState(.setup) - - let subscribe = BridgeEventFrame(event: "chat.subscribe", payloadJSON: "{\"sessionKey\":\"main\"}") - await self.handleEvent(nodeId: "node-1", evt: subscribe) - - let unsubscribe = BridgeEventFrame(event: "chat.unsubscribe", payloadJSON: "{\"sessionKey\":\"main\"}") - await self.handleEvent(nodeId: "node-1", evt: unsubscribe) - - let invalid = BridgeRPCRequest(id: "req-1", method: "invalid.method", paramsJSON: nil) - _ = await self.handleRequest(nodeId: "node-1", req: invalid) - } -} -#endif diff --git a/apps/macos/Sources/Clawdbot/Bridge/PairedNodesStore.swift b/apps/macos/Sources/Clawdbot/Bridge/PairedNodesStore.swift deleted file mode 100644 index e78215635..000000000 --- a/apps/macos/Sources/Clawdbot/Bridge/PairedNodesStore.swift +++ /dev/null @@ -1,59 +0,0 @@ -import Foundation - -struct PairedNode: Codable, Equatable { - var nodeId: String - var displayName: String? - var platform: String? - var version: String? - var deviceFamily: String? - var modelIdentifier: String? - var token: String - var createdAtMs: Int - var lastSeenAtMs: Int? -} - -actor PairedNodesStore { - private let fileURL: URL - private var nodes: [String: PairedNode] = [:] - - init(fileURL: URL) { - self.fileURL = fileURL - } - - func load() { - do { - let data = try Data(contentsOf: self.fileURL) - let decoded = try JSONDecoder().decode([String: PairedNode].self, from: data) - self.nodes = decoded - } catch { - self.nodes = [:] - } - } - - func all() -> [PairedNode] { - self.nodes.values.sorted { a, b in (a.displayName ?? a.nodeId) < (b.displayName ?? b.nodeId) } - } - - func find(nodeId: String) -> PairedNode? { - self.nodes[nodeId] - } - - func upsert(_ node: PairedNode) async throws { - self.nodes[node.nodeId] = node - try await self.persist() - } - - func touchSeen(nodeId: String) async throws { - guard var node = self.nodes[nodeId] else { return } - node.lastSeenAtMs = Int(Date().timeIntervalSince1970 * 1000) - self.nodes[nodeId] = node - try await self.persist() - } - - private func persist() async throws { - let dir = self.fileURL.deletingLastPathComponent() - try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) - let data = try JSONEncoder().encode(self.nodes) - try data.write(to: self.fileURL, options: [.atomic]) - } -} diff --git a/apps/macos/Sources/Clawdbot/CLIInstaller.swift b/apps/macos/Sources/Clawdbot/CLIInstaller.swift index d967002f5..b9113e27a 100644 --- a/apps/macos/Sources/Clawdbot/CLIInstaller.swift +++ b/apps/macos/Sources/Clawdbot/CLIInstaller.swift @@ -61,7 +61,7 @@ enum CLIInstaller { } private static func installPrefix() -> String { - FileManager.default.homeDirectoryForCurrentUser + FileManager().homeDirectoryForCurrentUser .appendingPathComponent(".clawdbot") .path } diff --git a/apps/macos/Sources/Clawdbot/CameraCaptureService.swift b/apps/macos/Sources/Clawdbot/CameraCaptureService.swift index b7a7de308..c9893bbfb 100644 --- a/apps/macos/Sources/Clawdbot/CameraCaptureService.swift +++ b/apps/macos/Sources/Clawdbot/CameraCaptureService.swift @@ -167,20 +167,20 @@ actor CameraCaptureService { defer { session.stopRunning() } await Self.warmUpCaptureSession() - let tmpMovURL = FileManager.default.temporaryDirectory + let tmpMovURL = FileManager().temporaryDirectory .appendingPathComponent("clawdbot-camera-\(UUID().uuidString).mov") - defer { try? FileManager.default.removeItem(at: tmpMovURL) } + defer { try? FileManager().removeItem(at: tmpMovURL) } let outputURL: URL = { if let outPath, !outPath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return URL(fileURLWithPath: outPath) } - return FileManager.default.temporaryDirectory + return FileManager().temporaryDirectory .appendingPathComponent("clawdbot-camera-\(UUID().uuidString).mp4") }() // Ensure we don't fail exporting due to an existing file. - try? FileManager.default.removeItem(at: outputURL) + try? FileManager().removeItem(at: outputURL) let logger = self.logger var delegate: MovieFileDelegate? diff --git a/apps/macos/Sources/Clawdbot/CanvasManager.swift b/apps/macos/Sources/Clawdbot/CanvasManager.swift index 2c0f73253..f65c123ca 100644 --- a/apps/macos/Sources/Clawdbot/CanvasManager.swift +++ b/apps/macos/Sources/Clawdbot/CanvasManager.swift @@ -1,5 +1,6 @@ import AppKit import ClawdbotIPC +import ClawdbotKit import Foundation import OSLog @@ -24,7 +25,7 @@ final class CanvasManager { var defaultAnchorProvider: (() -> NSRect?)? private nonisolated static let canvasRoot: URL = { - let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! + let base = FileManager().urls(for: .applicationSupportDirectory, in: .userDomainMask).first! return base.appendingPathComponent("Clawdbot/canvas", isDirectory: true) }() @@ -82,7 +83,7 @@ final class CanvasManager { self.panelSessionKey = nil Self.logger.debug("showDetailed ensure canvas root dir") - try FileManager.default.createDirectory(at: Self.canvasRoot, withIntermediateDirectories: true) + try FileManager().createDirectory(at: Self.canvasRoot, withIntermediateDirectories: true) Self.logger.debug("showDetailed init CanvasWindowController") let controller = try CanvasWindowController( sessionKey: session, @@ -257,7 +258,7 @@ final class CanvasManager { // (Avoid treating Canvas routes like "/" as filesystem paths.) if trimmed.hasPrefix("/") { var isDir: ObjCBool = false - if FileManager.default.fileExists(atPath: trimmed, isDirectory: &isDir), !isDir.boolValue { + if FileManager().fileExists(atPath: trimmed, isDirectory: &isDir), !isDir.boolValue { return URL(fileURLWithPath: trimmed) } } @@ -292,7 +293,7 @@ final class CanvasManager { } private static func localStatus(sessionDir: URL, target: String) -> CanvasShowStatus { - let fm = FileManager.default + let fm = FileManager() let trimmed = target.trimmingCharacters(in: .whitespacesAndNewlines) let withoutQuery = trimmed.split(separator: "?", maxSplits: 1, omittingEmptySubsequences: false).first .map(String.init) ?? trimmed @@ -330,7 +331,7 @@ final class CanvasManager { } private static func indexExists(in dir: URL) -> Bool { - let fm = FileManager.default + let fm = FileManager() let a = dir.appendingPathComponent("index.html", isDirectory: false) if fm.fileExists(atPath: a.path) { return true } let b = dir.appendingPathComponent("index.htm", isDirectory: false) diff --git a/apps/macos/Sources/Clawdbot/CanvasSchemeHandler.swift b/apps/macos/Sources/Clawdbot/CanvasSchemeHandler.swift index 8617a2d49..27dbd93bf 100644 --- a/apps/macos/Sources/Clawdbot/CanvasSchemeHandler.swift +++ b/apps/macos/Sources/Clawdbot/CanvasSchemeHandler.swift @@ -69,8 +69,8 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler { if path.isEmpty { let indexA = sessionRoot.appendingPathComponent("index.html", isDirectory: false) let indexB = sessionRoot.appendingPathComponent("index.htm", isDirectory: false) - if !FileManager.default.fileExists(atPath: indexA.path), - !FileManager.default.fileExists(atPath: indexB.path) + if !FileManager().fileExists(atPath: indexA.path), + !FileManager().fileExists(atPath: indexB.path) { return self.scaffoldPage(sessionRoot: sessionRoot) } @@ -106,7 +106,7 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler { } private func resolveFileURL(sessionRoot: URL, requestPath: String) -> URL? { - let fm = FileManager.default + let fm = FileManager() var candidate = sessionRoot.appendingPathComponent(requestPath, isDirectory: false) var isDir: ObjCBool = false @@ -137,7 +137,7 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler { } private func resolveIndex(in dir: URL) -> URL? { - let fm = FileManager.default + let fm = FileManager() let a = dir.appendingPathComponent("index.html", isDirectory: false) if fm.fileExists(atPath: a.path) { return a } let b = dir.appendingPathComponent("index.htm", isDirectory: false) diff --git a/apps/macos/Sources/Clawdbot/CanvasWindowController.swift b/apps/macos/Sources/Clawdbot/CanvasWindowController.swift index d0d4e4ff5..b119efd85 100644 --- a/apps/macos/Sources/Clawdbot/CanvasWindowController.swift +++ b/apps/macos/Sources/Clawdbot/CanvasWindowController.swift @@ -32,7 +32,7 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS let safeSessionKey = CanvasWindowController.sanitizeSessionKey(sessionKey) canvasWindowLogger.debug("CanvasWindowController init sanitized session=\(safeSessionKey, privacy: .public)") self.sessionDir = root.appendingPathComponent(safeSessionKey, isDirectory: true) - try FileManager.default.createDirectory(at: self.sessionDir, withIntermediateDirectories: true) + try FileManager().createDirectory(at: self.sessionDir, withIntermediateDirectories: true) canvasWindowLogger.debug("CanvasWindowController init session dir ready") self.schemeHandler = CanvasSchemeHandler(root: root) @@ -143,8 +143,8 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS if path == "/" || path.isEmpty { let indexA = sessionDir.appendingPathComponent("index.html", isDirectory: false) let indexB = sessionDir.appendingPathComponent("index.htm", isDirectory: false) - if !FileManager.default.fileExists(atPath: indexA.path), - !FileManager.default.fileExists(atPath: indexB.path) + if !FileManager().fileExists(atPath: indexA.path), + !FileManager().fileExists(atPath: indexB.path) { return } @@ -233,7 +233,7 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS // (Avoid treating Canvas routes like "/" as filesystem paths.) if trimmed.hasPrefix("/") { var isDir: ObjCBool = false - if FileManager.default.fileExists(atPath: trimmed, isDirectory: &isDir), !isDir.boolValue { + if FileManager().fileExists(atPath: trimmed, isDirectory: &isDir), !isDir.boolValue { let url = URL(fileURLWithPath: trimmed) canvasWindowLogger.debug("canvas load file \(url.absoluteString, privacy: .public)") self.loadFile(url) diff --git a/apps/macos/Sources/Clawdbot/ChannelsSettings+ChannelState.swift b/apps/macos/Sources/Clawdbot/ChannelsSettings+ChannelState.swift index e4def6116..79dd97cf9 100644 --- a/apps/macos/Sources/Clawdbot/ChannelsSettings+ChannelState.swift +++ b/apps/macos/Sources/Clawdbot/ChannelsSettings+ChannelState.swift @@ -426,34 +426,17 @@ extension ChannelsSettings { } private func resolveChannelTitle(_ id: String) -> String { - if let label = self.store.snapshot?.channelLabels[id], !label.isEmpty { - return label - } + let label = self.store.resolveChannelLabel(id) + if label != id { return label } return id.prefix(1).uppercased() + id.dropFirst() } private func resolveChannelDetailTitle(_ id: String) -> String { - switch id { - case "whatsapp": "WhatsApp Web" - case "telegram": "Telegram Bot" - case "discord": "Discord Bot" - case "slack": "Slack Bot" - case "signal": "Signal REST" - case "imessage": "iMessage" - default: self.resolveChannelTitle(id) - } + self.store.resolveChannelDetailLabel(id) } private func resolveChannelSystemImage(_ id: String) -> String { - switch id { - case "whatsapp": "message" - case "telegram": "paperplane" - case "discord": "bubble.left.and.bubble.right" - case "slack": "number" - case "signal": "antenna.radiowaves.left.and.right" - case "imessage": "message.fill" - default: "message" - } + self.store.resolveChannelSystemImage(id) } private func channelStatusDictionary(_ id: String) -> [String: AnyCodable]? { diff --git a/apps/macos/Sources/Clawdbot/ChannelsStore.swift b/apps/macos/Sources/Clawdbot/ChannelsStore.swift index 13a68ab2f..e62e737a4 100644 --- a/apps/macos/Sources/Clawdbot/ChannelsStore.swift +++ b/apps/macos/Sources/Clawdbot/ChannelsStore.swift @@ -153,9 +153,19 @@ struct ChannelsStatusSnapshot: Codable { let application: AnyCodable? } + struct ChannelUiMetaEntry: Codable { + let id: String + let label: String + let detailLabel: String + let systemImage: String? + } + let ts: Double let channelOrder: [String] let channelLabels: [String: String] + let channelDetailLabels: [String: String]? + let channelSystemImages: [String: String]? + let channelMeta: [ChannelUiMetaEntry]? let channels: [String: AnyCodable] let channelAccounts: [String: [ChannelAccountSnapshot]] let channelDefaultAccountId: [String: String] @@ -217,6 +227,47 @@ final class ChannelsStore { var configRoot: [String: Any] = [:] var configLoaded = false + func channelMetaEntry(_ id: String) -> ChannelsStatusSnapshot.ChannelUiMetaEntry? { + self.snapshot?.channelMeta?.first(where: { $0.id == id }) + } + + func resolveChannelLabel(_ id: String) -> String { + if let meta = self.channelMetaEntry(id), !meta.label.isEmpty { + return meta.label + } + if let label = self.snapshot?.channelLabels[id], !label.isEmpty { + return label + } + return id + } + + func resolveChannelDetailLabel(_ id: String) -> String { + if let meta = self.channelMetaEntry(id), !meta.detailLabel.isEmpty { + return meta.detailLabel + } + if let detail = self.snapshot?.channelDetailLabels?[id], !detail.isEmpty { + return detail + } + return self.resolveChannelLabel(id) + } + + func resolveChannelSystemImage(_ id: String) -> String { + if let meta = self.channelMetaEntry(id), let symbol = meta.systemImage, !symbol.isEmpty { + return symbol + } + if let symbol = self.snapshot?.channelSystemImages?[id], !symbol.isEmpty { + return symbol + } + return "message" + } + + func orderedChannelIds() -> [String] { + if let meta = self.snapshot?.channelMeta, !meta.isEmpty { + return meta.map(\.id) + } + return self.snapshot?.channelOrder ?? [] + } + init(isPreview: Bool = ProcessInfo.processInfo.isPreview) { self.isPreview = isPreview } diff --git a/apps/macos/Sources/Clawdbot/ClawdbotConfigFile.swift b/apps/macos/Sources/Clawdbot/ClawdbotConfigFile.swift index 6b2169010..1c054b557 100644 --- a/apps/macos/Sources/Clawdbot/ClawdbotConfigFile.swift +++ b/apps/macos/Sources/Clawdbot/ClawdbotConfigFile.swift @@ -18,7 +18,7 @@ enum ClawdbotConfigFile { static func loadDict() -> [String: Any] { let url = self.url() - guard FileManager.default.fileExists(atPath: url.path) else { return [:] } + guard FileManager().fileExists(atPath: url.path) else { return [:] } do { let data = try Data(contentsOf: url) guard let root = self.parseConfigData(data) else { @@ -38,7 +38,7 @@ enum ClawdbotConfigFile { do { let data = try JSONSerialization.data(withJSONObject: dict, options: [.prettyPrinted, .sortedKeys]) let url = self.url() - try FileManager.default.createDirectory( + try FileManager().createDirectory( at: url.deletingLastPathComponent(), withIntermediateDirectories: true) try data.write(to: url, options: [.atomic]) diff --git a/apps/macos/Sources/Clawdbot/ClawdbotPaths.swift b/apps/macos/Sources/Clawdbot/ClawdbotPaths.swift index 3e32782c0..7cda49ea6 100644 --- a/apps/macos/Sources/Clawdbot/ClawdbotPaths.swift +++ b/apps/macos/Sources/Clawdbot/ClawdbotPaths.swift @@ -21,7 +21,7 @@ enum ClawdbotPaths { if let override = ClawdbotEnv.path(self.stateDirEnv) { return URL(fileURLWithPath: override, isDirectory: true) } - return FileManager.default.homeDirectoryForCurrentUser + return FileManager().homeDirectoryForCurrentUser .appendingPathComponent(".clawdbot", isDirectory: true) } diff --git a/apps/macos/Sources/Clawdbot/CommandResolver.swift b/apps/macos/Sources/Clawdbot/CommandResolver.swift index 9e8ae1c41..7661c48f1 100644 --- a/apps/macos/Sources/Clawdbot/CommandResolver.swift +++ b/apps/macos/Sources/Clawdbot/CommandResolver.swift @@ -6,9 +6,9 @@ enum CommandResolver { static func gatewayEntrypoint(in root: URL) -> String? { let distEntry = root.appendingPathComponent("dist/index.js").path - if FileManager.default.isReadableFile(atPath: distEntry) { return distEntry } + if FileManager().isReadableFile(atPath: distEntry) { return distEntry } let binEntry = root.appendingPathComponent("bin/clawdbot.js").path - if FileManager.default.isReadableFile(atPath: binEntry) { return binEntry } + if FileManager().isReadableFile(atPath: binEntry) { return binEntry } return nil } @@ -47,16 +47,16 @@ enum CommandResolver { static func projectRoot() -> URL { if let stored = UserDefaults.standard.string(forKey: self.projectRootDefaultsKey), let url = self.expandPath(stored), - FileManager.default.fileExists(atPath: url.path) + FileManager().fileExists(atPath: url.path) { return url } - let fallback = FileManager.default.homeDirectoryForCurrentUser + let fallback = FileManager().homeDirectoryForCurrentUser .appendingPathComponent("Projects/clawdbot") - if FileManager.default.fileExists(atPath: fallback.path) { + if FileManager().fileExists(atPath: fallback.path) { return fallback } - return FileManager.default.homeDirectoryForCurrentUser + return FileManager().homeDirectoryForCurrentUser } static func setProjectRoot(_ path: String) { @@ -70,7 +70,7 @@ enum CommandResolver { static func preferredPaths() -> [String] { let current = ProcessInfo.processInfo.environment["PATH"]? .split(separator: ":").map(String.init) ?? [] - let home = FileManager.default.homeDirectoryForCurrentUser + let home = FileManager().homeDirectoryForCurrentUser let projectRoot = self.projectRoot() return self.preferredPaths(home: home, current: current, projectRoot: projectRoot) } @@ -99,10 +99,10 @@ enum CommandResolver { let bin = base.appendingPathComponent("bin") let nodeBin = base.appendingPathComponent("tools/node/bin") var paths: [String] = [] - if FileManager.default.fileExists(atPath: bin.path) { + if FileManager().fileExists(atPath: bin.path) { paths.append(bin.path) } - if FileManager.default.fileExists(atPath: nodeBin.path) { + if FileManager().fileExists(atPath: nodeBin.path) { paths.append(nodeBin.path) } return paths @@ -113,13 +113,13 @@ enum CommandResolver { // Volta let volta = home.appendingPathComponent(".volta/bin") - if FileManager.default.fileExists(atPath: volta.path) { + if FileManager().fileExists(atPath: volta.path) { bins.append(volta.path) } // asdf let asdf = home.appendingPathComponent(".asdf/shims") - if FileManager.default.fileExists(atPath: asdf.path) { + if FileManager().fileExists(atPath: asdf.path) { bins.append(asdf.path) } @@ -137,10 +137,10 @@ enum CommandResolver { } private static func versionedNodeBinPaths(base: URL, suffix: String) -> [String] { - guard FileManager.default.fileExists(atPath: base.path) else { return [] } + guard FileManager().fileExists(atPath: base.path) else { return [] } let entries: [String] do { - entries = try FileManager.default.contentsOfDirectory(atPath: base.path) + entries = try FileManager().contentsOfDirectory(atPath: base.path) } catch { return [] } @@ -167,7 +167,7 @@ enum CommandResolver { for entry in sorted { let binDir = base.appendingPathComponent(entry).appendingPathComponent(suffix) let node = binDir.appendingPathComponent("node") - if FileManager.default.isExecutableFile(atPath: node.path) { + if FileManager().isExecutableFile(atPath: node.path) { paths.append(binDir.path) } } @@ -177,7 +177,7 @@ enum CommandResolver { static func findExecutable(named name: String, searchPaths: [String]? = nil) -> String? { for dir in searchPaths ?? self.preferredPaths() { let candidate = (dir as NSString).appendingPathComponent(name) - if FileManager.default.isExecutableFile(atPath: candidate) { + if FileManager().isExecutableFile(atPath: candidate) { return candidate } } @@ -191,12 +191,12 @@ enum CommandResolver { static func projectClawdbotExecutable(projectRoot: URL? = nil) -> String? { let root = projectRoot ?? self.projectRoot() let candidate = root.appendingPathComponent("node_modules/.bin").appendingPathComponent(self.helperName).path - return FileManager.default.isExecutableFile(atPath: candidate) ? candidate : nil + return FileManager().isExecutableFile(atPath: candidate) ? candidate : nil } static func nodeCliPath() -> String? { let candidate = self.projectRoot().appendingPathComponent("bin/clawdbot.js").path - return FileManager.default.isReadableFile(atPath: candidate) ? candidate : nil + return FileManager().isReadableFile(atPath: candidate) ? candidate : nil } static func hasAnyClawdbotInvoker(searchPaths: [String]? = nil) -> Bool { @@ -284,13 +284,16 @@ enum CommandResolver { var args: [String] = [ "-o", "BatchMode=yes", - "-o", "IdentitiesOnly=yes", "-o", "StrictHostKeyChecking=accept-new", "-o", "UpdateHostKeys=yes", ] if parsed.port > 0 { args.append(contentsOf: ["-p", String(parsed.port)]) } - if !settings.identity.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - args.append(contentsOf: ["-i", settings.identity]) + 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) @@ -459,7 +462,7 @@ enum CommandResolver { private static func expandPath(_ path: String) -> URL? { var expanded = path if expanded.hasPrefix("~") { - let home = FileManager.default.homeDirectoryForCurrentUser.path + let home = FileManager().homeDirectoryForCurrentUser.path expanded.replaceSubrange(expanded.startIndex...expanded.startIndex, with: home) } return URL(fileURLWithPath: expanded) diff --git a/apps/macos/Sources/Clawdbot/ConfigSettings.swift b/apps/macos/Sources/Clawdbot/ConfigSettings.swift index d6962a324..3846ec55f 100644 --- a/apps/macos/Sources/Clawdbot/ConfigSettings.swift +++ b/apps/macos/Sources/Clawdbot/ConfigSettings.swift @@ -6,15 +6,19 @@ struct ConfigSettings: View { private let isNixMode = ProcessInfo.processInfo.isNixMode @Bindable var store: ChannelsStore @State private var hasLoaded = false + @State private var activeSectionKey: String? + @State private var activeSubsection: SubsectionSelection? init(store: ChannelsStore = .shared) { self.store = store } var body: some View { - ScrollView { - self.content + HStack(spacing: 16) { + self.sidebar + self.detail } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .task { guard !self.hasLoaded else { return } guard !self.isPreview else { return } @@ -22,42 +26,125 @@ struct ConfigSettings: View { await self.store.loadConfigSchema() await self.store.loadConfig() } + .onAppear { self.ensureSelection() } + .onChange(of: self.store.configSchemaLoading) { _, loading in + if !loading { self.ensureSelection() } + } } } extension ConfigSettings { - private var content: some View { - VStack(alignment: .leading, spacing: 16) { - self.header - if let status = self.store.configStatus { - Text(status) - .font(.callout) - .foregroundStyle(.secondary) - } - self.actionRow - Group { - if self.store.configSchemaLoading { - ProgressView().controlSize(.small) - } else if let schema = self.store.configSchema { - ConfigSchemaForm(store: self.store, schema: schema, path: []) - .disabled(self.isNixMode) - } else { - Text("Schema unavailable.") + private enum SubsectionSelection: Hashable { + case all + case key(String) + } + + private struct ConfigSection: Identifiable { + let key: String + let label: String + let help: String? + let node: ConfigSchemaNode + + var id: String { self.key } + } + + private struct ConfigSubsection: Identifiable { + let key: String + let label: String + let help: String? + let node: ConfigSchemaNode + let path: ConfigPath + + var id: String { self.key } + } + + private var sections: [ConfigSection] { + guard let schema = self.store.configSchema else { return [] } + return self.resolveSections(schema) + } + + private var activeSection: ConfigSection? { + self.sections.first { $0.key == self.activeSectionKey } + } + + private var sidebar: some View { + ScrollView { + LazyVStack(alignment: .leading, spacing: 8) { + if self.sections.isEmpty { + Text("No config sections available.") .font(.caption) .foregroundStyle(.secondary) + .padding(.horizontal, 6) + .padding(.vertical, 4) + } else { + ForEach(self.sections) { section in + self.sidebarRow(section) + } } } - if self.store.configDirty, !self.isNixMode { - Text("Unsaved changes") + .padding(.vertical, 10) + .padding(.horizontal, 10) + } + .frame(minWidth: 220, idealWidth: 240, maxWidth: 280, maxHeight: .infinity, alignment: .topLeading) + .background( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(Color(nsColor: .windowBackgroundColor))) + .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) + } + + private var detail: some View { + VStack(alignment: .leading, spacing: 16) { + if self.store.configSchemaLoading { + ProgressView().controlSize(.small) + } else if let section = self.activeSection { + self.sectionDetail(section) + } else if self.store.configSchema != nil { + self.emptyDetail + } else { + Text("Schema unavailable.") .font(.caption) .foregroundStyle(.secondary) } - Spacer(minLength: 0) } - .frame(maxWidth: .infinity, alignment: .leading) + .frame(minWidth: 460, maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + } + + private var emptyDetail: some View { + VStack(alignment: .leading, spacing: 8) { + self.header + Text("Select a config section to view settings.") + .font(.callout) + .foregroundStyle(.secondary) + } .padding(.horizontal, 24) .padding(.vertical, 18) - .groupBoxStyle(PlainSettingsGroupBoxStyle()) + } + + private func sectionDetail(_ section: ConfigSection) -> some View { + ScrollView(.vertical) { + VStack(alignment: .leading, spacing: 16) { + self.header + if let status = self.store.configStatus { + Text(status) + .font(.callout) + .foregroundStyle(.secondary) + } + self.actionRow + self.sectionHeader(section) + self.subsectionNav(section) + self.sectionForm(section) + if self.store.configDirty, !self.isNixMode { + Text("Unsaved changes") + .font(.caption) + .foregroundStyle(.secondary) + } + Spacer(minLength: 0) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 24) + .padding(.vertical, 18) + .groupBoxStyle(PlainSettingsGroupBoxStyle()) + } } @ViewBuilder @@ -71,6 +158,18 @@ extension ConfigSettings { .foregroundStyle(.secondary) } + private func sectionHeader(_ section: ConfigSection) -> some View { + VStack(alignment: .leading, spacing: 6) { + Text(section.label) + .font(.title3.weight(.semibold)) + if let help = section.help { + Text(help) + .font(.callout) + .foregroundStyle(.secondary) + } + } + } + private var actionRow: some View { HStack(spacing: 10) { Button("Reload") { @@ -85,6 +184,204 @@ extension ConfigSettings { } .buttonStyle(.bordered) } + + private func sidebarRow(_ section: ConfigSection) -> some View { + let isSelected = self.activeSectionKey == section.key + return Button { + self.selectSection(section) + } label: { + VStack(alignment: .leading, spacing: 2) { + Text(section.label) + if let help = section.help { + Text(help) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(2) + } + } + .padding(.vertical, 6) + .padding(.horizontal, 8) + .frame(maxWidth: .infinity, alignment: .leading) + .background(isSelected ? Color.accentColor.opacity(0.18) : Color.clear) + .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) + .background(Color.clear) + .contentShape(Rectangle()) + } + .frame(maxWidth: .infinity, alignment: .leading) + .buttonStyle(.plain) + .contentShape(Rectangle()) + } + + @ViewBuilder + private func subsectionNav(_ section: ConfigSection) -> some View { + let subsections = self.resolveSubsections(for: section) + if subsections.isEmpty { + EmptyView() + } else { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + self.subsectionButton( + title: "All", + isSelected: self.activeSubsection == .all) + { + self.activeSubsection = .all + } + ForEach(subsections) { subsection in + self.subsectionButton( + title: subsection.label, + isSelected: self.activeSubsection == .key(subsection.key)) + { + self.activeSubsection = .key(subsection.key) + } + } + } + .padding(.vertical, 2) + } + } + } + + private func subsectionButton( + title: String, + isSelected: Bool, + action: @escaping () -> Void) -> some View + { + Button(action: action) { + Text(title) + .font(.callout.weight(.semibold)) + .foregroundStyle(isSelected ? Color.accentColor : .primary) + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background(isSelected ? Color.accentColor.opacity(0.18) : Color(nsColor: .controlBackgroundColor)) + .clipShape(Capsule()) + } + .buttonStyle(.plain) + } + + private func sectionForm(_ section: ConfigSection) -> some View { + let subsection = self.activeSubsection + let defaultPath: ConfigPath = [.key(section.key)] + let subsections = self.resolveSubsections(for: section) + let resolved: (ConfigSchemaNode, ConfigPath) = { + if case let .key(key) = subsection, + let match = subsections.first(where: { $0.key == key }) + { + return (match.node, match.path) + } + return (self.resolvedSchemaNode(section.node), defaultPath) + }() + + return ConfigSchemaForm(store: self.store, schema: resolved.0, path: resolved.1) + .disabled(self.isNixMode) + } + + private func ensureSelection() { + guard let schema = self.store.configSchema else { return } + let sections = self.resolveSections(schema) + guard !sections.isEmpty else { return } + + let active = sections.first { $0.key == self.activeSectionKey } ?? sections[0] + if self.activeSectionKey != active.key { + self.activeSectionKey = active.key + } + self.ensureSubsection(for: active) + } + + private func ensureSubsection(for section: ConfigSection) { + let subsections = self.resolveSubsections(for: section) + guard !subsections.isEmpty else { + self.activeSubsection = nil + return + } + + switch self.activeSubsection { + case .all: + return + case let .key(key): + if subsections.contains(where: { $0.key == key }) { return } + case .none: + break + } + + if let first = subsections.first { + self.activeSubsection = .key(first.key) + } + } + + private func selectSection(_ section: ConfigSection) { + guard self.activeSectionKey != section.key else { return } + self.activeSectionKey = section.key + let subsections = self.resolveSubsections(for: section) + if let first = subsections.first { + self.activeSubsection = .key(first.key) + } else { + self.activeSubsection = nil + } + } + + private func resolveSections(_ root: ConfigSchemaNode) -> [ConfigSection] { + let node = self.resolvedSchemaNode(root) + let hints = self.store.configUiHints + let keys = node.properties.keys.sorted { lhs, rhs in + let orderA = hintForPath([.key(lhs)], hints: hints)?.order ?? 0 + let orderB = hintForPath([.key(rhs)], hints: hints)?.order ?? 0 + if orderA != orderB { return orderA < orderB } + return lhs < rhs + } + + return keys.compactMap { key in + guard let child = node.properties[key] else { return nil } + let path: ConfigPath = [.key(key)] + let hint = hintForPath(path, hints: hints) + let label = hint?.label + ?? child.title + ?? self.humanize(key) + let help = hint?.help ?? child.description + return ConfigSection(key: key, label: label, help: help, node: child) + } + } + + private func resolveSubsections(for section: ConfigSection) -> [ConfigSubsection] { + let node = self.resolvedSchemaNode(section.node) + guard node.schemaType == "object" else { return [] } + let hints = self.store.configUiHints + let keys = node.properties.keys.sorted { lhs, rhs in + let orderA = hintForPath([.key(section.key), .key(lhs)], hints: hints)?.order ?? 0 + let orderB = hintForPath([.key(section.key), .key(rhs)], hints: hints)?.order ?? 0 + if orderA != orderB { return orderA < orderB } + return lhs < rhs + } + + return keys.compactMap { key in + guard let child = node.properties[key] else { return nil } + let path: ConfigPath = [.key(section.key), .key(key)] + let hint = hintForPath(path, hints: hints) + let label = hint?.label + ?? child.title + ?? self.humanize(key) + let help = hint?.help ?? child.description + return ConfigSubsection( + key: key, + label: label, + help: help, + node: child, + path: path) + } + } + + private func resolvedSchemaNode(_ node: ConfigSchemaNode) -> ConfigSchemaNode { + let variants = node.anyOf.isEmpty ? node.oneOf : node.anyOf + if !variants.isEmpty { + let nonNull = variants.filter { !$0.isNullSchema } + if nonNull.count == 1, let only = nonNull.first { return only } + } + return node + } + + private func humanize(_ key: String) -> String { + key.replacingOccurrences(of: "_", with: " ") + .replacingOccurrences(of: "-", with: " ") + .capitalized + } } struct ConfigSettings_Previews: PreviewProvider { diff --git a/apps/macos/Sources/Clawdbot/ConnectionModeCoordinator.swift b/apps/macos/Sources/Clawdbot/ConnectionModeCoordinator.swift index 1f6d829f1..00f93bd85 100644 --- a/apps/macos/Sources/Clawdbot/ConnectionModeCoordinator.swift +++ b/apps/macos/Sources/Clawdbot/ConnectionModeCoordinator.swift @@ -6,12 +6,20 @@ final class ConnectionModeCoordinator { static let shared = ConnectionModeCoordinator() private let logger = Logger(subsystem: "com.clawdbot", category: "connection") + private var lastMode: AppState.ConnectionMode? /// Apply the requested connection mode by starting/stopping local gateway, /// managing the control-channel SSH tunnel, and cleaning up chat windows/panels. func apply(mode: AppState.ConnectionMode, paused: Bool) async { + if let lastMode = self.lastMode, lastMode != mode { + GatewayProcessManager.shared.clearLastFailure() + NodesStore.shared.lastError = nil + } + self.lastMode = mode switch mode { case .unconfigured: + _ = await NodeServiceManager.stop() + NodesStore.shared.lastError = nil await RemoteTunnelManager.shared.stopAll() WebChatManager.shared.resetTunnels() GatewayProcessManager.shared.stop() @@ -20,6 +28,8 @@ final class ConnectionModeCoordinator { Task.detached { await PortGuardian.shared.sweep(mode: .unconfigured) } case .local: + _ = await NodeServiceManager.stop() + NodesStore.shared.lastError = nil await RemoteTunnelManager.shared.stopAll() WebChatManager.shared.resetTunnels() let shouldStart = GatewayAutostartPolicy.shouldStartGateway(mode: .local, paused: paused) @@ -50,6 +60,10 @@ final class ConnectionModeCoordinator { WebChatManager.shared.resetTunnels() do { + NodesStore.shared.lastError = nil + if let error = await NodeServiceManager.start() { + NodesStore.shared.lastError = "Node service start failed: \(error)" + } _ = try await GatewayEndpointStore.shared.ensureRemoteControlTunnel() let settings = CommandResolver.connectionSettings() try await ControlChannel.shared.configure(mode: .remote( diff --git a/apps/macos/Sources/Clawdbot/Constants.swift b/apps/macos/Sources/Clawdbot/Constants.swift index 25f2589e3..b55bd6d20 100644 --- a/apps/macos/Sources/Clawdbot/Constants.swift +++ b/apps/macos/Sources/Clawdbot/Constants.swift @@ -12,6 +12,8 @@ let voiceWakeTriggerChimeKey = "clawdbot.voiceWakeTriggerChime" let voiceWakeSendChimeKey = "clawdbot.voiceWakeSendChime" let showDockIconKey = "clawdbot.showDockIcon" let defaultVoiceWakeTriggers = ["clawd", "claude"] +let voiceWakeMaxWords = 32 +let voiceWakeMaxWordLength = 64 let voiceWakeMicKey = "clawdbot.voiceWakeMicID" let voiceWakeMicNameKey = "clawdbot.voiceWakeMicName" let voiceWakeLocaleKey = "clawdbot.voiceWakeLocaleID" diff --git a/apps/macos/Sources/Clawdbot/ControlChannel.swift b/apps/macos/Sources/Clawdbot/ControlChannel.swift index 78160a43a..4c47ee26e 100644 --- a/apps/macos/Sources/Clawdbot/ControlChannel.swift +++ b/apps/macos/Sources/Clawdbot/ControlChannel.swift @@ -1,3 +1,4 @@ +import ClawdbotKit import ClawdbotProtocol import Foundation import Observation @@ -19,7 +20,7 @@ struct ControlAgentEvent: Codable, Sendable, Identifiable { let seq: Int let stream: String let ts: Double - let data: [String: AnyCodable] + let data: [String: ClawdbotProtocol.AnyCodable] let summary: String? } @@ -73,6 +74,7 @@ final class ControlChannel { } private(set) var lastPingMs: Double? + private(set) var authSourceLabel: String? private let logger = Logger(subsystem: "com.clawdbot", category: "control") @@ -86,15 +88,7 @@ final class ControlChannel { func configure() async { self.logger.info("control channel configure mode=local") - self.state = .connecting - do { - try await GatewayConnection.shared.refresh() - self.state = .connected - PresenceReporter.shared.sendImmediate(reason: "connect") - } catch { - let message = self.friendlyGatewayMessage(error) - self.state = .degraded(message) - } + await self.refreshEndpoint(reason: "configure") } func configure(mode: Mode = .local) async throws { @@ -110,7 +104,7 @@ final class ControlChannel { "target=\(target, privacy: .public) identitySet=\(idSet, privacy: .public)") self.state = .connecting _ = try await GatewayEndpointStore.shared.ensureRemoteControlTunnel() - await self.configure() + await self.refreshEndpoint(reason: "configure") } catch { self.state = .degraded(error.localizedDescription) throw error @@ -118,10 +112,24 @@ final class ControlChannel { } } + func refreshEndpoint(reason: String) async { + self.logger.info("control channel refresh endpoint reason=\(reason, privacy: .public)") + self.state = .connecting + do { + try await self.establishGatewayConnection() + self.state = .connected + PresenceReporter.shared.sendImmediate(reason: "connect") + } catch { + let message = self.friendlyGatewayMessage(error) + self.state = .degraded(message) + } + } + func disconnect() async { await GatewayConnection.shared.shutdown() self.state = .disconnected self.lastPingMs = nil + self.authSourceLabel = nil } func health(timeout: TimeInterval? = nil) async throws -> Data { @@ -155,8 +163,8 @@ final class ControlChannel { timeoutMs: Double? = nil) async throws -> Data { do { - let rawParams = params?.reduce(into: [String: AnyCodable]()) { - $0[$1.key] = AnyCodable($1.value.base) + let rawParams = params?.reduce(into: [String: ClawdbotKit.AnyCodable]()) { + $0[$1.key] = ClawdbotKit.AnyCodable($1.value.base) } let data = try await GatewayConnection.shared.request( method: method, @@ -182,8 +190,11 @@ final class ControlChannel { urlErr.code == .dataNotAllowed // used for WS close 1008 auth failures { let reason = urlErr.failureURLString ?? urlErr.localizedDescription + let tokenKey = CommandResolver.connectionModeIsRemote() + ? "gateway.remote.token" + : "gateway.auth.token" return - "Gateway rejected token; set gateway.auth.token (or CLAWDBOT_GATEWAY_TOKEN) " + + "Gateway rejected token; set \(tokenKey) (or CLAWDBOT_GATEWAY_TOKEN) " + "or clear it on the gateway. " + "Reason: \(reason)" } @@ -274,18 +285,49 @@ final class ControlChannel { } } - do { - try await GatewayConnection.shared.refresh() + await self.refreshEndpoint(reason: "recovery:\(reasonText)") + if case .connected = self.state { self.logger.info("control channel recovery finished") - } catch { - self.logger.error( - "control channel recovery failed \(error.localizedDescription, privacy: .public)") + } else if case let .degraded(message) = self.state { + self.logger.error("control channel recovery failed \(message, privacy: .public)") } self.recoveryTask = nil } } + private func establishGatewayConnection(timeoutMs: Int = 5000) async throws { + try await GatewayConnection.shared.refresh() + let ok = try await GatewayConnection.shared.healthOK(timeoutMs: timeoutMs) + if ok == false { + throw NSError( + domain: "Gateway", + code: 0, + userInfo: [NSLocalizedDescriptionKey: "gateway health not ok"]) + } + await self.refreshAuthSourceLabel() + } + + private func refreshAuthSourceLabel() async { + let isRemote = CommandResolver.connectionModeIsRemote() + let authSource = await GatewayConnection.shared.authSource() + self.authSourceLabel = Self.formatAuthSource(authSource, isRemote: isRemote) + } + + private static func formatAuthSource(_ source: GatewayAuthSource?, isRemote: Bool) -> String? { + guard let source else { return nil } + switch source { + case .deviceToken: + return "Auth: device token (paired device)" + case .sharedToken: + return "Auth: shared token (\(isRemote ? "gateway.remote.token" : "gateway.auth.token"))" + case .password: + return "Auth: password (\(isRemote ? "gateway.remote.password" : "gateway.auth.password"))" + case .none: + return "Auth: none" + } + } + func sendSystemEvent(_ text: String, params: [String: AnyHashable] = [:]) async throws { var merged = params merged["text"] = AnyHashable(text) @@ -345,7 +387,7 @@ final class ControlChannel { let phase = event.data["phase"]?.value as? String ?? "" let name = event.data["name"]?.value as? String let meta = event.data["meta"]?.value as? String - let args = event.data["args"]?.value as? [String: AnyCodable] + let args = Self.bridgeToProtocolArgs(event.data["args"]) WorkActivityStore.shared.handleTool( sessionKey: sessionKey, phase: phase, @@ -356,6 +398,27 @@ final class ControlChannel { break } } + + private static func bridgeToProtocolArgs( + _ value: ClawdbotProtocol.AnyCodable?) -> [String: ClawdbotProtocol.AnyCodable]? + { + guard let value else { return nil } + if let dict = value.value as? [String: ClawdbotProtocol.AnyCodable] { + return dict + } + if let dict = value.value as? [String: ClawdbotKit.AnyCodable], + let data = try? JSONEncoder().encode(dict), + let decoded = try? JSONDecoder().decode([String: ClawdbotProtocol.AnyCodable].self, from: data) + { + return decoded + } + if let data = try? JSONEncoder().encode(value), + let decoded = try? JSONDecoder().decode([String: ClawdbotProtocol.AnyCodable].self, from: data) + { + return decoded + } + return nil + } } extension Notification.Name { diff --git a/apps/macos/Sources/Clawdbot/CostUsageMenuView.swift b/apps/macos/Sources/Clawdbot/CostUsageMenuView.swift new file mode 100644 index 000000000..c94a4de35 --- /dev/null +++ b/apps/macos/Sources/Clawdbot/CostUsageMenuView.swift @@ -0,0 +1,99 @@ +import Charts +import SwiftUI + +struct CostUsageHistoryMenuView: View { + let summary: GatewayCostUsageSummary + let width: CGFloat + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + self.header + self.chart + self.footer + } + .padding(.horizontal, 12) + .padding(.vertical, 10) + .frame(width: max(1, self.width), alignment: .leading) + } + + private var header: some View { + let todayKey = CostUsageMenuDateParser.format(Date()) + let todayEntry = self.summary.daily.first { $0.date == todayKey } + let todayCost = CostUsageFormatting.formatUsd(todayEntry?.totalCost) ?? "n/a" + let totalCost = CostUsageFormatting.formatUsd(self.summary.totals.totalCost) ?? "n/a" + + return HStack(alignment: .firstTextBaseline, spacing: 12) { + VStack(alignment: .leading, spacing: 2) { + Text("Today") + .font(.caption2) + .foregroundStyle(.secondary) + Text(todayCost) + .font(.system(size: 14, weight: .semibold)) + } + VStack(alignment: .leading, spacing: 2) { + Text("Last \(self.summary.days)d") + .font(.caption2) + .foregroundStyle(.secondary) + Text(totalCost) + .font(.system(size: 14, weight: .semibold)) + } + Spacer() + } + } + + private var chart: some View { + let entries = self.summary.daily.compactMap { entry -> (Date, Double)? in + guard let date = CostUsageMenuDateParser.parse(entry.date) else { return nil } + return (date, entry.totalCost) + } + + return Chart(entries, id: \.0) { entry in + BarMark( + x: .value("Day", entry.0), + y: .value("Cost", entry.1)) + .foregroundStyle(Color.accentColor) + .cornerRadius(3) + } + .chartXAxis { + AxisMarks(values: .stride(by: .day, count: 7)) { + AxisGridLine().foregroundStyle(.clear) + AxisValueLabel(format: .dateTime.month().day()) + } + } + .chartYAxis { + AxisMarks(position: .leading) { + AxisGridLine() + AxisValueLabel() + } + } + .frame(height: 110) + } + + private var footer: some View { + if self.summary.totals.missingCostEntries == 0 { + return AnyView(EmptyView()) + } + return AnyView( + Text("Partial: \(self.summary.totals.missingCostEntries) entries missing cost") + .font(.caption2) + .foregroundStyle(.secondary)) + } +} + +private enum CostUsageMenuDateParser { + static let formatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.timeZone = TimeZone.current + return formatter + }() + + static func parse(_ value: String) -> Date? { + self.formatter.date(from: value) + } + + static func format(_ date: Date) -> String { + self.formatter.string(from: date) + } +} diff --git a/apps/macos/Sources/Clawdbot/CronJobEditor+Helpers.swift b/apps/macos/Sources/Clawdbot/CronJobEditor+Helpers.swift index 52a18df0d..4bac28c4f 100644 --- a/apps/macos/Sources/Clawdbot/CronJobEditor+Helpers.swift +++ b/apps/macos/Sources/Clawdbot/CronJobEditor+Helpers.swift @@ -42,7 +42,8 @@ extension CronJobEditor { self.thinking = thinking ?? "" self.timeoutSeconds = timeoutSeconds.map(String.init) ?? "" self.deliver = deliver ?? false - self.channel = GatewayAgentChannel(raw: channel) + let trimmed = (channel ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + self.channel = trimmed.isEmpty ? "last" : trimmed self.to = to ?? "" self.bestEffortDeliver = bestEffortDeliver ?? false } @@ -210,7 +211,8 @@ extension CronJobEditor { if let n = Int(self.timeoutSeconds), n > 0 { payload["timeoutSeconds"] = n } payload["deliver"] = self.deliver if self.deliver { - payload["channel"] = self.channel.rawValue + let trimmed = self.channel.trimmingCharacters(in: .whitespacesAndNewlines) + payload["channel"] = trimmed.isEmpty ? "last" : trimmed let to = self.to.trimmingCharacters(in: .whitespacesAndNewlines) if !to.isEmpty { payload["to"] = to } payload["bestEffortDeliver"] = self.bestEffortDeliver diff --git a/apps/macos/Sources/Clawdbot/CronJobEditor+Testing.swift b/apps/macos/Sources/Clawdbot/CronJobEditor+Testing.swift index 603180b45..0d4c46523 100644 --- a/apps/macos/Sources/Clawdbot/CronJobEditor+Testing.swift +++ b/apps/macos/Sources/Clawdbot/CronJobEditor+Testing.swift @@ -14,7 +14,7 @@ extension CronJobEditor { self.payloadKind = .agentTurn self.agentMessage = "Run diagnostic" self.deliver = true - self.channel = .last + self.channel = "last" self.to = "+15551230000" self.thinking = "low" self.timeoutSeconds = "90" diff --git a/apps/macos/Sources/Clawdbot/CronJobEditor.swift b/apps/macos/Sources/Clawdbot/CronJobEditor.swift index 05659d179..cec2b96a6 100644 --- a/apps/macos/Sources/Clawdbot/CronJobEditor.swift +++ b/apps/macos/Sources/Clawdbot/CronJobEditor.swift @@ -1,10 +1,12 @@ import ClawdbotProtocol +import Observation import SwiftUI struct CronJobEditor: View { let job: CronJob? @Binding var isSaving: Bool @Binding var error: String? + @Bindable var channelsStore: ChannelsStore let onCancel: () -> Void let onSave: ([String: AnyCodable]) -> Void @@ -45,13 +47,29 @@ struct CronJobEditor: View { @State var systemEventText: String = "" @State var agentMessage: String = "" @State var deliver: Bool = false - @State var channel: GatewayAgentChannel = .last + @State var channel: String = "last" @State var to: String = "" @State var thinking: String = "" @State var timeoutSeconds: String = "" @State var bestEffortDeliver: Bool = false @State var postPrefix: String = "Cron" + var channelOptions: [String] { + let ordered = self.channelsStore.orderedChannelIds() + var options = ["last"] + ordered + let trimmed = self.channel.trimmingCharacters(in: .whitespacesAndNewlines) + if !trimmed.isEmpty, !options.contains(trimmed) { + options.append(trimmed) + } + var seen = Set() + return options.filter { seen.insert($0).inserted } + } + + func channelLabel(for id: String) -> String { + if id == "last" { return "last" } + return self.channelsStore.resolveChannelLabel(id) + } + var body: some View { VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 6) { @@ -333,13 +351,9 @@ struct CronJobEditor: View { GridRow { self.gridLabel("Channel") Picker("", selection: self.$channel) { - Text("last").tag(GatewayAgentChannel.last) - Text("whatsapp").tag(GatewayAgentChannel.whatsapp) - Text("telegram").tag(GatewayAgentChannel.telegram) - Text("discord").tag(GatewayAgentChannel.discord) - Text("slack").tag(GatewayAgentChannel.slack) - Text("signal").tag(GatewayAgentChannel.signal) - Text("imessage").tag(GatewayAgentChannel.imessage) + ForEach(self.channelOptions, id: \.self) { channel in + Text(self.channelLabel(for: channel)).tag(channel) + } } .labelsHidden() .pickerStyle(.segmented) diff --git a/apps/macos/Sources/Clawdbot/CronJobsStore.swift b/apps/macos/Sources/Clawdbot/CronJobsStore.swift index b3e363bf9..b44f9cb3a 100644 --- a/apps/macos/Sources/Clawdbot/CronJobsStore.swift +++ b/apps/macos/Sources/Clawdbot/CronJobsStore.swift @@ -1,3 +1,4 @@ +import ClawdbotKit import ClawdbotProtocol import Foundation import Observation diff --git a/apps/macos/Sources/Clawdbot/CronSettings+Layout.swift b/apps/macos/Sources/Clawdbot/CronSettings+Layout.swift index f1891bd3e..11c7c0a0e 100644 --- a/apps/macos/Sources/Clawdbot/CronSettings+Layout.swift +++ b/apps/macos/Sources/Clawdbot/CronSettings+Layout.swift @@ -8,13 +8,20 @@ extension CronSettings { self.content Spacer(minLength: 0) } - .onAppear { self.store.start() } - .onDisappear { self.store.stop() } + .onAppear { + self.store.start() + self.channelsStore.start() + } + .onDisappear { + self.store.stop() + self.channelsStore.stop() + } .sheet(isPresented: self.$showEditor) { CronJobEditor( job: self.editingJob, isSaving: self.$isSaving, error: self.$editorError, + channelsStore: self.channelsStore, onCancel: { self.showEditor = false self.editingJob = nil diff --git a/apps/macos/Sources/Clawdbot/CronSettings+Testing.swift b/apps/macos/Sources/Clawdbot/CronSettings+Testing.swift index 9a767c374..976049f66 100644 --- a/apps/macos/Sources/Clawdbot/CronSettings+Testing.swift +++ b/apps/macos/Sources/Clawdbot/CronSettings+Testing.swift @@ -47,7 +47,7 @@ struct CronSettings_Previews: PreviewProvider { durationMs: 1234, nextRunAtMs: nil), ] - return CronSettings(store: store) + return CronSettings(store: store, channelsStore: ChannelsStore(isPreview: true)) .frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight) } } @@ -103,7 +103,7 @@ extension CronSettings { store.selectedJobId = job.id store.runEntries = [run] - let view = CronSettings(store: store) + let view = CronSettings(store: store, channelsStore: ChannelsStore(isPreview: true)) _ = view.body _ = view.jobRow(job) _ = view.jobContextMenu(job) diff --git a/apps/macos/Sources/Clawdbot/CronSettings.swift b/apps/macos/Sources/Clawdbot/CronSettings.swift index d19a419b3..999712a59 100644 --- a/apps/macos/Sources/Clawdbot/CronSettings.swift +++ b/apps/macos/Sources/Clawdbot/CronSettings.swift @@ -3,13 +3,15 @@ import SwiftUI struct CronSettings: View { @Bindable var store: CronJobsStore + @Bindable var channelsStore: ChannelsStore @State var showEditor = false @State var editingJob: CronJob? @State var editorError: String? @State var isSaving = false @State var confirmDelete: CronJob? - init(store: CronJobsStore = .shared) { + init(store: CronJobsStore = .shared, channelsStore: ChannelsStore = .shared) { self.store = store + self.channelsStore = channelsStore } } diff --git a/apps/macos/Sources/Clawdbot/DebugActions.swift b/apps/macos/Sources/Clawdbot/DebugActions.swift index 1bfbeeb24..f0f84ed3c 100644 --- a/apps/macos/Sources/Clawdbot/DebugActions.swift +++ b/apps/macos/Sources/Clawdbot/DebugActions.swift @@ -26,7 +26,7 @@ enum DebugActions { static func openLog() { let path = self.pinoLogPath() let url = URL(fileURLWithPath: path) - guard FileManager.default.fileExists(atPath: path) else { + guard FileManager().fileExists(atPath: path) else { let alert = NSAlert() alert.messageText = "Log file not found" alert.informativeText = path @@ -38,7 +38,7 @@ enum DebugActions { @MainActor static func openConfigFolder() { - let url = FileManager.default + let url = FileManager() .homeDirectoryForCurrentUser .appendingPathComponent(".clawdbot", isDirectory: true) NSWorkspace.shared.activateFileViewerSelecting([url]) @@ -55,7 +55,7 @@ enum DebugActions { } let path = self.resolveSessionStorePath() let url = URL(fileURLWithPath: path) - if FileManager.default.fileExists(atPath: path) { + if FileManager().fileExists(atPath: path) { NSWorkspace.shared.activateFileViewerSelecting([url]) } else { NSWorkspace.shared.open(url.deletingLastPathComponent()) @@ -195,7 +195,7 @@ enum DebugActions { @MainActor private static func resolveSessionStorePath() -> String { let defaultPath = SessionLoader.defaultStorePath - let configURL = FileManager.default.homeDirectoryForCurrentUser + let configURL = FileManager().homeDirectoryForCurrentUser .appendingPathComponent(".clawdbot/clawdbot.json") guard let data = try? Data(contentsOf: configURL), diff --git a/apps/macos/Sources/Clawdbot/DebugSettings.swift b/apps/macos/Sources/Clawdbot/DebugSettings.swift index c338edad8..ec313b2a0 100644 --- a/apps/macos/Sources/Clawdbot/DebugSettings.swift +++ b/apps/macos/Sources/Clawdbot/DebugSettings.swift @@ -16,6 +16,8 @@ struct DebugSettings: View { @State private var modelsError: String? private let gatewayManager = GatewayProcessManager.shared private let healthStore = HealthStore.shared + @State private var launchAgentWriteDisabled = GatewayLaunchAgentManager.isLaunchAgentWriteDisabled() + @State private var launchAgentWriteError: String? @State private var gatewayRootInput: String = GatewayProcessManager.shared.projectRootPath() @State private var sessionStorePath: String = SessionLoader.defaultStorePath @State private var sessionStoreSaveError: String? @@ -47,6 +49,7 @@ struct DebugSettings: View { VStack(alignment: .leading, spacing: 14) { self.header + self.launchdSection self.appInfoSection self.gatewaySection self.logsSection @@ -79,6 +82,41 @@ struct DebugSettings: View { } } + private var launchdSection: some View { + GroupBox("Gateway startup") { + VStack(alignment: .leading, spacing: 8) { + Toggle("Attach only (skip launchd install)", isOn: self.$launchAgentWriteDisabled) + .onChange(of: self.launchAgentWriteDisabled) { _, newValue in + self.launchAgentWriteError = GatewayLaunchAgentManager.setLaunchAgentWriteDisabled(newValue) + if self.launchAgentWriteError != nil { + self.launchAgentWriteDisabled = GatewayLaunchAgentManager.isLaunchAgentWriteDisabled() + return + } + if newValue { + Task { + _ = await GatewayLaunchAgentManager.set( + enabled: false, + bundlePath: Bundle.main.bundlePath, + port: GatewayEnvironment.gatewayPort()) + } + } + } + + Text( + "When enabled, Clawdbot won't install or manage \(gatewayLaunchdLabel). " + + "It will only attach to an existing Gateway.") + .font(.caption) + .foregroundStyle(.secondary) + + if let launchAgentWriteError { + Text(launchAgentWriteError) + .font(.caption) + .foregroundStyle(.red) + } + } + } + } + private var header: some View { VStack(alignment: .leading, spacing: 6) { Text("Debug") @@ -354,7 +392,7 @@ struct DebugSettings: View { Button("Save") { self.saveRelayRoot() } .buttonStyle(.borderedProminent) Button("Reset") { - let def = FileManager.default.homeDirectoryForCurrentUser + let def = FileManager().homeDirectoryForCurrentUser .appendingPathComponent("Projects/clawdbot").path self.gatewayRootInput = def self.saveRelayRoot() @@ -484,6 +522,22 @@ struct DebugSettings: View { } } + VStack(alignment: .leading, spacing: 6) { + Text( + "Note: macOS may require restarting Clawdbot after enabling Accessibility or Screen Recording.") + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + + Button { + LaunchdManager.startClawdbot() + } label: { + Label("Restart Clawdbot", systemImage: "arrow.counterclockwise") + } + .buttonStyle(.bordered) + .controlSize(.small) + } + HStack(spacing: 8) { Button("Restart app") { DebugActions.restartApp() } Button("Restart onboarding") { DebugActions.restartOnboarding() } @@ -743,7 +797,7 @@ struct DebugSettings: View { do { let data = try JSONSerialization.data(withJSONObject: root, options: [.prettyPrinted, .sortedKeys]) - try FileManager.default.createDirectory( + try FileManager().createDirectory( at: url.deletingLastPathComponent(), withIntermediateDirectories: true) try data.write(to: url, options: [.atomic]) @@ -776,7 +830,7 @@ struct DebugSettings: View { } private func configURL() -> URL { - FileManager.default.homeDirectoryForCurrentUser + FileManager().homeDirectoryForCurrentUser .appendingPathComponent(".clawdbot") .appendingPathComponent("clawdbot.json") } diff --git a/apps/macos/Sources/Clawdbot/DevicePairingApprovalPrompter.swift b/apps/macos/Sources/Clawdbot/DevicePairingApprovalPrompter.swift new file mode 100644 index 000000000..81d87c7d3 --- /dev/null +++ b/apps/macos/Sources/Clawdbot/DevicePairingApprovalPrompter.swift @@ -0,0 +1,334 @@ +import AppKit +import ClawdbotKit +import ClawdbotProtocol +import Foundation +import Observation +import OSLog + +@MainActor +@Observable +final class DevicePairingApprovalPrompter { + static let shared = DevicePairingApprovalPrompter() + + private let logger = Logger(subsystem: "com.clawdbot", category: "device-pairing") + private var task: Task? + private var isStopping = false + private var isPresenting = false + private var queue: [PendingRequest] = [] + var pendingCount: Int = 0 + var pendingRepairCount: Int = 0 + private var activeAlert: NSAlert? + private var activeRequestId: String? + private var alertHostWindow: NSWindow? + private var resolvedByRequestId: Set = [] + + private final class AlertHostWindow: NSWindow { + override var canBecomeKey: Bool { true } + override var canBecomeMain: Bool { true } + } + + private struct PairingList: Codable { + let pending: [PendingRequest] + let paired: [PairedDevice]? + } + + private struct PairedDevice: Codable, Equatable { + let deviceId: String + let approvedAtMs: Double? + let displayName: String? + let platform: String? + let remoteIp: String? + } + + private struct PendingRequest: Codable, Equatable, Identifiable { + let requestId: String + let deviceId: String + let publicKey: String + let displayName: String? + let platform: String? + let clientId: String? + let clientMode: String? + let role: String? + let scopes: [String]? + let remoteIp: String? + let silent: Bool? + let isRepair: Bool? + let ts: Double + + var id: String { self.requestId } + } + + private struct PairingResolvedEvent: Codable { + let requestId: String + let deviceId: String + let decision: String + let ts: Double + } + + private enum PairingResolution: String { + case approved + case rejected + } + + func start() { + guard self.task == nil else { return } + self.isStopping = false + self.task = Task { [weak self] in + guard let self else { return } + _ = try? await GatewayConnection.shared.refresh() + await self.loadPendingRequestsFromGateway() + let stream = await GatewayConnection.shared.subscribe(bufferingNewest: 200) + for await push in stream { + if Task.isCancelled { return } + await MainActor.run { [weak self] in self?.handle(push: push) } + } + } + } + + func stop() { + self.isStopping = true + self.endActiveAlert() + self.task?.cancel() + self.task = nil + self.queue.removeAll(keepingCapacity: false) + self.updatePendingCounts() + self.isPresenting = false + self.activeRequestId = nil + self.alertHostWindow?.orderOut(nil) + self.alertHostWindow?.close() + self.alertHostWindow = nil + self.resolvedByRequestId.removeAll(keepingCapacity: false) + } + + private func loadPendingRequestsFromGateway() async { + do { + let list: PairingList = try await GatewayConnection.shared.requestDecoded(method: .devicePairList) + await self.apply(list: list) + } catch { + self.logger.error("failed to load device pairing requests: \(error.localizedDescription, privacy: .public)") + } + } + + private func apply(list: PairingList) async { + self.queue = list.pending.sorted(by: { $0.ts > $1.ts }) + self.updatePendingCounts() + self.presentNextIfNeeded() + } + + private func updatePendingCounts() { + self.pendingCount = self.queue.count + self.pendingRepairCount = self.queue.count(where: { $0.isRepair == true }) + } + + private func presentNextIfNeeded() { + guard !self.isStopping else { return } + guard !self.isPresenting else { return } + guard let next = self.queue.first else { return } + self.isPresenting = true + self.presentAlert(for: next) + } + + private func presentAlert(for req: PendingRequest) { + self.logger.info("presenting device pairing alert requestId=\(req.requestId, privacy: .public)") + NSApp.activate(ignoringOtherApps: true) + + let alert = NSAlert() + alert.alertStyle = .warning + alert.messageText = "Allow device to connect?" + alert.informativeText = Self.describe(req) + alert.addButton(withTitle: "Later") + alert.addButton(withTitle: "Approve") + alert.addButton(withTitle: "Reject") + if #available(macOS 11.0, *), alert.buttons.indices.contains(2) { + alert.buttons[2].hasDestructiveAction = true + } + + self.activeAlert = alert + self.activeRequestId = req.requestId + let hostWindow = self.requireAlertHostWindow() + + let sheetSize = alert.window.frame.size + if let screen = hostWindow.screen ?? NSScreen.main { + let bounds = screen.visibleFrame + let x = bounds.midX - (sheetSize.width / 2) + let sheetOriginY = bounds.midY - (sheetSize.height / 2) + let hostY = sheetOriginY + sheetSize.height - hostWindow.frame.height + hostWindow.setFrameOrigin(NSPoint(x: x, y: hostY)) + } else { + hostWindow.center() + } + + hostWindow.makeKeyAndOrderFront(nil) + alert.beginSheetModal(for: hostWindow) { [weak self] response in + Task { @MainActor [weak self] in + guard let self else { return } + self.activeRequestId = nil + self.activeAlert = nil + await self.handleAlertResponse(response, request: req) + hostWindow.orderOut(nil) + } + } + } + + private func handleAlertResponse(_ response: NSApplication.ModalResponse, request: PendingRequest) async { + var shouldRemove = response != .alertFirstButtonReturn + defer { + if shouldRemove { + if self.queue.first == request { + self.queue.removeFirst() + } else { + self.queue.removeAll { $0 == request } + } + } + self.updatePendingCounts() + self.isPresenting = false + self.presentNextIfNeeded() + } + + guard !self.isStopping else { return } + + if self.resolvedByRequestId.remove(request.requestId) != nil { + return + } + + switch response { + case .alertFirstButtonReturn: + shouldRemove = false + if let idx = self.queue.firstIndex(of: request) { + self.queue.remove(at: idx) + } + self.queue.append(request) + return + case .alertSecondButtonReturn: + _ = await self.approve(requestId: request.requestId) + case .alertThirdButtonReturn: + await self.reject(requestId: request.requestId) + default: + return + } + } + + private func approve(requestId: String) async -> Bool { + do { + try await GatewayConnection.shared.devicePairApprove(requestId: requestId) + self.logger.info("approved device pairing requestId=\(requestId, privacy: .public)") + return true + } catch { + self.logger.error("approve failed requestId=\(requestId, privacy: .public)") + self.logger.error("approve failed: \(error.localizedDescription, privacy: .public)") + return false + } + } + + private func reject(requestId: String) async { + do { + try await GatewayConnection.shared.devicePairReject(requestId: requestId) + self.logger.info("rejected device pairing requestId=\(requestId, privacy: .public)") + } catch { + self.logger.error("reject failed requestId=\(requestId, privacy: .public)") + self.logger.error("reject failed: \(error.localizedDescription, privacy: .public)") + } + } + + private func endActiveAlert() { + guard let alert = self.activeAlert else { return } + if let parent = alert.window.sheetParent { + parent.endSheet(alert.window, returnCode: .abort) + } + self.activeAlert = nil + self.activeRequestId = nil + } + + private func requireAlertHostWindow() -> NSWindow { + if let alertHostWindow { + return alertHostWindow + } + + let window = AlertHostWindow( + contentRect: NSRect(x: 0, y: 0, width: 520, height: 1), + styleMask: [.borderless], + backing: .buffered, + defer: false) + window.title = "" + window.isReleasedWhenClosed = false + window.level = .floating + window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] + window.isOpaque = false + window.hasShadow = false + window.backgroundColor = .clear + window.ignoresMouseEvents = true + + self.alertHostWindow = window + return window + } + + private func handle(push: GatewayPush) { + switch push { + case let .event(evt) where evt.event == "device.pair.requested": + guard let payload = evt.payload else { return } + do { + let req = try GatewayPayloadDecoding.decode(payload, as: PendingRequest.self) + self.enqueue(req) + } catch { + self.logger + .error("failed to decode device pairing request: \(error.localizedDescription, privacy: .public)") + } + case let .event(evt) where evt.event == "device.pair.resolved": + guard let payload = evt.payload else { return } + do { + let resolved = try GatewayPayloadDecoding.decode(payload, as: PairingResolvedEvent.self) + self.handleResolved(resolved) + } catch { + self.logger + .error( + "failed to decode device pairing resolution: \(error.localizedDescription, privacy: .public)") + } + default: + break + } + } + + private func enqueue(_ req: PendingRequest) { + guard !self.queue.contains(req) else { return } + self.queue.append(req) + self.updatePendingCounts() + self.presentNextIfNeeded() + } + + private func handleResolved(_ resolved: PairingResolvedEvent) { + let resolution = resolved.decision == PairingResolution.approved.rawValue ? PairingResolution + .approved : .rejected + if let activeRequestId, activeRequestId == resolved.requestId { + self.resolvedByRequestId.insert(resolved.requestId) + self.endActiveAlert() + let decision = resolution.rawValue + self.logger.info( + "device pairing resolved while active requestId=\(resolved.requestId, privacy: .public) " + + "decision=\(decision, privacy: .public)") + return + } + self.queue.removeAll { $0.requestId == resolved.requestId } + self.updatePendingCounts() + } + + private static func describe(_ req: PendingRequest) -> String { + var lines: [String] = [] + lines.append("Device: \(req.displayName ?? req.deviceId)") + if let platform = req.platform { + lines.append("Platform: \(platform)") + } + if let role = req.role { + lines.append("Role: \(role)") + } + if let scopes = req.scopes, !scopes.isEmpty { + lines.append("Scopes: \(scopes.joined(separator: ", "))") + } + if let remoteIp = req.remoteIp { + lines.append("IP: \(remoteIp)") + } + if req.isRepair == true { + lines.append("Repair: yes") + } + return lines.joined(separator: "\n") + } +} diff --git a/apps/macos/Sources/Clawdbot/DiagnosticsFileLog.swift b/apps/macos/Sources/Clawdbot/DiagnosticsFileLog.swift index f62dd2c07..d42b76081 100644 --- a/apps/macos/Sources/Clawdbot/DiagnosticsFileLog.swift +++ b/apps/macos/Sources/Clawdbot/DiagnosticsFileLog.swift @@ -20,8 +20,8 @@ actor DiagnosticsFileLog { } nonisolated static func logDirectoryURL() -> URL { - let library = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first - ?? FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library", isDirectory: true) + let library = FileManager().urls(for: .libraryDirectory, in: .userDomainMask).first + ?? FileManager().homeDirectoryForCurrentUser.appendingPathComponent("Library", isDirectory: true) return library .appendingPathComponent("Logs", isDirectory: true) .appendingPathComponent("Clawdbot", isDirectory: true) @@ -43,7 +43,7 @@ actor DiagnosticsFileLog { } func clear() throws { - let fm = FileManager.default + let fm = FileManager() let base = Self.logFileURL() if fm.fileExists(atPath: base.path) { try fm.removeItem(at: base) @@ -67,7 +67,7 @@ actor DiagnosticsFileLog { } private func ensureDirectory() throws { - try FileManager.default.createDirectory( + try FileManager().createDirectory( at: Self.logDirectoryURL(), withIntermediateDirectories: true) } @@ -79,7 +79,7 @@ actor DiagnosticsFileLog { line.append(data) line.append(0x0A) // newline - let fm = FileManager.default + let fm = FileManager() if !fm.fileExists(atPath: url.path) { fm.createFile(atPath: url.path, contents: nil) } @@ -92,13 +92,13 @@ actor DiagnosticsFileLog { private func rotateIfNeeded() throws { let url = Self.logFileURL() - guard let attrs = try? FileManager.default.attributesOfItem(atPath: url.path), + guard let attrs = try? FileManager().attributesOfItem(atPath: url.path), let size = attrs[.size] as? NSNumber else { return } if size.int64Value < self.maxBytes { return } - let fm = FileManager.default + let fm = FileManager() let oldest = self.rotatedURL(index: self.maxBackups) if fm.fileExists(atPath: oldest.path) { diff --git a/apps/macos/Sources/Clawdbot/ExecApprovals.swift b/apps/macos/Sources/Clawdbot/ExecApprovals.swift index eab1aea11..c6f413922 100644 --- a/apps/macos/Sources/Clawdbot/ExecApprovals.swift +++ b/apps/macos/Sources/Clawdbot/ExecApprovals.swift @@ -53,11 +53,11 @@ enum ExecApprovalQuickMode: String, CaseIterable, Identifiable { static func from(security: ExecSecurity, ask: ExecAsk) -> ExecApprovalQuickMode { switch security { case .deny: - return .deny + .deny case .full: - return .allow + .allow case .allowlist: - return .ask + .ask } } } @@ -84,11 +84,52 @@ enum ExecApprovalDecision: String, Codable, Sendable { case deny } -struct ExecAllowlistEntry: Codable, Hashable { +struct ExecAllowlistEntry: Codable, Hashable, Identifiable { + var id: UUID var pattern: String - var lastUsedAt: Double? = nil - var lastUsedCommand: String? = nil - var lastResolvedPath: String? = nil + var lastUsedAt: Double? + var lastUsedCommand: String? + var lastResolvedPath: String? + + init( + id: UUID = UUID(), + pattern: String, + lastUsedAt: Double? = nil, + lastUsedCommand: String? = nil, + lastResolvedPath: String? = nil) + { + self.id = id + self.pattern = pattern + self.lastUsedAt = lastUsedAt + self.lastUsedCommand = lastUsedCommand + self.lastResolvedPath = lastResolvedPath + } + + private enum CodingKeys: String, CodingKey { + case id + case pattern + case lastUsedAt + case lastUsedCommand + case lastResolvedPath + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decodeIfPresent(UUID.self, forKey: .id) ?? UUID() + self.pattern = try container.decode(String.self, forKey: .pattern) + self.lastUsedAt = try container.decodeIfPresent(Double.self, forKey: .lastUsedAt) + self.lastUsedCommand = try container.decodeIfPresent(String.self, forKey: .lastUsedCommand) + self.lastResolvedPath = try container.decodeIfPresent(String.self, forKey: .lastResolvedPath) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.id, forKey: .id) + try container.encode(self.pattern, forKey: .pattern) + try container.encodeIfPresent(self.lastUsedAt, forKey: .lastUsedAt) + try container.encodeIfPresent(self.lastUsedCommand, forKey: .lastUsedCommand) + try container.encodeIfPresent(self.lastResolvedPath, forKey: .lastResolvedPath) + } } struct ExecApprovalsDefaults: Codable { @@ -106,7 +147,8 @@ struct ExecApprovalsAgent: Codable { var allowlist: [ExecAllowlistEntry]? var isEmpty: Bool { - security == nil && ask == nil && askFallback == nil && autoAllowSkills == nil && (allowlist?.isEmpty ?? true) + self.security == nil && self.ask == nil && self.askFallback == nil && self + .autoAllowSkills == nil && (self.allowlist?.isEmpty ?? true) } } @@ -148,6 +190,7 @@ struct ExecApprovalsResolvedDefaults { enum ExecApprovalsStore { private static let logger = Logger(subsystem: "com.clawdbot", category: "exec-approvals") + private static let defaultAgentId = "main" private static let defaultSecurity: ExecSecurity = .deny private static let defaultAsk: ExecAsk = .onMiss private static let defaultAskFallback: ExecSecurity = .deny @@ -164,18 +207,27 @@ enum ExecApprovalsStore { static func normalizeIncoming(_ file: ExecApprovalsFile) -> ExecApprovalsFile { let socketPath = file.socket?.path?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" let token = file.socket?.token?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + var agents = file.agents ?? [:] + if let legacyDefault = agents["default"] { + if let main = agents[self.defaultAgentId] { + agents[self.defaultAgentId] = self.mergeAgents(current: main, legacy: legacyDefault) + } else { + agents[self.defaultAgentId] = legacyDefault + } + agents.removeValue(forKey: "default") + } return ExecApprovalsFile( version: 1, socket: ExecApprovalsSocketConfig( path: socketPath.isEmpty ? nil : socketPath, token: token.isEmpty ? nil : token), defaults: file.defaults, - agents: file.agents) + agents: agents) } static func readSnapshot() -> ExecApprovalsSnapshot { let url = self.fileURL() - guard FileManager.default.fileExists(atPath: url.path) else { + guard FileManager().fileExists(atPath: url.path) else { return ExecApprovalsSnapshot( path: url.path, exists: false, @@ -215,7 +267,7 @@ enum ExecApprovalsStore { static func loadFile() -> ExecApprovalsFile { let url = self.fileURL() - guard FileManager.default.fileExists(atPath: url.path) else { + guard FileManager().fileExists(atPath: url.path) else { return ExecApprovalsFile(version: 1, socket: nil, defaults: nil, agents: [:]) } do { @@ -237,11 +289,11 @@ enum ExecApprovalsStore { encoder.outputFormatting = [.prettyPrinted, .sortedKeys] let data = try encoder.encode(file) let url = self.fileURL() - try FileManager.default.createDirectory( + try FileManager().createDirectory( at: url.deletingLastPathComponent(), withIntermediateDirectories: true) try data.write(to: url, options: [.atomic]) - try? FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: url.path) + try? FileManager().setAttributes([.posixPermissions: 0o600], ofItemAtPath: url.path) } catch { self.logger.error("exec approvals save failed: \(error.localizedDescription, privacy: .public)") } @@ -271,18 +323,20 @@ enum ExecApprovalsStore { ask: defaults.ask ?? self.defaultAsk, askFallback: defaults.askFallback ?? self.defaultAskFallback, autoAllowSkills: defaults.autoAllowSkills ?? self.defaultAutoAllowSkills) - let key = (agentId?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false) - ? agentId!.trimmingCharacters(in: .whitespacesAndNewlines) - : "default" + let key = self.agentKey(agentId) let agentEntry = file.agents?[key] ?? ExecApprovalsAgent() + let wildcardEntry = file.agents?["*"] ?? ExecApprovalsAgent() let resolvedAgent = ExecApprovalsResolvedDefaults( - security: agentEntry.security ?? resolvedDefaults.security, - ask: agentEntry.ask ?? resolvedDefaults.ask, - askFallback: agentEntry.askFallback ?? resolvedDefaults.askFallback, - autoAllowSkills: agentEntry.autoAllowSkills ?? resolvedDefaults.autoAllowSkills) - let allowlist = (agentEntry.allowlist ?? []) + security: agentEntry.security ?? wildcardEntry.security ?? resolvedDefaults.security, + ask: agentEntry.ask ?? wildcardEntry.ask ?? resolvedDefaults.ask, + askFallback: agentEntry.askFallback ?? wildcardEntry.askFallback + ?? resolvedDefaults.askFallback, + autoAllowSkills: agentEntry.autoAllowSkills ?? wildcardEntry.autoAllowSkills + ?? resolvedDefaults.autoAllowSkills) + let allowlist = ((wildcardEntry.allowlist ?? []) + (agentEntry.allowlist ?? [])) .map { entry in ExecAllowlistEntry( + id: entry.id, pattern: entry.pattern.trimmingCharacters(in: .whitespacesAndNewlines), lastUsedAt: entry.lastUsedAt, lastUsedCommand: entry.lastUsedCommand, @@ -367,6 +421,7 @@ enum ExecApprovalsStore { let allowlist = (entry.allowlist ?? []).map { item -> ExecAllowlistEntry in guard item.pattern == pattern else { return item } return ExecAllowlistEntry( + id: item.id, pattern: item.pattern, lastUsedAt: Date().timeIntervalSince1970 * 1000, lastUsedCommand: command, @@ -386,6 +441,7 @@ enum ExecApprovalsStore { let cleaned = allowlist .map { item in ExecAllowlistEntry( + id: item.id, pattern: item.pattern.trimmingCharacters(in: .whitespacesAndNewlines), lastUsedAt: item.lastUsedAt, lastUsedCommand: item.lastUsedCommand, @@ -441,11 +497,11 @@ enum ExecApprovalsStore { private static func expandPath(_ raw: String) -> String { let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) if trimmed == "~" { - return FileManager.default.homeDirectoryForCurrentUser.path + return FileManager().homeDirectoryForCurrentUser.path } if trimmed.hasPrefix("~/") { let suffix = trimmed.dropFirst(2) - return FileManager.default.homeDirectoryForCurrentUser + return FileManager().homeDirectoryForCurrentUser .appendingPathComponent(String(suffix)).path } return trimmed @@ -453,7 +509,40 @@ enum ExecApprovalsStore { private static func agentKey(_ agentId: String?) -> String { let trimmed = agentId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - return trimmed.isEmpty ? "default" : trimmed + return trimmed.isEmpty ? self.defaultAgentId : trimmed + } + + private static func normalizedPattern(_ pattern: String?) -> String? { + let trimmed = pattern?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + return trimmed.isEmpty ? nil : trimmed.lowercased() + } + + private static func mergeAgents( + current: ExecApprovalsAgent, + legacy: ExecApprovalsAgent) -> ExecApprovalsAgent + { + var seen = Set() + var allowlist: [ExecAllowlistEntry] = [] + func append(_ entry: ExecAllowlistEntry) { + guard let key = self.normalizedPattern(entry.pattern), !seen.contains(key) else { + return + } + seen.insert(key) + allowlist.append(entry) + } + for entry in current.allowlist ?? [] { + append(entry) + } + for entry in legacy.allowlist ?? [] { + append(entry) + } + + return ExecApprovalsAgent( + security: current.security ?? legacy.security, + ask: current.ask ?? legacy.ask, + askFallback: current.askFallback ?? legacy.askFallback, + autoAllowSkills: current.autoAllowSkills ?? legacy.autoAllowSkills, + allowlist: allowlist.isEmpty ? nil : allowlist) } } @@ -467,8 +556,8 @@ struct ExecCommandResolution: Sendable { command: [String], rawCommand: String?, cwd: String?, - env: [String: String]? - ) -> ExecCommandResolution? { + env: [String: String]?) -> ExecCommandResolution? + { let trimmedRaw = rawCommand?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" if !trimmedRaw.isEmpty, let token = self.parseFirstToken(trimmedRaw) { return self.resolveExecutable(rawExecutable: token, cwd: cwd, env: env) @@ -486,8 +575,8 @@ struct ExecCommandResolution: Sendable { private static func resolveExecutable( rawExecutable: String, cwd: String?, - env: [String: String]? - ) -> ExecCommandResolution? { + env: [String: String]?) -> ExecCommandResolution? + { let expanded = rawExecutable.hasPrefix("~") ? (rawExecutable as NSString).expandingTildeInPath : rawExecutable let hasPathSeparator = expanded.contains("/") || expanded.contains("\\") let resolvedPath: String? = { @@ -496,14 +585,18 @@ struct ExecCommandResolution: Sendable { return expanded } let base = cwd?.trimmingCharacters(in: .whitespacesAndNewlines) - let root = (base?.isEmpty == false) ? base! : FileManager.default.currentDirectoryPath + let root = (base?.isEmpty == false) ? base! : FileManager().currentDirectoryPath return URL(fileURLWithPath: root).appendingPathComponent(expanded).path } let searchPaths = self.searchPaths(from: env) return CommandResolver.findExecutable(named: expanded, searchPaths: searchPaths) }() let name = resolvedPath.map { URL(fileURLWithPath: $0).lastPathComponent } ?? expanded - return ExecCommandResolution(rawExecutable: expanded, resolvedPath: resolvedPath, executableName: name, cwd: cwd) + return ExecCommandResolution( + rawExecutable: expanded, + resolvedPath: resolvedPath, + executableName: name, + cwd: cwd) } private static func parseFirstToken(_ command: String) -> String? { @@ -548,6 +641,30 @@ enum ExecCommandFormatter { } } +enum ExecApprovalHelpers { + static func parseDecision(_ raw: String?) -> ExecApprovalDecision? { + let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + guard !trimmed.isEmpty else { return nil } + return ExecApprovalDecision(rawValue: trimmed) + } + + static func requiresAsk( + ask: ExecAsk, + security: ExecSecurity, + allowlistMatch: ExecAllowlistEntry?, + skillAllow: Bool) -> Bool + { + if ask == .always { return true } + if ask == .onMiss, security == .allowlist, allowlistMatch == nil, !skillAllow { return true } + return false + } + + static func allowlistPattern(command: [String], resolution: ExecCommandResolution?) -> String? { + let pattern = resolution?.resolvedPath ?? resolution?.rawExecutable ?? command.first ?? "" + return pattern.isEmpty ? nil : pattern + } +} + enum ExecAllowlistMatcher { static func match(entries: [ExecAllowlistEntry], resolution: ExecCommandResolution?) -> ExecAllowlistEntry? { guard let resolution, !entries.isEmpty else { return nil } @@ -624,7 +741,7 @@ struct ExecEventPayload: Codable, Sendable { var output: String? var reason: String? - static func truncateOutput(_ raw: String, maxChars: Int = 20_000) -> String? { + static func truncateOutput(_ raw: String, maxChars: Int = 20000) -> String? { let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return nil } if trimmed.count <= maxChars { return trimmed } diff --git a/apps/macos/Sources/Clawdbot/ExecApprovalsGatewayPrompter.swift b/apps/macos/Sources/Clawdbot/ExecApprovalsGatewayPrompter.swift new file mode 100644 index 000000000..dbe80ecfe --- /dev/null +++ b/apps/macos/Sources/Clawdbot/ExecApprovalsGatewayPrompter.swift @@ -0,0 +1,123 @@ +import ClawdbotKit +import ClawdbotProtocol +import CoreGraphics +import Foundation +import OSLog + +@MainActor +final class ExecApprovalsGatewayPrompter { + static let shared = ExecApprovalsGatewayPrompter() + + private let logger = Logger(subsystem: "com.clawdbot", category: "exec-approvals.gateway") + private var task: Task? + + struct GatewayApprovalRequest: Codable, Sendable { + var id: String + var request: ExecApprovalPromptRequest + var createdAtMs: Int + var expiresAtMs: Int + } + + func start() { + guard self.task == nil else { return } + self.task = Task { [weak self] in + await self?.run() + } + } + + func stop() { + self.task?.cancel() + self.task = nil + } + + private func run() async { + let stream = await GatewayConnection.shared.subscribe(bufferingNewest: 200) + for await push in stream { + if Task.isCancelled { return } + await self.handle(push: push) + } + } + + private func handle(push: GatewayPush) async { + guard case let .event(evt) = push else { return } + guard evt.event == "exec.approval.requested" else { return } + guard let payload = evt.payload else { return } + do { + let data = try JSONEncoder().encode(payload) + let request = try JSONDecoder().decode(GatewayApprovalRequest.self, from: data) + guard self.shouldPresent(request: request) else { return } + let decision = ExecApprovalsPromptPresenter.prompt(request.request) + try await GatewayConnection.shared.requestVoid( + method: .execApprovalResolve, + params: [ + "id": AnyCodable(request.id), + "decision": AnyCodable(decision.rawValue), + ], + timeoutMs: 10000) + } catch { + self.logger.error("exec approval handling failed \(error.localizedDescription, privacy: .public)") + } + } + + private func shouldPresent(request: GatewayApprovalRequest) -> Bool { + let mode = AppStateStore.shared.connectionMode + let activeSession = WebChatManager.shared.activeSessionKey?.trimmingCharacters(in: .whitespacesAndNewlines) + let requestSession = request.request.sessionKey?.trimmingCharacters(in: .whitespacesAndNewlines) + return Self.shouldPresent( + mode: mode, + activeSession: activeSession, + requestSession: requestSession, + lastInputSeconds: Self.lastInputSeconds(), + thresholdSeconds: 120) + } + + private static func shouldPresent( + mode: AppState.ConnectionMode, + activeSession: String?, + requestSession: String?, + lastInputSeconds: Int?, + thresholdSeconds: Int) -> Bool + { + let active = activeSession?.trimmingCharacters(in: .whitespacesAndNewlines) + let requested = requestSession?.trimmingCharacters(in: .whitespacesAndNewlines) + let recentlyActive = lastInputSeconds.map { $0 <= thresholdSeconds } ?? (mode == .local) + + if let session = requested, !session.isEmpty { + if let active, !active.isEmpty { + return active == session + } + return recentlyActive + } + + if let active, !active.isEmpty { + return true + } + return mode == .local + } + + private static func lastInputSeconds() -> Int? { + let anyEvent = CGEventType(rawValue: UInt32.max) ?? .null + let seconds = CGEventSource.secondsSinceLastEventType(.combinedSessionState, eventType: anyEvent) + if seconds.isNaN || seconds.isInfinite || seconds < 0 { return nil } + return Int(seconds.rounded()) + } +} + +#if DEBUG +extension ExecApprovalsGatewayPrompter { + static func _testShouldPresent( + mode: AppState.ConnectionMode, + activeSession: String?, + requestSession: String?, + lastInputSeconds: Int?, + thresholdSeconds: Int = 120) -> Bool + { + self.shouldPresent( + mode: mode, + activeSession: activeSession, + requestSession: requestSession, + lastInputSeconds: lastInputSeconds, + thresholdSeconds: thresholdSeconds) + } +} +#endif diff --git a/apps/macos/Sources/Clawdbot/ExecApprovalsSocket.swift b/apps/macos/Sources/Clawdbot/ExecApprovalsSocket.swift index ca5e85edf..68f8e906d 100644 --- a/apps/macos/Sources/Clawdbot/ExecApprovalsSocket.swift +++ b/apps/macos/Sources/Clawdbot/ExecApprovalsSocket.swift @@ -13,6 +13,7 @@ struct ExecApprovalPromptRequest: Codable, Sendable { var ask: String? var agentId: String? var resolvedPath: String? + var sessionKey: String? } private struct ExecApprovalSocketRequest: Codable { @@ -28,7 +29,7 @@ private struct ExecApprovalSocketDecision: Codable { var decision: ExecApprovalDecision } -fileprivate struct ExecHostSocketRequest: Codable { +private struct ExecHostSocketRequest: Codable { var type: String var id: String var nonce: String @@ -37,7 +38,7 @@ fileprivate struct ExecHostSocketRequest: Codable { var requestJson: String } -fileprivate struct ExecHostRequest: Codable { +private struct ExecHostRequest: Codable { var command: [String] var rawCommand: String? var cwd: String? @@ -46,9 +47,10 @@ fileprivate struct ExecHostRequest: Codable { var needsScreenRecording: Bool? var agentId: String? var sessionKey: String? + var approvalDecision: ExecApprovalDecision? } -fileprivate struct ExecHostRunResult: Codable { +private struct ExecHostRunResult: Codable { var exitCode: Int? var timedOut: Bool var success: Bool @@ -57,13 +59,13 @@ fileprivate struct ExecHostRunResult: Codable { var error: String? } -fileprivate struct ExecHostError: Codable { +private struct ExecHostError: Codable { var code: String var message: String var reason: String? } -fileprivate struct ExecHostResponse: Codable { +private struct ExecHostResponse: Codable { var type: String var id: String var ok: Bool @@ -74,29 +76,32 @@ fileprivate struct ExecHostResponse: Codable { enum ExecApprovalsSocketClient { private struct TimeoutError: LocalizedError { var message: String - var errorDescription: String? { message } + var errorDescription: String? { self.message } } static func requestDecision( socketPath: String, token: String, request: ExecApprovalPromptRequest, - timeoutMs: Int = 15_000) async -> ExecApprovalDecision? + timeoutMs: Int = 15000) async -> ExecApprovalDecision? { let trimmedPath = socketPath.trimmingCharacters(in: .whitespacesAndNewlines) let trimmedToken = token.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmedPath.isEmpty, !trimmedToken.isEmpty else { return nil } do { - return try await AsyncTimeout.withTimeoutMs(timeoutMs: timeoutMs, onTimeout: { - TimeoutError(message: "exec approvals socket timeout") - }, operation: { - try await Task.detached { - try self.requestDecisionSync( - socketPath: trimmedPath, - token: trimmedToken, - request: request) - }.value - }) + return try await AsyncTimeout.withTimeoutMs( + timeoutMs: timeoutMs, + onTimeout: { + TimeoutError(message: "exec approvals socket timeout") + }, + operation: { + try await Task.detached { + try self.requestDecisionSync( + socketPath: trimmedPath, + token: trimmedToken, + request: request) + }.value + }) } catch { return nil } @@ -211,36 +216,15 @@ enum ExecApprovalsPromptPresenter { let alert = NSAlert() alert.alertStyle = .warning alert.messageText = "Allow this command?" - - var details = "Clawdbot wants to run:\n\n\(request.command)" - let trimmedCwd = request.cwd?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - if !trimmedCwd.isEmpty { - details += "\n\nWorking directory:\n\(trimmedCwd)" - } - let trimmedAgent = request.agentId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - if !trimmedAgent.isEmpty { - details += "\n\nAgent:\n\(trimmedAgent)" - } - let trimmedPath = request.resolvedPath?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - if !trimmedPath.isEmpty { - details += "\n\nExecutable:\n\(trimmedPath)" - } - let trimmedHost = request.host?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - if !trimmedHost.isEmpty { - details += "\n\nHost:\n\(trimmedHost)" - } - if let security = request.security?.trimmingCharacters(in: .whitespacesAndNewlines), !security.isEmpty { - details += "\n\nSecurity:\n\(security)" - } - if let ask = request.ask?.trimmingCharacters(in: .whitespacesAndNewlines), !ask.isEmpty { - details += "\nAsk mode:\n\(ask)" - } - details += "\n\nThis runs on this machine." - alert.informativeText = details + alert.informativeText = "Review the command details before allowing." + alert.accessoryView = self.buildAccessoryView(request) alert.addButton(withTitle: "Allow Once") alert.addButton(withTitle: "Always Allow") alert.addButton(withTitle: "Don't Allow") + if #available(macOS 11.0, *), alert.buttons.indices.contains(2) { + alert.buttons[2].hasDestructiveAction = true + } switch alert.runModal() { case .alertFirstButtonReturn: @@ -251,10 +235,128 @@ enum ExecApprovalsPromptPresenter { return .deny } } + + @MainActor + private static func buildAccessoryView(_ request: ExecApprovalPromptRequest) -> NSView { + let stack = NSStackView() + stack.orientation = .vertical + stack.spacing = 8 + stack.alignment = .leading + + let commandTitle = NSTextField(labelWithString: "Command") + commandTitle.font = NSFont.boldSystemFont(ofSize: NSFont.systemFontSize) + stack.addArrangedSubview(commandTitle) + + let commandText = NSTextView() + commandText.isEditable = false + commandText.isSelectable = true + commandText.drawsBackground = true + commandText.backgroundColor = NSColor.textBackgroundColor + commandText.font = NSFont.monospacedSystemFont(ofSize: NSFont.systemFontSize, weight: .regular) + commandText.string = request.command + commandText.textContainerInset = NSSize(width: 6, height: 6) + commandText.textContainer?.lineFragmentPadding = 0 + commandText.textContainer?.widthTracksTextView = true + commandText.isHorizontallyResizable = false + commandText.isVerticallyResizable = false + + let commandScroll = NSScrollView() + commandScroll.borderType = .lineBorder + commandScroll.hasVerticalScroller = false + commandScroll.hasHorizontalScroller = false + commandScroll.documentView = commandText + commandScroll.translatesAutoresizingMaskIntoConstraints = false + commandScroll.widthAnchor.constraint(lessThanOrEqualToConstant: 440).isActive = true + commandScroll.heightAnchor.constraint(greaterThanOrEqualToConstant: 56).isActive = true + stack.addArrangedSubview(commandScroll) + + let contextTitle = NSTextField(labelWithString: "Context") + contextTitle.font = NSFont.boldSystemFont(ofSize: NSFont.systemFontSize) + stack.addArrangedSubview(contextTitle) + + let contextStack = NSStackView() + contextStack.orientation = .vertical + contextStack.spacing = 4 + contextStack.alignment = .leading + + let trimmedCwd = request.cwd?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + if !trimmedCwd.isEmpty { + self.addDetailRow(title: "Working directory", value: trimmedCwd, to: contextStack) + } + let trimmedAgent = request.agentId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + if !trimmedAgent.isEmpty { + self.addDetailRow(title: "Agent", value: trimmedAgent, to: contextStack) + } + let trimmedPath = request.resolvedPath?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + if !trimmedPath.isEmpty { + self.addDetailRow(title: "Executable", value: trimmedPath, to: contextStack) + } + let trimmedHost = request.host?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + if !trimmedHost.isEmpty { + self.addDetailRow(title: "Host", value: trimmedHost, to: contextStack) + } + if let security = request.security?.trimmingCharacters(in: .whitespacesAndNewlines), !security.isEmpty { + self.addDetailRow(title: "Security", value: security, to: contextStack) + } + if let ask = request.ask?.trimmingCharacters(in: .whitespacesAndNewlines), !ask.isEmpty { + self.addDetailRow(title: "Ask mode", value: ask, to: contextStack) + } + + if contextStack.arrangedSubviews.isEmpty { + let empty = NSTextField(labelWithString: "No additional context provided.") + empty.textColor = NSColor.secondaryLabelColor + empty.font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize) + contextStack.addArrangedSubview(empty) + } + + stack.addArrangedSubview(contextStack) + + let footer = NSTextField(labelWithString: "This runs on this machine.") + footer.textColor = NSColor.secondaryLabelColor + footer.font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize) + stack.addArrangedSubview(footer) + + return stack + } + + @MainActor + private static func addDetailRow(title: String, value: String, to stack: NSStackView) { + let row = NSStackView() + row.orientation = .horizontal + row.spacing = 6 + row.alignment = .firstBaseline + + let titleLabel = NSTextField(labelWithString: "\(title):") + titleLabel.font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize, weight: .semibold) + titleLabel.textColor = NSColor.secondaryLabelColor + + let valueLabel = NSTextField(labelWithString: value) + valueLabel.font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize) + valueLabel.lineBreakMode = .byTruncatingMiddle + valueLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + + row.addArrangedSubview(titleLabel) + row.addArrangedSubview(valueLabel) + stack.addArrangedSubview(row) + } } @MainActor -fileprivate enum ExecHostExecutor { +private enum ExecHostExecutor { + private struct ExecApprovalContext { + let command: [String] + let displayCommand: String + let trimmedAgent: String? + let approvals: ExecApprovalsResolved + let security: ExecSecurity + let ask: ExecAsk + let autoAllowSkills: Bool + let env: [String: String]? + let resolution: ExecCommandResolution? + let allowlistMatch: ExecAllowlistEntry? + let skillAllow: Bool + } + private static let blockedEnvKeys: Set = [ "PATH", "NODE_OPTIONS", @@ -273,14 +375,94 @@ fileprivate enum ExecHostExecutor { static func handle(_ request: ExecHostRequest) async -> ExecHostResponse { let command = request.command.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } guard !command.isEmpty else { - return ExecHostResponse( - type: "exec-res", - id: UUID().uuidString, - ok: false, - payload: nil, - error: ExecHostError(code: "INVALID_REQUEST", message: "command required", reason: "invalid")) + return self.errorResponse( + code: "INVALID_REQUEST", + message: "command required", + reason: "invalid") } + let context = await self.buildContext(request: request, command: command) + if context.security == .deny { + return self.errorResponse( + code: "UNAVAILABLE", + message: "SYSTEM_RUN_DISABLED: security=deny", + reason: "security=deny") + } + + let approvalDecision = request.approvalDecision + if approvalDecision == .deny { + return self.errorResponse( + code: "UNAVAILABLE", + message: "SYSTEM_RUN_DENIED: user denied", + reason: "user-denied") + } + + var approvedByAsk = approvalDecision != nil + if ExecApprovalHelpers.requiresAsk( + ask: context.ask, + security: context.security, + allowlistMatch: context.allowlistMatch, + skillAllow: context.skillAllow), + approvalDecision == nil + { + let decision = ExecApprovalsPromptPresenter.prompt( + ExecApprovalPromptRequest( + command: context.displayCommand, + cwd: request.cwd, + host: "node", + security: context.security.rawValue, + ask: context.ask.rawValue, + agentId: context.trimmedAgent, + resolvedPath: context.resolution?.resolvedPath, + sessionKey: request.sessionKey)) + + switch decision { + case .deny: + return self.errorResponse( + code: "UNAVAILABLE", + message: "SYSTEM_RUN_DENIED: user denied", + reason: "user-denied") + case .allowAlways: + approvedByAsk = true + self.persistAllowlistEntry(decision: decision, context: context) + case .allowOnce: + approvedByAsk = true + } + } + + self.persistAllowlistEntry(decision: approvalDecision, context: context) + + if context.security == .allowlist, + context.allowlistMatch == nil, + !context.skillAllow, + !approvedByAsk + { + return self.errorResponse( + code: "UNAVAILABLE", + message: "SYSTEM_RUN_DENIED: allowlist miss", + reason: "allowlist-miss") + } + + if let match = context.allowlistMatch { + ExecApprovalsStore.recordAllowlistUse( + agentId: context.trimmedAgent, + pattern: match.pattern, + command: context.displayCommand, + resolvedPath: context.resolution?.resolvedPath) + } + + if let errorResponse = await self.ensureScreenRecordingAccess(request.needsScreenRecording) { + return errorResponse + } + + return await self.runCommand( + command: command, + cwd: request.cwd, + env: context.env, + timeoutMs: request.timeoutMs) + } + + private static func buildContext(request: ExecHostRequest, command: [String]) async -> ExecApprovalContext { let displayCommand = ExecCommandFormatter.displayString( for: command, rawCommand: request.rawCommand) @@ -306,90 +488,56 @@ fileprivate enum ExecHostExecutor { } else { skillAllow = false } + return ExecApprovalContext( + command: command, + displayCommand: displayCommand, + trimmedAgent: trimmedAgent, + approvals: approvals, + security: security, + ask: ask, + autoAllowSkills: autoAllowSkills, + env: env, + resolution: resolution, + allowlistMatch: allowlistMatch, + skillAllow: skillAllow) + } - if security == .deny { - return ExecHostResponse( - type: "exec-res", - id: UUID().uuidString, - ok: false, - payload: nil, - error: ExecHostError(code: "UNAVAILABLE", message: "SYSTEM_RUN_DISABLED: security=deny", reason: "security=deny")) + private static func persistAllowlistEntry( + decision: ExecApprovalDecision?, + context: ExecApprovalContext) + { + guard decision == .allowAlways, context.security == .allowlist else { return } + guard let pattern = ExecApprovalHelpers.allowlistPattern( + command: context.command, + resolution: context.resolution) + else { + return } + ExecApprovalsStore.addAllowlistEntry(agentId: context.trimmedAgent, pattern: pattern) + } - let requiresAsk: Bool = { - if ask == .always { return true } - if ask == .onMiss && security == .allowlist && allowlistMatch == nil && !skillAllow { return true } - return false - }() + private static func ensureScreenRecordingAccess(_ needsScreenRecording: Bool?) async -> ExecHostResponse? { + guard needsScreenRecording == true else { return nil } + let authorized = await PermissionManager + .status([.screenRecording])[.screenRecording] ?? false + if authorized { return nil } + return self.errorResponse( + code: "UNAVAILABLE", + message: "PERMISSION_MISSING: screenRecording", + reason: "permission:screenRecording") + } - var approvedByAsk = false - if requiresAsk { - let decision = ExecApprovalsPromptPresenter.prompt( - ExecApprovalPromptRequest( - command: displayCommand, - cwd: request.cwd, - host: "node", - security: security.rawValue, - ask: ask.rawValue, - agentId: trimmedAgent, - resolvedPath: resolution?.resolvedPath)) - - switch decision { - case .deny: - return ExecHostResponse( - type: "exec-res", - id: UUID().uuidString, - ok: false, - payload: nil, - error: ExecHostError(code: "UNAVAILABLE", message: "SYSTEM_RUN_DENIED: user denied", reason: "user-denied")) - case .allowAlways: - approvedByAsk = true - if security == .allowlist { - let pattern = resolution?.resolvedPath ?? resolution?.rawExecutable ?? command.first ?? "" - if !pattern.isEmpty { - ExecApprovalsStore.addAllowlistEntry(agentId: trimmedAgent, pattern: pattern) - } - } - case .allowOnce: - approvedByAsk = true - } - } - - if security == .allowlist && allowlistMatch == nil && !skillAllow && !approvedByAsk { - return ExecHostResponse( - type: "exec-res", - id: UUID().uuidString, - ok: false, - payload: nil, - error: ExecHostError(code: "UNAVAILABLE", message: "SYSTEM_RUN_DENIED: allowlist miss", reason: "allowlist-miss")) - } - - if let match = allowlistMatch { - ExecApprovalsStore.recordAllowlistUse( - agentId: trimmedAgent, - pattern: match.pattern, - command: displayCommand, - resolvedPath: resolution?.resolvedPath) - } - - if request.needsScreenRecording == true { - let authorized = await PermissionManager - .status([.screenRecording])[.screenRecording] ?? false - if !authorized { - return ExecHostResponse( - type: "exec-res", - id: UUID().uuidString, - ok: false, - payload: nil, - error: ExecHostError(code: "UNAVAILABLE", message: "PERMISSION_MISSING: screenRecording", reason: "permission:screenRecording")) - } - } - - let timeoutSec = request.timeoutMs.flatMap { Double($0) / 1000.0 } + private static func runCommand( + command: [String], + cwd: String?, + env: [String: String]?, + timeoutMs: Int?) async -> ExecHostResponse + { + let timeoutSec = timeoutMs.flatMap { Double($0) / 1000.0 } let result = await Task.detached { () -> ShellExecutor.ShellResult in await ShellExecutor.runDetailed( command: command, - cwd: request.cwd, + cwd: cwd, env: env, timeout: timeoutSec) }.value @@ -400,7 +548,24 @@ fileprivate enum ExecHostExecutor { stdout: result.stdout, stderr: result.stderr, error: result.errorMessage) - return ExecHostResponse( + return self.successResponse(payload) + } + + private static func errorResponse( + code: String, + message: String, + reason: String?) -> ExecHostResponse + { + ExecHostResponse( + type: "exec-res", + id: UUID().uuidString, + ok: false, + payload: nil, + error: ExecHostError(code: code, message: message, reason: reason)) + } + + private static func successResponse(_ payload: ExecHostRunResult) -> ExecHostResponse { + ExecHostResponse( type: "exec-res", id: UUID().uuidString, ok: true, @@ -621,7 +786,7 @@ private final class ExecApprovalsSocketServer: @unchecked Sendable { private func handleExecRequest(_ request: ExecHostSocketRequest) async -> ExecHostResponse { let nowMs = Int(Date().timeIntervalSince1970 * 1000) - if abs(nowMs - request.ts) > 10_000 { + if abs(nowMs - request.ts) > 10000 { return ExecHostResponse( type: "exec-res", id: request.id, diff --git a/apps/macos/Sources/Clawdbot/GatewayConnection.swift b/apps/macos/Sources/Clawdbot/GatewayConnection.swift index 477a65954..9feb98ba9 100644 --- a/apps/macos/Sources/Clawdbot/GatewayConnection.swift +++ b/apps/macos/Sources/Clawdbot/GatewayConnection.swift @@ -1,4 +1,5 @@ import ClawdbotChatUI +import ClawdbotKit import ClawdbotProtocol import Foundation import OSLog @@ -14,6 +15,7 @@ enum GatewayAgentChannel: String, Codable, CaseIterable, Sendable { case signal case imessage case msteams + case bluebubbles case webchat init(raw: String?) { @@ -67,6 +69,7 @@ actor GatewayConnection { case channelsLogout = "channels.logout" case modelsList = "models.list" case chatHistory = "chat.history" + case sessionsPreview = "sessions.preview" case chatSend = "chat.send" case chatAbort = "chat.abort" case skillsStatus = "skills.status" @@ -76,6 +79,10 @@ actor GatewayConnection { case voicewakeSet = "voicewake.set" case nodePairApprove = "node.pair.approve" case nodePairReject = "node.pair.reject" + case devicePairList = "device.pair.list" + case devicePairApprove = "device.pair.approve" + case devicePairReject = "device.pair.reject" + case execApprovalResolve = "exec.approval.resolve" case cronList = "cron.list" case cronRuns = "cron.runs" case cronRun = "cron.run" @@ -142,6 +149,27 @@ actor GatewayConnection { } } + let nsError = lastError as NSError + if nsError.domain == URLError.errorDomain, + let fallback = await GatewayEndpointStore.shared.maybeFallbackToTailnet(from: cfg.url) + { + await self.configure(url: fallback.url, token: fallback.token, password: fallback.password) + for delayMs in [150, 400, 900] { + try await Task.sleep(nanoseconds: UInt64(delayMs) * 1_000_000) + do { + guard let client = self.client else { + throw NSError( + domain: "Gateway", + code: 0, + userInfo: [NSLocalizedDescriptionKey: "gateway not configured"]) + } + return try await client.request(method: method, params: params, timeoutMs: timeoutMs) + } catch { + lastError = error + } + } + } + throw lastError case .remote: let nsError = error as NSError @@ -222,6 +250,11 @@ actor GatewayConnection { await self.configure(url: cfg.url, token: cfg.token, password: cfg.password) } + func authSource() async -> GatewayAuthSource? { + guard let client else { return nil } + return await client.authSource() + } + func shutdown() async { if let client { await client.shutdown() @@ -238,9 +271,9 @@ actor GatewayConnection { return trimmed.isEmpty ? nil : trimmed } - private func sessionDefaultString(_ defaults: [String: AnyCodable]?, key: String) -> String { - (defaults?[key]?.stringValue ?? "") - .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + private func sessionDefaultString(_ defaults: [String: ClawdbotProtocol.AnyCodable]?, key: String) -> String { + let raw = defaults?[key]?.value as? String + return (raw ?? "").trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) } func cachedMainSessionKey() -> String? { @@ -508,6 +541,30 @@ extension GatewayConnection { return try await self.requestDecoded(method: .skillsUpdate, params: params) } + // MARK: - Sessions + + func sessionsPreview( + keys: [String], + limit: Int? = nil, + maxChars: Int? = nil, + timeoutMs: Int? = nil) async throws -> ClawdbotSessionsPreviewPayload + { + let resolvedKeys = keys + .map { self.canonicalizeSessionKey($0) } + .filter { !$0.isEmpty } + if resolvedKeys.isEmpty { + return ClawdbotSessionsPreviewPayload(ts: 0, previews: []) + } + var params: [String: AnyCodable] = ["keys": AnyCodable(resolvedKeys)] + if let limit { params["limit"] = AnyCodable(limit) } + if let maxChars { params["maxChars"] = AnyCodable(maxChars) } + let timeout = timeoutMs.map { Double($0) } + return try await self.requestDecoded( + method: .sessionsPreview, + params: params, + timeoutMs: timeout) + } + // MARK: - Chat func chatHistory( @@ -610,6 +667,22 @@ extension GatewayConnection { timeoutMs: 10000) } + // MARK: - Device pairing + + func devicePairApprove(requestId: String) async throws { + try await self.requestVoid( + method: .devicePairApprove, + params: ["requestId": AnyCodable(requestId)], + timeoutMs: 10000) + } + + func devicePairReject(requestId: String) async throws { + try await self.requestVoid( + method: .devicePairReject, + params: ["requestId": AnyCodable(requestId)], + timeoutMs: 10000) + } + // MARK: - Cron struct CronSchedulerStatus: Decodable, Sendable { diff --git a/apps/macos/Sources/Clawdbot/GatewayConnectivityCoordinator.swift b/apps/macos/Sources/Clawdbot/GatewayConnectivityCoordinator.swift new file mode 100644 index 000000000..ac65ec0ac --- /dev/null +++ b/apps/macos/Sources/Clawdbot/GatewayConnectivityCoordinator.swift @@ -0,0 +1,63 @@ +import Foundation +import Observation +import OSLog + +@MainActor +@Observable +final class GatewayConnectivityCoordinator { + static let shared = GatewayConnectivityCoordinator() + + private let logger = Logger(subsystem: "com.clawdbot", category: "gateway.connectivity") + private var endpointTask: Task? + private var lastResolvedURL: URL? + + private(set) var endpointState: GatewayEndpointState? + private(set) var resolvedURL: URL? + private(set) var resolvedMode: AppState.ConnectionMode? + private(set) var resolvedHostLabel: String? + + private init() { + self.start() + } + + func start() { + guard self.endpointTask == nil else { return } + self.endpointTask = Task { [weak self] in + guard let self else { return } + let stream = await GatewayEndpointStore.shared.subscribe() + for await state in stream { + await MainActor.run { self.handleEndpointState(state) } + } + } + } + + var localEndpointHostLabel: String? { + guard self.resolvedMode == .local, let url = self.resolvedURL else { return nil } + return Self.hostLabel(for: url) + } + + private func handleEndpointState(_ state: GatewayEndpointState) { + self.endpointState = state + switch state { + case let .ready(mode, url, _, _): + self.resolvedMode = mode + self.resolvedURL = url + self.resolvedHostLabel = Self.hostLabel(for: url) + let urlChanged = self.lastResolvedURL?.absoluteString != url.absoluteString + if urlChanged { + self.lastResolvedURL = url + Task { await ControlChannel.shared.refreshEndpoint(reason: "endpoint changed") } + } + case let .connecting(mode, _): + self.resolvedMode = mode + case let .unavailable(mode, _): + self.resolvedMode = mode + } + } + + private static func hostLabel(for url: URL) -> String { + let host = url.host ?? url.absoluteString + if let port = url.port { return "\(host):\(port)" } + return host + } +} diff --git a/apps/macos/Sources/Clawdbot/GatewayDiscoveryMenu.swift b/apps/macos/Sources/Clawdbot/GatewayDiscoveryMenu.swift index f0e5a40a0..a064b788a 100644 --- a/apps/macos/Sources/Clawdbot/GatewayDiscoveryMenu.swift +++ b/apps/macos/Sources/Clawdbot/GatewayDiscoveryMenu.swift @@ -19,7 +19,7 @@ struct GatewayDiscoveryInlineList: View { } if self.discovery.gateways.isEmpty { - Text("No bridges found yet.") + Text("No gateways found yet.") .font(.caption) .foregroundStyle(.secondary) } else { @@ -40,7 +40,7 @@ struct GatewayDiscoveryInlineList: View { .font(.callout.weight(.semibold)) .lineLimit(1) .truncationMode(.tail) - Text(target ?? "Bridge pairing only") + Text(target ?? "Gateway pairing only") .font(.caption.monospaced()) .foregroundStyle(.secondary) .lineLimit(1) @@ -83,7 +83,7 @@ struct GatewayDiscoveryInlineList: View { .fill(Color(NSColor.controlBackgroundColor))) } } - .help("Click a discovered bridge to fill the SSH target.") + .help("Click a discovered gateway to fill the SSH target.") } private func suggestedSSHTarget(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) -> String? { @@ -130,6 +130,6 @@ struct GatewayDiscoveryMenu: View { } label: { Image(systemName: "dot.radiowaves.left.and.right") } - .help("Discover Clawdbot bridges on your LAN") + .help("Discover Clawdbot gateways on your LAN") } } diff --git a/apps/macos/Sources/Clawdbot/BridgeDiscoveryPreferences.swift b/apps/macos/Sources/Clawdbot/GatewayDiscoveryPreferences.swift similarity index 51% rename from apps/macos/Sources/Clawdbot/BridgeDiscoveryPreferences.swift rename to apps/macos/Sources/Clawdbot/GatewayDiscoveryPreferences.swift index 6147bbfd2..d725fdba5 100644 --- a/apps/macos/Sources/Clawdbot/BridgeDiscoveryPreferences.swift +++ b/apps/macos/Sources/Clawdbot/GatewayDiscoveryPreferences.swift @@ -1,10 +1,13 @@ import Foundation -enum BridgeDiscoveryPreferences { - private static let preferredStableIDKey = "bridge.preferredStableID" +enum GatewayDiscoveryPreferences { + private static let preferredStableIDKey = "gateway.preferredStableID" + private static let legacyPreferredStableIDKey = "bridge.preferredStableID" static func preferredStableID() -> String? { - let raw = UserDefaults.standard.string(forKey: self.preferredStableIDKey) + let defaults = UserDefaults.standard + let raw = defaults.string(forKey: self.preferredStableIDKey) + ?? defaults.string(forKey: self.legacyPreferredStableIDKey) let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines) return trimmed?.isEmpty == false ? trimmed : nil } @@ -13,8 +16,10 @@ enum BridgeDiscoveryPreferences { let trimmed = stableID?.trimmingCharacters(in: .whitespacesAndNewlines) if let trimmed, !trimmed.isEmpty { UserDefaults.standard.set(trimmed, forKey: self.preferredStableIDKey) + UserDefaults.standard.removeObject(forKey: self.legacyPreferredStableIDKey) } else { UserDefaults.standard.removeObject(forKey: self.preferredStableIDKey) + UserDefaults.standard.removeObject(forKey: self.legacyPreferredStableIDKey) } } } diff --git a/apps/macos/Sources/Clawdbot/GatewayEndpointStore.swift b/apps/macos/Sources/Clawdbot/GatewayEndpointStore.swift index ff1e666b2..633b7d872 100644 --- a/apps/macos/Sources/Clawdbot/GatewayEndpointStore.swift +++ b/apps/macos/Sources/Clawdbot/GatewayEndpointStore.swift @@ -15,7 +15,13 @@ enum GatewayEndpointState: Sendable, Equatable { /// - The endpoint store owns observation + explicit "ensure tunnel" actions. actor GatewayEndpointStore { static let shared = GatewayEndpointStore() - private static let supportedBindModes: Set = ["loopback", "tailnet", "lan", "auto"] + private static let supportedBindModes: Set = [ + "loopback", + "tailnet", + "lan", + "auto", + "custom", + ] private static let remoteConnectingDetail = "Connecting to remote gateway…" private static let staticLogger = Logger(subsystem: "com.clawdbot", category: "gateway-endpoint") private enum EnvOverrideWarningKind: Sendable { @@ -60,9 +66,12 @@ actor GatewayEndpointStore { let bind = GatewayEndpointStore.resolveGatewayBindMode( root: root, env: ProcessInfo.processInfo.environment) + let customBindHost = GatewayEndpointStore.resolveGatewayCustomBindHost(root: root) let tailscaleIP = await MainActor.run { TailscaleService.shared.tailscaleIP } + ?? TailscaleService.fallbackTailnetIPv4() return GatewayEndpointStore.resolveLocalGatewayHost( bindMode: bind, + customBindHost: customBindHost, tailscaleIP: tailscaleIP) }, remotePortIfRunning: { await RemoteTunnelManager.shared.controlTunnelPortIfRunning() }, @@ -147,7 +156,8 @@ actor GatewayEndpointStore { let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) if !trimmed.isEmpty { if let configToken = self.resolveConfigToken(isRemote: isRemote, root: root), - !configToken.isEmpty + !configToken.isEmpty, + configToken != trimmed { self.warnEnvOverrideOnce( kind: .token, @@ -156,32 +166,23 @@ actor GatewayEndpointStore { } return trimmed } + + if let configToken = self.resolveConfigToken(isRemote: isRemote, root: root), + !configToken.isEmpty + { + return configToken + } + if isRemote { - if let gateway = root["gateway"] as? [String: Any], - let remote = gateway["remote"] as? [String: Any], - let token = remote["token"] as? String - { - let value = token.trimmingCharacters(in: .whitespacesAndNewlines) - if !value.isEmpty { - return value - } - } return nil } - if let gateway = root["gateway"] as? [String: Any], - let auth = gateway["auth"] as? [String: Any], - let token = auth["token"] as? String - { - let value = token.trimmingCharacters(in: .whitespacesAndNewlines) - if !value.isEmpty { - return value - } - } + if let token = launchdSnapshot?.token?.trimmingCharacters(in: .whitespacesAndNewlines), !token.isEmpty { return token } + return nil } @@ -250,14 +251,21 @@ actor GatewayEndpointStore { let bind = GatewayEndpointStore.resolveGatewayBindMode( root: ClawdbotConfigFile.loadDict(), env: ProcessInfo.processInfo.environment) - let host = GatewayEndpointStore.resolveLocalGatewayHost(bindMode: bind, tailscaleIP: nil) + let customBindHost = GatewayEndpointStore.resolveGatewayCustomBindHost(root: ClawdbotConfigFile.loadDict()) + let scheme = GatewayEndpointStore.resolveGatewayScheme( + root: ClawdbotConfigFile.loadDict(), + env: ProcessInfo.processInfo.environment) + let host = GatewayEndpointStore.resolveLocalGatewayHost( + bindMode: bind, + customBindHost: customBindHost, + tailscaleIP: nil) let token = deps.token() let password = deps.password() switch initialMode { case .local: self.state = .ready( mode: .local, - url: URL(string: "ws://\(host):\(port)")!, + url: URL(string: "\(scheme)://\(host):\(port)")!, token: token, password: password) case .remote: @@ -294,9 +302,12 @@ actor GatewayEndpointStore { self.cancelRemoteEnsure() let port = self.deps.localPort() let host = await self.deps.localHost() + let scheme = GatewayEndpointStore.resolveGatewayScheme( + root: ClawdbotConfigFile.loadDict(), + env: ProcessInfo.processInfo.environment) self.setState(.ready( mode: .local, - url: URL(string: "ws://\(host):\(port)")!, + url: URL(string: "\(scheme)://\(host):\(port)")!, token: token, password: password)) case .remote: @@ -307,9 +318,12 @@ actor GatewayEndpointStore { return } self.cancelRemoteEnsure() + let scheme = GatewayEndpointStore.resolveGatewayScheme( + root: ClawdbotConfigFile.loadDict(), + env: ProcessInfo.processInfo.environment) self.setState(.ready( mode: .remote, - url: URL(string: "ws://127.0.0.1:\(Int(port))")!, + url: URL(string: "\(scheme)://127.0.0.1:\(Int(port))")!, token: token, password: password)) case .unconfigured: @@ -408,7 +422,10 @@ actor GatewayEndpointStore { let token = self.deps.token() let password = self.deps.password() - let url = URL(string: "ws://127.0.0.1:\(Int(forwarded))")! + let scheme = GatewayEndpointStore.resolveGatewayScheme( + root: ClawdbotConfigFile.loadDict(), + env: ProcessInfo.processInfo.environment) + let url = URL(string: "\(scheme)://127.0.0.1:\(Int(forwarded))")! self.setState(.ready(mode: .remote, url: url, token: token, password: password)) return (url, token, password) } catch let err as CancellationError { @@ -457,6 +474,36 @@ actor GatewayEndpointStore { } } + func maybeFallbackToTailnet(from currentURL: URL) async -> GatewayConnection.Config? { + let mode = await self.deps.mode() + guard mode == .local else { return nil } + + let root = ClawdbotConfigFile.loadDict() + let bind = GatewayEndpointStore.resolveGatewayBindMode( + root: root, + env: ProcessInfo.processInfo.environment) + guard bind == "tailnet" else { return nil } + + let currentHost = currentURL.host?.lowercased() ?? "" + guard currentHost == "127.0.0.1" || currentHost == "localhost" else { return nil } + + let tailscaleIP = await MainActor.run { TailscaleService.shared.tailscaleIP } + ?? TailscaleService.fallbackTailnetIPv4() + guard let tailscaleIP, !tailscaleIP.isEmpty else { return nil } + + let scheme = GatewayEndpointStore.resolveGatewayScheme( + root: root, + env: ProcessInfo.processInfo.environment) + let port = self.deps.localPort() + let token = self.deps.token() + let password = self.deps.password() + let url = URL(string: "\(scheme)://\(tailscaleIP):\(port)")! + + self.logger.info("auto bind fallback to tailnet host=\(tailscaleIP, privacy: .public)") + self.setState(.ready(mode: .local, url: url, token: token, password: password)) + return (url, token, password) + } + private static func resolveGatewayBindMode( root: [String: Any], env: [String: String]) -> String? @@ -478,13 +525,46 @@ actor GatewayEndpointStore { return nil } + private static func resolveGatewayCustomBindHost(root: [String: Any]) -> String? { + if let gateway = root["gateway"] as? [String: Any], + let customBindHost = gateway["customBindHost"] as? String + { + let trimmed = customBindHost.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty ? nil : trimmed + } + return nil + } + + private static func resolveGatewayScheme( + root: [String: Any], + env: [String: String]) -> String + { + if let envValue = env["CLAWDBOT_GATEWAY_TLS"]?.trimmingCharacters(in: .whitespacesAndNewlines), + !envValue.isEmpty + { + return (envValue == "1" || envValue.lowercased() == "true") ? "wss" : "ws" + } + if let gateway = root["gateway"] as? [String: Any], + let tls = gateway["tls"] as? [String: Any], + let enabled = tls["enabled"] as? Bool + { + return enabled ? "wss" : "ws" + } + return "ws" + } + private static func resolveLocalGatewayHost( bindMode: String?, + customBindHost: String?, tailscaleIP: String?) -> String { switch bindMode { - case "tailnet", "auto": + case "tailnet": tailscaleIP ?? "127.0.0.1" + case "auto": + "127.0.0.1" + case "custom": + customBindHost ?? "127.0.0.1" default: "127.0.0.1" } @@ -557,9 +637,13 @@ extension GatewayEndpointStore { static func _testResolveLocalGatewayHost( bindMode: String?, - tailscaleIP: String?) -> String + tailscaleIP: String?, + customBindHost: String? = nil) -> String { - self.resolveLocalGatewayHost(bindMode: bindMode, tailscaleIP: tailscaleIP) + self.resolveLocalGatewayHost( + bindMode: bindMode, + customBindHost: customBindHost, + tailscaleIP: tailscaleIP) } } #endif diff --git a/apps/macos/Sources/Clawdbot/GatewayEnvironment.swift b/apps/macos/Sources/Clawdbot/GatewayEnvironment.swift index e66ff2e04..5817a8b3b 100644 --- a/apps/macos/Sources/Clawdbot/GatewayEnvironment.swift +++ b/apps/macos/Sources/Clawdbot/GatewayEnvironment.swift @@ -120,8 +120,8 @@ enum GatewayEnvironment { kind: .missingNode, nodeVersion: nil, gatewayVersion: nil, - requiredGateway: expectedString, - message: RuntimeLocator.describeFailure(err)) + requiredGateway: expectedString, + message: RuntimeLocator.describeFailure(err)) case let .success(runtime): let gatewayBin = CommandResolver.clawdbotExecutable() @@ -237,11 +237,10 @@ enum GatewayEnvironment { static func installGlobal(versionString: String?, statusHandler: @escaping @Sendable (String) -> Void) async { let preferred = CommandResolver.preferredPaths().joined(separator: ":") let trimmed = versionString?.trimmingCharacters(in: .whitespacesAndNewlines) - let target: String - if let trimmed, !trimmed.isEmpty { - target = trimmed + let target: String = if let trimmed, !trimmed.isEmpty { + trimmed } else { - target = "latest" + "latest" } let npm = CommandResolver.findExecutable(named: "npm") let pnpm = CommandResolver.findExecutable(named: "pnpm") diff --git a/apps/macos/Sources/Clawdbot/GatewayLaunchAgentManager.swift b/apps/macos/Sources/Clawdbot/GatewayLaunchAgentManager.swift index 700b79f19..f0896e691 100644 --- a/apps/macos/Sources/Clawdbot/GatewayLaunchAgentManager.swift +++ b/apps/macos/Sources/Clawdbot/GatewayLaunchAgentManager.swift @@ -4,11 +4,46 @@ enum GatewayLaunchAgentManager { private static let logger = Logger(subsystem: "com.clawdbot", category: "gateway.launchd") private static let disableLaunchAgentMarker = ".clawdbot/disable-launchagent" + private static var disableLaunchAgentMarkerURL: URL { + FileManager().homeDirectoryForCurrentUser + .appendingPathComponent(self.disableLaunchAgentMarker) + } + private static var plistURL: URL { - FileManager.default.homeDirectoryForCurrentUser + FileManager().homeDirectoryForCurrentUser .appendingPathComponent("Library/LaunchAgents/\(gatewayLaunchdLabel).plist") } + static func isLaunchAgentWriteDisabled() -> Bool { + FileManager().fileExists(atPath: self.disableLaunchAgentMarkerURL.path) + } + + static func setLaunchAgentWriteDisabled(_ disabled: Bool) -> String? { + let marker = self.disableLaunchAgentMarkerURL + if disabled { + do { + try FileManager().createDirectory( + at: marker.deletingLastPathComponent(), + withIntermediateDirectories: true) + if !FileManager().fileExists(atPath: marker.path) { + FileManager().createFile(atPath: marker.path, contents: nil) + } + } catch { + return error.localizedDescription + } + return nil + } + + if FileManager().fileExists(atPath: marker.path) { + do { + try FileManager().removeItem(at: marker) + } catch { + return error.localizedDescription + } + } + return nil + } + static func isLoaded() async -> Bool { guard let loaded = await self.readDaemonLoaded() else { return false } return loaded @@ -66,12 +101,6 @@ enum GatewayLaunchAgentManager { } extension GatewayLaunchAgentManager { - private static func isLaunchAgentWriteDisabled() -> Bool { - let marker = FileManager.default.homeDirectoryForCurrentUser - .appendingPathComponent(self.disableLaunchAgentMarker) - return FileManager.default.fileExists(atPath: marker.path) - } - private static func readDaemonLoaded() async -> Bool? { let result = await self.runDaemonCommandResult( ["status", "--json", "--no-probe"], @@ -115,7 +144,7 @@ extension GatewayLaunchAgentManager { quiet: Bool) async -> CommandResult { let command = CommandResolver.clawdbotCommand( - subcommand: "daemon", + subcommand: "gateway", extraArgs: self.withJsonFlag(args), // Launchd management must always run locally, even if remote mode is configured. configRoot: ["gateway": ["mode": "local"]]) diff --git a/apps/macos/Sources/Clawdbot/GatewayPayloadDecoding.swift b/apps/macos/Sources/Clawdbot/GatewayPayloadDecoding.swift deleted file mode 100644 index e0ad02aa4..000000000 --- a/apps/macos/Sources/Clawdbot/GatewayPayloadDecoding.swift +++ /dev/null @@ -1,16 +0,0 @@ -import ClawdbotProtocol -import Foundation - -enum GatewayPayloadDecoding { - static func decode(_ payload: ClawdbotProtocol.AnyCodable, as _: T.Type = T.self) throws -> T { - let data = try JSONEncoder().encode(payload) - return try JSONDecoder().decode(T.self, from: data) - } - - static func decodeIfPresent(_ payload: ClawdbotProtocol.AnyCodable?, as _: T.Type = T.self) throws - -> T? - { - guard let payload else { return nil } - return try self.decode(payload, as: T.self) - } -} diff --git a/apps/macos/Sources/Clawdbot/GatewayProcessManager.swift b/apps/macos/Sources/Clawdbot/GatewayProcessManager.swift index 35d81243b..60964fa39 100644 --- a/apps/macos/Sources/Clawdbot/GatewayProcessManager.swift +++ b/apps/macos/Sources/Clawdbot/GatewayProcessManager.swift @@ -42,10 +42,20 @@ final class GatewayProcessManager { private var environmentRefreshTask: Task? private var lastEnvironmentRefresh: Date? private var logRefreshTask: Task? + #if DEBUG + private var testingConnection: GatewayConnection? + #endif private let logger = Logger(subsystem: "com.clawdbot", category: "gateway.process") private let logLimit = 20000 // characters to keep in-memory private let environmentRefreshMinInterval: TimeInterval = 30 + private var connection: GatewayConnection { + #if DEBUG + return self.testingConnection ?? .shared + #else + return .shared + #endif + } func setActive(_ active: Bool) { // Remote mode should never spawn a local gateway; treat as stopped. @@ -69,6 +79,11 @@ final class GatewayProcessManager { func ensureLaunchAgentEnabledIfNeeded() async { guard !CommandResolver.connectionModeIsRemote() else { return } + if GatewayLaunchAgentManager.isLaunchAgentWriteDisabled() { + self.appendLog("[gateway] launchd auto-enable skipped (attach-only)\n") + self.logger.info("gateway launchd auto-enable skipped (disable marker set)") + return + } let enabled = await GatewayLaunchAgentManager.isLoaded() guard !enabled else { return } let bundlePath = Bundle.main.bundleURL.path @@ -126,6 +141,10 @@ final class GatewayProcessManager { } } + func clearLastFailure() { + self.lastFailureReason = nil + } + func refreshEnvironmentStatus(force: Bool = false) { let now = Date() if !force { @@ -178,7 +197,7 @@ final class GatewayProcessManager { let hasListener = instance != nil let attemptAttach = { - try await GatewayConnection.shared.requestRaw(method: .health, timeoutMs: 2000) + try await self.connection.requestRaw(method: .health, timeoutMs: 2000) } for attempt in 0..<(hasListener ? 3 : 1) { @@ -187,6 +206,7 @@ final class GatewayProcessManager { let snap = decodeHealthSnapshot(from: data) let details = self.describe(details: instanceText, port: port, snap: snap) self.existingGatewayDetails = details + self.clearLastFailure() self.status = .attachedExisting(details: details) self.appendLog("[gateway] using existing instance: \(details)\n") self.logger.info("gateway using existing instance details=\(details)") @@ -222,19 +242,17 @@ final class GatewayProcessManager { private func describe(details instance: String?, port: Int, snap: HealthSnapshot?) -> String { let instanceText = instance ?? "pid unknown" if let snap { - let linkId = snap.channelOrder?.first(where: { - if let summary = snap.channels[$0] { return summary.linked != nil } - return false - }) ?? snap.channels.keys.first(where: { - if let summary = snap.channels[$0] { return summary.linked != nil } - return false - }) - let linked = linkId.flatMap { snap.channels[$0]?.linked } ?? false - let authAge = linkId.flatMap { snap.channels[$0]?.authAgeMs }.flatMap(msToAge) ?? "unknown age" + let order = snap.channelOrder ?? Array(snap.channels.keys) + let linkId = order.first(where: { snap.channels[$0]?.linked == true }) + ?? order.first(where: { snap.channels[$0]?.linked != nil }) + guard let linkId else { + return "port \(port), health probe succeeded, \(instanceText)" + } + let linked = snap.channels[linkId]?.linked ?? false + let authAge = snap.channels[linkId]?.authAgeMs.flatMap(msToAge) ?? "unknown age" let label = - linkId.flatMap { snap.channelLabels?[$0] } ?? - linkId?.capitalized ?? - "channel" + snap.channelLabels?[linkId] ?? + linkId.capitalized let linkText = linked ? "linked" : "not linked" return "port \(port), \(label) \(linkText), auth \(authAge), \(instanceText)" } @@ -293,6 +311,15 @@ final class GatewayProcessManager { return } + if GatewayLaunchAgentManager.isLaunchAgentWriteDisabled() { + let message = "Launchd disabled; start the Gateway manually or disable attach-only." + self.status = .failed(message) + self.lastFailureReason = "launchd disabled" + self.appendLog("[gateway] launchd disabled; skipping auto-start\n") + self.logger.info("gateway launchd enable skipped (disable marker set)") + return + } + let bundlePath = Bundle.main.bundleURL.path let port = GatewayEnvironment.gatewayPort() self.appendLog("[gateway] enabling launchd job (\(gatewayLaunchdLabel)) on port \(port)\n") @@ -310,9 +337,10 @@ final class GatewayProcessManager { while Date() < deadline { if !self.desiredActive { return } do { - _ = try await GatewayConnection.shared.requestRaw(method: .health, timeoutMs: 1500) + _ = try await self.connection.requestRaw(method: .health, timeoutMs: 1500) let instance = await PortGuardian.shared.describe(port: port) let details = instance.map { "pid \($0.pid)" } + self.clearLastFailure() self.status = .running(details: details) self.logger.info("gateway started details=\(details ?? "ok")") self.refreshControlChannelIfNeeded(reason: "gateway started") @@ -352,7 +380,8 @@ final class GatewayProcessManager { while Date() < deadline { if !self.desiredActive { return false } do { - _ = try await GatewayConnection.shared.requestRaw(method: .health, timeoutMs: 1500) + _ = try await self.connection.requestRaw(method: .health, timeoutMs: 1500) + self.clearLastFailure() return true } catch { try? await Task.sleep(nanoseconds: 300_000_000) @@ -365,7 +394,7 @@ final class GatewayProcessManager { func clearLog() { self.log = "" - try? FileManager.default.removeItem(atPath: GatewayLaunchAgentManager.launchdGatewayLogPath()) + try? FileManager().removeItem(atPath: GatewayLaunchAgentManager.launchdGatewayLogPath()) self.logger.debug("gateway log cleared") } @@ -378,10 +407,26 @@ final class GatewayProcessManager { } private nonisolated static func readGatewayLog(path: String, limit: Int) -> String { - guard FileManager.default.fileExists(atPath: path) else { return "" } + guard FileManager().fileExists(atPath: path) else { return "" } guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else { return "" } let text = String(data: data, encoding: .utf8) ?? "" if text.count <= limit { return text } return String(text.suffix(limit)) } } + +#if DEBUG +extension GatewayProcessManager { + func setTestingConnection(_ connection: GatewayConnection?) { + self.testingConnection = connection + } + + func setTestingDesiredActive(_ active: Bool) { + self.desiredActive = active + } + + func setTestingLastFailureReason(_ reason: String?) { + self.lastFailureReason = reason + } +} +#endif diff --git a/apps/macos/Sources/Clawdbot/GeneralSettings.swift b/apps/macos/Sources/Clawdbot/GeneralSettings.swift index 5f144864b..bffd75b3c 100644 --- a/apps/macos/Sources/Clawdbot/GeneralSettings.swift +++ b/apps/macos/Sources/Clawdbot/GeneralSettings.swift @@ -2,52 +2,25 @@ import AppKit import ClawdbotDiscovery import ClawdbotIPC import ClawdbotKit -import CoreLocation import Observation import SwiftUI struct GeneralSettings: View { @Bindable var state: AppState @AppStorage(cameraEnabledKey) private var cameraEnabled: Bool = false - @AppStorage(locationModeKey) private var locationModeRaw: String = ClawdbotLocationMode.off.rawValue - @AppStorage(locationPreciseKey) private var locationPreciseEnabled: Bool = true private let healthStore = HealthStore.shared private let gatewayManager = GatewayProcessManager.shared @State private var gatewayDiscovery = GatewayDiscoveryModel( localDisplayName: InstanceIdentity.displayName) - @State private var isInstallingCLI = false - @State private var cliStatus: String? - @State private var cliInstalled = false - @State private var cliInstallLocation: String? @State private var gatewayStatus: GatewayEnvironmentStatus = .checking @State private var remoteStatus: RemoteStatus = .idle @State private var showRemoteAdvanced = false private let isPreview = ProcessInfo.processInfo.isPreview private var isNixMode: Bool { ProcessInfo.processInfo.isNixMode } - @State private var lastLocationModeRaw: String = ClawdbotLocationMode.off.rawValue var body: some View { ScrollView(.vertical) { VStack(alignment: .leading, spacing: 18) { - if !self.state.onboardingSeen { - Button { - DebugActions.restartOnboarding() - } label: { - HStack(spacing: 8) { - Label("Complete onboarding to finish setup", systemImage: "arrow.counterclockwise") - .font(.callout.weight(.semibold)) - .foregroundStyle(Color.accentColor) - Spacer(minLength: 0) - Image(systemName: "chevron.right") - .font(.caption.weight(.semibold)) - .foregroundStyle(.tertiary) - } - .contentShape(Rectangle()) - } - .buttonStyle(.plain) - .padding(.bottom, 2) - } - VStack(alignment: .leading, spacing: 12) { SettingsToggleRow( title: "Clawdbot active", @@ -83,29 +56,6 @@ struct GeneralSettings: View { subtitle: "Allow the agent to capture a photo or short video via the built-in camera.", binding: self.$cameraEnabled) - SystemRunSettingsView() - - VStack(alignment: .leading, spacing: 6) { - Text("Location Access") - .font(.body) - - Picker("", selection: self.$locationModeRaw) { - Text("Off").tag(ClawdbotLocationMode.off.rawValue) - Text("While Using").tag(ClawdbotLocationMode.whileUsing.rawValue) - Text("Always").tag(ClawdbotLocationMode.always.rawValue) - } - .labelsHidden() - .pickerStyle(.menu) - - Toggle("Precise Location", isOn: self.$locationPreciseEnabled) - .disabled(self.locationMode == .off) - - Text("Always may require System Settings to approve background location.") - .font(.footnote) - .foregroundStyle(.tertiary) - .fixedSize(horizontal: false, vertical: true) - } - SettingsToggleRow( title: "Enable Peekaboo Bridge", subtitle: "Allow signed tools (e.g. `peekaboo`) to drive UI automation via PeekabooBridge.", @@ -130,29 +80,13 @@ struct GeneralSettings: View { } .onAppear { guard !self.isPreview else { return } - self.refreshCLIStatus() self.refreshGatewayStatus() - self.lastLocationModeRaw = self.locationModeRaw } .onChange(of: self.state.canvasEnabled) { _, enabled in if !enabled { CanvasManager.shared.hideAll() } } - .onChange(of: self.locationModeRaw) { _, newValue in - let previous = self.lastLocationModeRaw - self.lastLocationModeRaw = newValue - guard let mode = ClawdbotLocationMode(rawValue: newValue) else { return } - Task { - let granted = await self.requestLocationAuthorization(mode: mode) - if !granted { - await MainActor.run { - self.locationModeRaw = previous - self.lastLocationModeRaw = previous - } - } - } - } } private var activeBinding: Binding { @@ -161,39 +95,20 @@ struct GeneralSettings: View { set: { self.state.isPaused = !$0 }) } - private var locationMode: ClawdbotLocationMode { - ClawdbotLocationMode(rawValue: self.locationModeRaw) ?? .off - } - - private func requestLocationAuthorization(mode: ClawdbotLocationMode) async -> Bool { - guard mode != .off else { return true } - guard CLLocationManager.locationServicesEnabled() else { - await MainActor.run { LocationPermissionHelper.openSettings() } - return false - } - - let status = CLLocationManager().authorizationStatus - let requireAlways = mode == .always - if PermissionManager.isLocationAuthorized(status: status, requireAlways: requireAlways) { - return true - } - let updated = await LocationPermissionRequester.shared.request(always: requireAlways) - return PermissionManager.isLocationAuthorized(status: updated, requireAlways: requireAlways) - } - private var connectionSection: some View { VStack(alignment: .leading, spacing: 10) { Text("Clawdbot runs") .font(.title3.weight(.semibold)) .frame(maxWidth: .infinity, alignment: .leading) - Picker("", selection: self.$state.connectionMode) { + Picker("Mode", selection: self.$state.connectionMode) { Text("Not configured").tag(AppState.ConnectionMode.unconfigured) Text("Local (this Mac)").tag(AppState.ConnectionMode.local) Text("Remote over SSH").tag(AppState.ConnectionMode.remote) } - .pickerStyle(.segmented) - .frame(width: 380, alignment: .leading) + .pickerStyle(.menu) + .labelsHidden() + .frame(width: 260, alignment: .leading) if self.state.connectionMode == .unconfigured { Text("Pick Local or Remote to start the Gateway.") @@ -216,8 +131,6 @@ struct GeneralSettings: View { if self.state.connectionMode == .remote { self.remoteCard } - - self.cliInstaller } } @@ -299,6 +212,11 @@ struct GeneralSettings: View { .font(.caption) .foregroundStyle(.secondary) } + if let authLabel = ControlChannel.shared.authSourceLabel { + Text(authLabel) + .font(.caption) + .foregroundStyle(.secondary) + } } Text("Tip: enable Tailscale for stable remote access.") @@ -346,59 +264,6 @@ struct GeneralSettings: View { return message == self.controlStatusLine } - private var cliInstaller: some View { - VStack(alignment: .leading, spacing: 8) { - HStack(spacing: 10) { - Button { - Task { await self.installCLI() } - } label: { - let title = self.cliInstalled ? "Reinstall CLI" : "Install CLI" - ZStack { - Text(title) - .opacity(self.isInstallingCLI ? 0 : 1) - if self.isInstallingCLI { - ProgressView() - .controlSize(.mini) - } - } - .frame(minWidth: 150) - } - .disabled(self.isInstallingCLI) - - if self.isInstallingCLI { - Text("Working...") - .font(.callout) - .foregroundStyle(.secondary) - } else if self.cliInstalled { - Label("Installed", systemImage: "checkmark.circle.fill") - .font(.callout) - .foregroundStyle(.secondary) - } else { - Text("Not installed") - .font(.callout) - .foregroundStyle(.secondary) - } - } - - if let status = cliStatus { - Text(status) - .font(.caption) - .foregroundStyle(.secondary) - .lineLimit(2) - } else if let installLocation = self.cliInstallLocation { - Text("Found at \(installLocation)") - .font(.caption) - .foregroundStyle(.secondary) - .lineLimit(2) - } else { - Text("Installs a user-space Node 22+ runtime and the CLI (no Homebrew).") - .font(.caption) - .foregroundStyle(.secondary) - .lineLimit(2) - } - } - } - private var gatewayInstallerCard: some View { VStack(alignment: .leading, spacing: 8) { HStack(spacing: 10) { @@ -454,22 +319,6 @@ struct GeneralSettings: View { .cornerRadius(10) } - private func installCLI() async { - guard !self.isInstallingCLI else { return } - self.isInstallingCLI = true - defer { isInstallingCLI = false } - await CLIInstaller.install { status in - self.cliStatus = status - self.refreshCLIStatus() - } - } - - private func refreshCLIStatus() { - let installLocation = CLIInstaller.installedLocation() - self.cliInstallLocation = installLocation - self.cliInstalled = installLocation != nil - } - private func refreshGatewayStatus() { Task { let status = await Task.detached(priority: .utility) { @@ -716,7 +565,7 @@ extension GeneralSettings { } private func applyDiscoveredGateway(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) { - MacNodeModeCoordinator.shared.setPreferredBridgeStableID(gateway.stableID) + MacNodeModeCoordinator.shared.setPreferredGatewayStableID(gateway.stableID) let host = gateway.tailnetDns ?? gateway.lanHost guard let host else { return } @@ -763,9 +612,6 @@ extension GeneralSettings { message: "Gateway ready") view.remoteStatus = .failed("SSH failed") view.showRemoteAdvanced = true - view.cliInstalled = true - view.cliInstallLocation = "/usr/local/bin/clawdbot" - view.cliStatus = "Installed" _ = view.body state.connectionMode = .unconfigured diff --git a/apps/macos/Sources/Clawdbot/HealthStore.swift b/apps/macos/Sources/Clawdbot/HealthStore.swift index 668056e00..44ebcc053 100644 --- a/apps/macos/Sources/Clawdbot/HealthStore.swift +++ b/apps/macos/Sources/Clawdbot/HealthStore.swift @@ -166,6 +166,11 @@ final class HealthStore { _ snap: HealthSnapshot) -> (id: String, summary: HealthSnapshot.ChannelSummary)? { let order = snap.channelOrder ?? Array(snap.channels.keys) + for id in order { + if let summary = snap.channels[id], summary.linked == true { + return (id: id, summary: summary) + } + } for id in order { if let summary = snap.channels[id], summary.linked != nil { return (id: id, summary: summary) @@ -235,8 +240,8 @@ final class HealthStore { let lower = error.lowercased() if lower.contains("connection refused") { let port = GatewayEnvironment.gatewayPort() - return "The gateway control port (127.0.0.1:\(port)) isn’t listening — " + - "restart Clawdbot to bring it back." + let host = GatewayConnectivityCoordinator.shared.localEndpointHostLabel ?? "127.0.0.1:\(port)" + return "The gateway control port (\(host)) isn’t listening — restart Clawdbot to bring it back." } if lower.contains("timeout") { return "Timed out waiting for the control server; the gateway may be crashed or still starting." diff --git a/apps/macos/Sources/Clawdbot/InstanceIdentity.swift b/apps/macos/Sources/Clawdbot/InstanceIdentity.swift deleted file mode 100644 index 2bd2e6ea6..000000000 --- a/apps/macos/Sources/Clawdbot/InstanceIdentity.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Darwin -import Foundation - -enum InstanceIdentity { - private static let suiteName = "com.clawdbot.shared" - private static let instanceIdKey = "instanceId" - - private static var defaults: UserDefaults { - UserDefaults(suiteName: suiteName) ?? .standard - } - - static let instanceId: String = { - let defaults = Self.defaults - if let existing = defaults.string(forKey: instanceIdKey)? - .trimmingCharacters(in: .whitespacesAndNewlines), - !existing.isEmpty - { - return existing - } - - let id = UUID().uuidString.lowercased() - defaults.set(id, forKey: instanceIdKey) - return id - }() - - static let displayName: String = { - if let name = Host.current().localizedName?.trimmingCharacters(in: .whitespacesAndNewlines), - !name.isEmpty - { - return name - } - return "clawdbot" - }() - - static let modelIdentifier: String? = { - var size = 0 - guard sysctlbyname("hw.model", nil, &size, nil, 0) == 0, size > 1 else { return nil } - - var buffer = [CChar](repeating: 0, count: size) - guard sysctlbyname("hw.model", &buffer, &size, nil, 0) == 0 else { return nil } - - let bytes = buffer.prefix { $0 != 0 }.map { UInt8(bitPattern: $0) } - guard let raw = String(bytes: bytes, encoding: .utf8) else { return nil } - let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) - return trimmed.isEmpty ? nil : trimmed - }() -} diff --git a/apps/macos/Sources/Clawdbot/InstancesStore.swift b/apps/macos/Sources/Clawdbot/InstancesStore.swift index 196737f07..c12f8c2e0 100644 --- a/apps/macos/Sources/Clawdbot/InstancesStore.swift +++ b/apps/macos/Sources/Clawdbot/InstancesStore.swift @@ -1,3 +1,4 @@ +import ClawdbotKit import ClawdbotProtocol import Cocoa import Foundation diff --git a/apps/macos/Sources/Clawdbot/LaunchAgentManager.swift b/apps/macos/Sources/Clawdbot/LaunchAgentManager.swift index 2d2c3342a..ca967a133 100644 --- a/apps/macos/Sources/Clawdbot/LaunchAgentManager.swift +++ b/apps/macos/Sources/Clawdbot/LaunchAgentManager.swift @@ -3,17 +3,17 @@ import Foundation enum LaunchAgentManager { private static let legacyLaunchdLabel = "com.steipete.clawdbot" private static var plistURL: URL { - FileManager.default.homeDirectoryForCurrentUser + FileManager().homeDirectoryForCurrentUser .appendingPathComponent("Library/LaunchAgents/com.clawdbot.mac.plist") } private static var legacyPlistURL: URL { - FileManager.default.homeDirectoryForCurrentUser + FileManager().homeDirectoryForCurrentUser .appendingPathComponent("Library/LaunchAgents/\(legacyLaunchdLabel).plist") } static func status() async -> Bool { - guard FileManager.default.fileExists(atPath: self.plistURL.path) else { return false } + guard FileManager().fileExists(atPath: self.plistURL.path) else { return false } let result = await self.runLaunchctl(["print", "gui/\(getuid())/\(launchdLabel)"]) return result == 0 } @@ -21,7 +21,7 @@ enum LaunchAgentManager { static func set(enabled: Bool, bundlePath: String) async { if enabled { _ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(self.legacyLaunchdLabel)"]) - try? FileManager.default.removeItem(at: self.legacyPlistURL) + try? FileManager().removeItem(at: self.legacyPlistURL) self.writePlist(bundlePath: bundlePath) _ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(launchdLabel)"]) _ = await self.runLaunchctl(["bootstrap", "gui/\(getuid())", self.plistURL.path]) @@ -29,7 +29,7 @@ enum LaunchAgentManager { } else { // Disable autostart going forward but leave the current app running. // bootout would terminate the launchd job immediately (and crash the app if launched via agent). - try? FileManager.default.removeItem(at: self.plistURL) + try? FileManager().removeItem(at: self.plistURL) } } @@ -46,7 +46,7 @@ enum LaunchAgentManager { \(bundlePath)/Contents/MacOS/Clawdbot WorkingDirectory - \(FileManager.default.homeDirectoryForCurrentUser.path) + \(FileManager().homeDirectoryForCurrentUser.path) RunAtLoad KeepAlive diff --git a/apps/macos/Sources/Clawdbot/LogLocator.swift b/apps/macos/Sources/Clawdbot/LogLocator.swift index 8714f7d0c..0ce7eea2f 100644 --- a/apps/macos/Sources/Clawdbot/LogLocator.swift +++ b/apps/macos/Sources/Clawdbot/LogLocator.swift @@ -18,7 +18,7 @@ enum LogLocator { } private static func ensureLogDirExists() { - try? FileManager.default.createDirectory(at: self.logDir, withIntermediateDirectories: true) + try? FileManager().createDirectory(at: self.logDir, withIntermediateDirectories: true) } private static func modificationDate(for url: URL) -> Date { @@ -28,7 +28,7 @@ enum LogLocator { /// Returns the newest log file under /tmp/clawdbot/ (rolling or stdout), or nil if none exist. static func bestLogFile() -> URL? { self.ensureLogDirExists() - let fm = FileManager.default + let fm = FileManager() let files = (try? fm.contentsOfDirectory( at: self.logDir, includingPropertiesForKeys: [.contentModificationDateKey], diff --git a/apps/macos/Sources/Clawdbot/MenuBar.swift b/apps/macos/Sources/Clawdbot/MenuBar.swift index 01c626f0d..26a467e91 100644 --- a/apps/macos/Sources/Clawdbot/MenuBar.swift +++ b/apps/macos/Sources/Clawdbot/MenuBar.swift @@ -3,6 +3,7 @@ import Darwin import Foundation import MenuBarExtraAccess import Observation +import OSLog import Security import SwiftUI @@ -10,9 +11,11 @@ import SwiftUI struct ClawdbotApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) private var delegate @State private var state: AppState + private static let logger = Logger(subsystem: "com.clawdbot", category: "app") private let gatewayManager = GatewayProcessManager.shared private let controlChannel = ControlChannel.shared private let activityStore = WorkActivityStore.shared + private let connectivityCoordinator = GatewayConnectivityCoordinator.shared @State private var statusItem: NSStatusItem? @State private var isMenuPresented = false @State private var isPanelVisible = false @@ -30,6 +33,7 @@ struct ClawdbotApp: App { init() { ClawdbotLogging.bootstrapIfNeeded() + Self.applyAttachOnlyOverrideIfNeeded() _state = State(initialValue: AppStateStore.shared) } @@ -90,6 +94,22 @@ struct ClawdbotApp: App { self.statusItem?.button?.appearsDisabled = paused || sleeping } + private static func applyAttachOnlyOverrideIfNeeded() { + let args = CommandLine.arguments + guard args.contains("--attach-only") || args.contains("--no-launchd") else { return } + if let error = GatewayLaunchAgentManager.setLaunchAgentWriteDisabled(true) { + Self.logger.error("attach-only flag failed: \(error, privacy: .public)") + return + } + Task { + _ = await GatewayLaunchAgentManager.set( + enabled: false, + bundlePath: Bundle.main.bundlePath, + port: GatewayEnvironment.gatewayPort()) + } + Self.logger.info("attach-only flag enabled") + } + private var isGatewaySleeping: Bool { if self.state.isPaused { return false } switch self.state.connectionMode { @@ -256,7 +276,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate { } TerminationSignalWatcher.shared.start() NodePairingApprovalPrompter.shared.start() + DevicePairingApprovalPrompter.shared.start() ExecApprovalsPromptServer.shared.start() + ExecApprovalsGatewayPrompter.shared.start() MacNodeModeCoordinator.shared.start() VoiceWakeGlobalSettingsSync.shared.start() Task { PresenceReporter.shared.start() } @@ -281,7 +303,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate { func applicationWillTerminate(_ notification: Notification) { PresenceReporter.shared.stop() NodePairingApprovalPrompter.shared.stop() + DevicePairingApprovalPrompter.shared.stop() ExecApprovalsPromptServer.shared.stop() + ExecApprovalsGatewayPrompter.shared.stop() MacNodeModeCoordinator.shared.stop() TerminationSignalWatcher.shared.stop() VoiceWakeGlobalSettingsSync.shared.stop() diff --git a/apps/macos/Sources/Clawdbot/MenuContentView.swift b/apps/macos/Sources/Clawdbot/MenuContentView.swift index 049e1de9e..8dbeb8f44 100644 --- a/apps/macos/Sources/Clawdbot/MenuContentView.swift +++ b/apps/macos/Sources/Clawdbot/MenuContentView.swift @@ -15,6 +15,7 @@ struct MenuContent: View { private let controlChannel = ControlChannel.shared private let activityStore = WorkActivityStore.shared @Bindable private var pairingPrompter = NodePairingApprovalPrompter.shared + @Bindable private var devicePairingPrompter = DevicePairingApprovalPrompter.shared @Environment(\.openSettings) private var openSettings @State private var availableMics: [AudioInputDevice] = [] @State private var loadingMics = false @@ -50,6 +51,13 @@ struct MenuContent: View { label: "Pairing approval pending (\(self.pairingPrompter.pendingCount))\(repairSuffix)", color: .orange) } + if self.devicePairingPrompter.pendingCount > 0 { + let repairCount = self.devicePairingPrompter.pendingRepairCount + let repairSuffix = repairCount > 0 ? " · \(repairCount) repair" : "" + self.statusLine( + label: "Device pairing pending (\(self.devicePairingPrompter.pendingCount))\(repairSuffix)", + color: .orange) + } } } .disabled(self.state.connectionMode == .unconfigured) diff --git a/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift b/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift index fd1727c00..4b8854cda 100644 --- a/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift +++ b/apps/macos/Sources/Clawdbot/MenuSessionsInjector.swift @@ -1,4 +1,5 @@ import AppKit +import Observation import SwiftUI @MainActor @@ -18,6 +19,7 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate { private var isMenuOpen = false private var lastKnownMenuWidth: CGFloat? private var menuOpenWidth: CGFloat? + private var isObservingControlChannel = false private var cachedSnapshot: SessionStoreSnapshot? private var cachedErrorText: String? @@ -27,6 +29,10 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate { private var cachedUsageErrorText: String? private var usageCacheUpdatedAt: Date? private let usageRefreshIntervalSeconds: TimeInterval = 30 + private var cachedCostSummary: GatewayCostUsageSummary? + private var cachedCostErrorText: String? + private var costCacheUpdatedAt: Date? + private let costRefreshIntervalSeconds: TimeInterval = 45 private let nodesStore = NodesStore.shared #if DEBUG private var testControlChannelConnected: Bool? @@ -46,6 +52,7 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate { self.loadTask = Task { await self.refreshCache(force: true) } } + self.startControlChannelObservation() self.nodesStore.start() } @@ -64,6 +71,7 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate { guard let self else { return } await self.refreshCache(force: forceRefresh) await self.refreshUsageCache(force: forceRefresh) + await self.refreshCostUsageCache(force: forceRefresh) await MainActor.run { guard self.isMenuOpen else { return } self.inject(into: menu) @@ -91,6 +99,50 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate { self.cancelPreviewTasks() } + private func startControlChannelObservation() { + guard !self.isObservingControlChannel else { return } + self.isObservingControlChannel = true + self.observeControlChannelState() + } + + private func observeControlChannelState() { + withObservationTracking { + _ = ControlChannel.shared.state + } onChange: { [weak self] in + Task { @MainActor [weak self] in + guard let self else { return } + self.handleControlChannelStateChange() + self.observeControlChannelState() + } + } + } + + private func handleControlChannelStateChange() { + guard self.isMenuOpen, let menu = self.statusItem?.menu else { return } + self.loadTask?.cancel() + self.loadTask = Task { [weak self, weak menu] in + guard let self, let menu else { return } + await self.refreshCache(force: true) + await self.refreshUsageCache(force: true) + await self.refreshCostUsageCache(force: true) + await MainActor.run { + guard self.isMenuOpen else { return } + self.inject(into: menu) + self.injectNodes(into: menu) + } + } + + self.nodesLoadTask?.cancel() + self.nodesLoadTask = Task { [weak self, weak menu] in + guard let self, let menu else { return } + await self.nodesStore.refresh() + await MainActor.run { + guard self.isMenuOpen else { return } + self.injectNodes(into: menu) + } + } + } + func menuNeedsUpdate(_ menu: NSMenu) { self.originalDelegate?.menuNeedsUpdate?(menu) } @@ -136,14 +188,23 @@ extension MenuSessionsInjector { if rhs.key == mainKey { return false } return (lhs.updatedAt ?? .distantPast) > (rhs.updatedAt ?? .distantPast) } + if !rows.isEmpty { + let previewKeys = rows.prefix(20).map(\.key) + let task = Task { + await SessionMenuPreviewLoader.prewarm(sessionKeys: previewKeys, maxItems: 10) + } + self.previewTasks.append(task) + } let headerItem = NSMenuItem() headerItem.tag = self.tag headerItem.isEnabled = false + let statusText = self + .cachedErrorText ?? (isConnected ? nil : self.controlChannelStatusText(for: channelState)) let hosted = self.makeHostedView( rootView: AnyView(MenuSessionsHeaderView( count: rows.count, - statusText: isConnected ? nil : self.controlChannelStatusText(for: channelState))), + statusText: statusText)), width: width, highlighted: false) headerItem.view = hosted @@ -200,6 +261,7 @@ extension MenuSessionsInjector { } cursor = self.insertUsageSection(into: menu, at: cursor, width: width) + cursor = self.insertCostUsageSection(into: menu, at: cursor, width: width) DispatchQueue.main.async { [weak self, weak headerView] in guard let self, let headerView else { return } @@ -344,6 +406,28 @@ extension MenuSessionsInjector { return cursor } + private func insertCostUsageSection(into menu: NSMenu, at cursor: Int, width: CGFloat) -> Int { + guard self.isControlChannelConnected else { return cursor } + guard let submenu = self.buildCostUsageSubmenu(width: width) else { return cursor } + var cursor = cursor + + if cursor > 0, !menu.items[cursor - 1].isSeparatorItem { + let separator = NSMenuItem.separator() + separator.tag = self.tag + menu.insertItem(separator, at: cursor) + cursor += 1 + } + + let item = NSMenuItem(title: "Usage cost (30 days)", action: nil, keyEquivalent: "") + item.tag = self.tag + item.isEnabled = true + item.image = NSImage(systemSymbolName: "chart.bar.xaxis", accessibilityDescription: nil) + item.submenu = submenu + menu.insertItem(item, at: cursor) + cursor += 1 + return cursor + } + private var selectedUsageProviderId: String? { guard let model = self.cachedSnapshot?.defaults.model.nonEmpty else { return nil } let trimmed = model.trimmingCharacters(in: .whitespacesAndNewlines) @@ -393,6 +477,36 @@ extension MenuSessionsInjector { } } + private func buildCostUsageSubmenu(width: CGFloat) -> NSMenu? { + if let error = self.cachedCostErrorText, !error.isEmpty, self.cachedCostSummary == nil { + let menu = NSMenu() + let item = NSMenuItem(title: error, action: nil, keyEquivalent: "") + item.isEnabled = false + menu.addItem(item) + return menu + } + + guard let summary = self.cachedCostSummary else { return nil } + guard !summary.daily.isEmpty else { return nil } + + let menu = NSMenu() + menu.delegate = self + + let chartView = CostUsageHistoryMenuView(summary: summary, width: width) + let hosting = NSHostingView(rootView: AnyView(chartView)) + let controller = NSHostingController(rootView: AnyView(chartView)) + let size = controller.sizeThatFits(in: CGSize(width: width, height: .greatestFiniteMagnitude)) + hosting.frame = NSRect(origin: .zero, size: NSSize(width: width, height: size.height)) + + let chartItem = NSMenuItem() + chartItem.view = hosting + chartItem.isEnabled = false + chartItem.representedObject = "costUsageChart" + menu.addItem(chartItem) + + return menu + } + private func gatewayEntry() -> NodeInfo? { let mode = AppStateStore.shared.connectionMode let isConnected = self.isControlChannelConnected @@ -411,7 +525,7 @@ extension MenuSessionsInjector { } case .local: platform = "local" - host = "127.0.0.1:\(port)" + host = GatewayConnectivityCoordinator.shared.localEndpointHostLabel ?? "127.0.0.1:\(port)" case .unconfigured: platform = nil host = nil @@ -540,8 +654,11 @@ extension MenuSessionsInjector { } guard self.isControlChannelConnected else { - self.cachedSnapshot = nil - self.cachedErrorText = nil + if self.cachedSnapshot != nil { + self.cachedErrorText = "Gateway disconnected (showing cached)" + } else { + self.cachedErrorText = nil + } self.cacheUpdatedAt = Date() return } @@ -566,8 +683,6 @@ extension MenuSessionsInjector { } guard self.isControlChannelConnected else { - self.cachedUsageSummary = nil - self.cachedUsageErrorText = nil self.usageCacheUpdatedAt = Date() return } @@ -581,6 +696,29 @@ extension MenuSessionsInjector { self.usageCacheUpdatedAt = Date() } + private func refreshCostUsageCache(force: Bool) async { + if !force, + let updated = self.costCacheUpdatedAt, + Date().timeIntervalSince(updated) < self.costRefreshIntervalSeconds + { + return + } + + guard self.isControlChannelConnected else { + self.costCacheUpdatedAt = Date() + return + } + + do { + self.cachedCostSummary = try await CostUsageLoader.loadSummary() + self.cachedCostErrorText = nil + } catch { + self.cachedCostSummary = nil + self.cachedCostErrorText = self.compactUsageError(error) + } + self.costCacheUpdatedAt = Date() + } + private func compactUsageError(_ error: Error) -> String { let message = error.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines) if message.isEmpty { return "Usage unavailable" } diff --git a/apps/macos/Sources/Clawdbot/ModelCatalogLoader.swift b/apps/macos/Sources/Clawdbot/ModelCatalogLoader.swift index bf55c564e..2f1c75fe6 100644 --- a/apps/macos/Sources/Clawdbot/ModelCatalogLoader.swift +++ b/apps/macos/Sources/Clawdbot/ModelCatalogLoader.swift @@ -2,14 +2,28 @@ import Foundation import JavaScriptCore enum ModelCatalogLoader { - static let defaultPath: String = FileManager.default.homeDirectoryForCurrentUser - .appendingPathComponent("Projects/pi-mono/packages/ai/src/models.generated.ts").path + static var defaultPath: String { self.resolveDefaultPath() } private static let logger = Logger(subsystem: "com.clawdbot", category: "models") + private nonisolated static let appSupportDir: URL = { + let base = FileManager().urls(for: .applicationSupportDirectory, in: .userDomainMask).first! + return base.appendingPathComponent("Clawdbot", isDirectory: true) + }() + + private static var cachePath: URL { + self.appSupportDir.appendingPathComponent("model-catalog/models.generated.js", isDirectory: false) + } static func load(from path: String) async throws -> [ModelChoice] { let expanded = (path as NSString).expandingTildeInPath - self.logger.debug("model catalog load start file=\(URL(fileURLWithPath: expanded).lastPathComponent)") - let source = try String(contentsOfFile: expanded, encoding: .utf8) + guard let resolved = self.resolvePath(preferred: expanded) else { + self.logger.error("model catalog load failed: file not found") + throw NSError( + domain: "ModelCatalogLoader", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "Model catalog file not found"]) + } + self.logger.debug("model catalog load start file=\(URL(fileURLWithPath: resolved.path).lastPathComponent)") + let source = try String(contentsOfFile: resolved.path, encoding: .utf8) let sanitized = self.sanitize(source: source) let ctx = JSContext() @@ -45,9 +59,82 @@ enum ModelCatalogLoader { return lhs.provider.localizedCaseInsensitiveCompare(rhs.provider) == .orderedAscending } self.logger.debug("model catalog loaded providers=\(rawModels.count) models=\(sorted.count)") + if resolved.shouldCache { + self.cacheCatalog(sourcePath: resolved.path) + } return sorted } + private static func resolveDefaultPath() -> String { + let cache = self.cachePath.path + if FileManager().isReadableFile(atPath: cache) { return cache } + if let bundlePath = self.bundleCatalogPath() { return bundlePath } + if let nodePath = self.nodeModulesCatalogPath() { return nodePath } + return cache + } + + private static func resolvePath(preferred: String) -> (path: String, shouldCache: Bool)? { + if FileManager().isReadableFile(atPath: preferred) { + return (preferred, preferred != self.cachePath.path) + } + + if let bundlePath = self.bundleCatalogPath(), bundlePath != preferred { + self.logger.warning("model catalog path missing; falling back to bundled catalog") + return (bundlePath, true) + } + + let cache = self.cachePath.path + if cache != preferred, FileManager().isReadableFile(atPath: cache) { + self.logger.warning("model catalog path missing; falling back to cached catalog") + return (cache, false) + } + + if let nodePath = self.nodeModulesCatalogPath(), nodePath != preferred { + self.logger.warning("model catalog path missing; falling back to node_modules catalog") + return (nodePath, true) + } + + return nil + } + + private static func bundleCatalogPath() -> String? { + guard let url = Bundle.main.url(forResource: "models.generated", withExtension: "js") else { + return nil + } + return url.path + } + + private static func nodeModulesCatalogPath() -> String? { + let roots = [ + URL(fileURLWithPath: CommandResolver.projectRootPath()), + URL(fileURLWithPath: FileManager().currentDirectoryPath), + ] + for root in roots { + let candidate = root + .appendingPathComponent("node_modules/@mariozechner/pi-ai/dist/models.generated.js") + if FileManager().isReadableFile(atPath: candidate.path) { + return candidate.path + } + } + return nil + } + + private static func cacheCatalog(sourcePath: String) { + let destination = self.cachePath + do { + try FileManager().createDirectory( + at: destination.deletingLastPathComponent(), + withIntermediateDirectories: true) + if FileManager().fileExists(atPath: destination.path) { + try FileManager().removeItem(at: destination) + } + try FileManager().copyItem(atPath: sourcePath, toPath: destination.path) + self.logger.debug("model catalog cached file=\(destination.lastPathComponent)") + } catch { + self.logger.warning("model catalog cache failed: \(error.localizedDescription)") + } + } + private static func sanitize(source: String) -> String { guard let exportRange = source.range(of: "export const MODELS"), let firstBrace = source[exportRange.upperBound...].firstIndex(of: "{"), diff --git a/apps/macos/Sources/Clawdbot/NodeMode/MacNodeBridgePairingClient.swift b/apps/macos/Sources/Clawdbot/NodeMode/MacNodeBridgePairingClient.swift deleted file mode 100644 index 22341d902..000000000 --- a/apps/macos/Sources/Clawdbot/NodeMode/MacNodeBridgePairingClient.swift +++ /dev/null @@ -1,238 +0,0 @@ -import ClawdbotKit -import Foundation -import Network - -actor MacNodeBridgePairingClient { - private let encoder = JSONEncoder() - private let decoder = JSONDecoder() - private var lineBuffer = Data() - - func pairAndHello( - endpoint: NWEndpoint, - hello: BridgeHello, - silent: Bool, - tls: MacNodeBridgeTLSParams? = nil, - onStatus: (@Sendable (String) -> Void)? = nil) async throws -> String - { - do { - return try await self.pairAndHelloOnce( - endpoint: endpoint, - hello: hello, - silent: silent, - tls: tls, - onStatus: onStatus) - } catch { - if let tls, !tls.required { - return try await self.pairAndHelloOnce( - endpoint: endpoint, - hello: hello, - silent: silent, - tls: nil, - onStatus: onStatus) - } - throw error - } - } - - private func pairAndHelloOnce( - endpoint: NWEndpoint, - hello: BridgeHello, - silent: Bool, - tls: MacNodeBridgeTLSParams?, - onStatus: (@Sendable (String) -> Void)? = nil) async throws -> String - { - self.lineBuffer = Data() - let params = self.makeParameters(tls: tls) - let connection = NWConnection(to: endpoint, using: params) - let queue = DispatchQueue(label: "com.clawdbot.macos.bridge-client") - defer { connection.cancel() } - try await AsyncTimeout.withTimeout( - seconds: 8, - onTimeout: { - NSError(domain: "Bridge", code: 0, userInfo: [ - NSLocalizedDescriptionKey: "connect timed out", - ]) - }, - operation: { - try await self.startAndWaitForReady(connection, queue: queue) - }) - - onStatus?("Authenticating…") - try await self.send(hello, over: connection) - - let first = try await AsyncTimeout.withTimeout( - seconds: 10, - onTimeout: { - NSError(domain: "Bridge", code: 0, userInfo: [ - NSLocalizedDescriptionKey: "hello timed out", - ]) - }, - operation: { () -> ReceivedFrame in - guard let frame = try await self.receiveFrame(over: connection) else { - throw NSError(domain: "Bridge", code: 0, userInfo: [ - NSLocalizedDescriptionKey: "Bridge closed connection during hello", - ]) - } - return frame - }) - - switch first.base.type { - case "hello-ok": - return hello.token ?? "" - - case "error": - let err = try self.decoder.decode(BridgeErrorFrame.self, from: first.data) - if err.code != "NOT_PAIRED", err.code != "UNAUTHORIZED" { - throw NSError(domain: "Bridge", code: 1, userInfo: [ - NSLocalizedDescriptionKey: "\(err.code): \(err.message)", - ]) - } - - onStatus?("Requesting approval…") - try await self.send( - BridgePairRequest( - nodeId: hello.nodeId, - displayName: hello.displayName, - platform: hello.platform, - version: hello.version, - coreVersion: hello.coreVersion, - uiVersion: hello.uiVersion, - deviceFamily: hello.deviceFamily, - modelIdentifier: hello.modelIdentifier, - caps: hello.caps, - commands: hello.commands, - silent: silent), - over: connection) - - onStatus?("Waiting for approval…") - let ok = try await AsyncTimeout.withTimeout( - seconds: 60, - onTimeout: { - NSError(domain: "Bridge", code: 0, userInfo: [ - NSLocalizedDescriptionKey: "pairing approval timed out", - ]) - }, - operation: { - while let next = try await self.receiveFrame(over: connection) { - switch next.base.type { - case "pair-ok": - return try self.decoder.decode(BridgePairOk.self, from: next.data) - case "error": - let e = try self.decoder.decode(BridgeErrorFrame.self, from: next.data) - throw NSError(domain: "Bridge", code: 2, userInfo: [ - NSLocalizedDescriptionKey: "\(e.code): \(e.message)", - ]) - default: - continue - } - } - throw NSError(domain: "Bridge", code: 3, userInfo: [ - NSLocalizedDescriptionKey: "Pairing failed: bridge closed connection", - ]) - }) - - return ok.token - - default: - throw NSError(domain: "Bridge", code: 0, userInfo: [ - NSLocalizedDescriptionKey: "Unexpected bridge response", - ]) - } - } - - private func send(_ obj: some Encodable, over connection: NWConnection) async throws { - let data = try self.encoder.encode(obj) - var line = Data() - line.append(data) - line.append(0x0A) - try await withCheckedThrowingContinuation(isolation: nil) { (cont: CheckedContinuation) in - connection.send(content: line, completion: .contentProcessed { err in - if let err { cont.resume(throwing: err) } else { cont.resume(returning: ()) } - }) - } - } - - private struct ReceivedFrame { - var base: BridgeBaseFrame - var data: Data - } - - private func receiveFrame(over connection: NWConnection) async throws -> ReceivedFrame? { - guard let lineData = try await self.receiveLineData(over: connection) else { - return nil - } - let base = try self.decoder.decode(BridgeBaseFrame.self, from: lineData) - return ReceivedFrame(base: base, data: lineData) - } - - private func receiveChunk(over connection: NWConnection) async throws -> Data { - try await withCheckedThrowingContinuation(isolation: nil) { (cont: CheckedContinuation) in - connection.receive(minimumIncompleteLength: 1, maximumLength: 64 * 1024) { data, _, isComplete, error in - if let error { - cont.resume(throwing: error) - return - } - if isComplete { - cont.resume(returning: Data()) - return - } - cont.resume(returning: data ?? Data()) - } - } - } - - private func receiveLineData(over connection: NWConnection) async throws -> Data? { - while true { - if let idx = self.lineBuffer.firstIndex(of: 0x0A) { - let line = self.lineBuffer.prefix(upTo: idx) - self.lineBuffer.removeSubrange(...idx) - return Data(line) - } - - let chunk = try await self.receiveChunk(over: connection) - if chunk.isEmpty { return nil } - self.lineBuffer.append(chunk) - } - } - - private func makeParameters(tls: MacNodeBridgeTLSParams?) -> NWParameters { - let tcpOptions = NWProtocolTCP.Options() - if let tlsOptions = makeMacNodeTLSOptions(tls) { - let params = NWParameters(tls: tlsOptions, tcp: tcpOptions) - params.includePeerToPeer = true - return params - } - let params = NWParameters.tcp - params.includePeerToPeer = true - return params - } - - private func startAndWaitForReady( - _ connection: NWConnection, - queue: DispatchQueue) async throws - { - let states = AsyncStream { continuation in - connection.stateUpdateHandler = { state in - continuation.yield(state) - if case .ready = state { continuation.finish() } - if case .failed = state { continuation.finish() } - if case .cancelled = state { continuation.finish() } - } - } - connection.start(queue: queue) - for await state in states { - switch state { - case .ready: - return - case let .failed(err): - throw err - case .cancelled: - throw NSError(domain: "Bridge", code: 0, userInfo: [ - NSLocalizedDescriptionKey: "Bridge connection cancelled", - ]) - default: - continue - } - } - } -} diff --git a/apps/macos/Sources/Clawdbot/NodeMode/MacNodeBridgeSession.swift b/apps/macos/Sources/Clawdbot/NodeMode/MacNodeBridgeSession.swift deleted file mode 100644 index c739cd389..000000000 --- a/apps/macos/Sources/Clawdbot/NodeMode/MacNodeBridgeSession.swift +++ /dev/null @@ -1,519 +0,0 @@ -import ClawdbotKit -import Foundation -import Network -import OSLog - -actor MacNodeBridgeSession { - private struct TimeoutError: LocalizedError { - var message: String - var errorDescription: String? { self.message } - } - - enum State: Sendable, Equatable { - case idle - case connecting - case connected(serverName: String) - case failed(message: String) - } - - private let logger = Logger(subsystem: "com.clawdbot", category: "node.bridge-session") - private let encoder = JSONEncoder() - private let decoder = JSONDecoder() - private let clock = ContinuousClock() - private var disconnectHandler: (@Sendable (String) async -> Void)? - - private var connection: NWConnection? - private var queue: DispatchQueue? - private var buffer = Data() - private var pendingRPC: [String: CheckedContinuation] = [:] - private var serverEventSubscribers: [UUID: AsyncStream.Continuation] = [:] - private var invokeTasks: [UUID: Task] = [:] - private var pingTask: Task? - private var lastPongAt: ContinuousClock.Instant? - - private(set) var state: State = .idle - - func connect( - endpoint: NWEndpoint, - hello: BridgeHello, - tls: MacNodeBridgeTLSParams? = nil, - onConnected: (@Sendable (String, String?) async -> Void)? = nil, - onDisconnected: (@Sendable (String) async -> Void)? = nil, - onInvoke: @escaping @Sendable (BridgeInvokeRequest) async -> BridgeInvokeResponse) - async throws - { - await self.disconnect() - self.disconnectHandler = onDisconnected - self.state = .connecting - do { - try await self.connectOnce( - endpoint: endpoint, - hello: hello, - tls: tls, - onConnected: onConnected, - onInvoke: onInvoke) - } catch { - if let tls, !tls.required { - try await self.connectOnce( - endpoint: endpoint, - hello: hello, - tls: nil, - onConnected: onConnected, - onInvoke: onInvoke) - return - } - throw error - } - } - - private func connectOnce( - endpoint: NWEndpoint, - hello: BridgeHello, - tls: MacNodeBridgeTLSParams?, - onConnected: (@Sendable (String, String?) async -> Void)? = nil, - onInvoke: @escaping @Sendable (BridgeInvokeRequest) async -> BridgeInvokeResponse) async throws - { - let params = self.makeParameters(tls: tls) - let connection = NWConnection(to: endpoint, using: params) - let queue = DispatchQueue(label: "com.clawdbot.macos.bridge-session") - self.connection = connection - self.queue = queue - - let stateStream = Self.makeStateStream(for: connection) - connection.start(queue: queue) - - try await Self.waitForReady(stateStream, timeoutSeconds: 6) - connection.stateUpdateHandler = { [weak self] state in - guard let self else { return } - Task { await self.handleConnectionState(state) } - } - - try await AsyncTimeout.withTimeout( - seconds: 6, - onTimeout: { - TimeoutError(message: "operation timed out") - }, - operation: { - try await self.send(hello) - }) - - guard let line = try await AsyncTimeout.withTimeout( - seconds: 6, - onTimeout: { - TimeoutError(message: "operation timed out") - }, - operation: { - try await self.receiveLine() - }), - let data = line.data(using: .utf8), - let base = try? self.decoder.decode(BridgeBaseFrame.self, from: data) - else { - self.logger.error("node bridge hello failed (unexpected response)") - await self.disconnect() - throw NSError(domain: "Bridge", code: 1, userInfo: [ - NSLocalizedDescriptionKey: "Unexpected bridge response", - ]) - } - - if base.type == "hello-ok" { - let ok = try self.decoder.decode(BridgeHelloOk.self, from: data) - self.state = .connected(serverName: ok.serverName) - self.startPingLoop() - let mainKey = ok.mainSessionKey?.trimmingCharacters(in: .whitespacesAndNewlines) - await onConnected?(ok.serverName, mainKey?.isEmpty == false ? mainKey : nil) - } else if base.type == "error" { - let err = try self.decoder.decode(BridgeErrorFrame.self, from: data) - self.state = .failed(message: "\(err.code): \(err.message)") - self.logger.error("node bridge hello error: \(err.code, privacy: .public)") - await self.disconnect() - throw NSError(domain: "Bridge", code: 2, userInfo: [ - NSLocalizedDescriptionKey: "\(err.code): \(err.message)", - ]) - } else { - self.state = .failed(message: "Unexpected bridge response") - self.logger.error("node bridge hello failed (unexpected frame)") - await self.disconnect() - throw NSError(domain: "Bridge", code: 3, userInfo: [ - NSLocalizedDescriptionKey: "Unexpected bridge response", - ]) - } - - do { - while true { - guard let next = try await self.receiveLine() else { break } - guard let nextData = next.data(using: .utf8) else { continue } - guard let nextBase = try? self.decoder.decode(BridgeBaseFrame.self, from: nextData) else { continue } - - switch nextBase.type { - case "res": - let res = try self.decoder.decode(BridgeRPCResponse.self, from: nextData) - if let cont = self.pendingRPC.removeValue(forKey: res.id) { - cont.resume(returning: res) - } - - case "event": - let evt = try self.decoder.decode(BridgeEventFrame.self, from: nextData) - self.broadcastServerEvent(evt) - - case "ping": - let ping = try self.decoder.decode(BridgePing.self, from: nextData) - try await self.send(BridgePong(type: "pong", id: ping.id)) - - case "pong": - let pong = try self.decoder.decode(BridgePong.self, from: nextData) - self.notePong(pong) - - case "invoke": - let req = try self.decoder.decode(BridgeInvokeRequest.self, from: nextData) - let taskID = UUID() - let task = Task { [weak self] in - let res = await onInvoke(req) - guard let self else { return } - await self.sendInvokeResponse(res, taskID: taskID) - } - self.invokeTasks[taskID] = task - - default: - continue - } - } - - await self.handleDisconnect(reason: "connection closed") - } catch { - self.logger.error( - "node bridge receive failed: \(error.localizedDescription, privacy: .public)") - await self.handleDisconnect(reason: "receive failed") - throw error - } - } - - func sendEvent(event: String, payloadJSON: String?) async throws { - try await self.send(BridgeEventFrame(type: "event", event: event, payloadJSON: payloadJSON)) - } - - func request(method: String, paramsJSON: String?, timeoutSeconds: Int = 15) async throws -> Data { - guard self.connection != nil else { - throw NSError(domain: "Bridge", code: 11, userInfo: [ - NSLocalizedDescriptionKey: "not connected", - ]) - } - - let id = UUID().uuidString - let req = BridgeRPCRequest(type: "req", id: id, method: method, paramsJSON: paramsJSON) - - let timeoutTask = Task { - try await Task.sleep(nanoseconds: UInt64(timeoutSeconds) * 1_000_000_000) - await self.timeoutRPC(id: id) - } - defer { timeoutTask.cancel() } - - let res: BridgeRPCResponse = try await withCheckedThrowingContinuation { cont in - Task { [weak self] in - guard let self else { return } - await self.beginRPC(id: id, request: req, continuation: cont) - } - } - - if res.ok { - let payload = res.payloadJSON ?? "" - guard let data = payload.data(using: .utf8) else { - throw NSError(domain: "Bridge", code: 12, userInfo: [ - NSLocalizedDescriptionKey: "Bridge response not UTF-8", - ]) - } - return data - } - - let code = res.error?.code ?? "UNAVAILABLE" - let message = res.error?.message ?? "request failed" - throw NSError(domain: "Bridge", code: 13, userInfo: [ - NSLocalizedDescriptionKey: "\(code): \(message)", - ]) - } - - func subscribeServerEvents(bufferingNewest: Int = 200) -> AsyncStream { - let id = UUID() - let session = self - return AsyncStream(bufferingPolicy: .bufferingNewest(bufferingNewest)) { continuation in - self.serverEventSubscribers[id] = continuation - continuation.onTermination = { @Sendable _ in - Task { await session.removeServerEventSubscriber(id) } - } - } - } - - func disconnect() async { - self.pingTask?.cancel() - self.pingTask = nil - self.lastPongAt = nil - self.disconnectHandler = nil - self.cancelInvokeTasks() - - self.connection?.cancel() - self.connection = nil - self.queue = nil - self.buffer = Data() - - let pending = self.pendingRPC.values - self.pendingRPC.removeAll() - for cont in pending { - cont.resume(throwing: NSError(domain: "Bridge", code: 14, userInfo: [ - NSLocalizedDescriptionKey: "UNAVAILABLE: connection closed", - ])) - } - - for (_, cont) in self.serverEventSubscribers { - cont.finish() - } - self.serverEventSubscribers.removeAll() - - self.state = .idle - } - - private func beginRPC( - id: String, - request: BridgeRPCRequest, - continuation: CheckedContinuation) async - { - self.pendingRPC[id] = continuation - do { - try await self.send(request) - } catch { - await self.failRPC(id: id, error: error) - } - } - - private func makeParameters(tls: MacNodeBridgeTLSParams?) -> NWParameters { - let tcpOptions = NWProtocolTCP.Options() - tcpOptions.enableKeepalive = true - tcpOptions.keepaliveIdle = 30 - tcpOptions.keepaliveInterval = 15 - tcpOptions.keepaliveCount = 3 - - if let tlsOptions = makeMacNodeTLSOptions(tls) { - let params = NWParameters(tls: tlsOptions, tcp: tcpOptions) - params.includePeerToPeer = true - return params - } - - let params = NWParameters.tcp - params.includePeerToPeer = true - params.defaultProtocolStack.transportProtocol = tcpOptions - return params - } - - private func failRPC(id: String, error: Error) async { - if let cont = self.pendingRPC.removeValue(forKey: id) { - cont.resume(throwing: error) - } - } - - private func timeoutRPC(id: String) async { - if let cont = self.pendingRPC.removeValue(forKey: id) { - cont.resume(throwing: TimeoutError(message: "request timed out")) - } - } - - private func removeServerEventSubscriber(_ id: UUID) { - self.serverEventSubscribers[id] = nil - } - - private func broadcastServerEvent(_ evt: BridgeEventFrame) { - for (_, cont) in self.serverEventSubscribers { - cont.yield(evt) - } - } - - private func send(_ obj: some Encodable) async throws { - guard let connection = self.connection else { - throw NSError(domain: "Bridge", code: 15, userInfo: [ - NSLocalizedDescriptionKey: "not connected", - ]) - } - let data = try self.encoder.encode(obj) - var line = Data() - line.append(data) - line.append(0x0A) - try await withCheckedThrowingContinuation(isolation: self) { (cont: CheckedContinuation) in - connection.send(content: line, completion: .contentProcessed { err in - if let err { cont.resume(throwing: err) } else { cont.resume(returning: ()) } - }) - } - } - - private func receiveLine() async throws -> String? { - while true { - if let idx = self.buffer.firstIndex(of: 0x0A) { - let line = self.buffer.prefix(upTo: idx) - self.buffer.removeSubrange(...idx) - return String(data: line, encoding: .utf8) - } - let chunk = try await self.receiveChunk() - if chunk.isEmpty { return nil } - self.buffer.append(chunk) - } - } - - private func receiveChunk() async throws -> Data { - guard let connection else { return Data() } - return try await withCheckedThrowingContinuation(isolation: self) { (cont: CheckedContinuation) in - connection.receive(minimumIncompleteLength: 1, maximumLength: 64 * 1024) { data, _, isComplete, error in - if let error { - cont.resume(throwing: error) - return - } - if isComplete { - cont.resume(returning: Data()) - return - } - cont.resume(returning: data ?? Data()) - } - } - } - - private func startPingLoop() { - self.pingTask?.cancel() - self.lastPongAt = self.clock.now - self.logger.debug("node bridge ping loop started") - self.pingTask = Task { [weak self] in - guard let self else { return } - await self.runPingLoop() - } - } - - private func runPingLoop() async { - let interval: Duration = .seconds(15) - let timeout: Duration = .seconds(45) - - while !Task.isCancelled { - try? await Task.sleep(for: interval) - - guard self.connection != nil else { return } - - if let last = self.lastPongAt { - let now = self.clock.now - if now > last.advanced(by: timeout) { - let age = last.duration(to: now) - let ageDescription = String(describing: age) - let message = - "Node bridge heartbeat timed out; disconnecting " + - "(age: \(ageDescription, privacy: .public))." - self.logger.warning(message) - await self.handleDisconnect(reason: "ping timeout") - return - } - } - - let id = UUID().uuidString - do { - try await self.send(BridgePing(type: "ping", id: id)) - } catch { - let errorDescription = String(describing: error) - let message = - "Node bridge ping send failed; disconnecting " + - "(error: \(errorDescription, privacy: .public))." - self.logger.warning(message) - await self.handleDisconnect(reason: "ping send failed") - return - } - } - } - - private func notePong(_ pong: BridgePong) { - _ = pong - self.lastPongAt = self.clock.now - } - - private func handleConnectionState(_ state: NWConnection.State) async { - switch state { - case let .failed(error): - let errorDescription = String(describing: error) - let message = - "Node bridge connection failed; disconnecting " + - "(error: \(errorDescription, privacy: .public))." - self.logger.warning(message) - await self.handleDisconnect(reason: "connection failed") - case .cancelled: - self.logger.warning("Node bridge connection cancelled; disconnecting.") - await self.handleDisconnect(reason: "connection cancelled") - default: - break - } - } - - private func handleDisconnect(reason: String) async { - self.logger.info("node bridge disconnect reason=\(reason, privacy: .public)") - if let handler = self.disconnectHandler { - await handler(reason) - } - await self.disconnect() - } - - private func logInvokeSendFailure(_ error: Error) { - self.logger.error( - "node bridge invoke response send failed: \(error.localizedDescription, privacy: .public)") - } - - private func sendInvokeResponse(_ response: BridgeInvokeResponse, taskID: UUID) async { - defer { self.invokeTasks[taskID] = nil } - if Task.isCancelled { return } - do { - try await self.send(response) - } catch { - self.logInvokeSendFailure(error) - } - } - - private func cancelInvokeTasks() { - for task in self.invokeTasks.values { - task.cancel() - } - self.invokeTasks.removeAll() - } - - private static func makeStateStream( - for connection: NWConnection) -> AsyncStream - { - AsyncStream { continuation in - connection.stateUpdateHandler = { state in - continuation.yield(state) - switch state { - case .ready, .failed, .cancelled: - continuation.finish() - default: - break - } - } - } - } - - private static func waitForReady( - _ stream: AsyncStream, - timeoutSeconds: Double) async throws - { - try await AsyncTimeout.withTimeout( - seconds: timeoutSeconds, - onTimeout: { - TimeoutError(message: "operation timed out") - }, - operation: { - for await state in stream { - switch state { - case .ready: - return - case let .failed(err): - throw err - case .cancelled: - throw NSError(domain: "Bridge", code: 20, userInfo: [ - NSLocalizedDescriptionKey: "Connection cancelled", - ]) - default: - continue - } - } - throw NSError(domain: "Bridge", code: 21, userInfo: [ - NSLocalizedDescriptionKey: "Connection closed", - ]) - }) - } -} diff --git a/apps/macos/Sources/Clawdbot/NodeMode/MacNodeBridgeTLS.swift b/apps/macos/Sources/Clawdbot/NodeMode/MacNodeBridgeTLS.swift deleted file mode 100644 index cd6d32f29..000000000 --- a/apps/macos/Sources/Clawdbot/NodeMode/MacNodeBridgeTLS.swift +++ /dev/null @@ -1,74 +0,0 @@ -import CryptoKit -import Foundation -import Network -import Security - -struct MacNodeBridgeTLSParams: Sendable { - let required: Bool - let expectedFingerprint: String? - let allowTOFU: Bool - let storeKey: String? -} - -enum MacNodeBridgeTLSStore { - private static let suiteName = "com.clawdbot.shared" - private static let keyPrefix = "mac.node.bridge.tls." - - private static var defaults: UserDefaults { - UserDefaults(suiteName: suiteName) ?? .standard - } - - static func loadFingerprint(stableID: String) -> String? { - let key = self.keyPrefix + stableID - let raw = self.defaults.string(forKey: key)?.trimmingCharacters(in: .whitespacesAndNewlines) - return raw?.isEmpty == false ? raw : nil - } - - static func saveFingerprint(_ value: String, stableID: String) { - let key = self.keyPrefix + stableID - self.defaults.set(value, forKey: key) - } -} - -func makeMacNodeTLSOptions(_ params: MacNodeBridgeTLSParams?) -> NWProtocolTLS.Options? { - guard let params else { return nil } - let options = NWProtocolTLS.Options() - let expected = params.expectedFingerprint.map(normalizeMacNodeFingerprint) - let allowTOFU = params.allowTOFU - let storeKey = params.storeKey - - sec_protocol_options_set_verify_block( - options.securityProtocolOptions, - { _, trust, complete in - let trustRef = sec_trust_copy_ref(trust).takeRetainedValue() - if let chain = SecTrustCopyCertificateChain(trustRef) as? [SecCertificate], - let cert = chain.first - { - let data = SecCertificateCopyData(cert) as Data - let fingerprint = sha256Hex(data) - if let expected { - complete(fingerprint == expected) - return - } - if allowTOFU { - if let storeKey { MacNodeBridgeTLSStore.saveFingerprint(fingerprint, stableID: storeKey) } - complete(true) - return - } - } - let ok = SecTrustEvaluateWithError(trustRef, nil) - complete(ok) - }, - DispatchQueue(label: "com.clawdbot.macos.bridge.tls.verify")) - - return options -} - -private func sha256Hex(_ data: Data) -> String { - let digest = SHA256.hash(data: data) - return digest.map { String(format: "%02x", $0) }.joined() -} - -private func normalizeMacNodeFingerprint(_ raw: String) -> String { - raw.lowercased().filter(\.isHexDigit) -} diff --git a/apps/macos/Sources/Clawdbot/NodeMode/MacNodeModeCoordinator.swift b/apps/macos/Sources/Clawdbot/NodeMode/MacNodeModeCoordinator.swift index 7217d76a0..3d86bc4e9 100644 --- a/apps/macos/Sources/Clawdbot/NodeMode/MacNodeModeCoordinator.swift +++ b/apps/macos/Sources/Clawdbot/NodeMode/MacNodeModeCoordinator.swift @@ -1,15 +1,7 @@ -import ClawdbotDiscovery import ClawdbotKit import Foundation -import Network import OSLog -private struct BridgeTarget { - let endpoint: NWEndpoint - let stableID: String - let tls: MacNodeBridgeTLSParams? -} - @MainActor final class MacNodeModeCoordinator { static let shared = MacNodeModeCoordinator() @@ -17,8 +9,7 @@ final class MacNodeModeCoordinator { private let logger = Logger(subsystem: "com.clawdbot", category: "mac-node") private var task: Task? private let runtime = MacNodeRuntime() - private let session = MacNodeBridgeSession() - private var tunnel: RemotePortTunnel? + private let session = GatewayNodeSession() func start() { guard self.task == nil else { return } @@ -31,12 +22,10 @@ final class MacNodeModeCoordinator { self.task?.cancel() self.task = nil Task { await self.session.disconnect() } - self.tunnel?.terminate() - self.tunnel = nil } - func setPreferredBridgeStableID(_ stableID: String?) { - BridgeDiscoveryPreferences.setPreferredStableID(stableID) + func setPreferredGatewayStableID(_ stableID: String?) { + GatewayDiscoveryPreferences.setPreferredStableID(stableID) Task { await self.session.disconnect() } } @@ -44,6 +33,7 @@ final class MacNodeModeCoordinator { var retryDelay: UInt64 = 1_000_000_000 var lastCameraEnabled: Bool? let defaults = UserDefaults.standard + while !Task.isCancelled { if await MainActor.run(body: { AppStateStore.shared.isPaused }) { try? await Task.sleep(nanoseconds: 1_000_000_000) @@ -59,34 +49,42 @@ final class MacNodeModeCoordinator { try? await Task.sleep(nanoseconds: 200_000_000) } - guard let target = await self.resolveBridgeEndpoint(timeoutSeconds: 5) else { - try? await Task.sleep(nanoseconds: min(retryDelay, 5_000_000_000)) - retryDelay = min(retryDelay * 2, 10_000_000_000) - continue - } - - retryDelay = 1_000_000_000 do { - let hello = await self.makeHello() - self.logger.info( - "mac node bridge connecting endpoint=\(target.endpoint, privacy: .public)") + let config = try await GatewayEndpointStore.shared.requireConfig() + let caps = self.currentCaps() + let commands = self.currentCommands(caps: caps) + let permissions = await self.currentPermissions() + let connectOptions = GatewayConnectOptions( + role: "node", + scopes: [], + caps: caps, + commands: commands, + permissions: permissions, + clientId: "clawdbot-macos", + clientMode: "node", + clientDisplayName: InstanceIdentity.displayName) + let sessionBox = self.buildSessionBox(url: config.url) + try await self.session.connect( - endpoint: target.endpoint, - hello: hello, - tls: target.tls, - onConnected: { [weak self] serverName, mainSessionKey in - self?.logger.info("mac node connected to \(serverName, privacy: .public)") - if let mainSessionKey { - await self?.runtime.updateMainSessionKey(mainSessionKey) - } - await self?.runtime.setEventSender { [weak self] event, payload in + url: config.url, + token: config.token, + password: config.password, + connectOptions: connectOptions, + sessionBox: sessionBox, + onConnected: { [weak self] in + guard let self else { return } + self.logger.info("mac node connected to gateway") + let mainSessionKey = await GatewayConnection.shared.mainSessionKey() + await self.runtime.updateMainSessionKey(mainSessionKey) + await self.runtime.setEventSender { [weak self] event, payload in guard let self else { return } - try? await self.session.sendEvent(event: event, payloadJSON: payload) + await self.session.sendEvent(event: event, payloadJSON: payload) } }, onDisconnected: { [weak self] reason in - await self?.runtime.setEventSender(nil) - await MacNodeModeCoordinator.handleBridgeDisconnect(reason: reason) + guard let self else { return } + await self.runtime.setEventSender(nil) + self.logger.error("mac node disconnected: \(reason, privacy: .public)") }, onInvoke: { [weak self] req in guard let self else { @@ -97,43 +95,17 @@ final class MacNodeModeCoordinator { } return await self.runtime.handleInvoke(req) }) + + retryDelay = 1_000_000_000 + try? await Task.sleep(nanoseconds: 1_000_000_000) } catch { - if await self.tryPair(target: target, error: error) { - continue - } - self.logger.error( - "mac node bridge connect failed: \(error.localizedDescription, privacy: .public)") - try? await Task.sleep(nanoseconds: min(retryDelay, 5_000_000_000)) + self.logger.error("mac node gateway connect failed: \(error.localizedDescription, privacy: .public)") + try? await Task.sleep(nanoseconds: min(retryDelay, 10_000_000_000)) retryDelay = min(retryDelay * 2, 10_000_000_000) } } } - private func makeHello() async -> BridgeHello { - let token = MacNodeTokenStore.loadToken() - let caps = self.currentCaps() - let commands = self.currentCommands(caps: caps) - let permissions = await self.currentPermissions() - let uiVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String - let liveGatewayVersion = await GatewayConnection.shared.cachedGatewayVersion() - let fallbackGatewayVersion = GatewayProcessManager.shared.environmentStatus.gatewayVersion - let coreVersion = (liveGatewayVersion ?? fallbackGatewayVersion)? - .trimmingCharacters(in: .whitespacesAndNewlines) - return BridgeHello( - nodeId: Self.nodeId(), - displayName: InstanceIdentity.displayName, - token: token, - platform: "macos", - version: uiVersion, - coreVersion: coreVersion?.isEmpty == false ? coreVersion : nil, - uiVersion: uiVersion, - deviceFamily: "Mac", - modelIdentifier: InstanceIdentity.modelIdentifier, - caps: caps, - commands: commands, - permissions: permissions) - } - private func currentCaps() -> [String] { var caps: [String] = [ClawdbotCapability.canvas.rawValue, ClawdbotCapability.screen.rawValue] if UserDefaults.standard.object(forKey: cameraEnabledKey) as? Bool ?? false { @@ -182,370 +154,18 @@ final class MacNodeModeCoordinator { return commands } - private func tryPair(target: BridgeTarget, error: Error) async -> Bool { - let text = error.localizedDescription.uppercased() - guard text.contains("NOT_PAIRED") || text.contains("UNAUTHORIZED") else { return false } - - do { - let shouldSilent = await MainActor.run { - AppStateStore.shared.connectionMode == .remote - } - let hello = await self.makeHello() - let token = try await MacNodeBridgePairingClient().pairAndHello( - endpoint: target.endpoint, - hello: hello, - silent: shouldSilent, - tls: target.tls, - onStatus: { [weak self] status in - self?.logger.info("mac node pairing: \(status, privacy: .public)") - }) - if !token.isEmpty { - MacNodeTokenStore.saveToken(token) - } - return true - } catch { - self.logger.error("mac node pairing failed: \(error.localizedDescription, privacy: .public)") - return false - } - } - - private static func nodeId() -> String { - "mac-\(InstanceIdentity.instanceId)" - } - - private func resolveLoopbackBridgeEndpoint(timeoutSeconds: Double) async -> BridgeTarget? { - guard let port = Self.loopbackBridgePort(), - let endpointPort = NWEndpoint.Port(rawValue: port) - else { - return nil - } - let endpoint = NWEndpoint.hostPort(host: "127.0.0.1", port: endpointPort) - let reachable = await Self.probeEndpoint(endpoint, timeoutSeconds: timeoutSeconds) - guard reachable else { return nil } - let stableID = BridgeEndpointID.stableID(endpoint) - let tlsParams = Self.resolveManualTLSParams(stableID: stableID) - return BridgeTarget(endpoint: endpoint, stableID: stableID, tls: tlsParams) - } - - static func loopbackBridgePort() -> UInt16? { - if let raw = ProcessInfo.processInfo.environment["CLAWDBOT_BRIDGE_PORT"], - let parsed = Int(raw.trimmingCharacters(in: .whitespacesAndNewlines)), - parsed > 0, - parsed <= Int(UInt16.max) - { - return UInt16(parsed) - } - return 18790 - } - - static func remoteBridgePort() -> Int { - let fallback = Int(Self.loopbackBridgePort() ?? 18790) - let settings = CommandResolver.connectionSettings() - let sshHost = CommandResolver.parseSSHTarget(settings.target)?.host ?? "" - let base = - ClawdbotConfigFile.remoteGatewayPort(matchingHost: sshHost) ?? - GatewayEnvironment.gatewayPort() - guard base > 0 else { return fallback } - return Self.derivePort(base: base, offset: 1, fallback: fallback) - } - - private static func derivePort(base: Int, offset: Int, fallback: Int) -> Int { - let derived = base + offset - guard derived > 0, derived <= Int(UInt16.max) else { return fallback } - return derived - } - - static func probeEndpoint(_ endpoint: NWEndpoint, timeoutSeconds: Double) async -> Bool { - let connection = NWConnection(to: endpoint, using: .tcp) - let stream = Self.makeStateStream(for: connection) - connection.start(queue: DispatchQueue(label: "com.clawdbot.macos.bridge-loopback-probe")) - do { - try await Self.waitForReady(stream, timeoutSeconds: timeoutSeconds) - connection.cancel() - return true - } catch { - connection.cancel() - return false - } - } - - private static func makeStateStream( - for connection: NWConnection) -> AsyncStream - { - AsyncStream { continuation in - connection.stateUpdateHandler = { state in - continuation.yield(state) - switch state { - case .ready, .failed, .cancelled: - continuation.finish() - default: - break - } - } - } - } - - private static func waitForReady( - _ stream: AsyncStream, - timeoutSeconds: Double) async throws - { - try await AsyncTimeout.withTimeout( - seconds: timeoutSeconds, - onTimeout: { - NSError(domain: "Bridge", code: 22, userInfo: [ - NSLocalizedDescriptionKey: "operation timed out", - ]) - }, - operation: { - for await state in stream { - switch state { - case .ready: - return - case let .failed(err): - throw err - case .cancelled: - throw NSError(domain: "Bridge", code: 20, userInfo: [ - NSLocalizedDescriptionKey: "Connection cancelled", - ]) - default: - continue - } - } - throw NSError(domain: "Bridge", code: 21, userInfo: [ - NSLocalizedDescriptionKey: "Connection closed", - ]) - }) - } - - private func resolveBridgeEndpoint(timeoutSeconds: Double) async -> BridgeTarget? { - let mode = await MainActor.run(body: { AppStateStore.shared.connectionMode }) - if mode == .remote { - do { - if let tunnel = self.tunnel, - tunnel.process.isRunning, - let localPort = tunnel.localPort - { - let healthy = await self.bridgeTunnelHealthy(localPort: localPort, timeoutSeconds: 1.0) - if healthy, let port = NWEndpoint.Port(rawValue: localPort) { - self.logger.info( - "reusing mac node bridge tunnel localPort=\(localPort, privacy: .public)") - let endpoint = NWEndpoint.hostPort(host: "127.0.0.1", port: port) - let stableID = BridgeEndpointID.stableID(endpoint) - let tlsParams = Self.resolveManualTLSParams(stableID: stableID) - return BridgeTarget(endpoint: endpoint, stableID: stableID, tls: tlsParams) - } - self.logger.error( - "mac node bridge tunnel unhealthy localPort=\(localPort, privacy: .public); restarting") - tunnel.terminate() - self.tunnel = nil - } - - let remotePort = Self.remoteBridgePort() - let preferredLocalPort = Self.loopbackBridgePort() - if let preferredLocalPort { - self.logger.info( - "mac node bridge tunnel starting " + - "preferredLocalPort=\(preferredLocalPort, privacy: .public) " + - "remotePort=\(remotePort, privacy: .public)") - } else { - self.logger.info( - "mac node bridge tunnel starting " + - "preferredLocalPort=none " + - "remotePort=\(remotePort, privacy: .public)") - } - self.tunnel = try await RemotePortTunnel.create( - remotePort: remotePort, - preferredLocalPort: preferredLocalPort, - allowRemoteUrlOverride: false, - allowRandomLocalPort: true) - if let localPort = self.tunnel?.localPort, - let port = NWEndpoint.Port(rawValue: localPort) - { - self.logger.info( - "mac node bridge tunnel ready " + - "localPort=\(localPort, privacy: .public) " + - "remotePort=\(remotePort, privacy: .public)") - let endpoint = NWEndpoint.hostPort(host: "127.0.0.1", port: port) - let stableID = BridgeEndpointID.stableID(endpoint) - let tlsParams = Self.resolveManualTLSParams(stableID: stableID) - return BridgeTarget(endpoint: endpoint, stableID: stableID, tls: tlsParams) - } - } catch { - self.logger.error("mac node bridge tunnel failed: \(error.localizedDescription, privacy: .public)") - self.tunnel?.terminate() - self.tunnel = nil - } - } else if let tunnel = self.tunnel { - tunnel.terminate() - self.tunnel = nil - } - if mode == .local, let target = await self.resolveLoopbackBridgeEndpoint(timeoutSeconds: 0.4) { - return target - } - return await Self.discoverBridgeEndpoint(timeoutSeconds: timeoutSeconds) - } - - @MainActor - private static func handleBridgeDisconnect(reason: String) async { - guard reason.localizedCaseInsensitiveContains("ping") else { return } - let coordinator = MacNodeModeCoordinator.shared - coordinator.logger.error( - "mac node bridge disconnected (\(reason, privacy: .public)); resetting tunnel") - coordinator.tunnel?.terminate() - coordinator.tunnel = nil - } - - private func bridgeTunnelHealthy(localPort: UInt16, timeoutSeconds: Double) async -> Bool { - guard let port = NWEndpoint.Port(rawValue: localPort) else { return false } - return await Self.probeEndpoint(.hostPort(host: "127.0.0.1", port: port), timeoutSeconds: timeoutSeconds) - } - - private static func discoverBridgeEndpoint(timeoutSeconds: Double) async -> BridgeTarget? { - final class DiscoveryState: @unchecked Sendable { - let lock = NSLock() - var resolved = false - var browsers: [NWBrowser] = [] - var continuation: CheckedContinuation? - - func finish(_ target: BridgeTarget?) { - self.lock.lock() - defer { lock.unlock() } - if self.resolved { return } - self.resolved = true - for browser in self.browsers { - browser.cancel() - } - self.continuation?.resume(returning: target) - self.continuation = nil - } - } - - return await withCheckedContinuation { cont in - let state = DiscoveryState() - state.continuation = cont - - let params = NWParameters.tcp - params.includePeerToPeer = true - - for domain in ClawdbotBonjour.bridgeServiceDomains { - let browser = NWBrowser( - for: .bonjour(type: ClawdbotBonjour.bridgeServiceType, domain: domain), - using: params) - browser.browseResultsChangedHandler = { results, _ in - let preferred = BridgeDiscoveryPreferences.preferredStableID() - if let preferred, - let match = results.first(where: { - if case .service = $0.endpoint { - return BridgeEndpointID.stableID($0.endpoint) == preferred - } - return false - }) - { - state.finish(Self.targetFromResult(match)) - return - } - - if let result = results.first(where: { if case .service = $0.endpoint { true } else { false } }) { - state.finish(Self.targetFromResult(result)) - } - } - browser.stateUpdateHandler = { browserState in - if case .failed = browserState { - state.finish(nil) - } - } - state.browsers.append(browser) - browser.start(queue: DispatchQueue(label: "com.clawdbot.macos.bridge-discovery.\(domain)")) - } - - Task { - try? await Task.sleep(nanoseconds: UInt64(timeoutSeconds * 1_000_000_000)) - state.finish(nil) - } - } - } - - private nonisolated static func targetFromResult(_ result: NWBrowser.Result) -> BridgeTarget? { - let endpoint = result.endpoint - guard case .service = endpoint else { return nil } - let stableID = BridgeEndpointID.stableID(endpoint) - let txt = result.endpoint.txtRecord?.dictionary ?? [:] - let tlsEnabled = Self.txtBoolValue(txt, key: "bridgeTls") - let tlsFingerprint = Self.txtValue(txt, key: "bridgeTlsSha256") - let tlsParams = Self.resolveDiscoveredTLSParams( - stableID: stableID, - tlsEnabled: tlsEnabled, - tlsFingerprintSha256: tlsFingerprint) - return BridgeTarget(endpoint: endpoint, stableID: stableID, tls: tlsParams) - } - - private nonisolated static func resolveDiscoveredTLSParams( - stableID: String, - tlsEnabled: Bool, - tlsFingerprintSha256: String?) -> MacNodeBridgeTLSParams? - { - let stored = MacNodeBridgeTLSStore.loadFingerprint(stableID: stableID) - - if tlsEnabled || tlsFingerprintSha256 != nil { - return MacNodeBridgeTLSParams( - required: true, - expectedFingerprint: tlsFingerprintSha256 ?? stored, - allowTOFU: stored == nil, - storeKey: stableID) - } - - if let stored { - return MacNodeBridgeTLSParams( - required: true, - expectedFingerprint: stored, - allowTOFU: false, - storeKey: stableID) - } - - return nil - } - - private nonisolated static func resolveManualTLSParams(stableID: String) -> MacNodeBridgeTLSParams? { - if let stored = MacNodeBridgeTLSStore.loadFingerprint(stableID: stableID) { - return MacNodeBridgeTLSParams( - required: true, - expectedFingerprint: stored, - allowTOFU: false, - storeKey: stableID) - } - - return MacNodeBridgeTLSParams( - required: false, - expectedFingerprint: nil, - allowTOFU: true, + private func buildSessionBox(url: URL) -> WebSocketSessionBox? { + guard url.scheme?.lowercased() == "wss" else { return nil } + let host = url.host ?? "gateway" + let port = url.port ?? 443 + let stableID = "\(host):\(port)" + let stored = GatewayTLSStore.loadFingerprint(stableID: stableID) + let params = GatewayTLSParams( + required: true, + expectedFingerprint: stored, + allowTOFU: stored == nil, storeKey: stableID) - } - - private nonisolated static func txtValue(_ dict: [String: String], key: String) -> String? { - let raw = dict[key]?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - return raw.isEmpty ? nil : raw - } - - private nonisolated static func txtBoolValue(_ dict: [String: String], key: String) -> Bool { - guard let raw = self.txtValue(dict, key: key)?.lowercased() else { return false } - return raw == "1" || raw == "true" || raw == "yes" - } -} - -enum MacNodeTokenStore { - private static let suiteName = "com.clawdbot.shared" - private static let tokenKey = "mac.node.bridge.token" - - private static var defaults: UserDefaults { - UserDefaults(suiteName: suiteName) ?? .standard - } - - static func loadToken() -> String? { - let raw = self.defaults.string(forKey: self.tokenKey)?.trimmingCharacters(in: .whitespacesAndNewlines) - return raw?.isEmpty == false ? raw : nil - } - - static func saveToken(_ token: String) { - self.defaults.set(token, forKey: self.tokenKey) + let session = GatewayTLSPinningSession(params: params) + return WebSocketSessionBox(session: session) } } diff --git a/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntime.swift b/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntime.swift index 16d5189ba..c3eacb8a1 100644 --- a/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntime.swift +++ b/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntime.swift @@ -133,7 +133,7 @@ actor MacNodeRuntime { let sessionKey = self.mainSessionKey let path = try await CanvasManager.shared.snapshot(sessionKey: sessionKey, outPath: nil) - defer { try? FileManager.default.removeItem(atPath: path) } + defer { try? FileManager().removeItem(atPath: path) } let data = try Data(contentsOf: URL(fileURLWithPath: path)) guard let image = NSImage(data: data) else { return Self.errorResponse(req, code: .unavailable, message: "canvas snapshot decode failed") @@ -206,7 +206,7 @@ actor MacNodeRuntime { includeAudio: params.includeAudio ?? true, deviceId: params.deviceId, outPath: nil) - defer { try? FileManager.default.removeItem(atPath: res.path) } + defer { try? FileManager().removeItem(atPath: res.path) } let data = try Data(contentsOf: URL(fileURLWithPath: res.path)) struct ClipPayload: Encodable { var format: String @@ -312,7 +312,7 @@ actor MacNodeRuntime { fps: params.fps, includeAudio: params.includeAudio, outPath: nil) - defer { try? FileManager.default.removeItem(atPath: res.path) } + defer { try? FileManager().removeItem(atPath: res.path) } let data = try Data(contentsOf: URL(fileURLWithPath: res.path)) struct ScreenPayload: Encodable { var format: String @@ -443,7 +443,6 @@ actor MacNodeRuntime { let approvals = ExecApprovalsStore.resolve(agentId: agentId) let security = approvals.agent.security let ask = approvals.agent.ask - let askFallback = approvals.agent.askFallback let autoAllowSkills = approvals.agent.autoAllowSkills let sessionKey = (params.sessionKey?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false) ? params.sessionKey!.trimmingCharacters(in: .whitespacesAndNewlines) @@ -481,89 +480,29 @@ actor MacNodeRuntime { message: "SYSTEM_RUN_DISABLED: security=deny") } - let requiresAsk: Bool = { - if ask == .always { return true } - if ask == .onMiss && security == .allowlist && allowlistMatch == nil && !skillAllow { return true } - return false - }() - - var approvedByAsk = false - if requiresAsk { - let decision: ExecApprovalDecision? = await ExecApprovalsPromptPresenter.prompt( - ExecApprovalPromptRequest( - command: displayCommand, - cwd: params.cwd, - host: "node", - security: security.rawValue, - ask: ask.rawValue, - agentId: agentId, - resolvedPath: resolution?.resolvedPath)) - - switch decision { - case .deny?: - await self.emitExecEvent( - "exec.denied", - payload: ExecEventPayload( - sessionKey: sessionKey, - runId: runId, - host: "node", - command: displayCommand, - reason: "user-denied")) - return Self.errorResponse( - req, - code: .unavailable, - message: "SYSTEM_RUN_DENIED: user denied") - case nil: - if askFallback == .full { - approvedByAsk = true - } else if askFallback == .allowlist { - if allowlistMatch != nil || skillAllow { - approvedByAsk = true - } else { - await self.emitExecEvent( - "exec.denied", - payload: ExecEventPayload( - sessionKey: sessionKey, - runId: runId, - host: "node", - command: displayCommand, - reason: "approval-required")) - return Self.errorResponse( - req, - code: .unavailable, - message: "SYSTEM_RUN_DENIED: approval required") - } - } else { - await self.emitExecEvent( - "exec.denied", - payload: ExecEventPayload( - sessionKey: sessionKey, - runId: runId, - host: "node", - command: displayCommand, - reason: "approval-required")) - return Self.errorResponse( - req, - code: .unavailable, - message: "SYSTEM_RUN_DENIED: approval required") - } - case .allowAlways?: - approvedByAsk = true - if security == .allowlist { - let pattern = resolution?.resolvedPath ?? - resolution?.rawExecutable ?? - command.first?.trimmingCharacters(in: .whitespacesAndNewlines) ?? - "" - if !pattern.isEmpty { - ExecApprovalsStore.addAllowlistEntry(agentId: agentId, pattern: pattern) - } - } - case .allowOnce?: - approvedByAsk = true - } + let approval = await self.resolveSystemRunApproval( + req: req, + params: params, + context: ExecRunContext( + displayCommand: displayCommand, + security: security, + ask: ask, + agentId: agentId, + resolution: resolution, + allowlistMatch: allowlistMatch, + skillAllow: skillAllow, + sessionKey: sessionKey, + runId: runId)) + if let response = approval.response { return response } + let approvedByAsk = approval.approvedByAsk + let persistAllowlist = approval.persistAllowlist + if persistAllowlist, security == .allowlist, + let pattern = ExecApprovalHelpers.allowlistPattern(command: command, resolution: resolution) + { + ExecApprovalsStore.addAllowlistEntry(agentId: agentId, pattern: pattern) } - if security == .allowlist && allowlistMatch == nil && !skillAllow && !approvedByAsk { + if security == .allowlist, allowlistMatch == nil, !skillAllow, !approvedByAsk { await self.emitExecEvent( "exec.denied", payload: ExecEventPayload( @@ -619,7 +558,7 @@ actor MacNodeRuntime { env: env, timeout: timeoutSec) let combined = [result.stdout, result.stderr, result.errorMessage] - .compactMap { $0 } + .compactMap(\.self) .filter { !$0.isEmpty } .joined(separator: "\n") await self.emitExecEvent( @@ -680,6 +619,100 @@ actor MacNodeRuntime { return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload) } + private struct ExecApprovalOutcome { + var approvedByAsk: Bool + var persistAllowlist: Bool + var response: BridgeInvokeResponse? + } + + private struct ExecRunContext { + var displayCommand: String + var security: ExecSecurity + var ask: ExecAsk + var agentId: String? + var resolution: ExecCommandResolution? + var allowlistMatch: ExecAllowlistEntry? + var skillAllow: Bool + var sessionKey: String + var runId: String + } + + private func resolveSystemRunApproval( + req: BridgeInvokeRequest, + params: ClawdbotSystemRunParams, + context: ExecRunContext) async -> ExecApprovalOutcome + { + let requiresAsk = ExecApprovalHelpers.requiresAsk( + ask: context.ask, + security: context.security, + allowlistMatch: context.allowlistMatch, + skillAllow: context.skillAllow) + + let decisionFromParams = ExecApprovalHelpers.parseDecision(params.approvalDecision) + var approvedByAsk = params.approved == true || decisionFromParams != nil + var persistAllowlist = decisionFromParams == .allowAlways + if decisionFromParams == .deny { + await self.emitExecEvent( + "exec.denied", + payload: ExecEventPayload( + sessionKey: context.sessionKey, + runId: context.runId, + host: "node", + command: context.displayCommand, + reason: "user-denied")) + return ExecApprovalOutcome( + approvedByAsk: approvedByAsk, + persistAllowlist: persistAllowlist, + response: Self.errorResponse( + req, + code: .unavailable, + message: "SYSTEM_RUN_DENIED: user denied")) + } + + if requiresAsk, !approvedByAsk { + let decision = await MainActor.run { + ExecApprovalsPromptPresenter.prompt( + ExecApprovalPromptRequest( + command: context.displayCommand, + cwd: params.cwd, + host: "node", + security: context.security.rawValue, + ask: context.ask.rawValue, + agentId: context.agentId, + resolvedPath: context.resolution?.resolvedPath, + sessionKey: context.sessionKey)) + } + switch decision { + case .deny: + await self.emitExecEvent( + "exec.denied", + payload: ExecEventPayload( + sessionKey: context.sessionKey, + runId: context.runId, + host: "node", + command: context.displayCommand, + reason: "user-denied")) + return ExecApprovalOutcome( + approvedByAsk: approvedByAsk, + persistAllowlist: persistAllowlist, + response: Self.errorResponse( + req, + code: .unavailable, + message: "SYSTEM_RUN_DENIED: user denied")) + case .allowAlways: + approvedByAsk = true + persistAllowlist = true + case .allowOnce: + approvedByAsk = true + } + } + + return ExecApprovalOutcome( + approvedByAsk: approvedByAsk, + persistAllowlist: persistAllowlist, + response: nil) + } + private func handleSystemExecApprovalsGet(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse { _ = ExecApprovalsStore.ensureFile() let snapshot = ExecApprovalsStore.readSnapshot() @@ -729,7 +762,7 @@ actor MacNodeRuntime { let resolvedPath = (socketPath?.isEmpty == false) ? socketPath! : current.socket?.path?.trimmingCharacters(in: .whitespacesAndNewlines) ?? - ExecApprovalsStore.socketPath() + ExecApprovalsStore.socketPath() let resolvedToken = (token?.isEmpty == false) ? token! : current.socket?.token?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" @@ -794,10 +827,12 @@ actor MacNodeRuntime { return BridgeInvokeResponse(id: req.id, ok: true) } } +} +extension MacNodeRuntime { private static func decodeParams(_ type: T.Type, from json: String?) throws -> T { guard let json, let data = json.data(using: .utf8) else { - throw NSError(domain: "Bridge", code: 20, userInfo: [ + throw NSError(domain: "Gateway", code: 20, userInfo: [ NSLocalizedDescriptionKey: "INVALID_REQUEST: paramsJSON required", ]) } diff --git a/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntimeMainActorServices.swift b/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntimeMainActorServices.swift index ef115b178..a6e03e3e3 100644 --- a/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntimeMainActorServices.swift +++ b/apps/macos/Sources/Clawdbot/NodeMode/MacNodeRuntimeMainActorServices.swift @@ -57,5 +57,4 @@ final class LiveMacNodeRuntimeMainActorServices: MacNodeRuntimeMainActorServices maxAgeMs: maxAgeMs, timeoutMs: timeoutMs) } - } diff --git a/apps/macos/Sources/Clawdbot/NodePairingApprovalPrompter.swift b/apps/macos/Sources/Clawdbot/NodePairingApprovalPrompter.swift index 2f6c3d0f5..b3f7e9295 100644 --- a/apps/macos/Sources/Clawdbot/NodePairingApprovalPrompter.swift +++ b/apps/macos/Sources/Clawdbot/NodePairingApprovalPrompter.swift @@ -1,6 +1,7 @@ import AppKit import ClawdbotDiscovery import ClawdbotIPC +import ClawdbotKit import ClawdbotProtocol import Foundation import Observation @@ -543,7 +544,7 @@ final class NodePairingApprovalPrompter { try? await Task.sleep(nanoseconds: 200_000_000) } - let preferred = BridgeDiscoveryPreferences.preferredStableID() + let preferred = GatewayDiscoveryPreferences.preferredStableID() let gateway = model.gateways.first { $0.stableID == preferred } ?? model.gateways.first guard let gateway else { return nil } let host = (gateway.tailnetDns?.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? diff --git a/apps/macos/Sources/Clawdbot/NodeServiceManager.swift b/apps/macos/Sources/Clawdbot/NodeServiceManager.swift new file mode 100644 index 000000000..2dd62d1e6 --- /dev/null +++ b/apps/macos/Sources/Clawdbot/NodeServiceManager.swift @@ -0,0 +1,150 @@ +import Foundation +import OSLog + +enum NodeServiceManager { + private static let logger = Logger(subsystem: "com.clawdbot", category: "node.service") + + static func start() async -> String? { + let result = await self.runServiceCommandResult( + ["node", "start"], + timeout: 20, + quiet: false) + if let error = self.errorMessage(from: result, treatNotLoadedAsError: true) { + self.logger.error("node service start failed: \(error, privacy: .public)") + return error + } + return nil + } + + static func stop() async -> String? { + let result = await self.runServiceCommandResult( + ["node", "stop"], + timeout: 15, + quiet: false) + if let error = self.errorMessage(from: result, treatNotLoadedAsError: false) { + self.logger.error("node service stop failed: \(error, privacy: .public)") + return error + } + return nil + } +} + +extension NodeServiceManager { + private struct CommandResult { + let success: Bool + let payload: Data? + let message: String? + let parsed: ParsedServiceJson? + } + + private struct ParsedServiceJson { + let text: String + let object: [String: Any] + let ok: Bool? + let result: String? + let message: String? + let error: String? + let hints: [String] + } + + private static func runServiceCommandResult( + _ args: [String], + timeout: Double, + quiet: Bool) async -> CommandResult + { + let command = CommandResolver.clawdbotCommand( + subcommand: "service", + extraArgs: self.withJsonFlag(args), + // Service management must always run locally, even if remote mode is configured. + configRoot: ["gateway": ["mode": "local"]]) + var env = ProcessInfo.processInfo.environment + env["PATH"] = CommandResolver.preferredPaths().joined(separator: ":") + let response = await ShellExecutor.runDetailed(command: command, cwd: nil, env: env, timeout: timeout) + let parsed = self.parseServiceJson(from: response.stdout) ?? self.parseServiceJson(from: response.stderr) + let ok = parsed?.ok + let message = parsed?.error ?? parsed?.message + let payload = parsed?.text.data(using: .utf8) + ?? (response.stdout.isEmpty ? response.stderr : response.stdout).data(using: .utf8) + let success = ok ?? response.success + if success { + return CommandResult(success: true, payload: payload, message: nil, parsed: parsed) + } + + if quiet { + return CommandResult(success: false, payload: payload, message: message, parsed: parsed) + } + + let detail = message ?? self.summarize(response.stderr) ?? self.summarize(response.stdout) + let exit = response.exitCode.map { "exit \($0)" } ?? (response.errorMessage ?? "failed") + let fullMessage = detail.map { "Node service command failed (\(exit)): \($0)" } + ?? "Node service command failed (\(exit))" + self.logger.error("\(fullMessage, privacy: .public)") + return CommandResult(success: false, payload: payload, message: detail, parsed: parsed) + } + + private static func errorMessage(from result: CommandResult, treatNotLoadedAsError: Bool) -> String? { + if !result.success { + return result.message ?? "Node service command failed" + } + guard let parsed = result.parsed else { return nil } + if parsed.ok == false { + return self.mergeHints(message: parsed.error ?? parsed.message, hints: parsed.hints) + } + if treatNotLoadedAsError, parsed.result == "not-loaded" { + let base = parsed.message ?? "Node service not loaded." + return self.mergeHints(message: base, hints: parsed.hints) + } + return nil + } + + private static func withJsonFlag(_ args: [String]) -> [String] { + if args.contains("--json") { return args } + return args + ["--json"] + } + + private static func parseServiceJson(from raw: String) -> ParsedServiceJson? { + let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) + guard let start = trimmed.firstIndex(of: "{"), + let end = trimmed.lastIndex(of: "}") + else { + return nil + } + let jsonText = String(trimmed[start...end]) + guard let data = jsonText.data(using: .utf8) else { return nil } + guard let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return nil } + let ok = object["ok"] as? Bool + let result = object["result"] as? String + let message = object["message"] as? String + let error = object["error"] as? String + let hints = (object["hints"] as? [String]) ?? [] + return ParsedServiceJson( + text: jsonText, + object: object, + ok: ok, + result: result, + message: message, + error: error, + hints: hints) + } + + private static func mergeHints(message: String?, hints: [String]) -> String? { + let trimmed = message?.trimmingCharacters(in: .whitespacesAndNewlines) + let nonEmpty = trimmed?.isEmpty == false ? trimmed : nil + guard !hints.isEmpty else { return nonEmpty } + let hintText = hints.prefix(2).joined(separator: " · ") + if let nonEmpty { + return "\(nonEmpty) (\(hintText))" + } + return hintText + } + + private static func summarize(_ text: String) -> String? { + let lines = text + .split(whereSeparator: \.isNewline) + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + guard let last = lines.last else { return nil } + let normalized = last.replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression) + return normalized.count > 200 ? String(normalized.prefix(199)) + "…" : normalized + } +} diff --git a/apps/macos/Sources/Clawdbot/OnboardingView+Actions.swift b/apps/macos/Sources/Clawdbot/OnboardingView+Actions.swift index b84ca5e8d..5942be760 100644 --- a/apps/macos/Sources/Clawdbot/OnboardingView+Actions.swift +++ b/apps/macos/Sources/Clawdbot/OnboardingView+Actions.swift @@ -9,7 +9,7 @@ extension OnboardingView { self.state.connectionMode = .local self.preferredGatewayID = nil self.showAdvancedConnection = false - BridgeDiscoveryPreferences.setPreferredStableID(nil) + GatewayDiscoveryPreferences.setPreferredStableID(nil) } func selectUnconfiguredGateway() { @@ -17,13 +17,13 @@ extension OnboardingView { self.state.connectionMode = .unconfigured self.preferredGatewayID = nil self.showAdvancedConnection = false - BridgeDiscoveryPreferences.setPreferredStableID(nil) + GatewayDiscoveryPreferences.setPreferredStableID(nil) } func selectRemoteGateway(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) { Task { await self.onboardingWizard.cancelIfRunning() } self.preferredGatewayID = gateway.stableID - BridgeDiscoveryPreferences.setPreferredStableID(gateway.stableID) + GatewayDiscoveryPreferences.setPreferredStableID(gateway.stableID) if let host = gateway.tailnetDns ?? gateway.lanHost { let user = NSUserName() @@ -36,7 +36,7 @@ extension OnboardingView { self.state.remoteCliPath = gateway.cliPath ?? "" self.state.connectionMode = .remote - MacNodeModeCoordinator.shared.setPreferredBridgeStableID(gateway.stableID) + MacNodeModeCoordinator.shared.setPreferredGatewayStableID(gateway.stableID) } func openSettings(tab: SettingsTab) { diff --git a/apps/macos/Sources/Clawdbot/OnboardingView+Layout.swift b/apps/macos/Sources/Clawdbot/OnboardingView+Layout.swift index f369b12af..1fb1e65a0 100644 --- a/apps/macos/Sources/Clawdbot/OnboardingView+Layout.swift +++ b/apps/macos/Sources/Clawdbot/OnboardingView+Layout.swift @@ -63,7 +63,7 @@ extension OnboardingView { await self.ensureDefaultWorkspace() self.refreshAnthropicOAuthStatus() self.refreshBootstrapStatus() - self.preferredGatewayID = BridgeDiscoveryPreferences.preferredStableID() + self.preferredGatewayID = GatewayDiscoveryPreferences.preferredStableID() } } @@ -210,6 +210,7 @@ extension OnboardingView { title: String, subtitle: String, systemImage: String, + buttonTitle: String, action: @escaping () -> Void) -> some View { HStack(alignment: .top, spacing: 12) { @@ -222,7 +223,7 @@ extension OnboardingView { Text(subtitle) .font(.subheadline) .foregroundStyle(.secondary) - Button("Open Settings → Skills", action: action) + Button(buttonTitle, action: action) .buttonStyle(.link) .padding(.top, 2) } diff --git a/apps/macos/Sources/Clawdbot/OnboardingView+Pages.swift b/apps/macos/Sources/Clawdbot/OnboardingView+Pages.swift index 03ca409c2..e32252e81 100644 --- a/apps/macos/Sources/Clawdbot/OnboardingView+Pages.swift +++ b/apps/macos/Sources/Clawdbot/OnboardingView+Pages.swift @@ -77,7 +77,7 @@ extension OnboardingView { .font(.largeTitle.weight(.semibold)) Text( "Clawdbot uses a single Gateway that stays running. Pick this Mac, " + - "connect to a discovered bridge nearby for pairing, or configure later.") + "connect to a discovered gateway nearby, or configure later.") .font(.body) .foregroundStyle(.secondary) .multilineTextAlignment(.center) @@ -126,13 +126,13 @@ extension OnboardingView { } if self.gatewayDiscovery.gateways.isEmpty { - Text("Searching for nearby bridges…") + Text("Searching for nearby gateways…") .font(.caption) .foregroundStyle(.secondary) .padding(.leading, 4) } else { VStack(alignment: .leading, spacing: 6) { - Text("Nearby bridges (pairing only)") + Text("Nearby gateways") .font(.caption) .foregroundStyle(.secondary) .padding(.leading, 4) @@ -229,12 +229,12 @@ extension OnboardingView { let portSuffix = gateway.sshPort != 22 ? " · ssh \(gateway.sshPort)" : "" return "\(host)\(portSuffix)" } - return "Bridge pairing only" + return "Gateway pairing only" } func isSelectedGateway(_ gateway: GatewayDiscoveryModel.DiscoveredGateway) -> Bool { guard self.state.connectionMode == .remote else { return false } - let preferred = self.preferredGatewayID ?? BridgeDiscoveryPreferences.preferredStableID() + let preferred = self.preferredGatewayID ?? GatewayDiscoveryPreferences.preferredStableID() return preferred == gateway.stableID } @@ -695,7 +695,8 @@ extension OnboardingView { self.featureActionRow( title: "Connect WhatsApp or Telegram", subtitle: "Open Settings → Channels to link channels and monitor status.", - systemImage: "link") + systemImage: "link", + buttonTitle: "Open Settings → Channels") { self.openSettings(tab: .channels) } @@ -711,7 +712,8 @@ extension OnboardingView { self.featureActionRow( title: "Give your agent more powers", subtitle: "Enable optional skills (Peekaboo, oracle, camsnap, …) from Settings → Skills.", - systemImage: "sparkles") + systemImage: "sparkles", + buttonTitle: "Open Settings → Skills") { self.openSettings(tab: .skills) } diff --git a/apps/macos/Sources/Clawdbot/OnboardingView+Testing.swift b/apps/macos/Sources/Clawdbot/OnboardingView+Testing.swift index f7a5920e4..33726b7c4 100644 --- a/apps/macos/Sources/Clawdbot/OnboardingView+Testing.swift +++ b/apps/macos/Sources/Clawdbot/OnboardingView+Testing.swift @@ -9,14 +9,14 @@ extension OnboardingView { let discovery = GatewayDiscoveryModel(localDisplayName: InstanceIdentity.displayName) discovery.statusText = "Searching..." let gateway = GatewayDiscoveryModel.DiscoveredGateway( - displayName: "Test Bridge", - lanHost: "bridge.local", - tailnetDns: "bridge.ts.net", + displayName: "Test Gateway", + lanHost: "gateway.local", + tailnetDns: "gateway.ts.net", sshPort: 2222, gatewayPort: 18789, cliPath: "/usr/local/bin/clawdbot", - stableID: "bridge-1", - debugID: "bridge-1", + stableID: "gateway-1", + debugID: "gateway-1", isLocal: false) discovery.gateways = [gateway] @@ -78,6 +78,7 @@ extension OnboardingView { title: "Action", subtitle: "Action subtitle", systemImage: "gearshape", + buttonTitle: "Action", action: {}) _ = view.gatewaySubtitle(for: gateway) _ = view.isSelectedGateway(gateway) diff --git a/apps/macos/Sources/Clawdbot/OnboardingWizard.swift b/apps/macos/Sources/Clawdbot/OnboardingWizard.swift index dc37ac86b..e9d071328 100644 --- a/apps/macos/Sources/Clawdbot/OnboardingWizard.swift +++ b/apps/macos/Sources/Clawdbot/OnboardingWizard.swift @@ -1,3 +1,4 @@ +import ClawdbotKit import ClawdbotProtocol import Foundation import Observation @@ -216,7 +217,7 @@ final class OnboardingWizardModel { struct OnboardingWizardStepView: View { let step: WizardStep let isSubmitting: Bool - let onSubmit: (AnyCodable?) -> Void + let onStepSubmit: (AnyCodable?) -> Void @State private var textValue: String @State private var confirmValue: Bool @@ -228,7 +229,7 @@ struct OnboardingWizardStepView: View { init(step: WizardStep, isSubmitting: Bool, onSubmit: @escaping (AnyCodable?) -> Void) { self.step = step self.isSubmitting = isSubmitting - self.onSubmit = onSubmit + self.onStepSubmit = onSubmit let options = parseWizardOptions(step.options).enumerated().map { index, option in WizardOptionItem(index: index, option: option) } @@ -378,27 +379,27 @@ struct OnboardingWizardStepView: View { private func submit() { switch wizardStepType(self.step) { case "note", "progress": - self.onSubmit(nil) + self.onStepSubmit(nil) case "text": - self.onSubmit(AnyCodable(self.textValue)) + self.onStepSubmit(AnyCodable(self.textValue)) case "confirm": - self.onSubmit(AnyCodable(self.confirmValue)) + self.onStepSubmit(AnyCodable(self.confirmValue)) case "select": guard self.optionItems.indices.contains(self.selectedIndex) else { - self.onSubmit(nil) + self.onStepSubmit(nil) return } let option = self.optionItems[self.selectedIndex].option - self.onSubmit(bridgeToLocal(option.value) ?? AnyCodable(option.label)) + self.onStepSubmit(bridgeToLocal(option.value) ?? AnyCodable(option.label)) case "multiselect": let values = self.optionItems .filter { self.selectedIndices.contains($0.index) } .map { bridgeToLocal($0.option.value) ?? AnyCodable($0.option.label) } - self.onSubmit(AnyCodable(values)) + self.onStepSubmit(AnyCodable(values)) case "action": - self.onSubmit(AnyCodable(true)) + self.onStepSubmit(AnyCodable(true)) default: - self.onSubmit(nil) + self.onStepSubmit(nil) } } } diff --git a/apps/macos/Sources/Clawdbot/PermissionsSettings.swift b/apps/macos/Sources/Clawdbot/PermissionsSettings.swift index d7ee5339c..f5a926032 100644 --- a/apps/macos/Sources/Clawdbot/PermissionsSettings.swift +++ b/apps/macos/Sources/Clawdbot/PermissionsSettings.swift @@ -1,4 +1,6 @@ import ClawdbotIPC +import ClawdbotKit +import CoreLocation import SwiftUI struct PermissionsSettings: View { @@ -8,6 +10,8 @@ struct PermissionsSettings: View { var body: some View { VStack(alignment: .leading, spacing: 14) { + SystemRunSettingsView() + Text("Allow these so Clawdbot can notify and capture when needed.") .padding(.top, 4) @@ -15,6 +19,8 @@ struct PermissionsSettings: View { .padding(.horizontal, 2) .padding(.vertical, 6) + LocationAccessSettings() + Button("Restart onboarding") { self.showOnboarding() } .buttonStyle(.bordered) Spacer() @@ -24,6 +30,72 @@ struct PermissionsSettings: View { } } +private struct LocationAccessSettings: View { + @AppStorage(locationModeKey) private var locationModeRaw: String = ClawdbotLocationMode.off.rawValue + @AppStorage(locationPreciseKey) private var locationPreciseEnabled: Bool = true + @State private var lastLocationModeRaw: String = ClawdbotLocationMode.off.rawValue + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + Text("Location Access") + .font(.body) + + Picker("", selection: self.$locationModeRaw) { + Text("Off").tag(ClawdbotLocationMode.off.rawValue) + Text("While Using").tag(ClawdbotLocationMode.whileUsing.rawValue) + Text("Always").tag(ClawdbotLocationMode.always.rawValue) + } + .labelsHidden() + .pickerStyle(.menu) + + Toggle("Precise Location", isOn: self.$locationPreciseEnabled) + .disabled(self.locationMode == .off) + + Text("Always may require System Settings to approve background location.") + .font(.footnote) + .foregroundStyle(.tertiary) + .fixedSize(horizontal: false, vertical: true) + } + .onAppear { + self.lastLocationModeRaw = self.locationModeRaw + } + .onChange(of: self.locationModeRaw) { _, newValue in + let previous = self.lastLocationModeRaw + self.lastLocationModeRaw = newValue + guard let mode = ClawdbotLocationMode(rawValue: newValue) else { return } + Task { + let granted = await self.requestLocationAuthorization(mode: mode) + if !granted { + await MainActor.run { + self.locationModeRaw = previous + self.lastLocationModeRaw = previous + } + } + } + } + } + + private var locationMode: ClawdbotLocationMode { + ClawdbotLocationMode(rawValue: self.locationModeRaw) ?? .off + } + + private func requestLocationAuthorization(mode: ClawdbotLocationMode) async -> Bool { + guard mode != .off else { return true } + guard CLLocationManager.locationServicesEnabled() else { + await MainActor.run { LocationPermissionHelper.openSettings() } + return false + } + + let status = CLLocationManager().authorizationStatus + let requireAlways = mode == .always + if PermissionManager.isLocationAuthorized(status: status, requireAlways: requireAlways) { + return true + } + let updated = await LocationPermissionRequester.shared.request(always: requireAlways) + return PermissionManager.isLocationAuthorized(status: updated, requireAlways: requireAlways) + } +} + struct PermissionStatusList: View { let status: [Capability: Bool] let refresh: () async -> Void @@ -45,25 +117,6 @@ struct PermissionStatusList: View { .font(.footnote) .padding(.top, 2) .help("Refresh status") - - if (self.status[.accessibility] ?? false) == false || (self.status[.screenRecording] ?? false) == false { - VStack(alignment: .leading, spacing: 8) { - Text( - "Note: macOS may require restarting Clawdbot after enabling Accessibility or Screen Recording.") - .font(.caption) - .foregroundStyle(.secondary) - .fixedSize(horizontal: false, vertical: true) - - Button { - LaunchdManager.startClawdbot() - } label: { - Label("Restart Clawdbot", systemImage: "arrow.counterclockwise") - } - .buttonStyle(.bordered) - .controlSize(.small) - } - .padding(.top, 4) - } } } diff --git a/apps/macos/Sources/Clawdbot/PortGuardian.swift b/apps/macos/Sources/Clawdbot/PortGuardian.swift index 071de5b2a..422300d59 100644 --- a/apps/macos/Sources/Clawdbot/PortGuardian.swift +++ b/apps/macos/Sources/Clawdbot/PortGuardian.swift @@ -24,7 +24,7 @@ actor PortGuardian { private var records: [Record] = [] private let logger = Logger(subsystem: "com.clawdbot", category: "portguard") private nonisolated static let appSupportDir: URL = { - let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! + let base = FileManager().urls(for: .applicationSupportDirectory, in: .userDomainMask).first! return base.appendingPathComponent("Clawdbot", isDirectory: true) }() @@ -71,7 +71,7 @@ actor PortGuardian { } func record(port: Int, pid: Int32, command: String, mode: AppState.ConnectionMode) async { - try? FileManager.default.createDirectory(at: Self.appSupportDir, withIntermediateDirectories: true) + try? FileManager().createDirectory(at: Self.appSupportDir, withIntermediateDirectories: true) self.records.removeAll { $0.pid == pid } self.records.append( Record( @@ -184,6 +184,14 @@ actor PortGuardian { } } + func isListening(port: Int, pid: Int32? = nil) async -> Bool { + let listeners = await self.listeners(on: port) + if let pid { + return listeners.contains(where: { $0.pid == pid }) + } + return !listeners.isEmpty + } + private func listeners(on port: Int) async -> [Listener] { let res = await ShellExecutor.run( command: ["lsof", "-nP", "-iTCP:\(port)", "-sTCP:LISTEN", "-Fpcn"], diff --git a/apps/macos/Sources/Clawdbot/RemotePortTunnel.swift b/apps/macos/Sources/Clawdbot/RemotePortTunnel.swift index ccbeb6e8d..8eaee1c05 100644 --- a/apps/macos/Sources/Clawdbot/RemotePortTunnel.swift +++ b/apps/macos/Sources/Clawdbot/RemotePortTunnel.swift @@ -72,7 +72,6 @@ final class RemotePortTunnel { } var args: [String] = [ "-o", "BatchMode=yes", - "-o", "IdentitiesOnly=yes", "-o", "ExitOnForwardFailure=yes", "-o", "StrictHostKeyChecking=accept-new", "-o", "UpdateHostKeys=yes", @@ -84,7 +83,12 @@ final class RemotePortTunnel { ] if parsed.port > 0 { args.append(contentsOf: ["-p", String(parsed.port)]) } let identity = settings.identity.trimmingCharacters(in: .whitespacesAndNewlines) - if !identity.isEmpty { args.append(contentsOf: ["-i", identity]) } + 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) diff --git a/apps/macos/Sources/Clawdbot/RemoteTunnelManager.swift b/apps/macos/Sources/Clawdbot/RemoteTunnelManager.swift index 5e42fbd05..898b1b482 100644 --- a/apps/macos/Sources/Clawdbot/RemoteTunnelManager.swift +++ b/apps/macos/Sources/Clawdbot/RemoteTunnelManager.swift @@ -20,11 +20,13 @@ actor RemoteTunnelManager { tunnel.process.isRunning, let local = tunnel.localPort { - if await self.isTunnelHealthy(port: local) { + let pid = tunnel.process.processIdentifier + if await PortGuardian.shared.isListening(port: Int(local), pid: pid) { self.logger.info("reusing active SSH tunnel localPort=\(local, privacy: .public)") return local } - self.logger.error("active SSH tunnel on port \(local, privacy: .public) is unhealthy; restarting") + self.logger.error( + "active SSH tunnel on port \(local, privacy: .public) is not listening; restarting") await self.beginRestart() tunnel.terminate() self.controlTunnel = nil @@ -35,19 +37,11 @@ actor RemoteTunnelManager { if let desc = await PortGuardian.shared.describe(port: Int(desiredPort)), self.isSshProcess(desc) { - if await self.isTunnelHealthy(port: desiredPort) { - self.logger.info( - "reusing existing SSH tunnel listener " + - "localPort=\(desiredPort, privacy: .public) " + - "pid=\(desc.pid, privacy: .public)") - return desiredPort - } - if self.restartInFlight { - self.logger.info("control tunnel restart in flight; skip stale tunnel cleanup") - return nil - } - await self.beginRestart() - await self.cleanupStaleTunnel(desc: desc, port: desiredPort) + self.logger.info( + "reusing existing SSH tunnel listener " + + "localPort=\(desiredPort, privacy: .public) " + + "pid=\(desc.pid, privacy: .public)") + return desiredPort } return nil } @@ -88,10 +82,6 @@ actor RemoteTunnelManager { self.controlTunnel = nil } - private func isTunnelHealthy(port: UInt16) async -> Bool { - await PortGuardian.shared.probeGatewayHealth(port: Int(port)) - } - private func isSshProcess(_ desc: PortGuardian.Descriptor) -> Bool { let cmd = desc.command.lowercased() if cmd.contains("ssh") { return true } @@ -128,21 +118,5 @@ actor RemoteTunnelManager { try? await Task.sleep(nanoseconds: UInt64(remaining * 1_000_000_000)) } - private func cleanupStaleTunnel(desc: PortGuardian.Descriptor, port: UInt16) async { - let pid = desc.pid - self.logger.error( - "stale SSH tunnel detected on port \(port, privacy: .public) pid \(pid, privacy: .public)") - let killed = await self.kill(pid: pid) - if !killed { - self.logger.error("failed to terminate stale SSH tunnel pid \(pid, privacy: .public)") - } - await PortGuardian.shared.removeRecord(pid: pid) - } - - private func kill(pid: Int32) async -> Bool { - let term = await ShellExecutor.run(command: ["kill", "-TERM", "\(pid)"], cwd: nil, env: nil, timeout: 2) - if term.ok { return true } - let sigkill = await ShellExecutor.run(command: ["kill", "-KILL", "\(pid)"], cwd: nil, env: nil, timeout: 2) - return sigkill.ok - } + // Keep tunnel reuse lightweight; restart only when the listener disappears. } diff --git a/apps/macos/Sources/Clawdbot/Resources/Info.plist b/apps/macos/Sources/Clawdbot/Resources/Info.plist index 77501e4d1..1c7d9619f 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.11-4 + 2026.1.24 CFBundleVersion - 202601113 + 202601240 CFBundleIconFile Clawdbot CFBundleURLTypes diff --git a/apps/macos/Sources/Clawdbot/RuntimeLocator.swift b/apps/macos/Sources/Clawdbot/RuntimeLocator.swift index 761c63b17..aadbe2d21 100644 --- a/apps/macos/Sources/Clawdbot/RuntimeLocator.swift +++ b/apps/macos/Sources/Clawdbot/RuntimeLocator.swift @@ -111,7 +111,7 @@ enum RuntimeLocator { // MARK: - Internals private static func findExecutable(named name: String, searchPaths: [String]) -> String? { - let fm = FileManager.default + let fm = FileManager() for dir in searchPaths { let candidate = (dir as NSString).appendingPathComponent(name) if fm.isExecutableFile(atPath: candidate) { diff --git a/apps/macos/Sources/Clawdbot/ScreenRecordService.swift b/apps/macos/Sources/Clawdbot/ScreenRecordService.swift index d48538e26..e0878a5c9 100644 --- a/apps/macos/Sources/Clawdbot/ScreenRecordService.swift +++ b/apps/macos/Sources/Clawdbot/ScreenRecordService.swift @@ -42,10 +42,10 @@ final class ScreenRecordService { if let outPath, !outPath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return URL(fileURLWithPath: outPath) } - return FileManager.default.temporaryDirectory + return FileManager().temporaryDirectory .appendingPathComponent("clawdbot-screen-record-\(UUID().uuidString).mp4") }() - try? FileManager.default.removeItem(at: outURL) + try? FileManager().removeItem(at: outURL) let content = try await SCShareableContent.current let displays = content.displays.sorted { $0.displayID < $1.displayID } diff --git a/apps/macos/Sources/Clawdbot/SessionActions.swift b/apps/macos/Sources/Clawdbot/SessionActions.swift index 6315f08c7..6e11c93cb 100644 --- a/apps/macos/Sources/Clawdbot/SessionActions.swift +++ b/apps/macos/Sources/Clawdbot/SessionActions.swift @@ -66,12 +66,12 @@ enum SessionActions { let dir = URL(fileURLWithPath: storePath).deletingLastPathComponent() urls.append(dir.appendingPathComponent("\(sessionId).jsonl")) } - let home = FileManager.default.homeDirectoryForCurrentUser + let home = FileManager().homeDirectoryForCurrentUser urls.append(home.appendingPathComponent(".clawdbot/sessions/\(sessionId).jsonl")) return urls }() - let existing = candidates.first(where: { FileManager.default.fileExists(atPath: $0.path) }) + let existing = candidates.first(where: { FileManager().fileExists(atPath: $0.path) }) guard let url = existing else { let alert = NSAlert() alert.messageText = "Session log not found" diff --git a/apps/macos/Sources/Clawdbot/SessionData.swift b/apps/macos/Sources/Clawdbot/SessionData.swift index 7ce1dc8fc..d4c85a4a2 100644 --- a/apps/macos/Sources/Clawdbot/SessionData.swift +++ b/apps/macos/Sources/Clawdbot/SessionData.swift @@ -246,7 +246,7 @@ enum SessionLoader { static let fallbackContextTokens = 200_000 static let defaultStorePath = standardize( - FileManager.default.homeDirectoryForCurrentUser + FileManager().homeDirectoryForCurrentUser .appendingPathComponent(".clawdbot/sessions/sessions.json").path) static func loadSnapshot( diff --git a/apps/macos/Sources/Clawdbot/SessionMenuPreviewView.swift b/apps/macos/Sources/Clawdbot/SessionMenuPreviewView.swift index 497e54a5c..e7d81659e 100644 --- a/apps/macos/Sources/Clawdbot/SessionMenuPreviewView.swift +++ b/apps/macos/Sources/Clawdbot/SessionMenuPreviewView.swift @@ -1,5 +1,6 @@ import ClawdbotChatUI import ClawdbotKit +import ClawdbotProtocol import OSLog import SwiftUI @@ -31,31 +32,80 @@ actor SessionPreviewCache { static let shared = SessionPreviewCache() private struct CacheEntry { - let items: [SessionPreviewItem] + let snapshot: SessionMenuPreviewSnapshot let updatedAt: Date } private var entries: [String: CacheEntry] = [:] - func cachedItems(for sessionKey: String, maxAge: TimeInterval) -> [SessionPreviewItem]? { + func cachedSnapshot(for sessionKey: String, maxAge: TimeInterval) -> SessionMenuPreviewSnapshot? { guard let entry = self.entries[sessionKey] else { return nil } guard Date().timeIntervalSince(entry.updatedAt) < maxAge else { return nil } - return entry.items + return entry.snapshot } - func store(items: [SessionPreviewItem], for sessionKey: String) { - self.entries[sessionKey] = CacheEntry(items: items, updatedAt: Date()) + func store(snapshot: SessionMenuPreviewSnapshot, for sessionKey: String) { + self.entries[sessionKey] = CacheEntry(snapshot: snapshot, updatedAt: Date()) } - func lastItems(for sessionKey: String) -> [SessionPreviewItem]? { - self.entries[sessionKey]?.items + func lastSnapshot(for sessionKey: String) -> SessionMenuPreviewSnapshot? { + self.entries[sessionKey]?.snapshot + } +} + +actor SessionPreviewLimiter { + static let shared = SessionPreviewLimiter(maxConcurrent: 2) + + private let maxConcurrent: Int + private var available: Int + private var waitQueue: [UUID] = [] + private var waiters: [UUID: CheckedContinuation] = [:] + + init(maxConcurrent: Int) { + let normalized = max(1, maxConcurrent) + self.maxConcurrent = normalized + self.available = normalized + } + + func withPermit(_ operation: () async throws -> T) async throws -> T { + await self.acquire() + defer { self.release() } + if Task.isCancelled { throw CancellationError() } + return try await operation() + } + + private func acquire() async { + if self.available > 0 { + self.available -= 1 + return + } + let id = UUID() + await withCheckedContinuation { cont in + self.waitQueue.append(id) + self.waiters[id] = cont + } + } + + private func release() { + if let id = self.waitQueue.first { + self.waitQueue.removeFirst() + if let cont = self.waiters.removeValue(forKey: id) { + cont.resume() + } + return + } + self.available = min(self.available + 1, self.maxConcurrent) } } #if DEBUG extension SessionPreviewCache { - func _testSet(items: [SessionPreviewItem], for sessionKey: String, updatedAt: Date = Date()) { - self.entries[sessionKey] = CacheEntry(items: items, updatedAt: updatedAt) + func _testSet( + snapshot: SessionMenuPreviewSnapshot, + for sessionKey: String, + updatedAt: Date = Date()) + { + self.entries[sessionKey] = CacheEntry(snapshot: snapshot, updatedAt: updatedAt) } func _testReset() { @@ -86,11 +136,17 @@ struct SessionMenuPreviewView: View { } private var primaryColor: Color { - self.isHighlighted ? Color(nsColor: .selectedMenuItemTextColor) : .primary + if self.isHighlighted { + return Color(nsColor: .selectedMenuItemTextColor) + } + return Color(nsColor: .labelColor) } private var secondaryColor: Color { - self.isHighlighted ? Color(nsColor: .selectedMenuItemTextColor).opacity(0.85) : .secondary + if self.isHighlighted { + return Color(nsColor: .selectedMenuItemTextColor).opacity(0.85) + } + return Color(nsColor: .secondaryLabelColor) } var body: some View { @@ -104,21 +160,19 @@ struct SessionMenuPreviewView: View { switch self.status { case .loading: - Text("Loading preview…") - .font(.caption) - .foregroundStyle(self.secondaryColor) + self.placeholder("Loading preview…") case .empty: - Text("No recent messages") - .font(.caption) - .foregroundStyle(self.secondaryColor) + self.placeholder("No recent messages") case let .error(message): - Text(message) - .font(.caption) - .foregroundStyle(self.secondaryColor) + self.placeholder(message) case .ready: - VStack(alignment: .leading, spacing: 6) { - ForEach(self.items) { item in - self.previewRow(item) + if self.items.isEmpty { + self.placeholder("No recent messages") + } else { + VStack(alignment: .leading, spacing: 6) { + ForEach(self.items) { item in + self.previewRow(item) + } } } } @@ -158,42 +212,56 @@ struct SessionMenuPreviewView: View { } } + @ViewBuilder + private func placeholder(_ text: String) -> some View { + Text(text) + .font(.caption) + .foregroundStyle(self.primaryColor) + } } enum SessionMenuPreviewLoader { private static let logger = Logger(subsystem: "com.clawdbot", category: "SessionPreview") private static let previewTimeoutSeconds: Double = 4 private static let cacheMaxAgeSeconds: TimeInterval = 30 + private static let previewMaxChars = 240 private struct PreviewTimeoutError: LocalizedError { var errorDescription: String? { "preview timeout" } } + static func prewarm(sessionKeys: [String], maxItems: Int) async { + let keys = self.uniqueKeys(sessionKeys) + guard !keys.isEmpty else { return } + do { + let payload = try await self.requestPreview(keys: keys, maxItems: maxItems) + await self.cache(payload: payload, maxItems: maxItems) + } catch { + if self.isUnknownMethodError(error) { return } + let errorDescription = String(describing: error) + Self.logger.debug( + "Session preview prewarm failed count=\(keys.count, privacy: .public) " + + "error=\(errorDescription, privacy: .public)") + } + } + static func load(sessionKey: String, maxItems: Int) async -> SessionMenuPreviewSnapshot { - if let cached = await SessionPreviewCache.shared.cachedItems(for: sessionKey, maxAge: cacheMaxAgeSeconds) { - return Self.snapshot(from: cached) + if let cached = await SessionPreviewCache.shared.cachedSnapshot( + for: sessionKey, + maxAge: cacheMaxAgeSeconds) + { + return cached } do { - let timeoutMs = Int(self.previewTimeoutSeconds * 1000) - let payload = try await AsyncTimeout.withTimeout( - seconds: self.previewTimeoutSeconds, - onTimeout: { PreviewTimeoutError() }, - operation: { - try await GatewayConnection.shared.chatHistory( - sessionKey: sessionKey, - limit: self.previewLimit(for: maxItems), - timeoutMs: timeoutMs) - }) - let built = Self.previewItems(from: payload, maxItems: maxItems) - await SessionPreviewCache.shared.store(items: built, for: sessionKey) - return Self.snapshot(from: built) + let snapshot = try await self.fetchSnapshot(sessionKey: sessionKey, maxItems: maxItems) + await SessionPreviewCache.shared.store(snapshot: snapshot, for: sessionKey) + return snapshot } catch is CancellationError { return SessionMenuPreviewSnapshot(items: [], status: .loading) } catch { - let fallback = await SessionPreviewCache.shared.lastItems(for: sessionKey) - if let fallback { - return Self.snapshot(from: fallback) + if let fallback = await SessionPreviewCache.shared.lastSnapshot(for: sessionKey) { + return fallback } let errorDescription = String(describing: error) Self.logger.warning( @@ -203,18 +271,120 @@ enum SessionMenuPreviewLoader { } } + private static func fetchSnapshot(sessionKey: String, maxItems: Int) async throws -> SessionMenuPreviewSnapshot { + do { + let payload = try await self.requestPreview(keys: [sessionKey], maxItems: maxItems) + if let entry = payload.previews.first(where: { $0.key == sessionKey }) ?? payload.previews.first { + return self.snapshot(from: entry, maxItems: maxItems) + } + return SessionMenuPreviewSnapshot(items: [], status: .error("Preview unavailable")) + } catch { + if self.isUnknownMethodError(error) { + return try await self.fetchHistorySnapshot(sessionKey: sessionKey, maxItems: maxItems) + } + throw error + } + } + + private static func requestPreview( + keys: [String], + maxItems: Int) async throws -> ClawdbotSessionsPreviewPayload + { + let boundedItems = self.normalizeMaxItems(maxItems) + let timeoutMs = Int(self.previewTimeoutSeconds * 1000) + return try await SessionPreviewLimiter.shared.withPermit { + try await AsyncTimeout.withTimeout( + seconds: self.previewTimeoutSeconds, + onTimeout: { PreviewTimeoutError() }, + operation: { + try await GatewayConnection.shared.sessionsPreview( + keys: keys, + limit: boundedItems, + maxChars: self.previewMaxChars, + timeoutMs: timeoutMs) + }) + } + } + + private static func fetchHistorySnapshot( + sessionKey: String, + maxItems: Int) async throws -> SessionMenuPreviewSnapshot + { + let timeoutMs = Int(self.previewTimeoutSeconds * 1000) + let payload = try await SessionPreviewLimiter.shared.withPermit { + try await AsyncTimeout.withTimeout( + seconds: self.previewTimeoutSeconds, + onTimeout: { PreviewTimeoutError() }, + operation: { + try await GatewayConnection.shared.chatHistory( + sessionKey: sessionKey, + limit: self.previewLimit(for: maxItems), + timeoutMs: timeoutMs) + }) + } + let built = Self.previewItems(from: payload, maxItems: maxItems) + return Self.snapshot(from: built) + } + private static func snapshot(from items: [SessionPreviewItem]) -> SessionMenuPreviewSnapshot { SessionMenuPreviewSnapshot(items: items, status: items.isEmpty ? .empty : .ready) } + private static func snapshot( + from entry: ClawdbotSessionPreviewEntry, + maxItems: Int) -> SessionMenuPreviewSnapshot + { + let items = self.previewItems(from: entry, maxItems: maxItems) + let normalized = entry.status.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + switch normalized { + case "ok": + return SessionMenuPreviewSnapshot(items: items, status: items.isEmpty ? .empty : .ready) + case "empty": + return SessionMenuPreviewSnapshot(items: items, status: .empty) + case "missing": + return SessionMenuPreviewSnapshot(items: items, status: .error("Session missing")) + default: + return SessionMenuPreviewSnapshot(items: items, status: .error("Preview unavailable")) + } + } + + private static func cache(payload: ClawdbotSessionsPreviewPayload, maxItems: Int) async { + for entry in payload.previews { + let snapshot = self.snapshot(from: entry, maxItems: maxItems) + await SessionPreviewCache.shared.store(snapshot: snapshot, for: entry.key) + } + } + private static func previewLimit(for maxItems: Int) -> Int { - min(max(maxItems * 3, 20), 120) + let boundedItems = self.normalizeMaxItems(maxItems) + return min(max(boundedItems * 3, 20), 120) + } + + private static func normalizeMaxItems(_ maxItems: Int) -> Int { + max(1, min(maxItems, 50)) + } + + private static func previewItems( + from entry: ClawdbotSessionPreviewEntry, + maxItems: Int) -> [SessionPreviewItem] + { + let boundedItems = self.normalizeMaxItems(maxItems) + let built: [SessionPreviewItem] = entry.items.enumerated().compactMap { index, item in + let text = item.text.trimmingCharacters(in: .whitespacesAndNewlines) + guard !text.isEmpty else { return nil } + let role = self.previewRoleFromRaw(item.role) + return SessionPreviewItem(id: "\(entry.key)-\(index)", role: role, text: text) + } + + let trimmed = built.suffix(boundedItems) + return Array(trimmed.reversed()) } private static func previewItems( from payload: ClawdbotChatHistoryPayload, maxItems: Int) -> [SessionPreviewItem] { + let boundedItems = self.normalizeMaxItems(maxItems) let raw: [ClawdbotKit.AnyCodable] = payload.messages ?? [] let messages = self.decodeMessages(raw) let built = messages.compactMap { message -> SessionPreviewItem? in @@ -225,7 +395,7 @@ enum SessionMenuPreviewLoader { return SessionPreviewItem(id: id, role: role, text: text) } - let trimmed = built.suffix(maxItems) + let trimmed = built.suffix(boundedItems) return Array(trimmed.reversed()) } @@ -238,12 +408,16 @@ enum SessionMenuPreviewLoader { private static func previewRole(_ raw: String, isTool: Bool) -> PreviewRole { if isTool { return .tool } + return self.previewRoleFromRaw(raw) + } + + private static func previewRoleFromRaw(_ raw: String) -> PreviewRole { switch raw.lowercased() { - case "user": return .user - case "assistant": return .assistant - case "system": return .system - case "tool": return .tool - default: return .other + case "user": .user + case "assistant": .assistant + case "system": .system + case "tool": .tool + default: .other } } @@ -306,4 +480,16 @@ enum SessionMenuPreviewLoader { } return result } + + private static func uniqueKeys(_ keys: [String]) -> [String] { + let trimmed = keys.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + return self.dedupePreservingOrder(trimmed.filter { !$0.isEmpty }) + } + + private static func isUnknownMethodError(_ error: Error) -> Bool { + guard let response = error as? GatewayResponseError else { return false } + guard response.code == ErrorCode.invalidRequest.rawValue else { return false } + let message = response.message.lowercased() + return message.contains("unknown method") + } } diff --git a/apps/macos/Sources/Clawdbot/SoundEffects.swift b/apps/macos/Sources/Clawdbot/SoundEffects.swift index c1f0cb9b4..b32123829 100644 --- a/apps/macos/Sources/Clawdbot/SoundEffects.swift +++ b/apps/macos/Sources/Clawdbot/SoundEffects.swift @@ -44,7 +44,7 @@ enum SoundEffectCatalog { ] private static let searchRoots: [URL] = [ - FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Sounds"), + FileManager().homeDirectoryForCurrentUser.appendingPathComponent("Library/Sounds"), URL(fileURLWithPath: "/Library/Sounds"), URL(fileURLWithPath: "/System/Applications/Mail.app/Contents/Resources"), // Mail “swoosh” URL(fileURLWithPath: "/System/Library/Sounds"), @@ -53,7 +53,7 @@ enum SoundEffectCatalog { private static let discoveredSoundMap: [String: URL] = { var map: [String: URL] = [:] for root in Self.searchRoots { - guard let contents = try? FileManager.default.contentsOfDirectory( + guard let contents = try? FileManager().contentsOfDirectory( at: root, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) diff --git a/apps/macos/Sources/Clawdbot/SystemRunSettingsView.swift b/apps/macos/Sources/Clawdbot/SystemRunSettingsView.swift index bb342a874..eef826c3f 100644 --- a/apps/macos/Sources/Clawdbot/SystemRunSettingsView.swift +++ b/apps/macos/Sources/Clawdbot/SystemRunSettingsView.swift @@ -80,9 +80,7 @@ struct SystemRunSettingsView: View { .labelsHidden() .pickerStyle(.menu) - Text(self.model.isDefaultsScope - ? "Defaults apply when an agent has no overrides. Ask controls prompt behavior; fallback is used when no companion UI is reachable." - : "Security controls whether system.run can execute on this Mac when paired as a node. Ask controls prompt behavior; fallback is used when no companion UI is reachable.") + Text(self.scopeMessage) .font(.footnote) .foregroundStyle(.tertiary) .fixedSize(horizontal: false, vertical: true) @@ -125,18 +123,27 @@ struct SystemRunSettingsView: View { .foregroundStyle(.secondary) } else { VStack(alignment: .leading, spacing: 8) { - ForEach(Array(self.model.entries.enumerated()), id: \.offset) { index, _ in + ForEach(self.model.entries, id: \.id) { entry in ExecAllowlistRow( entry: Binding( - get: { self.model.entries[index] }, - set: { self.model.updateEntry($0, at: index) }), - onRemove: { self.model.removeEntry(at: index) }) + get: { self.model.entry(for: entry.id) ?? entry }, + set: { self.model.updateEntry($0, id: entry.id) }), + onRemove: { self.model.removeEntry(id: entry.id) }) } } } } } } + + private var scopeMessage: String { + if self.model.isDefaultsScope { + return "Defaults apply when an agent has no overrides. " + + "Ask controls prompt behavior; fallback is used when no companion UI is reachable." + } + return "Security controls whether system.run can execute on this Mac when paired as a node. " + + "Ask controls prompt behavior; fallback is used when no companion UI is reachable." + } } private enum ExecApprovalsSettingsTab: String, CaseIterable, Identifiable { @@ -366,20 +373,24 @@ final class ExecApprovalsSettingsModel { ExecApprovalsStore.updateAllowlist(agentId: self.selectedAgentId, allowlist: self.entries) } - func updateEntry(_ entry: ExecAllowlistEntry, at index: Int) { + func updateEntry(_ entry: ExecAllowlistEntry, id: UUID) { guard !self.isDefaultsScope else { return } - guard self.entries.indices.contains(index) else { return } + guard let index = self.entries.firstIndex(where: { $0.id == id }) else { return } self.entries[index] = entry ExecApprovalsStore.updateAllowlist(agentId: self.selectedAgentId, allowlist: self.entries) } - func removeEntry(at index: Int) { + func removeEntry(id: UUID) { guard !self.isDefaultsScope else { return } - guard self.entries.indices.contains(index) else { return } + guard let index = self.entries.firstIndex(where: { $0.id == id }) else { return } self.entries.remove(at: index) ExecApprovalsStore.updateAllowlist(agentId: self.selectedAgentId, allowlist: self.entries) } + func entry(for id: UUID) -> ExecAllowlistEntry? { + self.entries.first(where: { $0.id == id }) + } + func refreshSkillBins(force: Bool = false) async { guard self.autoAllowSkills else { self.skillBins = [] diff --git a/apps/macos/Sources/Clawdbot/TailscaleService.swift b/apps/macos/Sources/Clawdbot/TailscaleService.swift index 262ac371e..413e8d0c8 100644 --- a/apps/macos/Sources/Clawdbot/TailscaleService.swift +++ b/apps/macos/Sources/Clawdbot/TailscaleService.swift @@ -2,6 +2,9 @@ import AppKit import Foundation import Observation import os +#if canImport(Darwin) +import Darwin +#endif /// Manages Tailscale integration and status checking. @Observable @@ -53,7 +56,7 @@ final class TailscaleService { #endif func checkAppInstallation() -> Bool { - let installed = FileManager.default.fileExists(atPath: "/Applications/Tailscale.app") + let installed = FileManager().fileExists(atPath: "/Applications/Tailscale.app") self.logger.info("Tailscale app installed: \(installed)") return installed } @@ -100,16 +103,14 @@ final class TailscaleService { } func checkTailscaleStatus() async { + let previousIP = self.tailscaleIP self.isInstalled = self.checkAppInstallation() - guard self.isInstalled else { + if !self.isInstalled { self.isRunning = false self.tailscaleHostname = nil self.tailscaleIP = nil self.statusError = "Tailscale is not installed" - return - } - - if let apiResponse = await fetchTailscaleStatus() { + } else if let apiResponse = await fetchTailscaleStatus() { self.isRunning = apiResponse.status.lowercased() == "running" if self.isRunning { @@ -138,6 +139,19 @@ final class TailscaleService { self.statusError = "Please start the Tailscale app" self.logger.info("Tailscale API not responding; app likely not running") } + + if self.tailscaleIP == nil, let fallback = Self.detectTailnetIPv4() { + self.tailscaleIP = fallback + if !self.isRunning { + self.isRunning = true + } + self.statusError = nil + self.logger.info("Tailscale interface IP detected (fallback) ip=\(fallback, privacy: .public)") + } + + if previousIP != self.tailscaleIP { + await GatewayEndpointStore.shared.refresh() + } } func openTailscaleApp() { @@ -163,4 +177,50 @@ final class TailscaleService { NSWorkspace.shared.open(url) } } + + private nonisolated static func isTailnetIPv4(_ address: String) -> Bool { + let parts = address.split(separator: ".") + guard parts.count == 4 else { return false } + let octets = parts.compactMap { Int($0) } + guard octets.count == 4 else { return false } + let a = octets[0] + let b = octets[1] + return a == 100 && b >= 64 && b <= 127 + } + + private nonisolated static func detectTailnetIPv4() -> String? { + var addrList: UnsafeMutablePointer? + guard getifaddrs(&addrList) == 0, let first = addrList else { return nil } + defer { freeifaddrs(addrList) } + + for ptr in sequence(first: first, next: { $0.pointee.ifa_next }) { + let flags = Int32(ptr.pointee.ifa_flags) + let isUp = (flags & IFF_UP) != 0 + let isLoopback = (flags & IFF_LOOPBACK) != 0 + let family = ptr.pointee.ifa_addr.pointee.sa_family + if !isUp || isLoopback || family != UInt8(AF_INET) { continue } + + var addr = ptr.pointee.ifa_addr.pointee + var buffer = [CChar](repeating: 0, count: Int(NI_MAXHOST)) + let result = getnameinfo( + &addr, + socklen_t(ptr.pointee.ifa_addr.pointee.sa_len), + &buffer, + socklen_t(buffer.count), + nil, + 0, + NI_NUMERICHOST) + guard result == 0 else { continue } + let len = buffer.prefix { $0 != 0 } + let bytes = len.map { UInt8(bitPattern: $0) } + guard let ip = String(bytes: bytes, encoding: .utf8) else { continue } + if Self.isTailnetIPv4(ip) { return ip } + } + + return nil + } + + nonisolated static func fallbackTailnetIPv4() -> String? { + self.detectTailnetIPv4() + } } diff --git a/apps/macos/Sources/Clawdbot/TerminationSignalWatcher.swift b/apps/macos/Sources/Clawdbot/TerminationSignalWatcher.swift index e00188521..7994016ef 100644 --- a/apps/macos/Sources/Clawdbot/TerminationSignalWatcher.swift +++ b/apps/macos/Sources/Clawdbot/TerminationSignalWatcher.swift @@ -42,6 +42,7 @@ final class TerminationSignalWatcher { self.logger.info("received signal \(sig, privacy: .public); terminating") // Ensure any pairing prompt can't accidentally approve during shutdown. NodePairingApprovalPrompter.shared.stop() + DevicePairingApprovalPrompter.shared.stop() NSApp.terminate(nil) // Safety net: don't hang forever if something blocks termination. diff --git a/apps/macos/Sources/Clawdbot/UsageCostData.swift b/apps/macos/Sources/Clawdbot/UsageCostData.swift new file mode 100644 index 000000000..ca1fb5cc3 --- /dev/null +++ b/apps/macos/Sources/Clawdbot/UsageCostData.swift @@ -0,0 +1,60 @@ +import Foundation + +struct GatewayCostUsageTotals: Codable { + let input: Int + let output: Int + let cacheRead: Int + let cacheWrite: Int + let totalTokens: Int + let totalCost: Double + let missingCostEntries: Int +} + +struct GatewayCostUsageDay: Codable { + let date: String + let input: Int + let output: Int + let cacheRead: Int + let cacheWrite: Int + let totalTokens: Int + let totalCost: Double + let missingCostEntries: Int +} + +struct GatewayCostUsageSummary: Codable { + let updatedAt: Double + let days: Int + let daily: [GatewayCostUsageDay] + let totals: GatewayCostUsageTotals +} + +enum CostUsageFormatting { + static func formatUsd(_ value: Double?) -> String? { + guard let value, value.isFinite else { return nil } + if value >= 1 { return String(format: "$%.2f", value) } + if value >= 0.01 { return String(format: "$%.2f", value) } + return String(format: "$%.4f", value) + } + + static func formatTokenCount(_ value: Int?) -> String? { + guard let value else { return nil } + let safe = max(0, value) + if safe >= 1_000_000 { return String(format: "%.1fm", Double(safe) / 1_000_000.0) } + if safe >= 1000 { return safe >= 10000 + ? String(format: "%.0fk", Double(safe) / 1000.0) + : String(format: "%.1fk", Double(safe) / 1000.0) + } + return String(safe) + } +} + +@MainActor +enum CostUsageLoader { + static func loadSummary() async throws -> GatewayCostUsageSummary { + let data = try await ControlChannel.shared.request( + method: "usage.cost", + params: nil, + timeoutMs: 7000) + return try JSONDecoder().decode(GatewayCostUsageSummary.self, from: data) + } +} diff --git a/apps/macos/Sources/Clawdbot/UsageData.swift b/apps/macos/Sources/Clawdbot/UsageData.swift index c291519b6..7800054c6 100644 --- a/apps/macos/Sources/Clawdbot/UsageData.swift +++ b/apps/macos/Sources/Clawdbot/UsageData.swift @@ -75,18 +75,6 @@ struct UsageRow: Identifiable { extension GatewayUsageSummary { func primaryRows() -> [UsageRow] { self.providers.compactMap { provider in - if let error = provider.error, provider.windows.isEmpty { - return UsageRow( - id: provider.provider, - providerId: provider.provider, - displayName: provider.displayName, - plan: provider.plan, - windowLabel: nil, - usedPercent: nil, - resetAt: nil, - error: error) - } - guard let window = provider.windows.max(by: { $0.usedPercent < $1.usedPercent }) else { return nil } @@ -99,7 +87,7 @@ extension GatewayUsageSummary { windowLabel: window.label, usedPercent: window.usedPercent, resetAt: window.resetAt.map { Date(timeIntervalSince1970: $0 / 1000) }, - error: provider.error) + error: nil) } } } diff --git a/apps/macos/Sources/Clawdbot/UsageMenuLabelView.swift b/apps/macos/Sources/Clawdbot/UsageMenuLabelView.swift index 5e3a52687..c7f95e476 100644 --- a/apps/macos/Sources/Clawdbot/UsageMenuLabelView.swift +++ b/apps/macos/Sources/Clawdbot/UsageMenuLabelView.swift @@ -37,14 +37,12 @@ struct UsageMenuLabelView: View { Spacer(minLength: 4) - if !self.row.hasError { - Text(self.row.detailText()) - .font(.caption.monospacedDigit()) - .foregroundStyle(self.secondaryTextColor) - .lineLimit(1) - .truncationMode(.tail) - .layoutPriority(2) - } + Text(self.row.detailText()) + .font(.caption.monospacedDigit()) + .foregroundStyle(self.secondaryTextColor) + .lineLimit(1) + .truncationMode(.tail) + .layoutPriority(2) if self.showsChevron { Image(systemName: "chevron.right") @@ -53,16 +51,6 @@ struct UsageMenuLabelView: View { .padding(.leading, 2) } } - - if let error = self.row.error?.nonEmpty { - Text(error) - .font(.caption) - .foregroundStyle(self.secondaryTextColor) - .multilineTextAlignment(.leading) - .lineLimit(2) - .truncationMode(.tail) - .fixedSize(horizontal: false, vertical: true) - } } .padding(.vertical, 10) .padding(.leading, self.paddingLeading) diff --git a/apps/macos/Sources/Clawdbot/VoicePushToTalk.swift b/apps/macos/Sources/Clawdbot/VoicePushToTalk.swift index 5984a8641..2bb1ec1f5 100644 --- a/apps/macos/Sources/Clawdbot/VoicePushToTalk.swift +++ b/apps/macos/Sources/Clawdbot/VoicePushToTalk.swift @@ -37,7 +37,7 @@ final class VoicePushToTalkHotkey: @unchecked Sendable { } private func startMonitoring() { - assert(Thread.isMainThread) + // assert(Thread.isMainThread) - Removed for Swift 6 guard self.globalMonitor == nil, self.localMonitor == nil else { return } // Listen-only global monitor; we rely on Input Monitoring permission to receive events. self.globalMonitor = NSEvent.addGlobalMonitorForEvents(matching: .flagsChanged) { [weak self] event in @@ -55,7 +55,7 @@ final class VoicePushToTalkHotkey: @unchecked Sendable { } private func stopMonitoring() { - assert(Thread.isMainThread) + // assert(Thread.isMainThread) - Removed for Swift 6 if let globalMonitor { NSEvent.removeMonitor(globalMonitor) self.globalMonitor = nil @@ -75,15 +75,11 @@ final class VoicePushToTalkHotkey: @unchecked Sendable { } private func withMainThread(_ block: @escaping @Sendable () -> Void) { - if Thread.isMainThread { - block() - } else { - DispatchQueue.main.async(execute: block) - } + DispatchQueue.main.async(execute: block) } private func updateModifierState(keyCode: UInt16, modifierFlags: NSEvent.ModifierFlags) { - assert(Thread.isMainThread) + // assert(Thread.isMainThread) - Removed for Swift 6 // Right Option (keyCode 61) acts as a hold-to-talk modifier. if keyCode == 61 { self.optionDown = modifierFlags.contains(.option) diff --git a/apps/macos/Sources/Clawdbot/VoiceWakeGlobalSettingsSync.swift b/apps/macos/Sources/Clawdbot/VoiceWakeGlobalSettingsSync.swift index 95ea5e364..9a12486cb 100644 --- a/apps/macos/Sources/Clawdbot/VoiceWakeGlobalSettingsSync.swift +++ b/apps/macos/Sources/Clawdbot/VoiceWakeGlobalSettingsSync.swift @@ -1,3 +1,4 @@ +import ClawdbotKit import Foundation import OSLog diff --git a/apps/macos/Sources/Clawdbot/VoiceWakeHelpers.swift b/apps/macos/Sources/Clawdbot/VoiceWakeHelpers.swift index a60aa7d7c..98cdc0cb5 100644 --- a/apps/macos/Sources/Clawdbot/VoiceWakeHelpers.swift +++ b/apps/macos/Sources/Clawdbot/VoiceWakeHelpers.swift @@ -4,6 +4,8 @@ func sanitizeVoiceWakeTriggers(_ words: [String]) -> [String] { let cleaned = words .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { !$0.isEmpty } + .prefix(voiceWakeMaxWords) + .map { String($0.prefix(voiceWakeMaxWordLength)) } return cleaned.isEmpty ? defaultVoiceWakeTriggers : cleaned } diff --git a/apps/macos/Sources/Clawdbot/VoiceWakeSettings.swift b/apps/macos/Sources/Clawdbot/VoiceWakeSettings.swift index 176980cc5..a41e8bb1f 100644 --- a/apps/macos/Sources/Clawdbot/VoiceWakeSettings.swift +++ b/apps/macos/Sources/Clawdbot/VoiceWakeSettings.swift @@ -21,6 +21,7 @@ struct VoiceWakeSettings: View { @State private var micObserver = AudioInputDeviceObserver() @State private var micRefreshTask: Task? @State private var availableLocales: [Locale] = [] + @State private var triggerEntries: [TriggerEntry] = [] private let fieldLabelWidth: CGFloat = 140 private let controlWidth: CGFloat = 240 private let isPreview = ProcessInfo.processInfo.isPreview @@ -31,9 +32,9 @@ struct VoiceWakeSettings: View { var id: String { self.uid } } - private struct IndexedWord: Identifiable { - let id: Int - let value: String + private struct TriggerEntry: Identifiable { + let id: UUID + var value: String } private var voiceWakeBinding: Binding { @@ -105,6 +106,7 @@ struct VoiceWakeSettings: View { .onAppear { guard !self.isPreview else { return } self.startMicObserver() + self.loadTriggerEntries() } .onChange(of: self.state.voiceWakeMicID) { _, _ in guard !self.isPreview else { return } @@ -122,8 +124,10 @@ struct VoiceWakeSettings: View { self.micRefreshTask = nil Task { await self.meter.stop() } self.micObserver.stop() + self.syncTriggerEntriesToState() } else { self.startMicObserver() + self.loadTriggerEntries() } } .onDisappear { @@ -136,11 +140,16 @@ struct VoiceWakeSettings: View { self.micRefreshTask = nil self.micObserver.stop() Task { await self.meter.stop() } + self.syncTriggerEntriesToState() } } - private var indexedWords: [IndexedWord] { - self.state.swabbleTriggerWords.enumerated().map { IndexedWord(id: $0.offset, value: $0.element) } + private func loadTriggerEntries() { + self.triggerEntries = self.state.swabbleTriggerWords.map { TriggerEntry(id: UUID(), value: $0) } + } + + private func syncTriggerEntriesToState() { + self.state.swabbleTriggerWords = self.triggerEntries.map(\.value) } private var triggerTable: some View { @@ -154,29 +163,42 @@ struct VoiceWakeSettings: View { } label: { Label("Add word", systemImage: "plus") } - .disabled(self.state.swabbleTriggerWords - .contains(where: { $0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty })) + .disabled(self.triggerEntries + .contains(where: { $0.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty })) - Button("Reset defaults") { self.state.swabbleTriggerWords = defaultVoiceWakeTriggers } + Button("Reset defaults") { + self.triggerEntries = defaultVoiceWakeTriggers.map { TriggerEntry(id: UUID(), value: $0) } + self.syncTriggerEntriesToState() + } } - Table(self.indexedWords) { - TableColumn("Word") { row in - TextField("Wake word", text: self.binding(for: row.id)) - .textFieldStyle(.roundedBorder) - } - TableColumn("") { row in - Button { - self.removeWord(at: row.id) - } label: { - Image(systemName: "trash") + VStack(spacing: 0) { + ForEach(self.$triggerEntries) { $entry in + HStack(spacing: 8) { + TextField("Wake word", text: $entry.value) + .textFieldStyle(.roundedBorder) + .onSubmit { + self.syncTriggerEntriesToState() + } + + Button { + self.removeWord(id: entry.id) + } label: { + Image(systemName: "trash") + } + .buttonStyle(.borderless) + .help("Remove trigger word") + .frame(width: 24) + } + .padding(8) + + if entry.id != self.triggerEntries.last?.id { + Divider() } - .buttonStyle(.borderless) - .help("Remove trigger word") } - .width(36) } - .frame(minHeight: 180) + .frame(maxWidth: .infinity, minHeight: 180, alignment: .topLeading) + .background(Color(nsColor: .textBackgroundColor)) .clipShape(RoundedRectangle(cornerRadius: 6)) .overlay( RoundedRectangle(cornerRadius: 6) @@ -211,24 +233,12 @@ struct VoiceWakeSettings: View { } private func addWord() { - self.state.swabbleTriggerWords.append("") + self.triggerEntries.append(TriggerEntry(id: UUID(), value: "")) } - private func removeWord(at index: Int) { - guard self.state.swabbleTriggerWords.indices.contains(index) else { return } - self.state.swabbleTriggerWords.remove(at: index) - } - - private func binding(for index: Int) -> Binding { - Binding( - get: { - guard self.state.swabbleTriggerWords.indices.contains(index) else { return "" } - return self.state.swabbleTriggerWords[index] - }, - set: { newValue in - guard self.state.swabbleTriggerWords.indices.contains(index) else { return } - self.state.swabbleTriggerWords[index] = newValue - }) + private func removeWord(id: UUID) { + self.triggerEntries.removeAll { $0.id == id } + self.syncTriggerEntriesToState() } private func toggleTest() { @@ -638,13 +648,14 @@ extension VoiceWakeSettings { state.voicePushToTalkEnabled = true state.swabbleTriggerWords = ["Claude", "Hey"] - let view = VoiceWakeSettings(state: state, isActive: true) + var view = VoiceWakeSettings(state: state, isActive: true) view.availableMics = [AudioInputDevice(uid: "mic-1", name: "Built-in")] view.availableLocales = [Locale(identifier: "en_US")] view.meterLevel = 0.42 view.meterError = "No input" view.testState = .detected("ok") view.isTesting = true + view.triggerEntries = [TriggerEntry(id: UUID(), value: "Claude")] _ = view.body _ = view.localePicker @@ -654,8 +665,9 @@ extension VoiceWakeSettings { _ = view.chimeSection view.addWord() - _ = view.binding(for: 0).wrappedValue - view.removeWord(at: 0) + if let entryId = view.triggerEntries.first?.id { + view.removeWord(id: entryId) + } } } #endif diff --git a/apps/macos/Sources/Clawdbot/WebChatSwiftUI.swift b/apps/macos/Sources/Clawdbot/WebChatSwiftUI.swift index 0910fb798..665416d31 100644 --- a/apps/macos/Sources/Clawdbot/WebChatSwiftUI.swift +++ b/apps/macos/Sources/Clawdbot/WebChatSwiftUI.swift @@ -1,5 +1,6 @@ import AppKit import ClawdbotChatUI +import ClawdbotKit import ClawdbotProtocol import Foundation import OSLog diff --git a/apps/macos/Sources/ClawdbotDiscovery/GatewayDiscoveryModel.swift b/apps/macos/Sources/ClawdbotDiscovery/GatewayDiscoveryModel.swift index 9fced5a7d..4d953baac 100644 --- a/apps/macos/Sources/ClawdbotDiscovery/GatewayDiscoveryModel.swift +++ b/apps/macos/Sources/ClawdbotDiscovery/GatewayDiscoveryModel.swift @@ -81,11 +81,11 @@ public final class GatewayDiscoveryModel { public func start() { if !self.browsers.isEmpty { return } - for domain in ClawdbotBonjour.bridgeServiceDomains { + for domain in ClawdbotBonjour.gatewayServiceDomains { let params = NWParameters.tcp params.includePeerToPeer = true let browser = NWBrowser( - for: .bonjour(type: ClawdbotBonjour.bridgeServiceType, domain: domain), + for: .bonjour(type: ClawdbotBonjour.gatewayServiceType, domain: domain), using: params) browser.stateUpdateHandler = { [weak self] state in @@ -113,7 +113,7 @@ public final class GatewayDiscoveryModel { } public func refreshWideAreaFallbackNow(timeoutSeconds: TimeInterval = 5.0) { - let domain = ClawdbotBonjour.wideAreaBridgeServiceDomain + let domain = ClawdbotBonjour.wideAreaGatewayServiceDomain Task.detached(priority: .utility) { [weak self] in guard let self else { return } let beacons = WideAreaGatewayDiscovery.discover(timeoutSeconds: timeoutSeconds) @@ -174,7 +174,7 @@ public final class GatewayDiscoveryModel { } // Bonjour can return only "local" results for the wide-area domain (or no results at all), - // which makes onboarding look empty even though Tailscale DNS-SD can already see bridges. + // which makes onboarding look empty even though Tailscale DNS-SD can already see gateways. guard !self.wideAreaFallbackGateways.isEmpty else { self.gateways = primaryFiltered return @@ -194,7 +194,7 @@ public final class GatewayDiscoveryModel { guard case let .service(name, type, resultDomain, _) = result.endpoint else { return nil } let decodedName = BonjourEscapes.decode(name) - let stableID = BridgeEndpointID.stableID(result.endpoint) + let stableID = GatewayEndpointID.stableID(result.endpoint) let resolvedTXT = self.resolvedTXTByID[stableID] ?? [:] let txt = Self.txtDictionary(from: result).merging( resolvedTXT, @@ -230,12 +230,12 @@ public final class GatewayDiscoveryModel { gatewayPort: parsedTXT.gatewayPort, cliPath: parsedTXT.cliPath, stableID: stableID, - debugID: BridgeEndpointID.prettyDescription(result.endpoint), + debugID: GatewayEndpointID.prettyDescription(result.endpoint), isLocal: isLocal) } .sorted { $0.displayName.localizedCaseInsensitiveCompare($1.displayName) == .orderedAscending } - if domain == ClawdbotBonjour.wideAreaBridgeServiceDomain, + if domain == ClawdbotBonjour.wideAreaGatewayServiceDomain, self.hasUsableWideAreaResults { self.wideAreaFallbackGateways = [] @@ -243,7 +243,7 @@ public final class GatewayDiscoveryModel { } private func scheduleWideAreaFallback() { - let domain = ClawdbotBonjour.wideAreaBridgeServiceDomain + let domain = ClawdbotBonjour.wideAreaGatewayServiceDomain if Self.isRunningTests { return } guard self.wideAreaFallbackTask == nil else { return } self.wideAreaFallbackTask = Task.detached(priority: .utility) { [weak self] in @@ -276,7 +276,7 @@ public final class GatewayDiscoveryModel { } private var hasUsableWideAreaResults: Bool { - let domain = ClawdbotBonjour.wideAreaBridgeServiceDomain + let domain = ClawdbotBonjour.wideAreaGatewayServiceDomain guard let gateways = self.gatewaysByDomain[domain], !gateways.isEmpty else { return false } if !self.filterLocalGateways { return true } return gateways.contains(where: { !$0.isLocal }) @@ -462,7 +462,7 @@ public final class GatewayDiscoveryModel { private nonisolated static func prettifyServiceName(_ decodedName: String) -> String { let normalized = Self.prettifyInstanceName(decodedName) - var cleaned = normalized.replacingOccurrences(of: #"\s*-?bridge$"#, with: "", options: .regularExpression) + var cleaned = normalized.replacingOccurrences(of: #"\s*-?gateway$"#, with: "", options: .regularExpression) cleaned = cleaned .replacingOccurrences(of: "_", with: " ") .replacingOccurrences(of: "-", with: " ") @@ -598,11 +598,11 @@ public final class GatewayDiscoveryModel { private nonisolated static func normalizeServiceHostToken(_ raw: String?) -> String? { guard let raw else { return nil } let prettified = Self.prettifyInstanceName(raw) - let strippedBridge = prettified.replacingOccurrences( - of: #"\s*-?\s*bridge$"#, + let strippedGateway = prettified.replacingOccurrences( + of: #"\s*-?\s*gateway$"#, with: "", options: .regularExpression) - return self.normalizeHostToken(strippedBridge) + return self.normalizeHostToken(strippedGateway) } } diff --git a/apps/macos/Sources/ClawdbotDiscovery/WideAreaGatewayDiscovery.swift b/apps/macos/Sources/ClawdbotDiscovery/WideAreaGatewayDiscovery.swift index f70e862a0..64d9353c2 100644 --- a/apps/macos/Sources/ClawdbotDiscovery/WideAreaGatewayDiscovery.swift +++ b/apps/macos/Sources/ClawdbotDiscovery/WideAreaGatewayDiscovery.swift @@ -9,7 +9,6 @@ struct WideAreaGatewayBeacon: Sendable, Equatable { var lanHost: String? var tailnetDns: String? var gatewayPort: Int? - var bridgePort: Int? var sshPort: Int? var cliPath: String? } @@ -51,9 +50,9 @@ enum WideAreaGatewayDiscovery { return [] } - let domain = ClawdbotBonjour.wideAreaBridgeServiceDomain + let domain = ClawdbotBonjour.wideAreaGatewayServiceDomain let domainTrimmed = domain.trimmingCharacters(in: CharacterSet(charactersIn: ".")) - let probeName = "_clawdbot-bridge._tcp.\(domainTrimmed)" + let probeName = "_clawdbot-gw._tcp.\(domainTrimmed)" guard let ptrLines = context.dig( ["+short", "+time=1", "+tries=1", "@\(nameserver)", probeName, "PTR"], min(defaultTimeoutSeconds, remaining()))?.split(whereSeparator: \.isNewline), @@ -67,7 +66,7 @@ enum WideAreaGatewayDiscovery { let ptr = raw.trimmingCharacters(in: .whitespacesAndNewlines) if ptr.isEmpty { continue } let ptrName = ptr.hasSuffix(".") ? String(ptr.dropLast()) : ptr - let suffix = "._clawdbot-bridge._tcp.\(domainTrimmed)" + let suffix = "._clawdbot-gw._tcp.\(domainTrimmed)" let rawInstanceName = ptrName.hasSuffix(suffix) ? String(ptrName.dropLast(suffix.count)) : ptrName @@ -94,7 +93,6 @@ enum WideAreaGatewayDiscovery { lanHost: txt["lanHost"], tailnetDns: txt["tailnetDns"], gatewayPort: parseInt(txt["gatewayPort"]), - bridgePort: parseInt(txt["bridgePort"]), sshPort: parseInt(txt["sshPort"]), cliPath: txt["cliPath"]) beacons.append(beacon) @@ -156,9 +154,9 @@ enum WideAreaGatewayDiscovery { remaining: () -> TimeInterval, dig: @escaping @Sendable (_ args: [String], _ timeout: TimeInterval) -> String?) -> String? { - let domain = ClawdbotBonjour.wideAreaBridgeServiceDomain + let domain = ClawdbotBonjour.wideAreaGatewayServiceDomain let domainTrimmed = domain.trimmingCharacters(in: CharacterSet(charactersIn: ".")) - let probeName = "_clawdbot-bridge._tcp.\(domainTrimmed)" + let probeName = "_clawdbot-gw._tcp.\(domainTrimmed)" let ips = candidates candidates.removeAll(keepingCapacity: true) diff --git a/apps/macos/Sources/ClawdbotDiscoveryCLI/main.swift b/apps/macos/Sources/ClawdbotDiscoveryCLI/main.swift deleted file mode 100644 index d5fc5789c..000000000 --- a/apps/macos/Sources/ClawdbotDiscoveryCLI/main.swift +++ /dev/null @@ -1,150 +0,0 @@ -import ClawdbotDiscovery -import Foundation - -struct DiscoveryOptions { - var timeoutMs: Int = 2000 - var json: Bool = false - var includeLocal: Bool = false - var help: Bool = false - - static func parse(_ args: [String]) -> DiscoveryOptions { - var opts = DiscoveryOptions() - var i = 0 - while i < args.count { - let arg = args[i] - switch arg { - case "-h", "--help": - opts.help = true - case "--json": - opts.json = true - case "--include-local": - opts.includeLocal = true - case "--timeout": - let next = (i + 1 < args.count) ? args[i + 1] : nil - if let next, let parsed = Int(next.trimmingCharacters(in: .whitespacesAndNewlines)) { - opts.timeoutMs = max(100, parsed) - i += 1 - } - default: - break - } - i += 1 - } - return opts - } -} - -struct DiscoveryOutput: Encodable { - struct Gateway: Encodable { - var displayName: String - var lanHost: String? - var tailnetDns: String? - var sshPort: Int - var gatewayPort: Int? - var cliPath: String? - var stableID: String - var debugID: String - var isLocal: Bool - } - - var status: String - var timeoutMs: Int - var includeLocal: Bool - var count: Int - var gateways: [Gateway] -} - -@main -struct ClawdbotDiscoveryCLI { - static func main() async { - let opts = DiscoveryOptions.parse(Array(CommandLine.arguments.dropFirst())) - if opts.help { - print(""" - clawdbot-mac-discovery - - Usage: - clawdbot-mac-discovery [--timeout ] [--json] [--include-local] - - Options: - --timeout Discovery window in milliseconds (default: 2000) - --json Emit JSON - --include-local Include gateways considered local - -h, --help Show help - """) - return - } - - let displayName = Host.current().localizedName ?? ProcessInfo.processInfo.hostName - let model = GatewayDiscoveryModel( - localDisplayName: displayName, - filterLocalGateways: !opts.includeLocal) - - await MainActor.run { - model.start() - } - - let nanos = UInt64(max(100, opts.timeoutMs)) * 1_000_000 - try? await Task.sleep(nanoseconds: nanos) - - let gateways = await MainActor.run { model.gateways } - let status = await MainActor.run { model.statusText } - - await MainActor.run { - model.stop() - } - - if opts.json { - let payload = DiscoveryOutput( - status: status, - timeoutMs: opts.timeoutMs, - includeLocal: opts.includeLocal, - count: gateways.count, - gateways: gateways.map { - DiscoveryOutput.Gateway( - displayName: $0.displayName, - lanHost: $0.lanHost, - tailnetDns: $0.tailnetDns, - sshPort: $0.sshPort, - gatewayPort: $0.gatewayPort, - cliPath: $0.cliPath, - stableID: $0.stableID, - debugID: $0.debugID, - isLocal: $0.isLocal) - }) - let encoder = JSONEncoder() - encoder.outputFormatting = [.prettyPrinted, .sortedKeys] - if let data = try? encoder.encode(payload), - let json = String(data: data, encoding: .utf8) - { - print(json) - } else { - print("{\"error\":\"failed to encode JSON\"}") - } - return - } - - print("Gateway Discovery (macOS NWBrowser)") - print("Status: \(status)") - print("Found \(gateways.count) gateway(s)\(opts.includeLocal ? "" : " (local filtered)")") - if gateways.isEmpty { return } - - for gateway in gateways { - let hosts = [gateway.tailnetDns, gateway.lanHost] - .compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) } - .filter { !$0.isEmpty } - .joined(separator: ", ") - print("- \(gateway.displayName)") - print(" hosts: \(hosts.isEmpty ? "(none)" : hosts)") - print(" ssh: \(gateway.sshPort)") - if let port = gateway.gatewayPort { - print(" gatewayPort: \(port)") - } - if let cliPath = gateway.cliPath { - print(" cliPath: \(cliPath)") - } - print(" isLocal: \(gateway.isLocal)") - print(" stableID: \(gateway.stableID)") - print(" debugID: \(gateway.debugID)") - } - } -} diff --git a/apps/macos/Sources/ClawdbotIPC/IPC.swift b/apps/macos/Sources/ClawdbotIPC/IPC.swift index dc066a10d..058cb0508 100644 --- a/apps/macos/Sources/ClawdbotIPC/IPC.swift +++ b/apps/macos/Sources/ClawdbotIPC/IPC.swift @@ -408,8 +408,7 @@ extension Request: Codable { } // Shared transport settings -public let controlSocketPath = - FileManager.default - .homeDirectoryForCurrentUser - .appendingPathComponent("Library/Application Support/clawdbot/control.sock") - .path +public let controlSocketPath = FileManager() + .homeDirectoryForCurrentUser + .appendingPathComponent("Library/Application Support/clawdbot/control.sock") + .path diff --git a/apps/macos/Sources/ClawdbotMacCLI/ConnectCommand.swift b/apps/macos/Sources/ClawdbotMacCLI/ConnectCommand.swift new file mode 100644 index 000000000..4df1c96a1 --- /dev/null +++ b/apps/macos/Sources/ClawdbotMacCLI/ConnectCommand.swift @@ -0,0 +1,359 @@ +import ClawdbotKit +import ClawdbotProtocol +import Foundation +#if canImport(Darwin) +import Darwin +#endif + +struct ConnectOptions { + var url: String? + var token: String? + var password: String? + var mode: String? + var timeoutMs: Int = 15000 + var json: Bool = false + var probe: Bool = false + var clientId: String = "clawdbot-macos" + var clientMode: String = "ui" + var displayName: String? + var role: String = "operator" + var scopes: [String] = ["operator.admin", "operator.approvals", "operator.pairing"] + var help: Bool = false + + static func parse(_ args: [String]) -> ConnectOptions { + var opts = ConnectOptions() + let flagHandlers: [String: (inout ConnectOptions) -> Void] = [ + "-h": { $0.help = true }, + "--help": { $0.help = true }, + "--json": { $0.json = true }, + "--probe": { $0.probe = true }, + ] + let valueHandlers: [String: (inout ConnectOptions, String) -> Void] = [ + "--url": { $0.url = $1 }, + "--token": { $0.token = $1 }, + "--password": { $0.password = $1 }, + "--mode": { $0.mode = $1 }, + "--timeout": { opts, raw in + if let parsed = Int(raw.trimmingCharacters(in: .whitespacesAndNewlines)) { + opts.timeoutMs = max(250, parsed) + } + }, + "--client-id": { $0.clientId = $1 }, + "--client-mode": { $0.clientMode = $1 }, + "--display-name": { $0.displayName = $1 }, + "--role": { $0.role = $1 }, + "--scopes": { opts, raw in + opts.scopes = raw.split(separator: ",").map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + }, + ] + var i = 0 + while i < args.count { + let arg = args[i] + if let handler = flagHandlers[arg] { + handler(&opts) + i += 1 + continue + } + if let handler = valueHandlers[arg], let value = self.nextValue(args, index: &i) { + handler(&opts, value) + i += 1 + continue + } + i += 1 + } + return opts + } + + private static func nextValue(_ args: [String], index: inout Int) -> String? { + guard index + 1 < args.count else { return nil } + index += 1 + return args[index].trimmingCharacters(in: .whitespacesAndNewlines) + } +} + +struct ConnectOutput: Encodable { + var status: String + var url: String + var mode: String + var role: String + var clientId: String + var clientMode: String + var scopes: [String] + var snapshot: HelloOk? + var health: ProtoAnyCodable? + var error: String? +} + +actor SnapshotStore { + private var value: HelloOk? + + func set(_ snapshot: HelloOk) { + self.value = snapshot + } + + func get() -> HelloOk? { + self.value + } +} + +func runConnect(_ args: [String]) async { + let opts = ConnectOptions.parse(args) + if opts.help { + print(""" + clawdbot-mac connect + + Usage: + clawdbot-mac connect [--url ] [--token ] [--password ] + [--mode ] [--timeout ] [--probe] [--json] + [--client-id ] [--client-mode ] [--display-name ] + [--role ] [--scopes ] + + Options: + --url Gateway WebSocket URL (overrides config) + --token Gateway token (if required) + --password Gateway password (if required) + --mode Resolve from config: local|remote (default: config or local) + --timeout Request timeout (default: 15000) + --probe Force a fresh health probe + --json Emit JSON + --client-id Override client id (default: clawdbot-macos) + --client-mode Override client mode (default: ui) + --display-name Override display name + --role Override role (default: operator) + --scopes Override scopes list + -h, --help Show help + """) + return + } + + let config = loadGatewayConfig() + do { + let endpoint = try resolveGatewayEndpoint(opts: opts, config: config) + let displayName = opts.displayName ?? Host.current().localizedName ?? "Clawdbot macOS Debug CLI" + let connectOptions = GatewayConnectOptions( + role: opts.role, + scopes: opts.scopes, + caps: [], + commands: [], + permissions: [:], + clientId: opts.clientId, + clientMode: opts.clientMode, + clientDisplayName: displayName) + + let snapshotStore = SnapshotStore() + let channel = GatewayChannelActor( + url: endpoint.url, + token: endpoint.token, + password: endpoint.password, + pushHandler: { push in + if case let .snapshot(ok) = push { + await snapshotStore.set(ok) + } + }, + connectOptions: connectOptions) + + let params: [String: KitAnyCodable]? = opts.probe ? ["probe": KitAnyCodable(true)] : nil + let data = try await channel.request( + method: "health", + params: params, + timeoutMs: Double(opts.timeoutMs)) + let health = try? JSONDecoder().decode(ProtoAnyCodable.self, from: data) + let snapshot = await snapshotStore.get() + await channel.shutdown() + + let output = ConnectOutput( + status: "ok", + url: endpoint.url.absoluteString, + mode: endpoint.mode, + role: opts.role, + clientId: opts.clientId, + clientMode: opts.clientMode, + scopes: opts.scopes, + snapshot: snapshot, + health: health, + error: nil) + printConnectOutput(output, json: opts.json) + } catch { + let endpoint = bestEffortEndpoint(opts: opts, config: config) + let fallbackMode = (opts.mode ?? config.mode ?? "local").lowercased() + let output = ConnectOutput( + status: "error", + url: endpoint?.url.absoluteString ?? "unknown", + mode: endpoint?.mode ?? fallbackMode, + role: opts.role, + clientId: opts.clientId, + clientMode: opts.clientMode, + scopes: opts.scopes, + snapshot: nil, + health: nil, + error: error.localizedDescription) + printConnectOutput(output, json: opts.json) + exit(1) + } +} + +private func printConnectOutput(_ output: ConnectOutput, json: Bool) { + if json { + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + if let data = try? encoder.encode(output), + let text = String(data: data, encoding: .utf8) + { + print(text) + } else { + print("{\"error\":\"failed to encode JSON\"}") + } + return + } + + print("Clawdbot macOS Gateway Connect") + print("Status: \(output.status)") + print("URL: \(output.url)") + print("Mode: \(output.mode)") + print("Client: \(output.clientId) (\(output.clientMode))") + print("Role: \(output.role)") + print("Scopes: \(output.scopes.joined(separator: ", "))") + if let snapshot = output.snapshot { + print("Protocol: \(snapshot._protocol)") + if let version = snapshot.server["version"]?.value as? String { + print("Server: \(version)") + } + } + if let health = output.health, + let ok = (health.value as? [String: ProtoAnyCodable])?["ok"]?.value as? Bool + { + print("Health: \(ok ? "ok" : "error")") + } else if output.health != nil { + print("Health: received") + } + if let error = output.error { + print("Error: \(error)") + } +} + +private func resolveGatewayEndpoint(opts: ConnectOptions, config: GatewayConfig) throws -> GatewayEndpoint { + let resolvedMode = (opts.mode ?? config.mode ?? "local").lowercased() + if let raw = opts.url, !raw.isEmpty { + guard let url = URL(string: raw) else { + throw NSError(domain: "Gateway", code: 1, userInfo: [NSLocalizedDescriptionKey: "invalid url: \(raw)"]) + } + return GatewayEndpoint( + url: url, + token: resolvedToken(opts: opts, mode: resolvedMode, config: config), + password: resolvedPassword(opts: opts, mode: resolvedMode, config: config), + mode: resolvedMode) + } + + if resolvedMode == "remote" { + guard let raw = config.remoteUrl?.trimmingCharacters(in: .whitespacesAndNewlines), + !raw.isEmpty + else { + throw NSError( + domain: "Gateway", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "gateway.remote.url is missing"]) + } + guard let url = URL(string: raw) else { + throw NSError(domain: "Gateway", code: 1, userInfo: [NSLocalizedDescriptionKey: "invalid url: \(raw)"]) + } + return GatewayEndpoint( + url: url, + token: resolvedToken(opts: opts, mode: resolvedMode, config: config), + password: resolvedPassword(opts: opts, mode: resolvedMode, config: config), + mode: resolvedMode) + } + + let port = config.port ?? 18789 + let host = resolveLocalHost(bind: config.bind) + guard let url = URL(string: "ws://\(host):\(port)") else { + throw NSError( + domain: "Gateway", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "invalid url: ws://\(host):\(port)"]) + } + return GatewayEndpoint( + url: url, + token: resolvedToken(opts: opts, mode: resolvedMode, config: config), + password: resolvedPassword(opts: opts, mode: resolvedMode, config: config), + mode: resolvedMode) +} + +private func bestEffortEndpoint(opts: ConnectOptions, config: GatewayConfig) -> GatewayEndpoint? { + try? resolveGatewayEndpoint(opts: opts, config: config) +} + +private func resolvedToken(opts: ConnectOptions, mode: String, config: GatewayConfig) -> String? { + if let token = opts.token, !token.isEmpty { return token } + if let token = ProcessInfo.processInfo.environment["CLAWDBOT_GATEWAY_TOKEN"], !token.isEmpty { + return token + } + if mode == "remote" { + return config.remoteToken + } + return config.token +} + +private func resolvedPassword(opts: ConnectOptions, mode: String, config: GatewayConfig) -> String? { + if let password = opts.password, !password.isEmpty { return password } + if let password = ProcessInfo.processInfo.environment["CLAWDBOT_GATEWAY_PASSWORD"], !password.isEmpty { + return password + } + if mode == "remote" { + return config.remotePassword + } + return config.password +} + +private func resolveLocalHost(bind: String?) -> String { + let normalized = (bind ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + let tailnetIP = detectTailnetIPv4() + switch normalized { + case "tailnet": + return tailnetIP ?? "127.0.0.1" + default: + return "127.0.0.1" + } +} + +private func detectTailnetIPv4() -> String? { + var addrList: UnsafeMutablePointer? + guard getifaddrs(&addrList) == 0, let first = addrList else { return nil } + defer { freeifaddrs(addrList) } + + for ptr in sequence(first: first, next: { $0.pointee.ifa_next }) { + let flags = Int32(ptr.pointee.ifa_flags) + let isUp = (flags & IFF_UP) != 0 + let isLoopback = (flags & IFF_LOOPBACK) != 0 + let family = ptr.pointee.ifa_addr.pointee.sa_family + if !isUp || isLoopback || family != UInt8(AF_INET) { continue } + + var addr = ptr.pointee.ifa_addr.pointee + var buffer = [CChar](repeating: 0, count: Int(NI_MAXHOST)) + let result = getnameinfo( + &addr, + socklen_t(ptr.pointee.ifa_addr.pointee.sa_len), + &buffer, + socklen_t(buffer.count), + nil, + 0, + NI_NUMERICHOST) + guard result == 0 else { continue } + let len = buffer.prefix { $0 != 0 } + let bytes = len.map { UInt8(bitPattern: $0) } + guard let ip = String(bytes: bytes, encoding: .utf8) else { continue } + if isTailnetIPv4(ip) { return ip } + } + + return nil +} + +private func isTailnetIPv4(_ address: String) -> Bool { + let parts = address.split(separator: ".") + guard parts.count == 4 else { return false } + let octets = parts.compactMap { Int($0) } + guard octets.count == 4 else { return false } + let a = octets[0] + let b = octets[1] + return a == 100 && b >= 64 && b <= 127 +} diff --git a/apps/macos/Sources/ClawdbotMacCLI/DiscoverCommand.swift b/apps/macos/Sources/ClawdbotMacCLI/DiscoverCommand.swift new file mode 100644 index 000000000..12fe1ea37 --- /dev/null +++ b/apps/macos/Sources/ClawdbotMacCLI/DiscoverCommand.swift @@ -0,0 +1,149 @@ +import ClawdbotDiscovery +import Foundation + +struct DiscoveryOptions { + var timeoutMs: Int = 2000 + var json: Bool = false + var includeLocal: Bool = false + var help: Bool = false + + static func parse(_ args: [String]) -> DiscoveryOptions { + var opts = DiscoveryOptions() + var i = 0 + while i < args.count { + let arg = args[i] + switch arg { + case "-h", "--help": + opts.help = true + case "--json": + opts.json = true + case "--include-local": + opts.includeLocal = true + case "--timeout": + let next = (i + 1 < args.count) ? args[i + 1] : nil + if let next, let parsed = Int(next.trimmingCharacters(in: .whitespacesAndNewlines)) { + opts.timeoutMs = max(100, parsed) + i += 1 + } + default: + break + } + i += 1 + } + return opts + } +} + +struct DiscoveryOutput: Encodable { + struct Gateway: Encodable { + var displayName: String + var lanHost: String? + var tailnetDns: String? + var sshPort: Int + var gatewayPort: Int? + var cliPath: String? + var stableID: String + var debugID: String + var isLocal: Bool + } + + var status: String + var timeoutMs: Int + var includeLocal: Bool + var count: Int + var gateways: [Gateway] +} + +func runDiscover(_ args: [String]) async { + let opts = DiscoveryOptions.parse(args) + if opts.help { + print(""" + clawdbot-mac discover + + Usage: + clawdbot-mac discover [--timeout ] [--json] [--include-local] + + Options: + --timeout Discovery window in milliseconds (default: 2000) + --json Emit JSON + --include-local Include gateways considered local + -h, --help Show help + """) + return + } + + let displayName = Host.current().localizedName ?? ProcessInfo.processInfo.hostName + let model = await MainActor.run { + GatewayDiscoveryModel( + localDisplayName: displayName, + filterLocalGateways: !opts.includeLocal) + } + + await MainActor.run { + model.start() + } + + let nanos = UInt64(max(100, opts.timeoutMs)) * 1_000_000 + try? await Task.sleep(nanoseconds: nanos) + + let gateways = await MainActor.run { model.gateways } + let status = await MainActor.run { model.statusText } + + await MainActor.run { + model.stop() + } + + if opts.json { + let payload = DiscoveryOutput( + status: status, + timeoutMs: opts.timeoutMs, + includeLocal: opts.includeLocal, + count: gateways.count, + gateways: gateways.map { + DiscoveryOutput.Gateway( + displayName: $0.displayName, + lanHost: $0.lanHost, + tailnetDns: $0.tailnetDns, + sshPort: $0.sshPort, + gatewayPort: $0.gatewayPort, + cliPath: $0.cliPath, + stableID: $0.stableID, + debugID: $0.debugID, + isLocal: $0.isLocal) + }) + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + if let data = try? encoder.encode(payload), + let json = String(data: data, encoding: .utf8) + { + print(json) + } else { + print("{\"error\":\"failed to encode JSON\"}") + } + return + } + + print("Gateway Discovery (macOS NWBrowser)") + print("Status: \(status)") + print("Found \(gateways.count) gateway(s)\(opts.includeLocal ? "" : " (local filtered)")") + if gateways.isEmpty { return } + + for gateway in gateways { + let hosts = [gateway.tailnetDns, gateway.lanHost] + .compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + .joined(separator: ", ") + print("- \(gateway.displayName)") + print(" hosts: \(hosts.isEmpty ? "(none)" : hosts)") + print(" ssh: \(gateway.sshPort)") + if let port = gateway.gatewayPort { + print(" gatewayPort: \(port)") + } + if let cliPath = gateway.cliPath { + print(" cliPath: \(cliPath)") + } + print(" isLocal: \(gateway.isLocal)") + print(" stableID: \(gateway.stableID)") + print(" debugID: \(gateway.debugID)") + } +} diff --git a/apps/macos/Sources/ClawdbotMacCLI/EntryPoint.swift b/apps/macos/Sources/ClawdbotMacCLI/EntryPoint.swift new file mode 100644 index 000000000..58f4501ae --- /dev/null +++ b/apps/macos/Sources/ClawdbotMacCLI/EntryPoint.swift @@ -0,0 +1,56 @@ +import Foundation + +private struct RootCommand { + var name: String + var args: [String] +} + +@main +struct ClawdbotMacCLI { + static func main() async { + let args = Array(CommandLine.arguments.dropFirst()) + let command = parseRootCommand(args) + switch command?.name { + case nil: + printUsage() + case "-h", "--help", "help": + printUsage() + case "connect": + await runConnect(command?.args ?? []) + case "discover": + await runDiscover(command?.args ?? []) + case "wizard": + await runWizardCommand(command?.args ?? []) + default: + fputs("clawdbot-mac: unknown command\n", stderr) + printUsage() + exit(1) + } + } +} + +private func parseRootCommand(_ args: [String]) -> RootCommand? { + guard let first = args.first else { return nil } + return RootCommand(name: first, args: Array(args.dropFirst())) +} + +private func printUsage() { + print(""" + clawdbot-mac + + Usage: + clawdbot-mac connect [--url ] [--token ] [--password ] + [--mode ] [--timeout ] [--probe] [--json] + [--client-id ] [--client-mode ] [--display-name ] + [--role ] [--scopes ] + clawdbot-mac discover [--timeout ] [--json] [--include-local] + clawdbot-mac wizard [--url ] [--token ] [--password ] + [--mode ] [--workspace ] [--json] + + Examples: + clawdbot-mac connect + clawdbot-mac connect --url ws://127.0.0.1:18789 --json + clawdbot-mac discover --timeout 3000 --json + clawdbot-mac wizard --mode local + """) +} diff --git a/apps/macos/Sources/ClawdbotMacCLI/GatewayConfig.swift b/apps/macos/Sources/ClawdbotMacCLI/GatewayConfig.swift new file mode 100644 index 000000000..d39572396 --- /dev/null +++ b/apps/macos/Sources/ClawdbotMacCLI/GatewayConfig.swift @@ -0,0 +1,60 @@ +import Foundation + +struct GatewayConfig { + var mode: String? + var bind: String? + var port: Int? + var remoteUrl: String? + var token: String? + var password: String? + var remoteToken: String? + var remotePassword: String? +} + +struct GatewayEndpoint { + let url: URL + let token: String? + let password: String? + let mode: String +} + +func loadGatewayConfig() -> GatewayConfig { + let url = FileManager().homeDirectoryForCurrentUser + .appendingPathComponent(".clawdbot") + .appendingPathComponent("clawdbot.json") + guard let data = try? Data(contentsOf: url) else { return GatewayConfig() } + guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { + return GatewayConfig() + } + + var cfg = GatewayConfig() + if let gateway = json["gateway"] as? [String: Any] { + cfg.mode = gateway["mode"] as? String + cfg.bind = gateway["bind"] as? String + cfg.port = gateway["port"] as? Int ?? parseInt(gateway["port"]) + + if let auth = gateway["auth"] as? [String: Any] { + cfg.token = auth["token"] as? String + cfg.password = auth["password"] as? String + } + if let remote = gateway["remote"] as? [String: Any] { + cfg.remoteUrl = remote["url"] as? String + cfg.remoteToken = remote["token"] as? String + cfg.remotePassword = remote["password"] as? String + } + } + return cfg +} + +func parseInt(_ value: Any?) -> Int? { + switch value { + case let number as Int: + number + case let number as Double: + Int(number) + case let raw as String: + Int(raw.trimmingCharacters(in: .whitespacesAndNewlines)) + default: + nil + } +} diff --git a/apps/macos/Sources/ClawdbotMacCLI/TypeAliases.swift b/apps/macos/Sources/ClawdbotMacCLI/TypeAliases.swift new file mode 100644 index 000000000..191b8976b --- /dev/null +++ b/apps/macos/Sources/ClawdbotMacCLI/TypeAliases.swift @@ -0,0 +1,5 @@ +import ClawdbotKit +import ClawdbotProtocol + +typealias ProtoAnyCodable = ClawdbotProtocol.AnyCodable +typealias KitAnyCodable = ClawdbotKit.AnyCodable diff --git a/apps/macos/Sources/ClawdbotWizardCLI/main.swift b/apps/macos/Sources/ClawdbotMacCLI/WizardCommand.swift similarity index 67% rename from apps/macos/Sources/ClawdbotWizardCLI/main.swift rename to apps/macos/Sources/ClawdbotMacCLI/WizardCommand.swift index bf328b87c..6b7e342e5 100644 --- a/apps/macos/Sources/ClawdbotWizardCLI/main.swift +++ b/apps/macos/Sources/ClawdbotMacCLI/WizardCommand.swift @@ -1,3 +1,4 @@ +import ClawdbotKit import ClawdbotProtocol import Darwin import Foundation @@ -48,17 +49,6 @@ struct WizardCliOptions { } } -struct GatewayConfig { - var mode: String? - var bind: String? - var port: Int? - var remoteUrl: String? - var token: String? - var password: String? - var remoteToken: String? - var remotePassword: String? -} - enum WizardCliError: Error, CustomStringConvertible { case invalidUrl(String) case missingRemoteUrl @@ -77,68 +67,56 @@ enum WizardCliError: Error, CustomStringConvertible { } } -@main -struct ClawdbotWizardCLI { - static func main() async { - let opts = WizardCliOptions.parse(Array(CommandLine.arguments.dropFirst())) - if opts.help { - printUsage() - return - } +func runWizardCommand(_ args: [String]) async { + let opts = WizardCliOptions.parse(args) + if opts.help { + print(""" + clawdbot-mac wizard - let config = loadGatewayConfig() - do { - guard isatty(STDIN_FILENO) != 0 else { - throw WizardCliError.gatewayError("Wizard requires an interactive TTY.") - } - let endpoint = try resolveGatewayEndpoint(opts: opts, config: config) - let client = GatewayWizardClient( - url: endpoint.url, - token: endpoint.token, - password: endpoint.password, - json: opts.json) - try await client.connect() - defer { Task { await client.close() } } - try await runWizard(client: client, opts: opts) - } catch { - fputs("wizard: \(error)\n", stderr) - exit(1) + Usage: + clawdbot-mac wizard [--url ] [--token ] [--password ] + [--mode ] [--workspace ] [--json] + + Options: + --url Gateway WebSocket URL (overrides config) + --token Gateway token (if required) + --password Gateway password (if required) + --mode Wizard mode (local|remote). Default: local + --workspace Wizard workspace override + --json Print raw wizard responses + -h, --help Show help + """) + return + } + + let config = loadGatewayConfig() + do { + guard isatty(STDIN_FILENO) != 0 else { + throw WizardCliError.gatewayError("Wizard requires an interactive TTY.") } + let endpoint = try resolveWizardGatewayEndpoint(opts: opts, config: config) + let client = GatewayWizardClient( + url: endpoint.url, + token: endpoint.token, + password: endpoint.password, + json: opts.json) + try await client.connect() + defer { Task { await client.close() } } + try await runWizard(client: client, opts: opts) + } catch { + fputs("wizard: \(error)\n", stderr) + exit(1) } } -private struct GatewayEndpoint { - let url: URL - let token: String? - let password: String? -} - -private func printUsage() { - print(""" - clawdbot-mac-wizard - - Usage: - clawdbot-mac-wizard [--url ] [--token ] [--password ] - [--mode ] [--workspace ] [--json] - - Options: - --url Gateway WebSocket URL (overrides config) - --token Gateway token (if required) - --password Gateway password (if required) - --mode Wizard mode (local|remote). Default: local - --workspace Wizard workspace override - --json Print raw wizard responses - -h, --help Show help - """) -} - -private func resolveGatewayEndpoint(opts: WizardCliOptions, config: GatewayConfig) throws -> GatewayEndpoint { +private func resolveWizardGatewayEndpoint(opts: WizardCliOptions, config: GatewayConfig) throws -> GatewayEndpoint { if let raw = opts.url, !raw.isEmpty { guard let url = URL(string: raw) else { throw WizardCliError.invalidUrl(raw) } return GatewayEndpoint( url: url, token: resolvedToken(opts: opts, config: config), - password: resolvedPassword(opts: opts, config: config)) + password: resolvedPassword(opts: opts, config: config), + mode: (config.mode ?? "local").lowercased()) } let mode = (config.mode ?? "local").lowercased() @@ -150,7 +128,8 @@ private func resolveGatewayEndpoint(opts: WizardCliOptions, config: GatewayConfi return GatewayEndpoint( url: url, token: resolvedToken(opts: opts, config: config), - password: resolvedPassword(opts: opts, config: config)) + password: resolvedPassword(opts: opts, config: config), + mode: mode) } let port = config.port ?? 18789 @@ -161,7 +140,8 @@ private func resolveGatewayEndpoint(opts: WizardCliOptions, config: GatewayConfi return GatewayEndpoint( url: url, token: resolvedToken(opts: opts, config: config), - password: resolvedPassword(opts: opts, config: config)) + password: resolvedPassword(opts: opts, config: config), + mode: mode) } private func resolvedToken(opts: WizardCliOptions, config: GatewayConfig) -> String? { @@ -186,48 +166,11 @@ private func resolvedPassword(opts: WizardCliOptions, config: GatewayConfig) -> return config.password } -private func loadGatewayConfig() -> GatewayConfig { - let url = FileManager.default.homeDirectoryForCurrentUser - .appendingPathComponent(".clawdbot") - .appendingPathComponent("clawdbot.json") - guard let data = try? Data(contentsOf: url) else { return GatewayConfig() } - guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { - return GatewayConfig() - } - - var cfg = GatewayConfig() - if let gateway = json["gateway"] as? [String: Any] { - cfg.mode = gateway["mode"] as? String - cfg.bind = gateway["bind"] as? String - cfg.port = gateway["port"] as? Int ?? parseInt(gateway["port"]) - - if let auth = gateway["auth"] as? [String: Any] { - cfg.token = auth["token"] as? String - cfg.password = auth["password"] as? String - } - if let remote = gateway["remote"] as? [String: Any] { - cfg.remoteUrl = remote["url"] as? String - cfg.remoteToken = remote["token"] as? String - cfg.remotePassword = remote["password"] as? String - } - } - return cfg -} - -private func parseInt(_ value: Any?) -> Int? { - switch value { - case let number as Int: - number - case let number as Double: - Int(number) - case let raw as String: - Int(raw.trimmingCharacters(in: .whitespacesAndNewlines)) - default: - nil - } -} - actor GatewayWizardClient { + private enum ConnectChallengeError: Error { + case timeout + } + private let url: URL private let token: String? private let password: String? @@ -235,6 +178,7 @@ actor GatewayWizardClient { private let encoder = JSONEncoder() private let decoder = JSONDecoder() private let session = URLSession(configuration: .default) + private let connectChallengeTimeoutSeconds: Double = 0.75 private var task: URLSessionWebSocketTask? init(url: URL, token: String?, password: String?, json: Bool) { @@ -257,7 +201,7 @@ actor GatewayWizardClient { self.task = nil } - func request(method: String, params: [String: AnyCodable]?) async throws -> ResponseFrame { + func request(method: String, params: [String: ProtoAnyCodable]?) async throws -> ResponseFrame { guard let task = self.task else { throw WizardCliError.gatewayError("gateway not connected") } @@ -266,7 +210,7 @@ actor GatewayWizardClient { type: "req", id: id, method: method, - params: params.map { AnyCodable($0) }) + params: params.map { ProtoAnyCodable($0) }) let data = try self.encoder.encode(frame) try await task.send(.data(data)) @@ -309,28 +253,66 @@ actor GatewayWizardClient { } let osVersion = ProcessInfo.processInfo.operatingSystemVersion let platform = "macos \(osVersion.majorVersion).\(osVersion.minorVersion).\(osVersion.patchVersion)" - let client: [String: AnyCodable] = [ - "id": AnyCodable("clawdbot-macos"), - "displayName": AnyCodable(Host.current().localizedName ?? "Clawdbot macOS Wizard CLI"), - "version": AnyCodable("dev"), - "platform": AnyCodable(platform), - "deviceFamily": AnyCodable("Mac"), - "mode": AnyCodable("ui"), - "instanceId": AnyCodable(UUID().uuidString), + let clientId = "clawdbot-macos" + let clientMode = "ui" + let role = "operator" + let scopes: [String] = [] + let client: [String: ProtoAnyCodable] = [ + "id": ProtoAnyCodable(clientId), + "displayName": ProtoAnyCodable(Host.current().localizedName ?? "Clawdbot macOS Wizard CLI"), + "version": ProtoAnyCodable("dev"), + "platform": ProtoAnyCodable(platform), + "deviceFamily": ProtoAnyCodable("Mac"), + "mode": ProtoAnyCodable(clientMode), + "instanceId": ProtoAnyCodable(UUID().uuidString), ] - var params: [String: AnyCodable] = [ - "minProtocol": AnyCodable(GATEWAY_PROTOCOL_VERSION), - "maxProtocol": AnyCodable(GATEWAY_PROTOCOL_VERSION), - "client": AnyCodable(client), - "caps": AnyCodable([String]()), - "locale": AnyCodable(Locale.preferredLanguages.first ?? Locale.current.identifier), - "userAgent": AnyCodable(ProcessInfo.processInfo.operatingSystemVersionString), + var params: [String: ProtoAnyCodable] = [ + "minProtocol": ProtoAnyCodable(GATEWAY_PROTOCOL_VERSION), + "maxProtocol": ProtoAnyCodable(GATEWAY_PROTOCOL_VERSION), + "client": ProtoAnyCodable(client), + "caps": ProtoAnyCodable([String]()), + "locale": ProtoAnyCodable(Locale.preferredLanguages.first ?? Locale.current.identifier), + "userAgent": ProtoAnyCodable(ProcessInfo.processInfo.operatingSystemVersionString), + "role": ProtoAnyCodable(role), + "scopes": ProtoAnyCodable(scopes), ] if let token = self.token { - params["auth"] = AnyCodable(["token": AnyCodable(token)]) + params["auth"] = ProtoAnyCodable(["token": ProtoAnyCodable(token)]) } else if let password = self.password { - params["auth"] = AnyCodable(["password": AnyCodable(password)]) + params["auth"] = ProtoAnyCodable(["password": ProtoAnyCodable(password)]) + } + let connectNonce = try await self.waitForConnectChallenge() + let identity = DeviceIdentityStore.loadOrCreate() + let signedAtMs = Int(Date().timeIntervalSince1970 * 1000) + let scopesValue = scopes.joined(separator: ",") + var payloadParts = [ + connectNonce == nil ? "v1" : "v2", + identity.deviceId, + clientId, + clientMode, + role, + scopesValue, + String(signedAtMs), + self.token ?? "", + ] + if let connectNonce { + payloadParts.append(connectNonce) + } + let payload = payloadParts.joined(separator: "|") + if let signature = DeviceIdentityStore.signPayload(payload, identity: identity), + let publicKey = DeviceIdentityStore.publicKeyBase64Url(identity) + { + var device: [String: ProtoAnyCodable] = [ + "id": ProtoAnyCodable(identity.deviceId), + "publicKey": ProtoAnyCodable(publicKey), + "signature": ProtoAnyCodable(signature), + "signedAt": ProtoAnyCodable(signedAtMs), + ] + if let connectNonce { + device["nonce"] = ProtoAnyCodable(connectNonce) + } + params["device"] = ProtoAnyCodable(device) } let reqId = UUID().uuidString @@ -338,31 +320,58 @@ actor GatewayWizardClient { type: "req", id: reqId, method: "connect", - params: AnyCodable(params)) + params: ProtoAnyCodable(params)) let data = try self.encoder.encode(frame) try await task.send(.data(data)) - let message = try await task.receive() - let frameResponse = try decodeFrame(message) - guard case let .res(res) = frameResponse, res.id == reqId else { - throw WizardCliError.gatewayError("connect failed (unexpected response)") + while true { + let message = try await task.receive() + let frameResponse = try decodeFrame(message) + if case let .res(res) = frameResponse, res.id == reqId { + if res.ok == false { + let msg = (res.error?["message"]?.value as? String) ?? "gateway connect failed" + throw WizardCliError.gatewayError(msg) + } + _ = try self.decodePayload(res, as: HelloOk.self) + return + } } - if res.ok == false { - let msg = (res.error?["message"]?.value as? String) ?? "gateway connect failed" - throw WizardCliError.gatewayError(msg) + } + + private func waitForConnectChallenge() async throws -> String? { + guard let task = self.task else { return nil } + do { + return try await AsyncTimeout.withTimeout( + seconds: self.connectChallengeTimeoutSeconds, + onTimeout: { ConnectChallengeError.timeout }, + operation: { + while true { + let message = try await task.receive() + let frame = try await self.decodeFrame(message) + if case let .event(evt) = frame, evt.event == "connect.challenge" { + if let payload = evt.payload?.value as? [String: ProtoAnyCodable], + let nonce = payload["nonce"]?.value as? String + { + return nonce + } + } + } + }) + } catch { + if error is ConnectChallengeError { return nil } + throw error } - _ = try self.decodePayload(res, as: HelloOk.self) } } private func runWizard(client: GatewayWizardClient, opts: WizardCliOptions) async throws { - var params: [String: AnyCodable] = [:] + var params: [String: ProtoAnyCodable] = [:] let mode = opts.mode.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() if mode == "local" || mode == "remote" { - params["mode"] = AnyCodable(mode) + params["mode"] = ProtoAnyCodable(mode) } if let workspace = opts.workspace?.trimmingCharacters(in: .whitespacesAndNewlines), !workspace.isEmpty { - params["workspace"] = AnyCodable(workspace) + params["workspace"] = ProtoAnyCodable(workspace) } let startResponse = try await client.request(method: "wizard.start", params: params) @@ -395,17 +404,17 @@ private func runWizard(client: GatewayWizardClient, opts: WizardCliOptions) asyn if let step = decodeWizardStep(nextResult.step) { let answer = try promptAnswer(for: step) - var answerPayload: [String: AnyCodable] = [ - "stepId": AnyCodable(step.id), + var answerPayload: [String: ProtoAnyCodable] = [ + "stepId": ProtoAnyCodable(step.id), ] if !(answer is NSNull) { - answerPayload["value"] = AnyCodable(answer) + answerPayload["value"] = ProtoAnyCodable(answer) } let response = try await client.request( method: "wizard.next", params: [ - "sessionId": AnyCodable(sessionId), - "answer": AnyCodable(answerPayload), + "sessionId": ProtoAnyCodable(sessionId), + "answer": ProtoAnyCodable(answerPayload), ]) nextResult = try await client.decodePayload(response, as: WizardNextResult.self) if opts.json { @@ -414,7 +423,7 @@ private func runWizard(client: GatewayWizardClient, opts: WizardCliOptions) asyn } else { let response = try await client.request( method: "wizard.next", - params: ["sessionId": AnyCodable(sessionId)]) + params: ["sessionId": ProtoAnyCodable(sessionId)]) nextResult = try await client.decodePayload(response, as: WizardNextResult.self) if opts.json { dumpResult(response) @@ -424,7 +433,7 @@ private func runWizard(client: GatewayWizardClient, opts: WizardCliOptions) asyn } catch WizardCliError.cancelled { _ = try? await client.request( method: "wizard.cancel", - params: ["sessionId": AnyCodable(sessionId)]) + params: ["sessionId": ProtoAnyCodable(sessionId)]) throw WizardCliError.cancelled } } diff --git a/apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift b/apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift index a44f6739a..aef9a5e0e 100644 --- a/apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift +++ b/apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift @@ -5,6 +5,7 @@ public let GATEWAY_PROTOCOL_VERSION = 3 public enum ErrorCode: String, Codable, Sendable { case notLinked = "NOT_LINKED" + case notPaired = "NOT_PAIRED" case agentTimeout = "AGENT_TIMEOUT" case invalidRequest = "INVALID_REQUEST" case unavailable = "UNAVAILABLE" @@ -15,6 +16,12 @@ public struct ConnectParams: Codable, Sendable { public let maxprotocol: Int public let client: [String: AnyCodable] public let caps: [String]? + public let commands: [String]? + public let permissions: [String: AnyCodable]? + public let pathenv: String? + public let role: String? + public let scopes: [String]? + public let device: [String: AnyCodable]? public let auth: [String: AnyCodable]? public let locale: String? public let useragent: String? @@ -24,6 +31,12 @@ public struct ConnectParams: Codable, Sendable { maxprotocol: Int, client: [String: AnyCodable], caps: [String]?, + commands: [String]?, + permissions: [String: AnyCodable]?, + pathenv: String?, + role: String?, + scopes: [String]?, + device: [String: AnyCodable]?, auth: [String: AnyCodable]?, locale: String?, useragent: String? @@ -32,6 +45,12 @@ public struct ConnectParams: Codable, Sendable { self.maxprotocol = maxprotocol self.client = client self.caps = caps + self.commands = commands + self.permissions = permissions + self.pathenv = pathenv + self.role = role + self.scopes = scopes + self.device = device self.auth = auth self.locale = locale self.useragent = useragent @@ -41,6 +60,12 @@ public struct ConnectParams: Codable, Sendable { case maxprotocol = "maxProtocol" case client case caps + case commands + case permissions + case pathenv = "pathEnv" + case role + case scopes + case device case auth case locale case useragent = "userAgent" @@ -54,6 +79,7 @@ public struct HelloOk: Codable, Sendable { public let features: [String: AnyCodable] public let snapshot: Snapshot public let canvashosturl: String? + public let auth: [String: AnyCodable]? public let policy: [String: AnyCodable] public init( @@ -63,6 +89,7 @@ public struct HelloOk: Codable, Sendable { features: [String: AnyCodable], snapshot: Snapshot, canvashosturl: String?, + auth: [String: AnyCodable]?, policy: [String: AnyCodable] ) { self.type = type @@ -71,6 +98,7 @@ public struct HelloOk: Codable, Sendable { self.features = features self.snapshot = snapshot self.canvashosturl = canvashosturl + self.auth = auth self.policy = policy } private enum CodingKeys: String, CodingKey { @@ -80,6 +108,7 @@ public struct HelloOk: Codable, Sendable { case features case snapshot case canvashosturl = "canvasHostUrl" + case auth case policy } } @@ -180,6 +209,9 @@ public struct PresenceEntry: Codable, Sendable { public let tags: [String]? public let text: String? public let ts: Int + public let deviceid: String? + public let roles: [String]? + public let scopes: [String]? public let instanceid: String? public init( @@ -195,6 +227,9 @@ public struct PresenceEntry: Codable, Sendable { tags: [String]?, text: String?, ts: Int, + deviceid: String?, + roles: [String]?, + scopes: [String]?, instanceid: String? ) { self.host = host @@ -209,6 +244,9 @@ public struct PresenceEntry: Codable, Sendable { self.tags = tags self.text = text self.ts = ts + self.deviceid = deviceid + self.roles = roles + self.scopes = scopes self.instanceid = instanceid } private enum CodingKeys: String, CodingKey { @@ -224,6 +262,9 @@ public struct PresenceEntry: Codable, Sendable { case tags case text case ts + case deviceid = "deviceId" + case roles + case scopes case instanceid = "instanceId" } } @@ -344,6 +385,7 @@ public struct SendParams: Codable, Sendable { public let to: String public let message: String public let mediaurl: String? + public let mediaurls: [String]? public let gifplayback: Bool? public let channel: String? public let accountid: String? @@ -354,6 +396,7 @@ public struct SendParams: Codable, Sendable { to: String, message: String, mediaurl: String?, + mediaurls: [String]?, gifplayback: Bool?, channel: String?, accountid: String?, @@ -363,6 +406,7 @@ public struct SendParams: Codable, Sendable { self.to = to self.message = message self.mediaurl = mediaurl + self.mediaurls = mediaurls self.gifplayback = gifplayback self.channel = channel self.accountid = accountid @@ -373,6 +417,7 @@ public struct SendParams: Codable, Sendable { case to case message case mediaurl = "mediaUrl" + case mediaurls = "mediaUrls" case gifplayback = "gifPlayback" case channel case accountid = "accountId" @@ -424,14 +469,22 @@ public struct PollParams: Codable, Sendable { public struct AgentParams: Codable, Sendable { public let message: String + public let agentid: String? public let to: String? + public let replyto: String? public let sessionid: String? public let sessionkey: String? public let thinking: String? public let deliver: Bool? public let attachments: [AnyCodable]? public let channel: String? + public let replychannel: String? public let accountid: String? + public let replyaccountid: String? + public let threadid: String? + public let groupid: String? + public let groupchannel: String? + public let groupspace: String? public let timeout: Int? public let lane: String? public let extrasystemprompt: String? @@ -441,14 +494,22 @@ public struct AgentParams: Codable, Sendable { public init( message: String, + agentid: String?, to: String?, + replyto: String?, sessionid: String?, sessionkey: String?, thinking: String?, deliver: Bool?, attachments: [AnyCodable]?, channel: String?, + replychannel: String?, accountid: String?, + replyaccountid: String?, + threadid: String?, + groupid: String?, + groupchannel: String?, + groupspace: String?, timeout: Int?, lane: String?, extrasystemprompt: String?, @@ -457,14 +518,22 @@ public struct AgentParams: Codable, Sendable { spawnedby: String? ) { self.message = message + self.agentid = agentid self.to = to + self.replyto = replyto self.sessionid = sessionid self.sessionkey = sessionkey self.thinking = thinking self.deliver = deliver self.attachments = attachments self.channel = channel + self.replychannel = replychannel self.accountid = accountid + self.replyaccountid = replyaccountid + self.threadid = threadid + self.groupid = groupid + self.groupchannel = groupchannel + self.groupspace = groupspace self.timeout = timeout self.lane = lane self.extrasystemprompt = extrasystemprompt @@ -474,14 +543,22 @@ public struct AgentParams: Codable, Sendable { } private enum CodingKeys: String, CodingKey { case message + case agentid = "agentId" case to + case replyto = "replyTo" case sessionid = "sessionId" case sessionkey = "sessionKey" case thinking case deliver case attachments case channel + case replychannel = "replyChannel" case accountid = "accountId" + case replyaccountid = "replyAccountId" + case threadid = "threadId" + case groupid = "groupId" + case groupchannel = "groupChannel" + case groupspace = "groupSpace" case timeout case lane case extrasystemprompt = "extraSystemPrompt" @@ -491,6 +568,44 @@ public struct AgentParams: Codable, Sendable { } } +public struct AgentIdentityParams: Codable, Sendable { + public let agentid: String? + public let sessionkey: String? + + public init( + agentid: String?, + sessionkey: String? + ) { + self.agentid = agentid + self.sessionkey = sessionkey + } + private enum CodingKeys: String, CodingKey { + case agentid = "agentId" + case sessionkey = "sessionKey" + } +} + +public struct AgentIdentityResult: Codable, Sendable { + public let agentid: String + public let name: String? + public let avatar: String? + + public init( + agentid: String, + name: String?, + avatar: String? + ) { + self.agentid = agentid + self.name = name + self.avatar = avatar + } + private enum CodingKeys: String, CodingKey { + case agentid = "agentId" + case name + case avatar + } +} + public struct AgentWaitParams: Codable, Sendable { public let runid: String public let timeoutms: Int? @@ -690,45 +805,166 @@ public struct NodeInvokeParams: Codable, Sendable { } } +public struct NodeInvokeResultParams: Codable, Sendable { + public let id: String + public let nodeid: String + public let ok: Bool + public let payload: AnyCodable? + public let payloadjson: String? + public let error: [String: AnyCodable]? + + public init( + id: String, + nodeid: String, + ok: Bool, + payload: AnyCodable?, + payloadjson: String?, + error: [String: AnyCodable]? + ) { + self.id = id + self.nodeid = nodeid + self.ok = ok + self.payload = payload + self.payloadjson = payloadjson + self.error = error + } + private enum CodingKeys: String, CodingKey { + case id + case nodeid = "nodeId" + case ok + case payload + case payloadjson = "payloadJSON" + case error + } +} + +public struct NodeEventParams: Codable, Sendable { + public let event: String + public let payload: AnyCodable? + public let payloadjson: String? + + public init( + event: String, + payload: AnyCodable?, + payloadjson: String? + ) { + self.event = event + self.payload = payload + self.payloadjson = payloadjson + } + private enum CodingKeys: String, CodingKey { + case event + case payload + case payloadjson = "payloadJSON" + } +} + +public struct NodeInvokeRequestEvent: Codable, Sendable { + public let id: String + public let nodeid: String + public let command: String + public let paramsjson: String? + public let timeoutms: Int? + public let idempotencykey: String? + + public init( + id: String, + nodeid: String, + command: String, + paramsjson: String?, + timeoutms: Int?, + idempotencykey: String? + ) { + self.id = id + self.nodeid = nodeid + self.command = command + self.paramsjson = paramsjson + self.timeoutms = timeoutms + self.idempotencykey = idempotencykey + } + private enum CodingKeys: String, CodingKey { + case id + case nodeid = "nodeId" + case command + case paramsjson = "paramsJSON" + case timeoutms = "timeoutMs" + case idempotencykey = "idempotencyKey" + } +} + public struct SessionsListParams: Codable, Sendable { public let limit: Int? public let activeminutes: Int? public let includeglobal: Bool? public let includeunknown: Bool? + public let includederivedtitles: Bool? + public let includelastmessage: Bool? public let label: String? public let spawnedby: String? public let agentid: String? + public let search: String? public init( limit: Int?, activeminutes: Int?, includeglobal: Bool?, includeunknown: Bool?, + includederivedtitles: Bool?, + includelastmessage: Bool?, label: String?, spawnedby: String?, - agentid: String? + agentid: String?, + search: String? ) { self.limit = limit self.activeminutes = activeminutes self.includeglobal = includeglobal self.includeunknown = includeunknown + self.includederivedtitles = includederivedtitles + self.includelastmessage = includelastmessage self.label = label self.spawnedby = spawnedby self.agentid = agentid + self.search = search } private enum CodingKeys: String, CodingKey { case limit case activeminutes = "activeMinutes" case includeglobal = "includeGlobal" case includeunknown = "includeUnknown" + case includederivedtitles = "includeDerivedTitles" + case includelastmessage = "includeLastMessage" case label case spawnedby = "spawnedBy" case agentid = "agentId" + case search + } +} + +public struct SessionsPreviewParams: Codable, Sendable { + public let keys: [String] + public let limit: Int? + public let maxchars: Int? + + public init( + keys: [String], + limit: Int?, + maxchars: Int? + ) { + self.keys = keys + self.limit = limit + self.maxchars = maxchars + } + private enum CodingKeys: String, CodingKey { + case keys + case limit + case maxchars = "maxChars" } } public struct SessionsResolveParams: Codable, Sendable { public let key: String? + public let sessionid: String? public let label: String? public let agentid: String? public let spawnedby: String? @@ -737,6 +973,7 @@ public struct SessionsResolveParams: Codable, Sendable { public init( key: String?, + sessionid: String?, label: String?, agentid: String?, spawnedby: String?, @@ -744,6 +981,7 @@ public struct SessionsResolveParams: Codable, Sendable { includeunknown: Bool? ) { self.key = key + self.sessionid = sessionid self.label = label self.agentid = agentid self.spawnedby = spawnedby @@ -752,6 +990,7 @@ public struct SessionsResolveParams: Codable, Sendable { } private enum CodingKeys: String, CodingKey { case key + case sessionid = "sessionId" case label case agentid = "agentId" case spawnedby = "spawnedBy" @@ -1184,6 +1423,9 @@ public struct ChannelsStatusResult: Codable, Sendable { public let ts: Int public let channelorder: [String] public let channellabels: [String: AnyCodable] + public let channeldetaillabels: [String: AnyCodable]? + public let channelsystemimages: [String: AnyCodable]? + public let channelmeta: [[String: AnyCodable]]? public let channels: [String: AnyCodable] public let channelaccounts: [String: AnyCodable] public let channeldefaultaccountid: [String: AnyCodable] @@ -1192,6 +1434,9 @@ public struct ChannelsStatusResult: Codable, Sendable { ts: Int, channelorder: [String], channellabels: [String: AnyCodable], + channeldetaillabels: [String: AnyCodable]?, + channelsystemimages: [String: AnyCodable]?, + channelmeta: [[String: AnyCodable]]?, channels: [String: AnyCodable], channelaccounts: [String: AnyCodable], channeldefaultaccountid: [String: AnyCodable] @@ -1199,6 +1444,9 @@ public struct ChannelsStatusResult: Codable, Sendable { self.ts = ts self.channelorder = channelorder self.channellabels = channellabels + self.channeldetaillabels = channeldetaillabels + self.channelsystemimages = channelsystemimages + self.channelmeta = channelmeta self.channels = channels self.channelaccounts = channelaccounts self.channeldefaultaccountid = channeldefaultaccountid @@ -1207,6 +1455,9 @@ public struct ChannelsStatusResult: Codable, Sendable { case ts case channelorder = "channelOrder" case channellabels = "channelLabels" + case channeldetaillabels = "channelDetailLabels" + case channelsystemimages = "channelSystemImages" + case channelmeta = "channelMeta" case channels case channelaccounts = "channelAccounts" case channeldefaultaccountid = "channelDefaultAccountId" @@ -1275,17 +1526,21 @@ public struct WebLoginWaitParams: Codable, Sendable { public struct AgentSummary: Codable, Sendable { public let id: String public let name: String? + public let identity: [String: AnyCodable]? public init( id: String, - name: String? + name: String?, + identity: [String: AnyCodable]? ) { self.id = id self.name = name + self.identity = identity } private enum CodingKeys: String, CodingKey { case id case name + case identity } } @@ -1365,6 +1620,22 @@ public struct ModelsListResult: Codable, Sendable { public struct SkillsStatusParams: Codable, Sendable { } +public struct SkillsBinsParams: Codable, Sendable { +} + +public struct SkillsBinsResult: Codable, Sendable { + public let bins: [String] + + public init( + bins: [String] + ) { + self.bins = bins + } + private enum CodingKeys: String, CodingKey { + case bins + } +} + public struct SkillsInstallParams: Codable, Sendable { public let name: String public let installid: String @@ -1719,6 +1990,229 @@ public struct ExecApprovalsSnapshot: Codable, Sendable { } } +public struct ExecApprovalRequestParams: Codable, Sendable { + public let id: String? + public let command: String + public let cwd: AnyCodable? + public let host: AnyCodable? + public let security: AnyCodable? + public let ask: AnyCodable? + public let agentid: AnyCodable? + public let resolvedpath: AnyCodable? + public let sessionkey: AnyCodable? + public let timeoutms: Int? + + public init( + id: String?, + command: String, + cwd: AnyCodable?, + host: AnyCodable?, + security: AnyCodable?, + ask: AnyCodable?, + agentid: AnyCodable?, + resolvedpath: AnyCodable?, + sessionkey: AnyCodable?, + timeoutms: Int? + ) { + self.id = id + self.command = command + self.cwd = cwd + self.host = host + self.security = security + self.ask = ask + self.agentid = agentid + self.resolvedpath = resolvedpath + self.sessionkey = sessionkey + self.timeoutms = timeoutms + } + private enum CodingKeys: String, CodingKey { + case id + case command + case cwd + case host + case security + case ask + case agentid = "agentId" + case resolvedpath = "resolvedPath" + case sessionkey = "sessionKey" + case timeoutms = "timeoutMs" + } +} + +public struct ExecApprovalResolveParams: Codable, Sendable { + public let id: String + public let decision: String + + public init( + id: String, + decision: String + ) { + self.id = id + self.decision = decision + } + private enum CodingKeys: String, CodingKey { + case id + case decision + } +} + +public struct DevicePairListParams: Codable, Sendable { +} + +public struct DevicePairApproveParams: Codable, Sendable { + public let requestid: String + + public init( + requestid: String + ) { + self.requestid = requestid + } + private enum CodingKeys: String, CodingKey { + case requestid = "requestId" + } +} + +public struct DevicePairRejectParams: Codable, Sendable { + public let requestid: String + + public init( + requestid: String + ) { + self.requestid = requestid + } + private enum CodingKeys: String, CodingKey { + case requestid = "requestId" + } +} + +public struct DeviceTokenRotateParams: Codable, Sendable { + public let deviceid: String + public let role: String + public let scopes: [String]? + + public init( + deviceid: String, + role: String, + scopes: [String]? + ) { + self.deviceid = deviceid + self.role = role + self.scopes = scopes + } + private enum CodingKeys: String, CodingKey { + case deviceid = "deviceId" + case role + case scopes + } +} + +public struct DeviceTokenRevokeParams: Codable, Sendable { + public let deviceid: String + public let role: String + + public init( + deviceid: String, + role: String + ) { + self.deviceid = deviceid + self.role = role + } + private enum CodingKeys: String, CodingKey { + case deviceid = "deviceId" + case role + } +} + +public struct DevicePairRequestedEvent: Codable, Sendable { + public let requestid: String + public let deviceid: String + public let publickey: String + public let displayname: String? + public let platform: String? + public let clientid: String? + public let clientmode: String? + public let role: String? + public let roles: [String]? + public let scopes: [String]? + public let remoteip: String? + public let silent: Bool? + public let isrepair: Bool? + public let ts: Int + + public init( + requestid: String, + deviceid: String, + publickey: String, + displayname: String?, + platform: String?, + clientid: String?, + clientmode: String?, + role: String?, + roles: [String]?, + scopes: [String]?, + remoteip: String?, + silent: Bool?, + isrepair: Bool?, + ts: Int + ) { + self.requestid = requestid + self.deviceid = deviceid + self.publickey = publickey + self.displayname = displayname + self.platform = platform + self.clientid = clientid + self.clientmode = clientmode + self.role = role + self.roles = roles + self.scopes = scopes + self.remoteip = remoteip + self.silent = silent + self.isrepair = isrepair + self.ts = ts + } + private enum CodingKeys: String, CodingKey { + case requestid = "requestId" + case deviceid = "deviceId" + case publickey = "publicKey" + case displayname = "displayName" + case platform + case clientid = "clientId" + case clientmode = "clientMode" + case role + case roles + case scopes + case remoteip = "remoteIp" + case silent + case isrepair = "isRepair" + case ts + } +} + +public struct DevicePairResolvedEvent: Codable, Sendable { + public let requestid: String + public let deviceid: String + public let decision: String + public let ts: Int + + public init( + requestid: String, + deviceid: String, + decision: String, + ts: Int + ) { + self.requestid = requestid + self.deviceid = deviceid + self.decision = decision + self.ts = ts + } + private enum CodingKeys: String, CodingKey { + case requestid = "requestId" + case deviceid = "deviceId" + case decision + case ts + } +} + public struct ChatHistoryParams: Codable, Sendable { public let sessionkey: String public let limit: Int? diff --git a/apps/macos/Tests/ClawdbotIPCTests/AgentWorkspaceTests.swift b/apps/macos/Tests/ClawdbotIPCTests/AgentWorkspaceTests.swift index 320a44c39..9368f643c 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/AgentWorkspaceTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/AgentWorkspaceTests.swift @@ -6,7 +6,7 @@ import Testing struct AgentWorkspaceTests { @Test func displayPathUsesTildeForHome() { - let home = FileManager.default.homeDirectoryForCurrentUser + let home = FileManager().homeDirectoryForCurrentUser #expect(AgentWorkspace.displayPath(for: home) == "~") let inside = home.appendingPathComponent("Projects", isDirectory: true) @@ -28,12 +28,12 @@ struct AgentWorkspaceTests { @Test func bootstrapCreatesAgentsFileWhenMissing() throws { - let tmp = FileManager.default.temporaryDirectory + let tmp = FileManager().temporaryDirectory .appendingPathComponent("clawdbot-ws-\(UUID().uuidString)", isDirectory: true) - defer { try? FileManager.default.removeItem(at: tmp) } + defer { try? FileManager().removeItem(at: tmp) } let agentsURL = try AgentWorkspace.bootstrap(workspaceURL: tmp) - #expect(FileManager.default.fileExists(atPath: agentsURL.path)) + #expect(FileManager().fileExists(atPath: agentsURL.path)) let contents = try String(contentsOf: agentsURL, encoding: .utf8) #expect(contents.contains("# AGENTS.md")) @@ -41,9 +41,9 @@ struct AgentWorkspaceTests { let identityURL = tmp.appendingPathComponent(AgentWorkspace.identityFilename) let userURL = tmp.appendingPathComponent(AgentWorkspace.userFilename) let bootstrapURL = tmp.appendingPathComponent(AgentWorkspace.bootstrapFilename) - #expect(FileManager.default.fileExists(atPath: identityURL.path)) - #expect(FileManager.default.fileExists(atPath: userURL.path)) - #expect(FileManager.default.fileExists(atPath: bootstrapURL.path)) + #expect(FileManager().fileExists(atPath: identityURL.path)) + #expect(FileManager().fileExists(atPath: userURL.path)) + #expect(FileManager().fileExists(atPath: bootstrapURL.path)) let second = try AgentWorkspace.bootstrap(workspaceURL: tmp) #expect(second == agentsURL) @@ -51,10 +51,10 @@ struct AgentWorkspaceTests { @Test func bootstrapSafetyRejectsNonEmptyFolderWithoutAgents() throws { - let tmp = FileManager.default.temporaryDirectory + let tmp = FileManager().temporaryDirectory .appendingPathComponent("clawdbot-ws-\(UUID().uuidString)", isDirectory: true) - defer { try? FileManager.default.removeItem(at: tmp) } - try FileManager.default.createDirectory(at: tmp, withIntermediateDirectories: true) + defer { try? FileManager().removeItem(at: tmp) } + try FileManager().createDirectory(at: tmp, withIntermediateDirectories: true) let marker = tmp.appendingPathComponent("notes.txt") try "hello".write(to: marker, atomically: true, encoding: .utf8) @@ -69,10 +69,10 @@ struct AgentWorkspaceTests { @Test func bootstrapSafetyAllowsExistingAgentsFile() throws { - let tmp = FileManager.default.temporaryDirectory + let tmp = FileManager().temporaryDirectory .appendingPathComponent("clawdbot-ws-\(UUID().uuidString)", isDirectory: true) - defer { try? FileManager.default.removeItem(at: tmp) } - try FileManager.default.createDirectory(at: tmp, withIntermediateDirectories: true) + defer { try? FileManager().removeItem(at: tmp) } + try FileManager().createDirectory(at: tmp, withIntermediateDirectories: true) let agents = tmp.appendingPathComponent(AgentWorkspace.agentsFilename) try "# AGENTS.md".write(to: agents, atomically: true, encoding: .utf8) @@ -87,25 +87,25 @@ struct AgentWorkspaceTests { @Test func bootstrapSkipsBootstrapFileWhenWorkspaceHasContent() throws { - let tmp = FileManager.default.temporaryDirectory + let tmp = FileManager().temporaryDirectory .appendingPathComponent("clawdbot-ws-\(UUID().uuidString)", isDirectory: true) - defer { try? FileManager.default.removeItem(at: tmp) } - try FileManager.default.createDirectory(at: tmp, withIntermediateDirectories: true) + defer { try? FileManager().removeItem(at: tmp) } + try FileManager().createDirectory(at: tmp, withIntermediateDirectories: true) let marker = tmp.appendingPathComponent("notes.txt") try "hello".write(to: marker, atomically: true, encoding: .utf8) _ = try AgentWorkspace.bootstrap(workspaceURL: tmp) let bootstrapURL = tmp.appendingPathComponent(AgentWorkspace.bootstrapFilename) - #expect(!FileManager.default.fileExists(atPath: bootstrapURL.path)) + #expect(!FileManager().fileExists(atPath: bootstrapURL.path)) } @Test func needsBootstrapFalseWhenIdentityAlreadySet() throws { - let tmp = FileManager.default.temporaryDirectory + let tmp = FileManager().temporaryDirectory .appendingPathComponent("clawdbot-ws-\(UUID().uuidString)", isDirectory: true) - defer { try? FileManager.default.removeItem(at: tmp) } - try FileManager.default.createDirectory(at: tmp, withIntermediateDirectories: true) + defer { try? FileManager().removeItem(at: tmp) } + try FileManager().createDirectory(at: tmp, withIntermediateDirectories: true) let identityURL = tmp.appendingPathComponent(AgentWorkspace.identityFilename) try """ # IDENTITY.md - Agent Identity diff --git a/apps/macos/Tests/ClawdbotIPCTests/AnthropicAuthResolverTests.swift b/apps/macos/Tests/ClawdbotIPCTests/AnthropicAuthResolverTests.swift index 41f9ffefb..1af9108c2 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/AnthropicAuthResolverTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/AnthropicAuthResolverTests.swift @@ -6,9 +6,9 @@ import Testing struct AnthropicAuthResolverTests { @Test func prefersOAuthFileOverEnv() throws { - let dir = FileManager.default.temporaryDirectory + let dir = FileManager().temporaryDirectory .appendingPathComponent("clawdbot-oauth-\(UUID().uuidString)", isDirectory: true) - try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) + try FileManager().createDirectory(at: dir, withIntermediateDirectories: true) let oauthFile = dir.appendingPathComponent("oauth.json") let payload = [ "anthropic": [ diff --git a/apps/macos/Tests/ClawdbotIPCTests/BridgeServerTests.swift b/apps/macos/Tests/ClawdbotIPCTests/BridgeServerTests.swift deleted file mode 100644 index 9b43d669b..000000000 --- a/apps/macos/Tests/ClawdbotIPCTests/BridgeServerTests.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Testing -@testable import Clawdbot - -@Suite(.serialized) -struct BridgeServerTests { - @Test func bridgeServerExercisesPaths() async { - let server = BridgeServer() - await server.exerciseForTesting() - } -} diff --git a/apps/macos/Tests/ClawdbotIPCTests/CLIInstallerTests.swift b/apps/macos/Tests/ClawdbotIPCTests/CLIInstallerTests.swift index 829dd28a4..46144a455 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/CLIInstallerTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/CLIInstallerTests.swift @@ -6,7 +6,7 @@ import Testing @MainActor struct CLIInstallerTests { @Test func installedLocationFindsExecutable() throws { - let fm = FileManager.default + let fm = FileManager() let root = fm.temporaryDirectory.appendingPathComponent( "clawdbot-cli-installer-\(UUID().uuidString)") defer { try? fm.removeItem(at: root) } diff --git a/apps/macos/Tests/ClawdbotIPCTests/CanvasFileWatcherTests.swift b/apps/macos/Tests/ClawdbotIPCTests/CanvasFileWatcherTests.swift index 03fe58fda..28093abc8 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/CanvasFileWatcherTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/CanvasFileWatcherTests.swift @@ -7,13 +7,13 @@ import Testing private func makeTempDir() throws -> URL { let base = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) let dir = base.appendingPathComponent("clawdbot-canvaswatch-\(UUID().uuidString)", isDirectory: true) - try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) + try FileManager().createDirectory(at: dir, withIntermediateDirectories: true) return dir } @Test func detectsInPlaceFileWrites() async throws { let dir = try self.makeTempDir() - defer { try? FileManager.default.removeItem(at: dir) } + defer { try? FileManager().removeItem(at: dir) } let file = dir.appendingPathComponent("index.html") try "hello".write(to: file, atomically: false, encoding: .utf8) diff --git a/apps/macos/Tests/ClawdbotIPCTests/CanvasWindowSmokeTests.swift b/apps/macos/Tests/ClawdbotIPCTests/CanvasWindowSmokeTests.swift index 1168802b2..92f20bf0b 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/CanvasWindowSmokeTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/CanvasWindowSmokeTests.swift @@ -8,10 +8,10 @@ import Testing @MainActor struct CanvasWindowSmokeTests { @Test func panelControllerShowsAndHides() async throws { - let root = FileManager.default.temporaryDirectory + let root = FileManager().temporaryDirectory .appendingPathComponent("clawdbot-canvas-test-\(UUID().uuidString)") - try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) - defer { try? FileManager.default.removeItem(at: root) } + try FileManager().createDirectory(at: root, withIntermediateDirectories: true) + defer { try? FileManager().removeItem(at: root) } let anchor = { NSRect(x: 200, y: 400, width: 40, height: 40) } let controller = try CanvasWindowController( @@ -31,10 +31,10 @@ struct CanvasWindowSmokeTests { } @Test func windowControllerShowsAndCloses() async throws { - let root = FileManager.default.temporaryDirectory + let root = FileManager().temporaryDirectory .appendingPathComponent("clawdbot-canvas-test-\(UUID().uuidString)") - try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) - defer { try? FileManager.default.removeItem(at: root) } + try FileManager().createDirectory(at: root, withIntermediateDirectories: true) + defer { try? FileManager().removeItem(at: root) } let controller = try CanvasWindowController( sessionKey: "main", diff --git a/apps/macos/Tests/ClawdbotIPCTests/ChannelsSettingsSmokeTests.swift b/apps/macos/Tests/ClawdbotIPCTests/ChannelsSettingsSmokeTests.swift index c84ba0ba0..2b1eced84 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/ChannelsSettingsSmokeTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/ChannelsSettingsSmokeTests.swift @@ -3,6 +3,8 @@ import SwiftUI import Testing @testable import Clawdbot +private typealias SnapshotAnyCodable = Clawdbot.AnyCodable + @Suite(.serialized) @MainActor struct ChannelsSettingsSmokeTests { @@ -17,8 +19,11 @@ struct ChannelsSettingsSmokeTests { "signal": "Signal", "imessage": "iMessage", ], + channelDetailLabels: nil, + channelSystemImages: nil, + channelMeta: nil, channels: [ - "whatsapp": AnyCodable([ + "whatsapp": SnapshotAnyCodable([ "configured": true, "linked": true, "authAgeMs": 86_400_000, @@ -37,7 +42,7 @@ struct ChannelsSettingsSmokeTests { "lastEventAt": 1_700_000_060_000, "lastError": "needs login", ]), - "telegram": AnyCodable([ + "telegram": SnapshotAnyCodable([ "configured": true, "tokenSource": "env", "running": true, @@ -52,7 +57,7 @@ struct ChannelsSettingsSmokeTests { ], "lastProbeAt": 1_700_000_050_000, ]), - "signal": AnyCodable([ + "signal": SnapshotAnyCodable([ "configured": true, "baseUrl": "http://127.0.0.1:8080", "running": true, @@ -65,7 +70,7 @@ struct ChannelsSettingsSmokeTests { ], "lastProbeAt": 1_700_000_050_000, ]), - "imessage": AnyCodable([ + "imessage": SnapshotAnyCodable([ "configured": false, "running": false, "lastError": "not configured", @@ -100,15 +105,18 @@ struct ChannelsSettingsSmokeTests { "signal": "Signal", "imessage": "iMessage", ], + channelDetailLabels: nil, + channelSystemImages: nil, + channelMeta: nil, channels: [ - "whatsapp": AnyCodable([ + "whatsapp": SnapshotAnyCodable([ "configured": false, "linked": false, "running": false, "connected": false, "reconnectAttempts": 0, ]), - "telegram": AnyCodable([ + "telegram": SnapshotAnyCodable([ "configured": false, "running": false, "lastError": "bot missing", @@ -120,7 +128,7 @@ struct ChannelsSettingsSmokeTests { ], "lastProbeAt": 1_700_000_100_000, ]), - "signal": AnyCodable([ + "signal": SnapshotAnyCodable([ "configured": false, "baseUrl": "http://127.0.0.1:8080", "running": false, @@ -133,7 +141,7 @@ struct ChannelsSettingsSmokeTests { ], "lastProbeAt": 1_700_000_200_000, ]), - "imessage": AnyCodable([ + "imessage": SnapshotAnyCodable([ "configured": false, "running": false, "lastError": "not configured", diff --git a/apps/macos/Tests/ClawdbotIPCTests/ClawdbotConfigFileTests.swift b/apps/macos/Tests/ClawdbotIPCTests/ClawdbotConfigFileTests.swift index 9ee97e22c..15a0f3905 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/ClawdbotConfigFileTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/ClawdbotConfigFileTests.swift @@ -6,7 +6,7 @@ import Testing struct ClawdbotConfigFileTests { @Test func configPathRespectsEnvOverride() async { - let override = FileManager.default.temporaryDirectory + let override = FileManager().temporaryDirectory .appendingPathComponent("clawdbot-config-\(UUID().uuidString)") .appendingPathComponent("clawdbot.json") .path @@ -19,7 +19,7 @@ struct ClawdbotConfigFileTests { @MainActor @Test func remoteGatewayPortParsesAndMatchesHost() async { - let override = FileManager.default.temporaryDirectory + let override = FileManager().temporaryDirectory .appendingPathComponent("clawdbot-config-\(UUID().uuidString)") .appendingPathComponent("clawdbot.json") .path @@ -28,13 +28,13 @@ struct ClawdbotConfigFileTests { ClawdbotConfigFile.saveDict([ "gateway": [ "remote": [ - "url": "ws://bridge.ts.net:19999", + "url": "ws://gateway.ts.net:19999", ], ], ]) #expect(ClawdbotConfigFile.remoteGatewayPort() == 19999) - #expect(ClawdbotConfigFile.remoteGatewayPort(matchingHost: "bridge.ts.net") == 19999) - #expect(ClawdbotConfigFile.remoteGatewayPort(matchingHost: "bridge") == 19999) + #expect(ClawdbotConfigFile.remoteGatewayPort(matchingHost: "gateway.ts.net") == 19999) + #expect(ClawdbotConfigFile.remoteGatewayPort(matchingHost: "gateway") == 19999) #expect(ClawdbotConfigFile.remoteGatewayPort(matchingHost: "other.ts.net") == nil) } } @@ -42,7 +42,7 @@ struct ClawdbotConfigFileTests { @MainActor @Test func setRemoteGatewayUrlPreservesScheme() async { - let override = FileManager.default.temporaryDirectory + let override = FileManager().temporaryDirectory .appendingPathComponent("clawdbot-config-\(UUID().uuidString)") .appendingPathComponent("clawdbot.json") .path @@ -64,7 +64,7 @@ struct ClawdbotConfigFileTests { @Test func stateDirOverrideSetsConfigPath() async { - let dir = FileManager.default.temporaryDirectory + let dir = FileManager().temporaryDirectory .appendingPathComponent("clawdbot-state-\(UUID().uuidString)", isDirectory: true) .path diff --git a/apps/macos/Tests/ClawdbotIPCTests/ClawdbotOAuthStoreTests.swift b/apps/macos/Tests/ClawdbotIPCTests/ClawdbotOAuthStoreTests.swift index 4f4c8542a..0fcfdec84 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/ClawdbotOAuthStoreTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/ClawdbotOAuthStoreTests.swift @@ -6,7 +6,7 @@ import Testing struct ClawdbotOAuthStoreTests { @Test func returnsMissingWhenFileAbsent() { - let url = FileManager.default.temporaryDirectory + let url = FileManager().temporaryDirectory .appendingPathComponent("clawdbot-oauth-\(UUID().uuidString)") .appendingPathComponent("oauth.json") #expect(ClawdbotOAuthStore.anthropicOAuthStatus(at: url) == .missingFile) @@ -24,7 +24,7 @@ struct ClawdbotOAuthStoreTests { } } - let dir = FileManager.default.temporaryDirectory + let dir = FileManager().temporaryDirectory .appendingPathComponent("clawdbot-oauth-\(UUID().uuidString)", isDirectory: true) setenv(key, dir.path, 1) @@ -85,9 +85,9 @@ struct ClawdbotOAuthStoreTests { } private func writeOAuthFile(_ json: [String: Any]) throws -> URL { - let dir = FileManager.default.temporaryDirectory + let dir = FileManager().temporaryDirectory .appendingPathComponent("clawdbot-oauth-\(UUID().uuidString)", isDirectory: true) - try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) + try FileManager().createDirectory(at: dir, withIntermediateDirectories: true) let url = dir.appendingPathComponent("oauth.json") let data = try JSONSerialization.data(withJSONObject: json, options: [.prettyPrinted, .sortedKeys]) diff --git a/apps/macos/Tests/ClawdbotIPCTests/CommandResolverTests.swift b/apps/macos/Tests/ClawdbotIPCTests/CommandResolverTests.swift index 8feb00f12..827057888 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/CommandResolverTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/CommandResolverTests.swift @@ -12,16 +12,16 @@ import Testing private func makeTempDir() throws -> URL { let base = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) let dir = base.appendingPathComponent(UUID().uuidString, isDirectory: true) - try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) + try FileManager().createDirectory(at: dir, withIntermediateDirectories: true) return dir } private func makeExec(at path: URL) throws { - try FileManager.default.createDirectory( + try FileManager().createDirectory( at: path.deletingLastPathComponent(), withIntermediateDirectories: true) - FileManager.default.createFile(atPath: path.path, contents: Data("echo ok\n".utf8)) - try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: path.path) + FileManager().createFile(atPath: path.path, contents: Data("echo ok\n".utf8)) + try FileManager().setAttributes([.posixPermissions: 0o755], ofItemAtPath: path.path) } @Test func prefersClawdbotBinary() async throws { @@ -49,7 +49,7 @@ import Testing let scriptPath = tmp.appendingPathComponent("bin/clawdbot.js") try self.makeExec(at: nodePath) try "#!/bin/sh\necho v22.0.0\n".write(to: nodePath, atomically: true, encoding: .utf8) - try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: nodePath.path) + try FileManager().setAttributes([.posixPermissions: 0o755], ofItemAtPath: nodePath.path) try self.makeExec(at: scriptPath) let cmd = CommandResolver.clawdbotCommand( diff --git a/apps/macos/Tests/ClawdbotIPCTests/CronJobEditorSmokeTests.swift b/apps/macos/Tests/ClawdbotIPCTests/CronJobEditorSmokeTests.swift index 02e8c1e69..1a0fbf786 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/CronJobEditorSmokeTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/CronJobEditorSmokeTests.swift @@ -11,16 +11,19 @@ struct CronJobEditorSmokeTests { } @Test func cronJobEditorBuildsBodyForNewJob() { + let channelsStore = ChannelsStore(isPreview: true) let view = CronJobEditor( job: nil, isSaving: .constant(false), error: .constant(nil), + channelsStore: channelsStore, onCancel: {}, onSave: { _ in }) _ = view.body } @Test func cronJobEditorBuildsBodyForExistingJob() { + let channelsStore = ChannelsStore(isPreview: true) let job = CronJob( id: "job-1", agentId: "ops", @@ -54,31 +57,36 @@ struct CronJobEditorSmokeTests { job: job, isSaving: .constant(false), error: .constant(nil), + channelsStore: channelsStore, onCancel: {}, onSave: { _ in }) _ = view.body } @Test func cronJobEditorExercisesBuilders() { + let channelsStore = ChannelsStore(isPreview: true) var view = CronJobEditor( job: nil, isSaving: .constant(false), error: .constant(nil), + channelsStore: channelsStore, onCancel: {}, onSave: { _ in }) view.exerciseForTesting() } @Test func cronJobEditorIncludesDeleteAfterRunForAtSchedule() throws { + let channelsStore = ChannelsStore(isPreview: true) let view = CronJobEditor( job: nil, isSaving: .constant(false), error: .constant(nil), + channelsStore: channelsStore, onCancel: {}, onSave: { _ in }) var root: [String: Any] = [:] - view.applyDeleteAfterRun(to: &root, scheduleKind: .at, deleteAfterRun: true) + view.applyDeleteAfterRun(to: &root, scheduleKind: CronJobEditor.ScheduleKind.at, deleteAfterRun: true) let raw = root["deleteAfterRun"] as? Bool #expect(raw == true) } diff --git a/apps/macos/Tests/ClawdbotIPCTests/ExecApprovalHelpersTests.swift b/apps/macos/Tests/ClawdbotIPCTests/ExecApprovalHelpersTests.swift new file mode 100644 index 000000000..a1774d49f --- /dev/null +++ b/apps/macos/Tests/ClawdbotIPCTests/ExecApprovalHelpersTests.swift @@ -0,0 +1,60 @@ +import Foundation +import Testing +@testable import Clawdbot + +@Suite struct ExecApprovalHelpersTests { + @Test func parseDecisionTrimsAndRejectsInvalid() { + #expect(ExecApprovalHelpers.parseDecision("allow-once") == .allowOnce) + #expect(ExecApprovalHelpers.parseDecision(" allow-always ") == .allowAlways) + #expect(ExecApprovalHelpers.parseDecision("deny") == .deny) + #expect(ExecApprovalHelpers.parseDecision("") == nil) + #expect(ExecApprovalHelpers.parseDecision("nope") == nil) + } + + @Test func allowlistPatternPrefersResolution() { + let resolved = ExecCommandResolution( + rawExecutable: "rg", + resolvedPath: "/opt/homebrew/bin/rg", + executableName: "rg", + cwd: nil) + #expect(ExecApprovalHelpers.allowlistPattern(command: ["rg"], resolution: resolved) == resolved.resolvedPath) + + let rawOnly = ExecCommandResolution( + rawExecutable: "rg", + resolvedPath: nil, + executableName: "rg", + cwd: nil) + #expect(ExecApprovalHelpers.allowlistPattern(command: ["rg"], resolution: rawOnly) == "rg") + #expect(ExecApprovalHelpers.allowlistPattern(command: ["rg"], resolution: nil) == "rg") + #expect(ExecApprovalHelpers.allowlistPattern(command: [], resolution: nil) == nil) + } + + @Test func requiresAskMatchesPolicy() { + let entry = ExecAllowlistEntry(pattern: "/bin/ls", lastUsedAt: nil, lastUsedCommand: nil, lastResolvedPath: nil) + #expect(ExecApprovalHelpers.requiresAsk( + ask: .always, + security: .deny, + allowlistMatch: nil, + skillAllow: false)) + #expect(ExecApprovalHelpers.requiresAsk( + ask: .onMiss, + security: .allowlist, + allowlistMatch: nil, + skillAllow: false)) + #expect(!ExecApprovalHelpers.requiresAsk( + ask: .onMiss, + security: .allowlist, + allowlistMatch: entry, + skillAllow: false)) + #expect(!ExecApprovalHelpers.requiresAsk( + ask: .onMiss, + security: .allowlist, + allowlistMatch: nil, + skillAllow: true)) + #expect(!ExecApprovalHelpers.requiresAsk( + ask: .off, + security: .allowlist, + allowlistMatch: nil, + skillAllow: false)) + } +} diff --git a/apps/macos/Tests/ClawdbotIPCTests/ExecApprovalsGatewayPrompterTests.swift b/apps/macos/Tests/ClawdbotIPCTests/ExecApprovalsGatewayPrompterTests.swift new file mode 100644 index 000000000..88fb02f2b --- /dev/null +++ b/apps/macos/Tests/ClawdbotIPCTests/ExecApprovalsGatewayPrompterTests.swift @@ -0,0 +1,56 @@ +import Testing +@testable import Clawdbot + +@Suite +@MainActor +struct ExecApprovalsGatewayPrompterTests { + @Test func sessionMatchPrefersActiveSession() { + let matches = ExecApprovalsGatewayPrompter._testShouldPresent( + mode: .remote, + activeSession: " main ", + requestSession: "main", + lastInputSeconds: nil) + #expect(matches) + + let mismatched = ExecApprovalsGatewayPrompter._testShouldPresent( + mode: .remote, + activeSession: "other", + requestSession: "main", + lastInputSeconds: 0) + #expect(!mismatched) + } + + @Test func sessionFallbackUsesRecentActivity() { + let recent = ExecApprovalsGatewayPrompter._testShouldPresent( + mode: .remote, + activeSession: nil, + requestSession: "main", + lastInputSeconds: 10, + thresholdSeconds: 120) + #expect(recent) + + let stale = ExecApprovalsGatewayPrompter._testShouldPresent( + mode: .remote, + activeSession: nil, + requestSession: "main", + lastInputSeconds: 200, + thresholdSeconds: 120) + #expect(!stale) + } + + @Test func defaultBehaviorMatchesMode() { + let local = ExecApprovalsGatewayPrompter._testShouldPresent( + mode: .local, + activeSession: nil, + requestSession: nil, + lastInputSeconds: 400) + #expect(local) + + let remote = ExecApprovalsGatewayPrompter._testShouldPresent( + mode: .remote, + activeSession: nil, + requestSession: nil, + lastInputSeconds: 400) + #expect(!remote) + } +} diff --git a/apps/macos/Tests/ClawdbotIPCTests/FileHandleLegacyAPIGuardTests.swift b/apps/macos/Tests/ClawdbotIPCTests/FileHandleLegacyAPIGuardTests.swift index 5e30cf584..2de87f693 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/FileHandleLegacyAPIGuardTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/FileHandleLegacyAPIGuardTests.swift @@ -32,7 +32,7 @@ import Testing } private static func swiftFiles(under root: URL) throws -> [URL] { - let fm = FileManager.default + let fm = FileManager() guard let enumerator = fm.enumerator(at: root, includingPropertiesForKeys: [.isRegularFileKey]) else { return [] } diff --git a/apps/macos/Tests/ClawdbotIPCTests/GatewayAgentChannelTests.swift b/apps/macos/Tests/ClawdbotIPCTests/GatewayAgentChannelTests.swift index 248712262..bf72af7e5 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/GatewayAgentChannelTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/GatewayAgentChannelTests.swift @@ -11,6 +11,7 @@ import Testing #expect(GatewayAgentChannel.last.shouldDeliver(true) == true) #expect(GatewayAgentChannel.whatsapp.shouldDeliver(true) == true) #expect(GatewayAgentChannel.telegram.shouldDeliver(true) == true) + #expect(GatewayAgentChannel.bluebubbles.shouldDeliver(true) == true) #expect(GatewayAgentChannel.last.shouldDeliver(false) == false) } @@ -18,6 +19,7 @@ import Testing #expect(GatewayAgentChannel(raw: nil) == .last) #expect(GatewayAgentChannel(raw: " ") == .last) #expect(GatewayAgentChannel(raw: "WEBCHAT") == .webchat) + #expect(GatewayAgentChannel(raw: "BLUEBUBBLES") == .bluebubbles) #expect(GatewayAgentChannel(raw: "unknown") == .last) } } diff --git a/apps/macos/Tests/ClawdbotIPCTests/GatewayChannelConfigureTests.swift b/apps/macos/Tests/ClawdbotIPCTests/GatewayChannelConfigureTests.swift index 22c83d4fd..8ae9c3c0d 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/GatewayChannelConfigureTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/GatewayChannelConfigureTests.swift @@ -1,3 +1,4 @@ +import ClawdbotKit import Foundation import os import Testing diff --git a/apps/macos/Tests/ClawdbotIPCTests/GatewayChannelConnectTests.swift b/apps/macos/Tests/ClawdbotIPCTests/GatewayChannelConnectTests.swift index 5b1892d80..624392cb9 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/GatewayChannelConnectTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/GatewayChannelConnectTests.swift @@ -1,3 +1,4 @@ +import ClawdbotKit import Foundation import os import Testing diff --git a/apps/macos/Tests/ClawdbotIPCTests/GatewayChannelRequestTests.swift b/apps/macos/Tests/ClawdbotIPCTests/GatewayChannelRequestTests.swift index d231bed4b..1e7a1740e 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/GatewayChannelRequestTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/GatewayChannelRequestTests.swift @@ -1,3 +1,4 @@ +import ClawdbotKit import Foundation import os import Testing diff --git a/apps/macos/Tests/ClawdbotIPCTests/GatewayChannelShutdownTests.swift b/apps/macos/Tests/ClawdbotIPCTests/GatewayChannelShutdownTests.swift index 0f1560cbd..be6ab91f8 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/GatewayChannelShutdownTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/GatewayChannelShutdownTests.swift @@ -1,3 +1,4 @@ +import ClawdbotKit import Foundation import os import Testing diff --git a/apps/macos/Tests/ClawdbotIPCTests/GatewayConnectionControlTests.swift b/apps/macos/Tests/ClawdbotIPCTests/GatewayConnectionControlTests.swift index 83d01b96f..a7b1c857b 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/GatewayConnectionControlTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/GatewayConnectionControlTests.swift @@ -1,3 +1,4 @@ +import ClawdbotKit import Foundation import Testing @testable import Clawdbot diff --git a/apps/macos/Tests/ClawdbotIPCTests/GatewayDiscoveryModelTests.swift b/apps/macos/Tests/ClawdbotIPCTests/GatewayDiscoveryModelTests.swift index 214cb7390..55c128a2f 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/GatewayDiscoveryModelTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/GatewayDiscoveryModelTests.swift @@ -48,7 +48,7 @@ struct GatewayDiscoveryModelTests { lanHost: "other.local", tailnetDns: "other.tailnet.example", displayName: "Other Mac", - serviceName: "other-bridge", + serviceName: "other-gateway", local: local)) } @@ -60,7 +60,7 @@ struct GatewayDiscoveryModelTests { lanHost: nil, tailnetDns: nil, displayName: nil, - serviceName: "studio-bridge", + serviceName: "studio-gateway", local: local)) } diff --git a/apps/macos/Tests/ClawdbotIPCTests/GatewayEndpointStoreTests.swift b/apps/macos/Tests/ClawdbotIPCTests/GatewayEndpointStoreTests.swift index da310a8b3..3513388a2 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/GatewayEndpointStoreTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/GatewayEndpointStoreTests.swift @@ -139,4 +139,40 @@ import Testing let resolved = ConnectionModeResolver.resolve(root: root, defaults: defaults) #expect(resolved.mode == .remote) } + + @Test func resolveLocalGatewayHostUsesLoopbackForAutoEvenWithTailnet() { + let host = GatewayEndpointStore._testResolveLocalGatewayHost( + bindMode: "auto", + tailscaleIP: "100.64.1.2") + #expect(host == "127.0.0.1") + } + + @Test func resolveLocalGatewayHostUsesLoopbackForAutoWithoutTailnet() { + let host = GatewayEndpointStore._testResolveLocalGatewayHost( + bindMode: "auto", + tailscaleIP: nil) + #expect(host == "127.0.0.1") + } + + @Test func resolveLocalGatewayHostPrefersTailnetForTailnetMode() { + let host = GatewayEndpointStore._testResolveLocalGatewayHost( + bindMode: "tailnet", + tailscaleIP: "100.64.1.5") + #expect(host == "100.64.1.5") + } + + @Test func resolveLocalGatewayHostFallsBackToLoopbackForTailnetMode() { + let host = GatewayEndpointStore._testResolveLocalGatewayHost( + bindMode: "tailnet", + tailscaleIP: nil) + #expect(host == "127.0.0.1") + } + + @Test func resolveLocalGatewayHostUsesCustomBindHost() { + let host = GatewayEndpointStore._testResolveLocalGatewayHost( + bindMode: "custom", + tailscaleIP: "100.64.1.9", + customBindHost: "192.168.1.10") + #expect(host == "192.168.1.10") + } } diff --git a/apps/macos/Tests/ClawdbotIPCTests/GatewayEnvironmentTests.swift b/apps/macos/Tests/ClawdbotIPCTests/GatewayEnvironmentTests.swift index 36bed4aa6..9446919e7 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/GatewayEnvironmentTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/GatewayEnvironmentTests.swift @@ -48,7 +48,10 @@ import Testing @Test func expectedGatewayVersionFromStringUsesParser() { #expect(GatewayEnvironment.expectedGatewayVersion(from: "v9.1.2") == Semver(major: 9, minor: 1, patch: 2)) - #expect(GatewayEnvironment.expectedGatewayVersion(from: "2026.1.11-4") == Semver(major: 2026, minor: 1, patch: 11)) + #expect(GatewayEnvironment.expectedGatewayVersion(from: "2026.1.11-4") == Semver( + major: 2026, + minor: 1, + patch: 11)) #expect(GatewayEnvironment.expectedGatewayVersion(from: nil) == nil) } } diff --git a/apps/macos/Tests/ClawdbotIPCTests/GatewayLaunchAgentManagerTests.swift b/apps/macos/Tests/ClawdbotIPCTests/GatewayLaunchAgentManagerTests.swift index ae8357b0c..af41a2a8e 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/GatewayLaunchAgentManagerTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/GatewayLaunchAgentManagerTests.swift @@ -4,7 +4,7 @@ import Testing @Suite struct GatewayLaunchAgentManagerTests { @Test func launchAgentPlistSnapshotParsesArgsAndEnv() throws { - let url = FileManager.default.temporaryDirectory + let url = FileManager().temporaryDirectory .appendingPathComponent("clawdbot-launchd-\(UUID().uuidString).plist") let plist: [String: Any] = [ "ProgramArguments": ["clawdbot", "gateway-daemon", "--port", "18789", "--bind", "loopback"], @@ -15,7 +15,7 @@ import Testing ] let data = try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0) try data.write(to: url, options: [.atomic]) - defer { try? FileManager.default.removeItem(at: url) } + defer { try? FileManager().removeItem(at: url) } let snapshot = try #require(LaunchAgentPlist.snapshot(url: url)) #expect(snapshot.port == 18789) @@ -25,14 +25,14 @@ import Testing } @Test func launchAgentPlistSnapshotAllowsMissingBind() throws { - let url = FileManager.default.temporaryDirectory + let url = FileManager().temporaryDirectory .appendingPathComponent("clawdbot-launchd-\(UUID().uuidString).plist") let plist: [String: Any] = [ "ProgramArguments": ["clawdbot", "gateway-daemon", "--port", "18789"], ] let data = try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0) try data.write(to: url, options: [.atomic]) - defer { try? FileManager.default.removeItem(at: url) } + defer { try? FileManager().removeItem(at: url) } let snapshot = try #require(LaunchAgentPlist.snapshot(url: url)) #expect(snapshot.port == 18789) diff --git a/apps/macos/Tests/ClawdbotIPCTests/GatewayProcessManagerTests.swift b/apps/macos/Tests/ClawdbotIPCTests/GatewayProcessManagerTests.swift new file mode 100644 index 000000000..05c96f8be --- /dev/null +++ b/apps/macos/Tests/ClawdbotIPCTests/GatewayProcessManagerTests.swift @@ -0,0 +1,147 @@ +import ClawdbotKit +import Foundation +import os +import Testing +@testable import Clawdbot + +@Suite(.serialized) +@MainActor +struct GatewayProcessManagerTests { + private final class FakeWebSocketTask: WebSocketTasking, @unchecked Sendable { + private let connectRequestID = OSAllocatedUnfairLock(initialState: nil) + private let pendingReceiveHandler = + OSAllocatedUnfairLock<(@Sendable (Result) + -> Void)?>(initialState: nil) + private let cancelCount = OSAllocatedUnfairLock(initialState: 0) + private let sendCount = OSAllocatedUnfairLock(initialState: 0) + + var state: URLSessionTask.State = .suspended + + func resume() { + self.state = .running + } + + func cancel(with closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { + _ = (closeCode, reason) + self.state = .canceling + self.cancelCount.withLock { $0 += 1 } + let handler = self.pendingReceiveHandler.withLock { handler in + defer { handler = nil } + return handler + } + handler?(Result.failure(URLError(.cancelled))) + } + + func send(_ message: URLSessionWebSocketTask.Message) async throws { + let currentSendCount = self.sendCount.withLock { count in + defer { count += 1 } + return count + } + + if currentSendCount == 0 { + guard case let .data(data) = message else { return } + if let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + (obj["type"] as? String) == "req", + (obj["method"] as? String) == "connect", + let id = obj["id"] as? String + { + self.connectRequestID.withLock { $0 = id } + } + return + } + + guard case let .data(data) = message else { return } + guard + let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + (obj["type"] as? String) == "req", + let id = obj["id"] as? String + else { + return + } + + let response = Self.responseData(id: id) + let handler = self.pendingReceiveHandler.withLock { $0 } + handler?(Result.success(.data(response))) + } + + func receive() async throws -> URLSessionWebSocketTask.Message { + let id = self.connectRequestID.withLock { $0 } ?? "connect" + return .data(Self.connectOkData(id: id)) + } + + func receive( + completionHandler: @escaping @Sendable (Result) -> Void) + { + self.pendingReceiveHandler.withLock { $0 = completionHandler } + } + + private static func connectOkData(id: String) -> Data { + let json = """ + { + "type": "res", + "id": "\(id)", + "ok": true, + "payload": { + "type": "hello-ok", + "protocol": 2, + "server": { "version": "test", "connId": "test" }, + "features": { "methods": [], "events": [] }, + "snapshot": { + "presence": [ { "ts": 1 } ], + "health": {}, + "stateVersion": { "presence": 0, "health": 0 }, + "uptimeMs": 0 + }, + "policy": { "maxPayload": 1, "maxBufferedBytes": 1, "tickIntervalMs": 30000 } + } + } + """ + return Data(json.utf8) + } + + private static func responseData(id: String) -> Data { + let json = """ + { + "type": "res", + "id": "\(id)", + "ok": true, + "payload": { "ok": true } + } + """ + return Data(json.utf8) + } + } + + private final class FakeWebSocketSession: WebSocketSessioning, @unchecked Sendable { + private let tasks = OSAllocatedUnfairLock(initialState: [FakeWebSocketTask]()) + + func makeWebSocketTask(url: URL) -> WebSocketTaskBox { + _ = url + let task = FakeWebSocketTask() + self.tasks.withLock { $0.append(task) } + return WebSocketTaskBox(task: task) + } + } + + @Test func clearsLastFailureWhenHealthSucceeds() async { + let session = FakeWebSocketSession() + let url = URL(string: "ws://example.invalid")! + let connection = GatewayConnection( + configProvider: { (url: url, token: nil, password: nil) }, + sessionBox: WebSocketSessionBox(session: session)) + + let manager = GatewayProcessManager.shared + manager.setTestingConnection(connection) + manager.setTestingDesiredActive(true) + manager.setTestingLastFailureReason("health failed") + defer { + manager.setTestingConnection(nil) + manager.setTestingDesiredActive(false) + manager.setTestingLastFailureReason(nil) + } + + let ready = await manager.waitForGatewayReady(timeout: 0.5) + #expect(ready) + #expect(manager.lastFailureReason == nil) + } +} diff --git a/apps/macos/Tests/ClawdbotIPCTests/LogLocatorTests.swift b/apps/macos/Tests/ClawdbotIPCTests/LogLocatorTests.swift index bc29f9e2b..6d87208ea 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/LogLocatorTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/LogLocatorTests.swift @@ -5,7 +5,7 @@ import Testing @Suite struct LogLocatorTests { @Test func launchdGatewayLogPathEnsuresTmpDirExists() throws { - let fm = FileManager.default + let fm = FileManager() let baseDir = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) let logDir = baseDir.appendingPathComponent("clawdbot-tests-\(UUID().uuidString)") diff --git a/apps/macos/Tests/ClawdbotIPCTests/LowCoverageHelperTests.swift b/apps/macos/Tests/ClawdbotIPCTests/LowCoverageHelperTests.swift index b68f98cb9..16d919225 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/LowCoverageHelperTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/LowCoverageHelperTests.swift @@ -7,15 +7,17 @@ import Testing @Suite(.serialized) struct LowCoverageHelperTests { + private typealias ProtoAnyCodable = ClawdbotProtocol.AnyCodable + @Test func anyCodableHelperAccessors() throws { - let payload: [String: AnyCodable] = [ - "title": AnyCodable("Hello"), - "flag": AnyCodable(true), - "count": AnyCodable(3), - "ratio": AnyCodable(1.25), - "list": AnyCodable([AnyCodable("a"), AnyCodable(2)]), + let payload: [String: ProtoAnyCodable] = [ + "title": ProtoAnyCodable("Hello"), + "flag": ProtoAnyCodable(true), + "count": ProtoAnyCodable(3), + "ratio": ProtoAnyCodable(1.25), + "list": ProtoAnyCodable([ProtoAnyCodable("a"), ProtoAnyCodable(2)]), ] - let any = AnyCodable(payload) + let any = ProtoAnyCodable(payload) let dict = try #require(any.dictionaryValue) #expect(dict["title"]?.stringValue == "Hello") #expect(dict["flag"]?.boolValue == true) @@ -76,31 +78,27 @@ struct LowCoverageHelperTests { #expect(result.stderr.contains("stderr-1999")) } - @Test func pairedNodesStorePersists() async throws { - let dir = FileManager.default.temporaryDirectory - .appendingPathComponent("paired-\(UUID().uuidString)", isDirectory: true) - try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) - let url = dir.appendingPathComponent("nodes.json") - let store = PairedNodesStore(fileURL: url) - await store.load() - #expect(await store.all().isEmpty) - - let node = PairedNode( + @Test func nodeInfoCodableRoundTrip() throws { + let info = NodeInfo( nodeId: "node-1", displayName: "Node One", platform: "macOS", version: "1.0", + coreVersion: "1.0-core", + uiVersion: "1.0-ui", deviceFamily: "Mac", modelIdentifier: "MacBookPro", - token: "token", - createdAtMs: 1, - lastSeenAtMs: nil) - try await store.upsert(node) - #expect(await store.find(nodeId: "node-1")?.displayName == "Node One") - - try await store.touchSeen(nodeId: "node-1") - let updated = await store.find(nodeId: "node-1") - #expect(updated?.lastSeenAtMs != nil) + remoteIp: "192.168.1.2", + caps: ["chat"], + commands: ["send"], + permissions: ["send": true], + paired: true, + connected: false) + let data = try JSONEncoder().encode(info) + let decoded = try JSONDecoder().decode(NodeInfo.self, from: data) + #expect(decoded.nodeId == "node-1") + #expect(decoded.isPaired == true) + #expect(decoded.isConnected == false) } @Test @MainActor func presenceReporterHelpers() { @@ -143,12 +141,12 @@ struct LowCoverageHelperTests { } @Test @MainActor func canvasSchemeHandlerResolvesFilesAndErrors() throws { - let root = FileManager.default.temporaryDirectory + let root = FileManager().temporaryDirectory .appendingPathComponent("canvas-\(UUID().uuidString)", isDirectory: true) - defer { try? FileManager.default.removeItem(at: root) } - try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) + defer { try? FileManager().removeItem(at: root) } + try FileManager().createDirectory(at: root, withIntermediateDirectories: true) let session = root.appendingPathComponent("main", isDirectory: true) - try FileManager.default.createDirectory(at: session, withIntermediateDirectories: true) + try FileManager().createDirectory(at: session, withIntermediateDirectories: true) let index = session.appendingPathComponent("index.html") try "

Hello

".write(to: index, atomically: true, encoding: .utf8) diff --git a/apps/macos/Tests/ClawdbotIPCTests/MacGatewayChatTransportMappingTests.swift b/apps/macos/Tests/ClawdbotIPCTests/MacGatewayChatTransportMappingTests.swift index 00cff847b..3e24dc5cb 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/MacGatewayChatTransportMappingTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/MacGatewayChatTransportMappingTests.swift @@ -21,6 +21,7 @@ import Testing features: [:], snapshot: snapshot, canvashosturl: nil, + auth: nil, policy: [:]) let mapped = MacGatewayChatTransport.mapPushToTransportEvent(.snapshot(hello)) diff --git a/apps/macos/Tests/ClawdbotIPCTests/MacNodeBridgeDiscoveryTests.swift b/apps/macos/Tests/ClawdbotIPCTests/MacNodeBridgeDiscoveryTests.swift deleted file mode 100644 index 3863f331c..000000000 --- a/apps/macos/Tests/ClawdbotIPCTests/MacNodeBridgeDiscoveryTests.swift +++ /dev/null @@ -1,215 +0,0 @@ -import Darwin -import Foundation -import Network -import Testing -@testable import Clawdbot - -@Suite struct MacNodeBridgeDiscoveryTests { - @MainActor - @Test func loopbackBridgePortDefaultsAndOverrides() { - withEnv("CLAWDBOT_BRIDGE_PORT", value: nil) { - #expect(MacNodeModeCoordinator.loopbackBridgePort() == 18790) - } - withEnv("CLAWDBOT_BRIDGE_PORT", value: "19991") { - #expect(MacNodeModeCoordinator.loopbackBridgePort() == 19991) - } - withEnv("CLAWDBOT_BRIDGE_PORT", value: "not-a-port") { - #expect(MacNodeModeCoordinator.loopbackBridgePort() == 18790) - } - } - - @MainActor - @Test func probeEndpointSucceedsForOpenPort() async throws { - let listener = try NWListener(using: .tcp, on: .any) - listener.newConnectionHandler = { connection in - connection.cancel() - } - listener.start(queue: DispatchQueue(label: "com.clawdbot.tests.bridge-listener")) - try await waitForListenerReady(listener, timeoutSeconds: 1.0) - - guard let port = listener.port else { - listener.cancel() - throw TestError(message: "listener port missing") - } - - let endpoint = NWEndpoint.hostPort(host: "127.0.0.1", port: port) - let ok = await MacNodeModeCoordinator.probeEndpoint(endpoint, timeoutSeconds: 0.6) - listener.cancel() - #expect(ok == true) - } - - @MainActor - @Test func probeEndpointFailsForClosedPort() async throws { - let port = try reserveEphemeralPort() - let endpoint = NWEndpoint.hostPort(host: "127.0.0.1", port: port) - let ok = await MacNodeModeCoordinator.probeEndpoint(endpoint, timeoutSeconds: 0.4) - #expect(ok == false) - } - - @MainActor - @Test func remoteBridgePortUsesMatchingRemoteUrlPort() { - let configPath = FileManager.default.temporaryDirectory - .appendingPathComponent("clawdbot-config-\(UUID().uuidString)") - .appendingPathComponent("clawdbot.json") - .path - - let defaults = UserDefaults.standard - let prevTarget = defaults.string(forKey: remoteTargetKey) - defer { - if let prevTarget { - defaults.set(prevTarget, forKey: remoteTargetKey) - } else { - defaults.removeObject(forKey: remoteTargetKey) - } - } - - withEnv("CLAWDBOT_CONFIG_PATH", value: configPath) { - withEnv("CLAWDBOT_GATEWAY_PORT", value: "20000") { - defaults.set("user@bridge.ts.net", forKey: remoteTargetKey) - ClawdbotConfigFile.saveDict([ - "gateway": [ - "remote": [ - "url": "ws://bridge.ts.net:25000", - ], - ], - ]) - #expect(MacNodeModeCoordinator.remoteBridgePort() == 25001) - } - } - } - - @MainActor - @Test func remoteBridgePortFallsBackWhenRemoteUrlHostMismatch() { - let configPath = FileManager.default.temporaryDirectory - .appendingPathComponent("clawdbot-config-\(UUID().uuidString)") - .appendingPathComponent("clawdbot.json") - .path - - let defaults = UserDefaults.standard - let prevTarget = defaults.string(forKey: remoteTargetKey) - defer { - if let prevTarget { - defaults.set(prevTarget, forKey: remoteTargetKey) - } else { - defaults.removeObject(forKey: remoteTargetKey) - } - } - - withEnv("CLAWDBOT_CONFIG_PATH", value: configPath) { - withEnv("CLAWDBOT_GATEWAY_PORT", value: "20000") { - defaults.set("user@other.ts.net", forKey: remoteTargetKey) - ClawdbotConfigFile.saveDict([ - "gateway": [ - "remote": [ - "url": "ws://bridge.ts.net:25000", - ], - ], - ]) - #expect(MacNodeModeCoordinator.remoteBridgePort() == 20001) - } - } - } -} - -private struct TestError: Error { - let message: String -} - -private struct ListenerTimeoutError: Error {} - -private func waitForListenerReady(_ listener: NWListener, timeoutSeconds: Double) async throws { - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { - try await withCheckedThrowingContinuation { cont in - final class ListenerState: @unchecked Sendable { - let lock = NSLock() - var finished = false - } - let state = ListenerState() - let finish: @Sendable (Result) -> Void = { result in - state.lock.lock() - defer { state.lock.unlock() } - guard !state.finished else { return } - state.finished = true - cont.resume(with: result) - } - - listener.stateUpdateHandler = { state in - switch state { - case .ready: - finish(.success(())) - case let .failed(err): - finish(.failure(err)) - case .cancelled: - finish(.failure(ListenerTimeoutError())) - default: - break - } - } - } - } - group.addTask { - try await Task.sleep(nanoseconds: UInt64(timeoutSeconds * 1_000_000_000)) - throw ListenerTimeoutError() - } - _ = try await group.next() - group.cancelAll() - } -} - -private func withEnv(_ key: String, value: String?, _ body: () -> Void) { - let existing = getenv(key).map { String(cString: $0) } - if let value { - setenv(key, value, 1) - } else { - unsetenv(key) - } - defer { - if let existing { - setenv(key, existing, 1) - } else { - unsetenv(key) - } - } - body() -} - -private func reserveEphemeralPort() throws -> NWEndpoint.Port { - let fd = socket(AF_INET, SOCK_STREAM, 0) - if fd < 0 { - throw TestError(message: "socket failed") - } - defer { close(fd) } - - var addr = sockaddr_in() - addr.sin_len = UInt8(MemoryLayout.size) - addr.sin_family = sa_family_t(AF_INET) - addr.sin_port = in_port_t(0) - addr.sin_addr = in_addr(s_addr: inet_addr("127.0.0.1")) - - let bindResult = withUnsafePointer(to: &addr) { pointer -> Int32 in - pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { - Darwin.bind(fd, $0, socklen_t(MemoryLayout.size)) - } - } - if bindResult != 0 { - throw TestError(message: "bind failed") - } - - var resolved = sockaddr_in() - var length = socklen_t(MemoryLayout.size) - let nameResult = withUnsafeMutablePointer(to: &resolved) { pointer -> Int32 in - pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { - getsockname(fd, $0, &length) - } - } - if nameResult != 0 { - throw TestError(message: "getsockname failed") - } - - let port = UInt16(bigEndian: resolved.sin_port) - guard let endpointPort = NWEndpoint.Port(rawValue: port), endpointPort.rawValue != 0 else { - throw TestError(message: "ephemeral port missing") - } - return endpointPort -} diff --git a/apps/macos/Tests/ClawdbotIPCTests/MacNodeBridgeSessionTests.swift b/apps/macos/Tests/ClawdbotIPCTests/MacNodeBridgeSessionTests.swift deleted file mode 100644 index e7f2b8651..000000000 --- a/apps/macos/Tests/ClawdbotIPCTests/MacNodeBridgeSessionTests.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation -import Testing -@testable import Clawdbot - -@Suite -struct MacNodeBridgeSessionTests { - @Test func sendEventThrowsWhenNotConnected() async { - let session = MacNodeBridgeSession() - - do { - try await session.sendEvent(event: "test", payloadJSON: "{}") - Issue.record("Expected sendEvent to throw when disconnected") - } catch { - let ns = error as NSError - #expect(ns.domain == "Bridge") - #expect(ns.code == 15) - } - } -} diff --git a/apps/macos/Tests/ClawdbotIPCTests/MacNodeRuntimeTests.swift b/apps/macos/Tests/ClawdbotIPCTests/MacNodeRuntimeTests.swift index 12d03c185..6c7343725 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/MacNodeRuntimeTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/MacNodeRuntimeTests.swift @@ -59,7 +59,7 @@ struct MacNodeRuntimeTests { includeAudio: Bool?, outPath: String?) async throws -> (path: String, hasAudio: Bool) { - let url = FileManager.default.temporaryDirectory + let url = FileManager().temporaryDirectory .appendingPathComponent("clawdbot-test-screen-record-\(UUID().uuidString).mp4") try Data("ok".utf8).write(to: url) return (path: url.path, hasAudio: false) diff --git a/apps/macos/Tests/ClawdbotIPCTests/ModelCatalogLoaderTests.swift b/apps/macos/Tests/ClawdbotIPCTests/ModelCatalogLoaderTests.swift index d446dad64..7b87dc5ec 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/ModelCatalogLoaderTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/ModelCatalogLoaderTests.swift @@ -19,9 +19,9 @@ struct ModelCatalogLoaderTests { }; """ - let tmp = FileManager.default.temporaryDirectory + let tmp = FileManager().temporaryDirectory .appendingPathComponent("models-\(UUID().uuidString).ts") - defer { try? FileManager.default.removeItem(at: tmp) } + defer { try? FileManager().removeItem(at: tmp) } try src.write(to: tmp, atomically: true, encoding: .utf8) let choices = try await ModelCatalogLoader.load(from: tmp.path) @@ -42,9 +42,9 @@ struct ModelCatalogLoaderTests { @Test func loadWithNoExportReturnsEmptyChoices() async throws { let src = "const NOPE = 1;" - let tmp = FileManager.default.temporaryDirectory + let tmp = FileManager().temporaryDirectory .appendingPathComponent("models-\(UUID().uuidString).ts") - defer { try? FileManager.default.removeItem(at: tmp) } + defer { try? FileManager().removeItem(at: tmp) } try src.write(to: tmp, atomically: true, encoding: .utf8) let choices = try await ModelCatalogLoader.load(from: tmp.path) diff --git a/apps/macos/Tests/ClawdbotIPCTests/NodeManagerPathsTests.swift b/apps/macos/Tests/ClawdbotIPCTests/NodeManagerPathsTests.swift index 1300cbf7e..bd4a3c0f9 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/NodeManagerPathsTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/NodeManagerPathsTests.swift @@ -6,16 +6,16 @@ import Testing private func makeTempDir() throws -> URL { let base = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) let dir = base.appendingPathComponent(UUID().uuidString, isDirectory: true) - try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) + try FileManager().createDirectory(at: dir, withIntermediateDirectories: true) return dir } private func makeExec(at path: URL) throws { - try FileManager.default.createDirectory( + try FileManager().createDirectory( at: path.deletingLastPathComponent(), withIntermediateDirectories: true) - FileManager.default.createFile(atPath: path.path, contents: Data("echo ok\n".utf8)) - try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: path.path) + FileManager().createFile(atPath: path.path, contents: Data("echo ok\n".utf8)) + try FileManager().setAttributes([.posixPermissions: 0o755], ofItemAtPath: path.path) } @Test func fnmNodeBinsPreferNewestInstalledVersion() throws { @@ -37,7 +37,7 @@ import Testing let home = try self.makeTempDir() let missingNodeBin = home .appendingPathComponent(".local/share/fnm/node-versions/v99.0.0/installation/bin") - try FileManager.default.createDirectory(at: missingNodeBin, withIntermediateDirectories: true) + try FileManager().createDirectory(at: missingNodeBin, withIntermediateDirectories: true) let bins = CommandResolver._testNodeManagerBinPaths(home: home) #expect(!bins.contains(missingNodeBin.path)) diff --git a/apps/macos/Tests/ClawdbotIPCTests/OnboardingWizardStepViewTests.swift b/apps/macos/Tests/ClawdbotIPCTests/OnboardingWizardStepViewTests.swift index 7fd9b9929..3ce965325 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/OnboardingWizardStepViewTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/OnboardingWizardStepViewTests.swift @@ -3,13 +3,15 @@ import SwiftUI import Testing @testable import Clawdbot +private typealias ProtoAnyCodable = ClawdbotProtocol.AnyCodable + @Suite(.serialized) @MainActor struct OnboardingWizardStepViewTests { @Test func noteStepBuilds() { let step = WizardStep( id: "step-1", - type: AnyCodable("note"), + type: ProtoAnyCodable("note"), title: "Welcome", message: "Hello", options: nil, @@ -22,17 +24,17 @@ struct OnboardingWizardStepViewTests { } @Test func selectStepBuilds() { - let options: [[String: AnyCodable]] = [ - ["value": AnyCodable("local"), "label": AnyCodable("Local"), "hint": AnyCodable("This Mac")], - ["value": AnyCodable("remote"), "label": AnyCodable("Remote")], + let options: [[String: ProtoAnyCodable]] = [ + ["value": ProtoAnyCodable("local"), "label": ProtoAnyCodable("Local"), "hint": ProtoAnyCodable("This Mac")], + ["value": ProtoAnyCodable("remote"), "label": ProtoAnyCodable("Remote")], ] let step = WizardStep( id: "step-2", - type: AnyCodable("select"), + type: ProtoAnyCodable("select"), title: "Mode", message: "Choose a mode", options: options, - initialvalue: AnyCodable("local"), + initialvalue: ProtoAnyCodable("local"), placeholder: nil, sensitive: nil, executor: nil) diff --git a/apps/macos/Tests/ClawdbotIPCTests/RuntimeLocatorTests.swift b/apps/macos/Tests/ClawdbotIPCTests/RuntimeLocatorTests.swift index c1bcc957f..81d2c7494 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/RuntimeLocatorTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/RuntimeLocatorTests.swift @@ -6,10 +6,10 @@ import Testing private func makeTempExecutable(contents: String) throws -> URL { let dir = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) .appendingPathComponent(UUID().uuidString, isDirectory: true) - try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) + try FileManager().createDirectory(at: dir, withIntermediateDirectories: true) let path = dir.appendingPathComponent("node") try contents.write(to: path, atomically: true, encoding: .utf8) - try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: path.path) + try FileManager().setAttributes([.posixPermissions: 0o755], ofItemAtPath: path.path) return path } diff --git a/apps/macos/Tests/ClawdbotIPCTests/SessionMenuPreviewTests.swift b/apps/macos/Tests/ClawdbotIPCTests/SessionMenuPreviewTests.swift index b1d7b462c..af25d5246 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/SessionMenuPreviewTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/SessionMenuPreviewTests.swift @@ -7,20 +7,22 @@ struct SessionMenuPreviewTests { @Test func loaderReturnsCachedItems() async { await SessionPreviewCache.shared._testReset() let items = [SessionPreviewItem(id: "1", role: .user, text: "Hi")] - await SessionPreviewCache.shared._testSet(items: items, for: "main") + let snapshot = SessionMenuPreviewSnapshot(items: items, status: .ready) + await SessionPreviewCache.shared._testSet(snapshot: snapshot, for: "main") - let snapshot = await SessionMenuPreviewLoader.load(sessionKey: "main", maxItems: 10) - #expect(snapshot.status == .ready) - #expect(snapshot.items.count == 1) - #expect(snapshot.items.first?.text == "Hi") + let loaded = await SessionMenuPreviewLoader.load(sessionKey: "main", maxItems: 10) + #expect(loaded.status == .ready) + #expect(loaded.items.count == 1) + #expect(loaded.items.first?.text == "Hi") } @Test func loaderReturnsEmptyWhenCachedEmpty() async { await SessionPreviewCache.shared._testReset() - await SessionPreviewCache.shared._testSet(items: [], for: "main") + let snapshot = SessionMenuPreviewSnapshot(items: [], status: .empty) + await SessionPreviewCache.shared._testSet(snapshot: snapshot, for: "main") - let snapshot = await SessionMenuPreviewLoader.load(sessionKey: "main", maxItems: 10) - #expect(snapshot.status == .empty) - #expect(snapshot.items.isEmpty) + let loaded = await SessionMenuPreviewLoader.load(sessionKey: "main", maxItems: 10) + #expect(loaded.status == .empty) + #expect(loaded.items.isEmpty) } } diff --git a/apps/macos/Tests/ClawdbotIPCTests/TestIsolation.swift b/apps/macos/Tests/ClawdbotIPCTests/TestIsolation.swift index c15606a06..6207df141 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/TestIsolation.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/TestIsolation.swift @@ -109,7 +109,7 @@ enum TestIsolation { } nonisolated static func tempConfigPath() -> String { - FileManager.default.temporaryDirectory + FileManager().temporaryDirectory .appendingPathComponent("clawdbot-test-config-\(UUID().uuidString).json") .path } diff --git a/apps/macos/Tests/ClawdbotIPCTests/UtilitiesTests.swift b/apps/macos/Tests/ClawdbotIPCTests/UtilitiesTests.swift index 4a01e8aee..7b8753ef1 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/UtilitiesTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/UtilitiesTests.swift @@ -47,17 +47,17 @@ import Testing .appendingPathComponent(UUID().uuidString, isDirectory: true) let dist = tmp.appendingPathComponent("dist/index.js") let bin = tmp.appendingPathComponent("bin/clawdbot.js") - try FileManager.default.createDirectory(at: dist.deletingLastPathComponent(), withIntermediateDirectories: true) - try FileManager.default.createDirectory(at: bin.deletingLastPathComponent(), withIntermediateDirectories: true) - FileManager.default.createFile(atPath: dist.path, contents: Data()) - FileManager.default.createFile(atPath: bin.path, contents: Data()) + try FileManager().createDirectory(at: dist.deletingLastPathComponent(), withIntermediateDirectories: true) + try FileManager().createDirectory(at: bin.deletingLastPathComponent(), withIntermediateDirectories: true) + FileManager().createFile(atPath: dist.path, contents: Data()) + FileManager().createFile(atPath: bin.path, contents: Data()) let entry = CommandResolver.gatewayEntrypoint(in: tmp) #expect(entry == dist.path) } @Test func logLocatorPicksNewestLogFile() throws { - let fm = FileManager.default + let fm = FileManager() let dir = URL(fileURLWithPath: "/tmp/clawdbot", isDirectory: true) try? fm.createDirectory(at: dir, withIntermediateDirectories: true) diff --git a/apps/macos/Tests/ClawdbotIPCTests/VoiceWakeHelpersTests.swift b/apps/macos/Tests/ClawdbotIPCTests/VoiceWakeHelpersTests.swift index e7f7e06fc..49ad5a124 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/VoiceWakeHelpersTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/VoiceWakeHelpersTests.swift @@ -12,6 +12,18 @@ struct VoiceWakeHelpersTests { #expect(cleaned == defaultVoiceWakeTriggers) } + @Test func sanitizeTriggersLimitsWordLength() { + let long = String(repeating: "x", count: voiceWakeMaxWordLength + 5) + let cleaned = sanitizeVoiceWakeTriggers(["ok", long]) + #expect(cleaned[1].count == voiceWakeMaxWordLength) + } + + @Test func sanitizeTriggersLimitsWordCount() { + let words = (1...voiceWakeMaxWords + 3).map { "w\($0)" } + let cleaned = sanitizeVoiceWakeTriggers(words) + #expect(cleaned.count == voiceWakeMaxWords) + } + @Test func normalizeLocaleStripsCollation() { #expect(normalizeLocaleIdentifier("en_US@collation=phonebook") == "en_US") } diff --git a/apps/macos/Tests/ClawdbotIPCTests/WideAreaGatewayDiscoveryTests.swift b/apps/macos/Tests/ClawdbotIPCTests/WideAreaGatewayDiscoveryTests.swift index 4f081538b..d1dbf7962 100644 --- a/apps/macos/Tests/ClawdbotIPCTests/WideAreaGatewayDiscoveryTests.swift +++ b/apps/macos/Tests/ClawdbotIPCTests/WideAreaGatewayDiscoveryTests.swift @@ -20,15 +20,15 @@ struct WideAreaGatewayDiscoveryTests { let nameserver = args.first(where: { $0.hasPrefix("@") }) ?? "" if recordType == "PTR" { if nameserver == "@100.123.224.76" { - return "steipetacstudio-bridge._clawdbot-bridge._tcp.clawdbot.internal.\n" + return "steipetacstudio-gateway._clawdbot-gw._tcp.clawdbot.internal.\n" } return "" } if recordType == "SRV" { - return "0 0 18790 steipetacstudio.clawdbot.internal." + return "0 0 18789 steipetacstudio.clawdbot.internal." } if recordType == "TXT" { - return "\"displayName=Peter\\226\\128\\153s Mac Studio (Clawdbot)\" \"transport=bridge\" \"bridgePort=18790\" \"gatewayPort=18789\" \"tailnetDns=peters-mac-studio-1.sheep-coho.ts.net\" \"cliPath=/Users/steipete/clawdbot/src/entry.ts\"" + return "\"displayName=Peter\\226\\128\\153s Mac Studio (Clawdbot)\" \"gatewayPort=18789\" \"tailnetDns=peters-mac-studio-1.sheep-coho.ts.net\" \"cliPath=/Users/steipete/clawdbot/src/entry.ts\"" } return "" }) @@ -41,7 +41,7 @@ struct WideAreaGatewayDiscoveryTests { let beacon = beacons[0] let expectedDisplay = "Peter\u{2019}s Mac Studio (Clawdbot)" #expect(beacon.displayName == expectedDisplay) - #expect(beacon.bridgePort == 18790) + #expect(beacon.port == 18789) #expect(beacon.gatewayPort == 18789) #expect(beacon.tailnetDns == "peters-mac-studio-1.sheep-coho.ts.net") #expect(beacon.cliPath == "/Users/steipete/clawdbot/src/entry.ts") diff --git a/apps/shared/ClawdbotKit/Package.swift b/apps/shared/ClawdbotKit/Package.swift index b094e0a1c..076842fce 100644 --- a/apps/shared/ClawdbotKit/Package.swift +++ b/apps/shared/ClawdbotKit/Package.swift @@ -9,6 +9,7 @@ let package = Package( .macOS(.v15), ], products: [ + .library(name: "ClawdbotProtocol", targets: ["ClawdbotProtocol"]), .library(name: "ClawdbotKit", targets: ["ClawdbotKit"]), .library(name: "ClawdbotChatUI", targets: ["ClawdbotChatUI"]), ], @@ -17,9 +18,15 @@ let package = Package( .package(url: "https://github.com/gonzalezreal/textual", exact: "0.2.0"), ], targets: [ + .target( + name: "ClawdbotProtocol", + swiftSettings: [ + .enableUpcomingFeature("StrictConcurrency"), + ]), .target( name: "ClawdbotKit", dependencies: [ + "ClawdbotProtocol", .product(name: "ElevenLabsKit", package: "ElevenLabsKit"), ], resources: [ diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatModels.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatModels.swift index ab86f6b53..da18963f4 100644 --- a/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatModels.swift +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatModels.swift @@ -235,6 +235,27 @@ public struct ClawdbotChatHistoryPayload: Codable, Sendable { public let thinkingLevel: String? } +public struct ClawdbotSessionPreviewItem: Codable, Hashable, Sendable { + public let role: String + public let text: String +} + +public struct ClawdbotSessionPreviewEntry: Codable, Sendable { + public let key: String + public let status: String + public let items: [ClawdbotSessionPreviewItem] +} + +public struct ClawdbotSessionsPreviewPayload: Codable, Sendable { + public let ts: Int + public let previews: [ClawdbotSessionPreviewEntry] + + public init(ts: Int, previews: [ClawdbotSessionPreviewEntry]) { + self.ts = ts + self.previews = previews + } +} + public struct ClawdbotChatSendResponse: Codable, Sendable { public let runId: String public let status: String diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatView.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatView.swift index d243f6a96..44399a3e6 100644 --- a/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatView.swift +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatView.swift @@ -12,6 +12,7 @@ public struct ClawdbotChatView: View { @State private var scrollPosition: UUID? @State private var showSessions = false @State private var hasPerformedInitialScroll = false + @State private var isPinnedToBottom = true private let showsSessionSwitcher: Bool private let style: Style private let markdownVariant: ChatMarkdownVariant @@ -87,36 +88,28 @@ public struct ClawdbotChatView: View { private var messageList: some View { ZStack { ScrollView { - #if os(macOS) - VStack(spacing: 0) { - LazyVStack(spacing: Layout.messageSpacing) { - self.messageListRows - } - - Color.clear - .frame(height: Layout.messageListPaddingBottom) - .id(self.scrollerBottomID) - } - // Use scroll targets for stable auto-scroll without ScrollViewReader relayout glitches. - .scrollTargetLayout() - .padding(.top, Layout.messageListPaddingTop) - .padding(.horizontal, Layout.messageListPaddingHorizontal) - #else LazyVStack(spacing: Layout.messageSpacing) { self.messageListRows Color.clear + #if os(macOS) + .frame(height: Layout.messageListPaddingBottom) + #else .frame(height: Layout.messageListPaddingBottom + 1) + #endif .id(self.scrollerBottomID) } // Use scroll targets for stable auto-scroll without ScrollViewReader relayout glitches. .scrollTargetLayout() .padding(.top, Layout.messageListPaddingTop) .padding(.horizontal, Layout.messageListPaddingHorizontal) - #endif } // Keep the scroll pinned to the bottom for new messages. .scrollPosition(id: self.$scrollPosition, anchor: .bottom) + .onChange(of: self.scrollPosition) { _, position in + guard let position else { return } + self.isPinnedToBottom = position == self.scrollerBottomID + } if self.viewModel.isLoading { ProgressView() @@ -133,18 +126,26 @@ public struct ClawdbotChatView: View { guard !isLoading, !self.hasPerformedInitialScroll else { return } self.scrollPosition = self.scrollerBottomID self.hasPerformedInitialScroll = true + self.isPinnedToBottom = true } .onChange(of: self.viewModel.sessionKey) { _, _ in self.hasPerformedInitialScroll = false + self.isPinnedToBottom = true } .onChange(of: self.viewModel.messages.count) { _, _ in - guard self.hasPerformedInitialScroll else { return } + guard self.hasPerformedInitialScroll, self.isPinnedToBottom else { return } withAnimation(.snappy(duration: 0.22)) { self.scrollPosition = self.scrollerBottomID } } .onChange(of: self.viewModel.pendingRunCount) { _, _ in - guard self.hasPerformedInitialScroll else { return } + guard self.hasPerformedInitialScroll, self.isPinnedToBottom else { return } + withAnimation(.snappy(duration: 0.22)) { + self.scrollPosition = self.scrollerBottomID + } + } + .onChange(of: self.viewModel.streamingAssistantText) { _, _ in + guard self.hasPerformedInitialScroll, self.isPinnedToBottom else { return } withAnimation(.snappy(duration: 0.22)) { self.scrollPosition = self.scrollerBottomID } diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/BonjourTypes.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/BonjourTypes.swift index 8f9df3617..4af6c5668 100644 --- a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/BonjourTypes.swift +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/BonjourTypes.swift @@ -2,24 +2,24 @@ import Foundation public enum ClawdbotBonjour { // v0: internal-only, subject to rename. - public static let bridgeServiceType = "_clawdbot-bridge._tcp" - public static let bridgeServiceDomain = "local." - public static let wideAreaBridgeServiceDomain = "clawdbot.internal." + public static let gatewayServiceType = "_clawdbot-gw._tcp" + public static let gatewayServiceDomain = "local." + public static let wideAreaGatewayServiceDomain = "clawdbot.internal." - public static let bridgeServiceDomains = [ - bridgeServiceDomain, - wideAreaBridgeServiceDomain, + public static let gatewayServiceDomains = [ + gatewayServiceDomain, + wideAreaGatewayServiceDomain, ] public static func normalizeServiceDomain(_ raw: String?) -> String { let trimmed = (raw ?? "").trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.isEmpty { - return self.bridgeServiceDomain + return self.gatewayServiceDomain } let lower = trimmed.lowercased() if lower == "local" || lower == "local." { - return self.bridgeServiceDomain + return self.gatewayServiceDomain } return lower.hasSuffix(".") ? lower : (lower + ".") diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/DeviceAuthStore.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/DeviceAuthStore.swift new file mode 100644 index 000000000..80ff20c3f --- /dev/null +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/DeviceAuthStore.swift @@ -0,0 +1,107 @@ +import Foundation + +public struct DeviceAuthEntry: Codable, Sendable { + public let token: String + public let role: String + public let scopes: [String] + public let updatedAtMs: Int + + public init(token: String, role: String, scopes: [String], updatedAtMs: Int) { + self.token = token + self.role = role + self.scopes = scopes + self.updatedAtMs = updatedAtMs + } +} + +private struct DeviceAuthStoreFile: Codable { + var version: Int + var deviceId: String + var tokens: [String: DeviceAuthEntry] +} + +public enum DeviceAuthStore { + private static let fileName = "device-auth.json" + + public static func loadToken(deviceId: String, role: String) -> DeviceAuthEntry? { + guard let store = readStore(), store.deviceId == deviceId else { return nil } + let role = normalizeRole(role) + return store.tokens[role] + } + + public static func storeToken( + deviceId: String, + role: String, + token: String, + scopes: [String] = [] + ) -> DeviceAuthEntry { + let normalizedRole = normalizeRole(role) + var next = readStore() + if next?.deviceId != deviceId { + next = DeviceAuthStoreFile(version: 1, deviceId: deviceId, tokens: [:]) + } + let entry = DeviceAuthEntry( + token: token, + role: normalizedRole, + scopes: normalizeScopes(scopes), + updatedAtMs: Int(Date().timeIntervalSince1970 * 1000) + ) + if next == nil { + next = DeviceAuthStoreFile(version: 1, deviceId: deviceId, tokens: [:]) + } + next?.tokens[normalizedRole] = entry + if let store = next { + writeStore(store) + } + return entry + } + + public static func clearToken(deviceId: String, role: String) { + guard var store = readStore(), store.deviceId == deviceId else { return } + let normalizedRole = normalizeRole(role) + guard store.tokens[normalizedRole] != nil else { return } + store.tokens.removeValue(forKey: normalizedRole) + writeStore(store) + } + + private static func normalizeRole(_ role: String) -> String { + role.trimmingCharacters(in: .whitespacesAndNewlines) + } + + private static func normalizeScopes(_ scopes: [String]) -> [String] { + let trimmed = scopes + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + return Array(Set(trimmed)).sorted() + } + + private static func fileURL() -> URL { + DeviceIdentityPaths.stateDirURL() + .appendingPathComponent("identity", isDirectory: true) + .appendingPathComponent(fileName, isDirectory: false) + } + + private static func readStore() -> DeviceAuthStoreFile? { + let url = fileURL() + guard let data = try? Data(contentsOf: url) else { return nil } + guard let decoded = try? JSONDecoder().decode(DeviceAuthStoreFile.self, from: data) else { + return nil + } + guard decoded.version == 1 else { return nil } + return decoded + } + + private static func writeStore(_ store: DeviceAuthStoreFile) { + let url = fileURL() + do { + try FileManager.default.createDirectory( + at: url.deletingLastPathComponent(), + withIntermediateDirectories: true) + let data = try JSONEncoder().encode(store) + try data.write(to: url, options: [.atomic]) + try? FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: url.path) + } catch { + // best-effort only + } + } +} diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/DeviceIdentity.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/DeviceIdentity.swift new file mode 100644 index 000000000..3a3244614 --- /dev/null +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/DeviceIdentity.swift @@ -0,0 +1,110 @@ +import CryptoKit +import Foundation + +public struct DeviceIdentity: Codable, Sendable { + public var deviceId: String + public var publicKey: String + public var privateKey: String + public var createdAtMs: Int + + public init(deviceId: String, publicKey: String, privateKey: String, createdAtMs: Int) { + self.deviceId = deviceId + self.publicKey = publicKey + self.privateKey = privateKey + self.createdAtMs = createdAtMs + } +} + +enum DeviceIdentityPaths { + private static let stateDirEnv = "CLAWDBOT_STATE_DIR" + + static func stateDirURL() -> URL { + if let raw = getenv(self.stateDirEnv) { + let value = String(cString: raw).trimmingCharacters(in: .whitespacesAndNewlines) + if !value.isEmpty { + return URL(fileURLWithPath: value, isDirectory: true) + } + } + + if let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first { + return appSupport.appendingPathComponent("clawdbot", isDirectory: true) + } + + return FileManager.default.temporaryDirectory.appendingPathComponent("clawdbot", isDirectory: true) + } +} + +public enum DeviceIdentityStore { + private static let fileName = "device.json" + + public static func loadOrCreate() -> DeviceIdentity { + let url = self.fileURL() + if let data = try? Data(contentsOf: url), + let decoded = try? JSONDecoder().decode(DeviceIdentity.self, from: data), + !decoded.deviceId.isEmpty, + !decoded.publicKey.isEmpty, + !decoded.privateKey.isEmpty { + return decoded + } + let identity = self.generate() + self.save(identity) + return identity + } + + public static func signPayload(_ payload: String, identity: DeviceIdentity) -> String? { + guard let privateKeyData = Data(base64Encoded: identity.privateKey) else { return nil } + do { + let privateKey = try Curve25519.Signing.PrivateKey(rawRepresentation: privateKeyData) + let signature = try privateKey.signature(for: Data(payload.utf8)) + return self.base64UrlEncode(signature) + } catch { + return nil + } + } + + private static func generate() -> DeviceIdentity { + let privateKey = Curve25519.Signing.PrivateKey() + let publicKey = privateKey.publicKey + let publicKeyData = publicKey.rawRepresentation + let privateKeyData = privateKey.rawRepresentation + let deviceId = SHA256.hash(data: publicKeyData).compactMap { String(format: "%02x", $0) }.joined() + return DeviceIdentity( + deviceId: deviceId, + publicKey: publicKeyData.base64EncodedString(), + privateKey: privateKeyData.base64EncodedString(), + createdAtMs: Int(Date().timeIntervalSince1970 * 1000)) + } + + private static func base64UrlEncode(_ data: Data) -> String { + let base64 = data.base64EncodedString() + return base64 + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + } + + public static func publicKeyBase64Url(_ identity: DeviceIdentity) -> String? { + guard let data = Data(base64Encoded: identity.publicKey) else { return nil } + return self.base64UrlEncode(data) + } + + private static func save(_ identity: DeviceIdentity) { + let url = self.fileURL() + do { + try FileManager.default.createDirectory( + at: url.deletingLastPathComponent(), + withIntermediateDirectories: true) + let data = try JSONEncoder().encode(identity) + try data.write(to: url, options: [.atomic]) + } catch { + // best-effort only + } + } + + private static func fileURL() -> URL { + let base = DeviceIdentityPaths.stateDirURL() + return base + .appendingPathComponent("identity", isDirectory: true) + .appendingPathComponent(fileName, isDirectory: false) + } +} diff --git a/apps/macos/Sources/Clawdbot/GatewayChannel.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayChannel.swift similarity index 62% rename from apps/macos/Sources/Clawdbot/GatewayChannel.swift rename to apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayChannel.swift index 81fe805cb..db2ffa36d 100644 --- a/apps/macos/Sources/Clawdbot/GatewayChannel.swift +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayChannel.swift @@ -1,9 +1,8 @@ -import ClawdbotKit import ClawdbotProtocol import Foundation import OSLog -protocol WebSocketTasking: AnyObject { +public protocol WebSocketTasking: AnyObject { var state: URLSessionTask.State { get } func resume() func cancel(with closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) @@ -14,36 +13,41 @@ protocol WebSocketTasking: AnyObject { extension URLSessionWebSocketTask: WebSocketTasking {} -struct WebSocketTaskBox: @unchecked Sendable { - let task: any WebSocketTasking +public struct WebSocketTaskBox: @unchecked Sendable { + public let task: any WebSocketTasking + public init(task: any WebSocketTasking) { + self.task = task + } - var state: URLSessionTask.State { self.task.state } + public var state: URLSessionTask.State { self.task.state } - func resume() { self.task.resume() } + public func resume() { self.task.resume() } - func cancel(with closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { + public func cancel(with closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { self.task.cancel(with: closeCode, reason: reason) } - func send(_ message: URLSessionWebSocketTask.Message) async throws { + public func send(_ message: URLSessionWebSocketTask.Message) async throws { try await self.task.send(message) } - func receive() async throws -> URLSessionWebSocketTask.Message { + public func receive() async throws -> URLSessionWebSocketTask.Message { try await self.task.receive() } - func receive(completionHandler: @escaping @Sendable (Result) -> Void) { + public func receive( + completionHandler: @escaping @Sendable (Result) -> Void) + { self.task.receive(completionHandler: completionHandler) } } -protocol WebSocketSessioning: AnyObject { +public protocol WebSocketSessioning: AnyObject { func makeWebSocketTask(url: URL) -> WebSocketTaskBox } extension URLSession: WebSocketSessioning { - func makeWebSocketTask(url: URL) -> WebSocketTaskBox { + public func makeWebSocketTask(url: URL) -> WebSocketTaskBox { let task = self.webSocketTask(with: url) // Avoid "Message too long" receive errors for large snapshots / history payloads. task.maximumMessageSize = 16 * 1024 * 1024 // 16 MB @@ -51,14 +55,60 @@ extension URLSession: WebSocketSessioning { } } -struct WebSocketSessionBox: @unchecked Sendable { - let session: any WebSocketSessioning +public struct WebSocketSessionBox: @unchecked Sendable { + public let session: any WebSocketSessioning + + public init(session: any WebSocketSessioning) { + self.session = session + } +} + +public struct GatewayConnectOptions: Sendable { + public var role: String + public var scopes: [String] + public var caps: [String] + public var commands: [String] + public var permissions: [String: Bool] + public var clientId: String + public var clientMode: String + public var clientDisplayName: String? + + public init( + role: String, + scopes: [String], + caps: [String], + commands: [String], + permissions: [String: Bool], + clientId: String, + clientMode: String, + clientDisplayName: String?) + { + self.role = role + self.scopes = scopes + self.caps = caps + self.commands = commands + self.permissions = permissions + self.clientId = clientId + self.clientMode = clientMode + self.clientDisplayName = clientDisplayName + } +} + +public enum GatewayAuthSource: String, Sendable { + case deviceToken = "device-token" + case sharedToken = "shared-token" + case password = "password" + case none = "none" } // Avoid ambiguity with the app's own AnyCodable type. private typealias ProtoAnyCodable = ClawdbotProtocol.AnyCodable -actor GatewayChannelActor { +private enum ConnectChallengeError: Error { + case timeout +} + +public actor GatewayChannelActor { private let logger = Logger(subsystem: "com.clawdbot", category: "gateway") private var task: WebSocketTaskBox? private var pending: [String: CheckedContinuation] = [:] @@ -74,32 +124,42 @@ actor GatewayChannelActor { private var lastSeq: Int? private var lastTick: Date? private var tickIntervalMs: Double = 30000 + private var lastAuthSource: GatewayAuthSource = .none private let decoder = JSONDecoder() private let encoder = JSONEncoder() private let connectTimeoutSeconds: Double = 6 + private let connectChallengeTimeoutSeconds: Double = 0.75 private var watchdogTask: Task? private var tickTask: Task? private let defaultRequestTimeoutMs: Double = 15000 private let pushHandler: (@Sendable (GatewayPush) async -> Void)? + private let connectOptions: GatewayConnectOptions? + private let disconnectHandler: (@Sendable (String) async -> Void)? - init( + public init( url: URL, token: String?, password: String? = nil, session: WebSocketSessionBox? = nil, - pushHandler: (@Sendable (GatewayPush) async -> Void)? = nil) + pushHandler: (@Sendable (GatewayPush) async -> Void)? = nil, + connectOptions: GatewayConnectOptions? = nil, + disconnectHandler: (@Sendable (String) async -> Void)? = nil) { self.url = url self.token = token self.password = password self.session = session?.session ?? URLSession(configuration: .default) self.pushHandler = pushHandler + self.connectOptions = connectOptions + self.disconnectHandler = disconnectHandler Task { [weak self] in await self?.startWatchdog() } } - func shutdown() async { + public func authSource() -> GatewayAuthSource { self.lastAuthSource } + + public func shutdown() async { self.shouldReconnect = false self.connected = false @@ -150,7 +210,7 @@ actor GatewayChannelActor { } } - func connect() async throws { + public func connect() async throws { if self.connected, self.task?.state == .running { return } if self.isConnecting { try await withCheckedThrowingContinuation { cont in @@ -178,6 +238,7 @@ actor GatewayChannelActor { let wrapped = self.wrap(error, context: "connect to gateway @ \(self.url.absoluteString)") self.connected = false self.task?.cancel(with: .goingAway, reason: nil) + await self.disconnectHandler?("connect failed: \(wrapped.localizedDescription)") let waiters = self.connectWaiters self.connectWaiters.removeAll() for waiter in waiters { @@ -199,11 +260,22 @@ actor GatewayChannelActor { } private func sendConnect() async throws { - let osVersion = ProcessInfo.processInfo.operatingSystemVersion - let platform = "macos \(osVersion.majorVersion).\(osVersion.minorVersion).\(osVersion.patchVersion)" + let platform = InstanceIdentity.platformString let primaryLocale = Locale.preferredLanguages.first ?? Locale.current.identifier - let clientDisplayName = InstanceIdentity.displayName - let clientId = "clawdbot-macos" + let options = self.connectOptions ?? GatewayConnectOptions( + role: "operator", + scopes: ["operator.admin", "operator.approvals", "operator.pairing"], + caps: [], + commands: [], + permissions: [:], + clientId: "clawdbot-macos", + clientMode: "ui", + clientDisplayName: InstanceIdentity.displayName) + let clientDisplayName = options.clientDisplayName ?? InstanceIdentity.displayName + let clientId = options.clientId + let clientMode = options.clientMode + let role = options.role + let scopes = options.scopes let reqId = UUID().uuidString var client: [String: ProtoAnyCodable] = [ @@ -212,10 +284,10 @@ actor GatewayChannelActor { "version": ProtoAnyCodable( Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "dev"), "platform": ProtoAnyCodable(platform), - "mode": ProtoAnyCodable("ui"), + "mode": ProtoAnyCodable(clientMode), "instanceId": ProtoAnyCodable(InstanceIdentity.instanceId), ] - client["deviceFamily"] = ProtoAnyCodable("Mac") + client["deviceFamily"] = ProtoAnyCodable(InstanceIdentity.deviceFamily) if let model = InstanceIdentity.modelIdentifier { client["modelIdentifier"] = ProtoAnyCodable(model) } @@ -223,15 +295,69 @@ actor GatewayChannelActor { "minProtocol": ProtoAnyCodable(GATEWAY_PROTOCOL_VERSION), "maxProtocol": ProtoAnyCodable(GATEWAY_PROTOCOL_VERSION), "client": ProtoAnyCodable(client), - "caps": ProtoAnyCodable([] as [String]), + "caps": ProtoAnyCodable(options.caps), "locale": ProtoAnyCodable(primaryLocale), "userAgent": ProtoAnyCodable(ProcessInfo.processInfo.operatingSystemVersionString), + "role": ProtoAnyCodable(role), + "scopes": ProtoAnyCodable(scopes), ] - if let token = self.token { - params["auth"] = ProtoAnyCodable(["token": ProtoAnyCodable(token)]) + if !options.commands.isEmpty { + params["commands"] = ProtoAnyCodable(options.commands) + } + if !options.permissions.isEmpty { + params["permissions"] = ProtoAnyCodable(options.permissions) + } + let identity = DeviceIdentityStore.loadOrCreate() + let storedToken = DeviceAuthStore.loadToken(deviceId: identity.deviceId, role: role)?.token + let authToken = storedToken ?? self.token + let authSource: GatewayAuthSource + if storedToken != nil { + authSource = .deviceToken + } else if authToken != nil { + authSource = .sharedToken + } else if self.password != nil { + authSource = .password + } else { + authSource = .none + } + self.lastAuthSource = authSource + self.logger.info("gateway connect auth=\(authSource.rawValue, privacy: .public)") + let canFallbackToShared = storedToken != nil && self.token != nil + if let authToken { + params["auth"] = ProtoAnyCodable(["token": ProtoAnyCodable(authToken)]) } else if let password = self.password { params["auth"] = ProtoAnyCodable(["password": ProtoAnyCodable(password)]) } + let signedAtMs = Int(Date().timeIntervalSince1970 * 1000) + let connectNonce = try await self.waitForConnectChallenge() + let scopesValue = scopes.joined(separator: ",") + var payloadParts = [ + connectNonce == nil ? "v1" : "v2", + identity.deviceId, + clientId, + clientMode, + role, + scopesValue, + String(signedAtMs), + authToken ?? "", + ] + if let connectNonce { + payloadParts.append(connectNonce) + } + let payload = payloadParts.joined(separator: "|") + if let signature = DeviceIdentityStore.signPayload(payload, identity: identity), + let publicKey = DeviceIdentityStore.publicKeyBase64Url(identity) { + var device: [String: ProtoAnyCodable] = [ + "id": ProtoAnyCodable(identity.deviceId), + "publicKey": ProtoAnyCodable(publicKey), + "signature": ProtoAnyCodable(signature), + "signedAt": ProtoAnyCodable(signedAtMs), + ] + if let connectNonce { + device["nonce"] = ProtoAnyCodable(connectNonce) + } + params["device"] = ProtoAnyCodable(device) + } let frame = RequestFrame( type: "req", @@ -240,40 +366,22 @@ actor GatewayChannelActor { params: ProtoAnyCodable(params)) let data = try self.encoder.encode(frame) try await self.task?.send(.data(data)) - guard let msg = try await task?.receive() else { - throw NSError( - domain: "Gateway", - code: 1, - userInfo: [NSLocalizedDescriptionKey: "connect failed (no response)"]) + do { + let response = try await self.waitForConnectResponse(reqId: reqId) + try await self.handleConnectResponse(response, identity: identity, role: role) + } catch { + if canFallbackToShared { + DeviceAuthStore.clearToken(deviceId: identity.deviceId, role: role) + } + throw error } - try await self.handleConnectResponse(msg, reqId: reqId) } - private func handleConnectResponse(_ msg: URLSessionWebSocketTask.Message, reqId: String) async throws { - let data: Data? = switch msg { - case let .data(d): d - case let .string(s): s.data(using: .utf8) - @unknown default: nil - } - guard let data else { - throw NSError( - domain: "Gateway", - code: 1, - userInfo: [NSLocalizedDescriptionKey: "connect failed (empty response)"]) - } - let decoder = JSONDecoder() - guard let frame = try? decoder.decode(GatewayFrame.self, from: data) else { - throw NSError( - domain: "Gateway", - code: 1, - userInfo: [NSLocalizedDescriptionKey: "connect failed (invalid response)"]) - } - guard case let .res(res) = frame, res.id == reqId else { - throw NSError( - domain: "Gateway", - code: 1, - userInfo: [NSLocalizedDescriptionKey: "connect failed (unexpected response)"]) - } + private func handleConnectResponse( + _ res: ResponseFrame, + identity: DeviceIdentity, + role: String + ) async throws { if res.ok == false { let msg = (res.error?["message"]?.value as? String) ?? "gateway connect failed" throw NSError(domain: "Gateway", code: 1008, userInfo: [NSLocalizedDescriptionKey: msg]) @@ -291,6 +399,17 @@ actor GatewayChannelActor { } else if let tick = ok.policy["tickIntervalMs"]?.value as? Int { self.tickIntervalMs = Double(tick) } + if let auth = ok.auth, + let deviceToken = auth["deviceToken"]?.value as? String { + let authRole = auth["role"]?.value as? String ?? role + let scopes = (auth["scopes"]?.value as? [ProtoAnyCodable])? + .compactMap { $0.value as? String } ?? [] + _ = DeviceAuthStore.storeToken( + deviceId: identity.deviceId, + role: authRole, + token: deviceToken, + scopes: scopes) + } self.lastTick = Date() self.tickTask?.cancel() self.tickTask = Task { [weak self] in @@ -319,6 +438,7 @@ actor GatewayChannelActor { let wrapped = self.wrap(err, context: "gateway receive") self.logger.error("gateway ws receive failed \(wrapped.localizedDescription, privacy: .public)") self.connected = false + await self.disconnectHandler?("receive failed: \(wrapped.localizedDescription)") await self.failPending(wrapped) await self.scheduleReconnect() } @@ -341,6 +461,7 @@ actor GatewayChannelActor { waiter.resume(returning: .res(res)) } case let .event(evt): + if evt.event == "connect.challenge" { return } if let seq = evt.seq { if let last = lastSeq, seq > last + 1 { await self.pushHandler?(.seqGap(expected: last + 1, received: seq)) @@ -354,6 +475,63 @@ actor GatewayChannelActor { } } + private func waitForConnectChallenge() async throws -> String? { + guard let task = self.task else { return nil } + do { + return try await AsyncTimeout.withTimeout( + seconds: self.connectChallengeTimeoutSeconds, + onTimeout: { ConnectChallengeError.timeout }, + operation: { [weak self] in + guard let self else { return nil } + while true { + let msg = try await task.receive() + guard let data = self.decodeMessageData(msg) else { continue } + guard let frame = try? self.decoder.decode(GatewayFrame.self, from: data) else { continue } + if case let .event(evt) = frame, evt.event == "connect.challenge" { + if let payload = evt.payload?.value as? [String: ProtoAnyCodable], + let nonce = payload["nonce"]?.value as? String { + return nonce + } + } + } + }) + } catch { + if error is ConnectChallengeError { return nil } + throw error + } + } + + private func waitForConnectResponse(reqId: String) async throws -> ResponseFrame { + guard let task = self.task else { + throw NSError( + domain: "Gateway", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "connect failed (no response)"]) + } + while true { + let msg = try await task.receive() + guard let data = self.decodeMessageData(msg) else { continue } + guard let frame = try? self.decoder.decode(GatewayFrame.self, from: data) else { + throw NSError( + domain: "Gateway", + code: 1, + userInfo: [NSLocalizedDescriptionKey: "connect failed (invalid response)"]) + } + if case let .res(res) = frame, res.id == reqId { + return res + } + } + } + + private nonisolated func decodeMessageData(_ msg: URLSessionWebSocketTask.Message) -> Data? { + let data: Data? = switch msg { + case let .data(data): data + case let .string(text): text.data(using: .utf8) + @unknown default: nil + } + return data + } + private func watchTicks() async { let tolerance = self.tickIntervalMs * 2 while self.connected { @@ -391,9 +569,9 @@ actor GatewayChannelActor { } } - func request( + public func request( method: String, - params: [String: ClawdbotProtocol.AnyCodable]?, + params: [String: AnyCodable]?, timeoutMs: Double? = nil) async throws -> Data { do { @@ -415,7 +593,14 @@ actor GatewayChannelActor { id: id, method: method, params: paramsObject) - let data = try self.encoder.encode(frame) + let data: Data + do { + data = try self.encoder.encode(frame) + } catch { + self.logger.error( + "gateway request encode failed \(method, privacy: .public) error=\(error.localizedDescription, privacy: .public)") + throw error + } let response = try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in self.pending[id] = cont Task { [weak self] in @@ -446,8 +631,8 @@ actor GatewayChannelActor { if res.ok == false { let code = res.error?["code"]?.value as? String let msg = res.error?["message"]?.value as? String - let details: [String: ClawdbotProtocol.AnyCodable] = (res.error ?? [:]).reduce(into: [:]) { acc, pair in - acc[pair.key] = ClawdbotProtocol.AnyCodable(pair.value.value) + let details: [String: AnyCodable] = (res.error ?? [:]).reduce(into: [:]) { acc, pair in + acc[pair.key] = AnyCodable(pair.value.value) } throw GatewayResponseError(method: method, code: code, message: msg, details: details) } diff --git a/apps/macos/Sources/ClawdbotDiscovery/BridgeEndpointID.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayEndpointID.swift similarity index 94% rename from apps/macos/Sources/ClawdbotDiscovery/BridgeEndpointID.swift rename to apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayEndpointID.swift index c89348122..eb2e94f51 100644 --- a/apps/macos/Sources/ClawdbotDiscovery/BridgeEndpointID.swift +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayEndpointID.swift @@ -1,8 +1,7 @@ -import ClawdbotKit import Foundation import Network -public enum BridgeEndpointID { +public enum GatewayEndpointID { public static func stableID(_ endpoint: NWEndpoint) -> String { switch endpoint { case let .service(name, type, domain, _): diff --git a/apps/macos/Sources/Clawdbot/GatewayErrors.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayErrors.swift similarity index 52% rename from apps/macos/Sources/Clawdbot/GatewayErrors.swift rename to apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayErrors.swift index 961880d76..6ff8b8293 100644 --- a/apps/macos/Sources/Clawdbot/GatewayErrors.swift +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayErrors.swift @@ -2,13 +2,13 @@ import ClawdbotProtocol import Foundation /// Structured error surfaced when the gateway responds with `{ ok: false }`. -struct GatewayResponseError: LocalizedError, @unchecked Sendable { - let method: String - let code: String - let message: String - let details: [String: AnyCodable] +public struct GatewayResponseError: LocalizedError, @unchecked Sendable { + public let method: String + public let code: String + public let message: String + public let details: [String: AnyCodable] - init(method: String, code: String?, message: String?, details: [String: AnyCodable]?) { + public init(method: String, code: String?, message: String?, details: [String: AnyCodable]?) { self.method = method self.code = (code?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false) ? code!.trimmingCharacters(in: .whitespacesAndNewlines) @@ -19,15 +19,20 @@ struct GatewayResponseError: LocalizedError, @unchecked Sendable { self.details = details ?? [:] } - var errorDescription: String? { + public var errorDescription: String? { if self.code == "GATEWAY_ERROR" { return "\(self.method): \(self.message)" } return "\(self.method): [\(self.code)] \(self.message)" } } -struct GatewayDecodingError: LocalizedError, Sendable { - let method: String - let message: String +public struct GatewayDecodingError: LocalizedError, Sendable { + public let method: String + public let message: String - var errorDescription: String? { "\(self.method): \(self.message)" } + public init(method: String, message: String) { + self.method = method + self.message = message + } + + public var errorDescription: String? { "\(self.method): \(self.message)" } } diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayNodeSession.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayNodeSession.swift new file mode 100644 index 000000000..a2ac2ad6d --- /dev/null +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayNodeSession.swift @@ -0,0 +1,262 @@ +import ClawdbotProtocol +import Foundation +import OSLog + +private struct NodeInvokeRequestPayload: Codable, Sendable { + var id: String + var nodeId: String + var command: String + var paramsJSON: String? + var timeoutMs: Int? + var idempotencyKey: String? +} + +public actor GatewayNodeSession { + private let logger = Logger(subsystem: "com.clawdbot", category: "node.gateway") + private let decoder = JSONDecoder() + private let encoder = JSONEncoder() + private var channel: GatewayChannelActor? + private var activeURL: URL? + private var activeToken: String? + private var activePassword: String? + private var connectOptions: GatewayConnectOptions? + private var onConnected: (@Sendable () async -> Void)? + private var onDisconnected: (@Sendable (String) async -> Void)? + private var onInvoke: (@Sendable (BridgeInvokeRequest) async -> BridgeInvokeResponse)? + + static func invokeWithTimeout( + request: BridgeInvokeRequest, + timeoutMs: Int?, + onInvoke: @escaping @Sendable (BridgeInvokeRequest) async -> BridgeInvokeResponse + ) async -> BridgeInvokeResponse { + let timeout = max(0, timeoutMs ?? 0) + guard timeout > 0 else { + return await onInvoke(request) + } + + return await withTaskGroup(of: BridgeInvokeResponse.self) { group in + group.addTask { await onInvoke(request) } + group.addTask { + try? await Task.sleep(nanoseconds: UInt64(timeout) * 1_000_000) + return BridgeInvokeResponse( + id: request.id, + ok: false, + error: ClawdbotNodeError( + code: .unavailable, + message: "node invoke timed out") + ) + } + + let first = await group.next()! + group.cancelAll() + return first + } + } + private var serverEventSubscribers: [UUID: AsyncStream.Continuation] = [:] + private var canvasHostUrl: String? + + public init() {} + + public func connect( + url: URL, + token: String?, + password: String?, + connectOptions: GatewayConnectOptions, + sessionBox: WebSocketSessionBox?, + onConnected: @escaping @Sendable () async -> Void, + onDisconnected: @escaping @Sendable (String) async -> Void, + onInvoke: @escaping @Sendable (BridgeInvokeRequest) async -> BridgeInvokeResponse + ) async throws { + let shouldReconnect = self.activeURL != url || + self.activeToken != token || + self.activePassword != password || + self.channel == nil + + self.connectOptions = connectOptions + self.onConnected = onConnected + self.onDisconnected = onDisconnected + self.onInvoke = onInvoke + + if shouldReconnect { + if let existing = self.channel { + await existing.shutdown() + } + let channel = GatewayChannelActor( + url: url, + token: token, + password: password, + session: sessionBox, + pushHandler: { [weak self] push in + await self?.handlePush(push) + }, + connectOptions: connectOptions, + disconnectHandler: { [weak self] reason in + await self?.onDisconnected?(reason) + }) + self.channel = channel + self.activeURL = url + self.activeToken = token + self.activePassword = password + } + + guard let channel = self.channel else { + throw NSError(domain: "Gateway", code: 0, userInfo: [ + NSLocalizedDescriptionKey: "gateway channel unavailable", + ]) + } + + do { + try await channel.connect() + await onConnected() + } catch { + await onDisconnected(error.localizedDescription) + throw error + } + } + + public func disconnect() async { + await self.channel?.shutdown() + self.channel = nil + self.activeURL = nil + self.activeToken = nil + self.activePassword = nil + } + + public func currentCanvasHostUrl() -> String? { + self.canvasHostUrl + } + + public func currentRemoteAddress() -> String? { + guard let url = self.activeURL else { return nil } + guard let host = url.host else { return url.absoluteString } + let port = url.port ?? (url.scheme == "wss" ? 443 : 80) + if host.contains(":") { + return "[\(host)]:\(port)" + } + return "\(host):\(port)" + } + + public func sendEvent(event: String, payloadJSON: String?) async { + guard let channel = self.channel else { return } + let params: [String: AnyCodable] = [ + "event": AnyCodable(event), + "payloadJSON": AnyCodable(payloadJSON ?? NSNull()), + ] + do { + _ = try await channel.request(method: "node.event", params: params, timeoutMs: 8000) + } catch { + self.logger.error("node event failed: \(error.localizedDescription, privacy: .public)") + } + } + + public func request(method: String, paramsJSON: String?, timeoutSeconds: Int = 15) async throws -> Data { + guard let channel = self.channel else { + throw NSError(domain: "Gateway", code: 11, userInfo: [ + NSLocalizedDescriptionKey: "not connected", + ]) + } + + let params = try self.decodeParamsJSON(paramsJSON) + return try await channel.request( + method: method, + params: params, + timeoutMs: Double(timeoutSeconds * 1000)) + } + + public func subscribeServerEvents(bufferingNewest: Int = 200) -> AsyncStream { + let id = UUID() + let session = self + return AsyncStream(bufferingPolicy: .bufferingNewest(bufferingNewest)) { continuation in + self.serverEventSubscribers[id] = continuation + continuation.onTermination = { @Sendable _ in + Task { await session.removeServerEventSubscriber(id) } + } + } + } + + private func handlePush(_ push: GatewayPush) async { + switch push { + case let .snapshot(ok): + let raw = ok.canvashosturl?.trimmingCharacters(in: .whitespacesAndNewlines) + self.canvasHostUrl = (raw?.isEmpty == false) ? raw : nil + await self.onConnected?() + case let .event(evt): + await self.handleEvent(evt) + default: + break + } + } + + private func handleEvent(_ evt: EventFrame) async { + self.broadcastServerEvent(evt) + guard evt.event == "node.invoke.request" else { return } + guard let payload = evt.payload else { return } + do { + let data = try self.encoder.encode(payload) + let request = try self.decoder.decode(NodeInvokeRequestPayload.self, from: data) + guard let onInvoke else { return } + let req = BridgeInvokeRequest(id: request.id, command: request.command, paramsJSON: request.paramsJSON) + let response = await Self.invokeWithTimeout( + request: req, + timeoutMs: request.timeoutMs, + onInvoke: onInvoke + ) + await self.sendInvokeResult(request: request, response: response) + } catch { + self.logger.error("node invoke decode failed: \(error.localizedDescription, privacy: .public)") + } + } + + private func sendInvokeResult(request: NodeInvokeRequestPayload, response: BridgeInvokeResponse) async { + guard let channel = self.channel else { return } + var params: [String: AnyCodable] = [ + "id": AnyCodable(request.id), + "nodeId": AnyCodable(request.nodeId), + "ok": AnyCodable(response.ok), + ] + if let payloadJSON = response.payloadJSON { + params["payloadJSON"] = AnyCodable(payloadJSON) + } + if let error = response.error { + params["error"] = AnyCodable([ + "code": error.code.rawValue, + "message": error.message, + ]) + } + do { + _ = try await channel.request(method: "node.invoke.result", params: params, timeoutMs: 15000) + } catch { + self.logger.error("node invoke result failed: \(error.localizedDescription, privacy: .public)") + } + } + + private func decodeParamsJSON( + _ paramsJSON: String?) throws -> [String: AnyCodable]? + { + guard let paramsJSON, !paramsJSON.isEmpty else { return nil } + guard let data = paramsJSON.data(using: .utf8) else { + throw NSError(domain: "Gateway", code: 12, userInfo: [ + NSLocalizedDescriptionKey: "paramsJSON not UTF-8", + ]) + } + let raw = try JSONSerialization.jsonObject(with: data) + guard let dict = raw as? [String: Any] else { + return nil + } + return dict.reduce(into: [:]) { acc, entry in + acc[entry.key] = AnyCodable(entry.value) + } + } + + private func broadcastServerEvent(_ evt: EventFrame) { + for (id, continuation) in self.serverEventSubscribers { + if case .terminated = continuation.yield(evt) { + self.serverEventSubscribers.removeValue(forKey: id) + } + } + } + + private func removeServerEventSubscriber(_ id: UUID) { + self.serverEventSubscribers.removeValue(forKey: id) + } +} diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayPayloadDecoding.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayPayloadDecoding.swift new file mode 100644 index 000000000..1895189f1 --- /dev/null +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayPayloadDecoding.swift @@ -0,0 +1,36 @@ +import ClawdbotProtocol +import Foundation + +public enum GatewayPayloadDecoding { + public static func decode( + _ payload: ClawdbotProtocol.AnyCodable, + as _: T.Type = T.self) throws -> T + { + let data = try JSONEncoder().encode(payload) + return try JSONDecoder().decode(T.self, from: data) + } + + public static func decode( + _ payload: AnyCodable, + as _: T.Type = T.self) throws -> T + { + let data = try JSONEncoder().encode(payload) + return try JSONDecoder().decode(T.self, from: data) + } + + public static func decodeIfPresent( + _ payload: ClawdbotProtocol.AnyCodable?, + as _: T.Type = T.self) throws -> T? + { + guard let payload else { return nil } + return try self.decode(payload, as: T.self) + } + + public static func decodeIfPresent( + _ payload: AnyCodable?, + as _: T.Type = T.self) throws -> T? + { + guard let payload else { return nil } + return try self.decode(payload, as: T.self) + } +} diff --git a/apps/macos/Sources/Clawdbot/GatewayPush.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayPush.swift similarity index 92% rename from apps/macos/Sources/Clawdbot/GatewayPush.swift rename to apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayPush.swift index 2183977fe..1e9b0e43b 100644 --- a/apps/macos/Sources/Clawdbot/GatewayPush.swift +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayPush.swift @@ -3,7 +3,7 @@ import ClawdbotProtocol /// Server-push messages from the gateway websocket. /// /// This is the in-process replacement for the legacy `NotificationCenter` fan-out. -enum GatewayPush: Sendable { +public enum GatewayPush: Sendable { /// A full snapshot that arrives on connect (or reconnect). case snapshot(HelloOk) /// A server push event frame. diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayTLSPinning.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayTLSPinning.swift new file mode 100644 index 000000000..f22505eff --- /dev/null +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/GatewayTLSPinning.swift @@ -0,0 +1,118 @@ +import CryptoKit +import Foundation +import Security + +public struct GatewayTLSParams: Sendable { + public let required: Bool + public let expectedFingerprint: String? + public let allowTOFU: Bool + public let storeKey: String? + + public init(required: Bool, expectedFingerprint: String?, allowTOFU: Bool, storeKey: String?) { + self.required = required + self.expectedFingerprint = expectedFingerprint + self.allowTOFU = allowTOFU + self.storeKey = storeKey + } +} + +public enum GatewayTLSStore { + private static let suiteName = "com.clawdbot.shared" + private static let keyPrefix = "gateway.tls." + + private static var defaults: UserDefaults { + UserDefaults(suiteName: suiteName) ?? .standard + } + + public static func loadFingerprint(stableID: String) -> String? { + let key = self.keyPrefix + stableID + let raw = self.defaults.string(forKey: key)?.trimmingCharacters(in: .whitespacesAndNewlines) + return raw?.isEmpty == false ? raw : nil + } + + public static func saveFingerprint(_ value: String, stableID: String) { + let key = self.keyPrefix + stableID + self.defaults.set(value, forKey: key) + } +} + +public final class GatewayTLSPinningSession: NSObject, WebSocketSessioning, URLSessionDelegate, @unchecked Sendable { + private let params: GatewayTLSParams + private lazy var session: URLSession = { + let config = URLSessionConfiguration.default + config.waitsForConnectivity = true + return URLSession(configuration: config, delegate: self, delegateQueue: nil) + }() + + public init(params: GatewayTLSParams) { + self.params = params + super.init() + } + + public func makeWebSocketTask(url: URL) -> WebSocketTaskBox { + let task = self.session.webSocketTask(with: url) + task.maximumMessageSize = 16 * 1024 * 1024 + return WebSocketTaskBox(task: task) + } + + public func urlSession( + _ session: URLSession, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) { + guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, + let trust = challenge.protectionSpace.serverTrust + else { + completionHandler(.performDefaultHandling, nil) + return + } + + let expected = params.expectedFingerprint.map(normalizeFingerprint) + if let fingerprint = certificateFingerprint(trust) { + if let expected { + if fingerprint == expected { + completionHandler(.useCredential, URLCredential(trust: trust)) + } else { + completionHandler(.cancelAuthenticationChallenge, nil) + } + return + } + if params.allowTOFU { + if let storeKey = params.storeKey { + GatewayTLSStore.saveFingerprint(fingerprint, stableID: storeKey) + } + completionHandler(.useCredential, URLCredential(trust: trust)) + return + } + } + + let ok = SecTrustEvaluateWithError(trust, nil) + if ok || !params.required { + completionHandler(.useCredential, URLCredential(trust: trust)) + } else { + completionHandler(.cancelAuthenticationChallenge, nil) + } + } +} + +private func certificateFingerprint(_ trust: SecTrust) -> String? { + guard let chain = SecTrustCopyCertificateChain(trust) as? [SecCertificate], + let cert = chain.first + else { + return nil + } + return sha256Hex(SecCertificateCopyData(cert) as Data) +} + +private func sha256Hex(_ data: Data) -> String { + let digest = SHA256.hash(data: data) + return digest.map { String(format: "%02x", $0) }.joined() +} + +private func normalizeFingerprint(_ raw: String) -> String { + let stripped = raw.replacingOccurrences( + of: #"(?i)^sha-?256\s*:?\s*"#, + with: "", + options: .regularExpression) + return stripped.lowercased().filter(\.isHexDigit) +} diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/InstanceIdentity.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/InstanceIdentity.swift new file mode 100644 index 000000000..56be75e04 --- /dev/null +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/InstanceIdentity.swift @@ -0,0 +1,108 @@ +import Foundation + +#if canImport(UIKit) +import UIKit +#endif + +public enum InstanceIdentity { + private static let suiteName = "com.clawdbot.shared" + private static let instanceIdKey = "instanceId" + + private static var defaults: UserDefaults { + UserDefaults(suiteName: suiteName) ?? .standard + } + +#if canImport(UIKit) + private static func readMainActor(_ body: @MainActor () -> T) -> T { + if Thread.isMainThread { + return MainActor.assumeIsolated { body() } + } + return DispatchQueue.main.sync { + MainActor.assumeIsolated { body() } + } + } +#endif + + public static let instanceId: String = { + let defaults = Self.defaults + if let existing = defaults.string(forKey: instanceIdKey)? + .trimmingCharacters(in: .whitespacesAndNewlines), + !existing.isEmpty + { + return existing + } + + let id = UUID().uuidString.lowercased() + defaults.set(id, forKey: instanceIdKey) + return id + }() + + public static let displayName: String = { +#if canImport(UIKit) + let name = Self.readMainActor { + UIDevice.current.name.trimmingCharacters(in: .whitespacesAndNewlines) + } + return name.isEmpty ? "clawdbot" : name +#else + if let name = Host.current().localizedName?.trimmingCharacters(in: .whitespacesAndNewlines), + !name.isEmpty + { + return name + } + return "clawdbot" +#endif + }() + + public static let modelIdentifier: String? = { +#if canImport(UIKit) + var systemInfo = utsname() + uname(&systemInfo) + let machine = withUnsafeBytes(of: &systemInfo.machine) { ptr in + String(bytes: ptr.prefix { $0 != 0 }, encoding: .utf8) + } + let trimmed = machine?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + return trimmed.isEmpty ? nil : trimmed +#else + var size = 0 + guard sysctlbyname("hw.model", nil, &size, nil, 0) == 0, size > 1 else { return nil } + + var buffer = [CChar](repeating: 0, count: size) + guard sysctlbyname("hw.model", &buffer, &size, nil, 0) == 0 else { return nil } + + let bytes = buffer.prefix { $0 != 0 }.map { UInt8(bitPattern: $0) } + guard let raw = String(bytes: bytes, encoding: .utf8) else { return nil } + let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty ? nil : trimmed +#endif + }() + + public static let deviceFamily: String = { +#if canImport(UIKit) + return Self.readMainActor { + switch UIDevice.current.userInterfaceIdiom { + case .pad: return "iPad" + case .phone: return "iPhone" + default: return "iOS" + } + } +#else + return "Mac" +#endif + }() + + public static let platformString: String = { + let v = ProcessInfo.processInfo.operatingSystemVersion +#if canImport(UIKit) + let name = Self.readMainActor { + switch UIDevice.current.userInterfaceIdiom { + case .pad: return "iPadOS" + case .phone: return "iOS" + default: return "iOS" + } + } + return "\(name) \(v.majorVersion).\(v.minorVersion).\(v.patchVersion)" +#else + return "macOS \(v.majorVersion).\(v.minorVersion).\(v.patchVersion)" +#endif + }() +} diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/StoragePaths.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/StoragePaths.swift index bcea1aaab..b785efee7 100644 --- a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/StoragePaths.swift +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/StoragePaths.swift @@ -2,7 +2,7 @@ import Foundation public enum ClawdbotNodeStorage { public static func appSupportDir() throws -> URL { - let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first + let base = FileManager().urls(for: .applicationSupportDirectory, in: .userDomainMask).first guard let base else { throw NSError(domain: "ClawdbotNodeStorage", code: 1, userInfo: [ NSLocalizedDescriptionKey: "Application Support directory unavailable", @@ -19,7 +19,7 @@ public enum ClawdbotNodeStorage { } public static func cachesDir() throws -> URL { - let base = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first + let base = FileManager().urls(for: .cachesDirectory, in: .userDomainMask).first guard let base else { throw NSError(domain: "ClawdbotNodeStorage", code: 2, userInfo: [ NSLocalizedDescriptionKey: "Caches directory unavailable", diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/SystemCommands.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/SystemCommands.swift index 1de76dbc6..bfe980f41 100644 --- a/apps/shared/ClawdbotKit/Sources/ClawdbotKit/SystemCommands.swift +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotKit/SystemCommands.swift @@ -29,6 +29,8 @@ public struct ClawdbotSystemRunParams: Codable, Sendable, Equatable { public var needsScreenRecording: Bool? public var agentId: String? public var sessionKey: String? + public var approved: Bool? + public var approvalDecision: String? public init( command: [String], @@ -38,7 +40,9 @@ public struct ClawdbotSystemRunParams: Codable, Sendable, Equatable { timeoutMs: Int? = nil, needsScreenRecording: Bool? = nil, agentId: String? = nil, - sessionKey: String? = nil) + sessionKey: String? = nil, + approved: Bool? = nil, + approvalDecision: String? = nil) { self.command = command self.rawCommand = rawCommand @@ -48,6 +52,8 @@ public struct ClawdbotSystemRunParams: Codable, Sendable, Equatable { self.needsScreenRecording = needsScreenRecording self.agentId = agentId self.sessionKey = sessionKey + self.approved = approved + self.approvalDecision = approvalDecision } } diff --git a/apps/macos/Sources/ClawdbotProtocol/AnyCodable.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotProtocol/AnyCodable.swift similarity index 100% rename from apps/macos/Sources/ClawdbotProtocol/AnyCodable.swift rename to apps/shared/ClawdbotKit/Sources/ClawdbotProtocol/AnyCodable.swift diff --git a/apps/shared/ClawdbotKit/Sources/ClawdbotProtocol/GatewayModels.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotProtocol/GatewayModels.swift new file mode 100644 index 000000000..aef9a5e0e --- /dev/null +++ b/apps/shared/ClawdbotKit/Sources/ClawdbotProtocol/GatewayModels.swift @@ -0,0 +1,2442 @@ +// Generated by scripts/protocol-gen-swift.ts — do not edit by hand +import Foundation + +public let GATEWAY_PROTOCOL_VERSION = 3 + +public enum ErrorCode: String, Codable, Sendable { + case notLinked = "NOT_LINKED" + case notPaired = "NOT_PAIRED" + case agentTimeout = "AGENT_TIMEOUT" + case invalidRequest = "INVALID_REQUEST" + case unavailable = "UNAVAILABLE" +} + +public struct ConnectParams: Codable, Sendable { + public let minprotocol: Int + public let maxprotocol: Int + public let client: [String: AnyCodable] + public let caps: [String]? + public let commands: [String]? + public let permissions: [String: AnyCodable]? + public let pathenv: String? + public let role: String? + public let scopes: [String]? + public let device: [String: AnyCodable]? + public let auth: [String: AnyCodable]? + public let locale: String? + public let useragent: String? + + public init( + minprotocol: Int, + maxprotocol: Int, + client: [String: AnyCodable], + caps: [String]?, + commands: [String]?, + permissions: [String: AnyCodable]?, + pathenv: String?, + role: String?, + scopes: [String]?, + device: [String: AnyCodable]?, + auth: [String: AnyCodable]?, + locale: String?, + useragent: String? + ) { + self.minprotocol = minprotocol + self.maxprotocol = maxprotocol + self.client = client + self.caps = caps + self.commands = commands + self.permissions = permissions + self.pathenv = pathenv + self.role = role + self.scopes = scopes + self.device = device + self.auth = auth + self.locale = locale + self.useragent = useragent + } + private enum CodingKeys: String, CodingKey { + case minprotocol = "minProtocol" + case maxprotocol = "maxProtocol" + case client + case caps + case commands + case permissions + case pathenv = "pathEnv" + case role + case scopes + case device + case auth + case locale + case useragent = "userAgent" + } +} + +public struct HelloOk: Codable, Sendable { + public let type: String + public let _protocol: Int + public let server: [String: AnyCodable] + public let features: [String: AnyCodable] + public let snapshot: Snapshot + public let canvashosturl: String? + public let auth: [String: AnyCodable]? + public let policy: [String: AnyCodable] + + public init( + type: String, + _protocol: Int, + server: [String: AnyCodable], + features: [String: AnyCodable], + snapshot: Snapshot, + canvashosturl: String?, + auth: [String: AnyCodable]?, + policy: [String: AnyCodable] + ) { + self.type = type + self._protocol = _protocol + self.server = server + self.features = features + self.snapshot = snapshot + self.canvashosturl = canvashosturl + self.auth = auth + self.policy = policy + } + private enum CodingKeys: String, CodingKey { + case type + case _protocol = "protocol" + case server + case features + case snapshot + case canvashosturl = "canvasHostUrl" + case auth + case policy + } +} + +public struct RequestFrame: Codable, Sendable { + public let type: String + public let id: String + public let method: String + public let params: AnyCodable? + + public init( + type: String, + id: String, + method: String, + params: AnyCodable? + ) { + self.type = type + self.id = id + self.method = method + self.params = params + } + private enum CodingKeys: String, CodingKey { + case type + case id + case method + case params + } +} + +public struct ResponseFrame: Codable, Sendable { + public let type: String + public let id: String + public let ok: Bool + public let payload: AnyCodable? + public let error: [String: AnyCodable]? + + public init( + type: String, + id: String, + ok: Bool, + payload: AnyCodable?, + error: [String: AnyCodable]? + ) { + self.type = type + self.id = id + self.ok = ok + self.payload = payload + self.error = error + } + private enum CodingKeys: String, CodingKey { + case type + case id + case ok + case payload + case error + } +} + +public struct EventFrame: Codable, Sendable { + public let type: String + public let event: String + public let payload: AnyCodable? + public let seq: Int? + public let stateversion: [String: AnyCodable]? + + public init( + type: String, + event: String, + payload: AnyCodable?, + seq: Int?, + stateversion: [String: AnyCodable]? + ) { + self.type = type + self.event = event + self.payload = payload + self.seq = seq + self.stateversion = stateversion + } + private enum CodingKeys: String, CodingKey { + case type + case event + case payload + case seq + case stateversion = "stateVersion" + } +} + +public struct PresenceEntry: Codable, Sendable { + public let host: String? + public let ip: String? + public let version: String? + public let platform: String? + public let devicefamily: String? + public let modelidentifier: String? + public let mode: String? + public let lastinputseconds: Int? + public let reason: String? + public let tags: [String]? + public let text: String? + public let ts: Int + public let deviceid: String? + public let roles: [String]? + public let scopes: [String]? + public let instanceid: String? + + public init( + host: String?, + ip: String?, + version: String?, + platform: String?, + devicefamily: String?, + modelidentifier: String?, + mode: String?, + lastinputseconds: Int?, + reason: String?, + tags: [String]?, + text: String?, + ts: Int, + deviceid: String?, + roles: [String]?, + scopes: [String]?, + instanceid: String? + ) { + self.host = host + self.ip = ip + self.version = version + self.platform = platform + self.devicefamily = devicefamily + self.modelidentifier = modelidentifier + self.mode = mode + self.lastinputseconds = lastinputseconds + self.reason = reason + self.tags = tags + self.text = text + self.ts = ts + self.deviceid = deviceid + self.roles = roles + self.scopes = scopes + self.instanceid = instanceid + } + private enum CodingKeys: String, CodingKey { + case host + case ip + case version + case platform + case devicefamily = "deviceFamily" + case modelidentifier = "modelIdentifier" + case mode + case lastinputseconds = "lastInputSeconds" + case reason + case tags + case text + case ts + case deviceid = "deviceId" + case roles + case scopes + case instanceid = "instanceId" + } +} + +public struct StateVersion: Codable, Sendable { + public let presence: Int + public let health: Int + + public init( + presence: Int, + health: Int + ) { + self.presence = presence + self.health = health + } + private enum CodingKeys: String, CodingKey { + case presence + case health + } +} + +public struct Snapshot: Codable, Sendable { + public let presence: [PresenceEntry] + public let health: AnyCodable + public let stateversion: StateVersion + public let uptimems: Int + public let configpath: String? + public let statedir: String? + public let sessiondefaults: [String: AnyCodable]? + + public init( + presence: [PresenceEntry], + health: AnyCodable, + stateversion: StateVersion, + uptimems: Int, + configpath: String?, + statedir: String?, + sessiondefaults: [String: AnyCodable]? + ) { + self.presence = presence + self.health = health + self.stateversion = stateversion + self.uptimems = uptimems + self.configpath = configpath + self.statedir = statedir + self.sessiondefaults = sessiondefaults + } + private enum CodingKeys: String, CodingKey { + case presence + case health + case stateversion = "stateVersion" + case uptimems = "uptimeMs" + case configpath = "configPath" + case statedir = "stateDir" + case sessiondefaults = "sessionDefaults" + } +} + +public struct ErrorShape: Codable, Sendable { + public let code: String + public let message: String + public let details: AnyCodable? + public let retryable: Bool? + public let retryafterms: Int? + + public init( + code: String, + message: String, + details: AnyCodable?, + retryable: Bool?, + retryafterms: Int? + ) { + self.code = code + self.message = message + self.details = details + self.retryable = retryable + self.retryafterms = retryafterms + } + private enum CodingKeys: String, CodingKey { + case code + case message + case details + case retryable + case retryafterms = "retryAfterMs" + } +} + +public struct AgentEvent: Codable, Sendable { + public let runid: String + public let seq: Int + public let stream: String + public let ts: Int + public let data: [String: AnyCodable] + + public init( + runid: String, + seq: Int, + stream: String, + ts: Int, + data: [String: AnyCodable] + ) { + self.runid = runid + self.seq = seq + self.stream = stream + self.ts = ts + self.data = data + } + private enum CodingKeys: String, CodingKey { + case runid = "runId" + case seq + case stream + case ts + case data + } +} + +public struct SendParams: Codable, Sendable { + public let to: String + public let message: String + public let mediaurl: String? + public let mediaurls: [String]? + public let gifplayback: Bool? + public let channel: String? + public let accountid: String? + public let sessionkey: String? + public let idempotencykey: String + + public init( + to: String, + message: String, + mediaurl: String?, + mediaurls: [String]?, + gifplayback: Bool?, + channel: String?, + accountid: String?, + sessionkey: String?, + idempotencykey: String + ) { + self.to = to + self.message = message + self.mediaurl = mediaurl + self.mediaurls = mediaurls + self.gifplayback = gifplayback + self.channel = channel + self.accountid = accountid + self.sessionkey = sessionkey + self.idempotencykey = idempotencykey + } + private enum CodingKeys: String, CodingKey { + case to + case message + case mediaurl = "mediaUrl" + case mediaurls = "mediaUrls" + case gifplayback = "gifPlayback" + case channel + case accountid = "accountId" + case sessionkey = "sessionKey" + case idempotencykey = "idempotencyKey" + } +} + +public struct PollParams: Codable, Sendable { + public let to: String + public let question: String + public let options: [String] + public let maxselections: Int? + public let durationhours: Int? + public let channel: String? + public let accountid: String? + public let idempotencykey: String + + public init( + to: String, + question: String, + options: [String], + maxselections: Int?, + durationhours: Int?, + channel: String?, + accountid: String?, + idempotencykey: String + ) { + self.to = to + self.question = question + self.options = options + self.maxselections = maxselections + self.durationhours = durationhours + self.channel = channel + self.accountid = accountid + self.idempotencykey = idempotencykey + } + private enum CodingKeys: String, CodingKey { + case to + case question + case options + case maxselections = "maxSelections" + case durationhours = "durationHours" + case channel + case accountid = "accountId" + case idempotencykey = "idempotencyKey" + } +} + +public struct AgentParams: Codable, Sendable { + public let message: String + public let agentid: String? + public let to: String? + public let replyto: String? + public let sessionid: String? + public let sessionkey: String? + public let thinking: String? + public let deliver: Bool? + public let attachments: [AnyCodable]? + public let channel: String? + public let replychannel: String? + public let accountid: String? + public let replyaccountid: String? + public let threadid: String? + public let groupid: String? + public let groupchannel: String? + public let groupspace: String? + public let timeout: Int? + public let lane: String? + public let extrasystemprompt: String? + public let idempotencykey: String + public let label: String? + public let spawnedby: String? + + public init( + message: String, + agentid: String?, + to: String?, + replyto: String?, + sessionid: String?, + sessionkey: String?, + thinking: String?, + deliver: Bool?, + attachments: [AnyCodable]?, + channel: String?, + replychannel: String?, + accountid: String?, + replyaccountid: String?, + threadid: String?, + groupid: String?, + groupchannel: String?, + groupspace: String?, + timeout: Int?, + lane: String?, + extrasystemprompt: String?, + idempotencykey: String, + label: String?, + spawnedby: String? + ) { + self.message = message + self.agentid = agentid + self.to = to + self.replyto = replyto + self.sessionid = sessionid + self.sessionkey = sessionkey + self.thinking = thinking + self.deliver = deliver + self.attachments = attachments + self.channel = channel + self.replychannel = replychannel + self.accountid = accountid + self.replyaccountid = replyaccountid + self.threadid = threadid + self.groupid = groupid + self.groupchannel = groupchannel + self.groupspace = groupspace + self.timeout = timeout + self.lane = lane + self.extrasystemprompt = extrasystemprompt + self.idempotencykey = idempotencykey + self.label = label + self.spawnedby = spawnedby + } + private enum CodingKeys: String, CodingKey { + case message + case agentid = "agentId" + case to + case replyto = "replyTo" + case sessionid = "sessionId" + case sessionkey = "sessionKey" + case thinking + case deliver + case attachments + case channel + case replychannel = "replyChannel" + case accountid = "accountId" + case replyaccountid = "replyAccountId" + case threadid = "threadId" + case groupid = "groupId" + case groupchannel = "groupChannel" + case groupspace = "groupSpace" + case timeout + case lane + case extrasystemprompt = "extraSystemPrompt" + case idempotencykey = "idempotencyKey" + case label + case spawnedby = "spawnedBy" + } +} + +public struct AgentIdentityParams: Codable, Sendable { + public let agentid: String? + public let sessionkey: String? + + public init( + agentid: String?, + sessionkey: String? + ) { + self.agentid = agentid + self.sessionkey = sessionkey + } + private enum CodingKeys: String, CodingKey { + case agentid = "agentId" + case sessionkey = "sessionKey" + } +} + +public struct AgentIdentityResult: Codable, Sendable { + public let agentid: String + public let name: String? + public let avatar: String? + + public init( + agentid: String, + name: String?, + avatar: String? + ) { + self.agentid = agentid + self.name = name + self.avatar = avatar + } + private enum CodingKeys: String, CodingKey { + case agentid = "agentId" + case name + case avatar + } +} + +public struct AgentWaitParams: Codable, Sendable { + public let runid: String + public let timeoutms: Int? + + public init( + runid: String, + timeoutms: Int? + ) { + self.runid = runid + self.timeoutms = timeoutms + } + private enum CodingKeys: String, CodingKey { + case runid = "runId" + case timeoutms = "timeoutMs" + } +} + +public struct WakeParams: Codable, Sendable { + public let mode: AnyCodable + public let text: String + + public init( + mode: AnyCodable, + text: String + ) { + self.mode = mode + self.text = text + } + private enum CodingKeys: String, CodingKey { + case mode + case text + } +} + +public struct NodePairRequestParams: Codable, Sendable { + public let nodeid: String + public let displayname: String? + public let platform: String? + public let version: String? + public let coreversion: String? + public let uiversion: String? + public let devicefamily: String? + public let modelidentifier: String? + public let caps: [String]? + public let commands: [String]? + public let remoteip: String? + public let silent: Bool? + + public init( + nodeid: String, + displayname: String?, + platform: String?, + version: String?, + coreversion: String?, + uiversion: String?, + devicefamily: String?, + modelidentifier: String?, + caps: [String]?, + commands: [String]?, + remoteip: String?, + silent: Bool? + ) { + self.nodeid = nodeid + self.displayname = displayname + self.platform = platform + self.version = version + self.coreversion = coreversion + self.uiversion = uiversion + self.devicefamily = devicefamily + self.modelidentifier = modelidentifier + self.caps = caps + self.commands = commands + self.remoteip = remoteip + self.silent = silent + } + private enum CodingKeys: String, CodingKey { + case nodeid = "nodeId" + case displayname = "displayName" + case platform + case version + case coreversion = "coreVersion" + case uiversion = "uiVersion" + case devicefamily = "deviceFamily" + case modelidentifier = "modelIdentifier" + case caps + case commands + case remoteip = "remoteIp" + case silent + } +} + +public struct NodePairListParams: Codable, Sendable { +} + +public struct NodePairApproveParams: Codable, Sendable { + public let requestid: String + + public init( + requestid: String + ) { + self.requestid = requestid + } + private enum CodingKeys: String, CodingKey { + case requestid = "requestId" + } +} + +public struct NodePairRejectParams: Codable, Sendable { + public let requestid: String + + public init( + requestid: String + ) { + self.requestid = requestid + } + private enum CodingKeys: String, CodingKey { + case requestid = "requestId" + } +} + +public struct NodePairVerifyParams: Codable, Sendable { + public let nodeid: String + public let token: String + + public init( + nodeid: String, + token: String + ) { + self.nodeid = nodeid + self.token = token + } + private enum CodingKeys: String, CodingKey { + case nodeid = "nodeId" + case token + } +} + +public struct NodeRenameParams: Codable, Sendable { + public let nodeid: String + public let displayname: String + + public init( + nodeid: String, + displayname: String + ) { + self.nodeid = nodeid + self.displayname = displayname + } + private enum CodingKeys: String, CodingKey { + case nodeid = "nodeId" + case displayname = "displayName" + } +} + +public struct NodeListParams: Codable, Sendable { +} + +public struct NodeDescribeParams: Codable, Sendable { + public let nodeid: String + + public init( + nodeid: String + ) { + self.nodeid = nodeid + } + private enum CodingKeys: String, CodingKey { + case nodeid = "nodeId" + } +} + +public struct NodeInvokeParams: Codable, Sendable { + public let nodeid: String + public let command: String + public let params: AnyCodable? + public let timeoutms: Int? + public let idempotencykey: String + + public init( + nodeid: String, + command: String, + params: AnyCodable?, + timeoutms: Int?, + idempotencykey: String + ) { + self.nodeid = nodeid + self.command = command + self.params = params + self.timeoutms = timeoutms + self.idempotencykey = idempotencykey + } + private enum CodingKeys: String, CodingKey { + case nodeid = "nodeId" + case command + case params + case timeoutms = "timeoutMs" + case idempotencykey = "idempotencyKey" + } +} + +public struct NodeInvokeResultParams: Codable, Sendable { + public let id: String + public let nodeid: String + public let ok: Bool + public let payload: AnyCodable? + public let payloadjson: String? + public let error: [String: AnyCodable]? + + public init( + id: String, + nodeid: String, + ok: Bool, + payload: AnyCodable?, + payloadjson: String?, + error: [String: AnyCodable]? + ) { + self.id = id + self.nodeid = nodeid + self.ok = ok + self.payload = payload + self.payloadjson = payloadjson + self.error = error + } + private enum CodingKeys: String, CodingKey { + case id + case nodeid = "nodeId" + case ok + case payload + case payloadjson = "payloadJSON" + case error + } +} + +public struct NodeEventParams: Codable, Sendable { + public let event: String + public let payload: AnyCodable? + public let payloadjson: String? + + public init( + event: String, + payload: AnyCodable?, + payloadjson: String? + ) { + self.event = event + self.payload = payload + self.payloadjson = payloadjson + } + private enum CodingKeys: String, CodingKey { + case event + case payload + case payloadjson = "payloadJSON" + } +} + +public struct NodeInvokeRequestEvent: Codable, Sendable { + public let id: String + public let nodeid: String + public let command: String + public let paramsjson: String? + public let timeoutms: Int? + public let idempotencykey: String? + + public init( + id: String, + nodeid: String, + command: String, + paramsjson: String?, + timeoutms: Int?, + idempotencykey: String? + ) { + self.id = id + self.nodeid = nodeid + self.command = command + self.paramsjson = paramsjson + self.timeoutms = timeoutms + self.idempotencykey = idempotencykey + } + private enum CodingKeys: String, CodingKey { + case id + case nodeid = "nodeId" + case command + case paramsjson = "paramsJSON" + case timeoutms = "timeoutMs" + case idempotencykey = "idempotencyKey" + } +} + +public struct SessionsListParams: Codable, Sendable { + public let limit: Int? + public let activeminutes: Int? + public let includeglobal: Bool? + public let includeunknown: Bool? + public let includederivedtitles: Bool? + public let includelastmessage: Bool? + public let label: String? + public let spawnedby: String? + public let agentid: String? + public let search: String? + + public init( + limit: Int?, + activeminutes: Int?, + includeglobal: Bool?, + includeunknown: Bool?, + includederivedtitles: Bool?, + includelastmessage: Bool?, + label: String?, + spawnedby: String?, + agentid: String?, + search: String? + ) { + self.limit = limit + self.activeminutes = activeminutes + self.includeglobal = includeglobal + self.includeunknown = includeunknown + self.includederivedtitles = includederivedtitles + self.includelastmessage = includelastmessage + self.label = label + self.spawnedby = spawnedby + self.agentid = agentid + self.search = search + } + private enum CodingKeys: String, CodingKey { + case limit + case activeminutes = "activeMinutes" + case includeglobal = "includeGlobal" + case includeunknown = "includeUnknown" + case includederivedtitles = "includeDerivedTitles" + case includelastmessage = "includeLastMessage" + case label + case spawnedby = "spawnedBy" + case agentid = "agentId" + case search + } +} + +public struct SessionsPreviewParams: Codable, Sendable { + public let keys: [String] + public let limit: Int? + public let maxchars: Int? + + public init( + keys: [String], + limit: Int?, + maxchars: Int? + ) { + self.keys = keys + self.limit = limit + self.maxchars = maxchars + } + private enum CodingKeys: String, CodingKey { + case keys + case limit + case maxchars = "maxChars" + } +} + +public struct SessionsResolveParams: Codable, Sendable { + public let key: String? + public let sessionid: String? + public let label: String? + public let agentid: String? + public let spawnedby: String? + public let includeglobal: Bool? + public let includeunknown: Bool? + + public init( + key: String?, + sessionid: String?, + label: String?, + agentid: String?, + spawnedby: String?, + includeglobal: Bool?, + includeunknown: Bool? + ) { + self.key = key + self.sessionid = sessionid + self.label = label + self.agentid = agentid + self.spawnedby = spawnedby + self.includeglobal = includeglobal + self.includeunknown = includeunknown + } + private enum CodingKeys: String, CodingKey { + case key + case sessionid = "sessionId" + case label + case agentid = "agentId" + case spawnedby = "spawnedBy" + case includeglobal = "includeGlobal" + case includeunknown = "includeUnknown" + } +} + +public struct SessionsPatchParams: Codable, Sendable { + public let key: String + public let label: AnyCodable? + public let thinkinglevel: AnyCodable? + public let verboselevel: AnyCodable? + public let reasoninglevel: AnyCodable? + public let responseusage: AnyCodable? + public let elevatedlevel: AnyCodable? + public let exechost: AnyCodable? + public let execsecurity: AnyCodable? + public let execask: AnyCodable? + public let execnode: AnyCodable? + public let model: AnyCodable? + public let spawnedby: AnyCodable? + public let sendpolicy: AnyCodable? + public let groupactivation: AnyCodable? + + public init( + key: String, + label: AnyCodable?, + thinkinglevel: AnyCodable?, + verboselevel: AnyCodable?, + reasoninglevel: AnyCodable?, + responseusage: AnyCodable?, + elevatedlevel: AnyCodable?, + exechost: AnyCodable?, + execsecurity: AnyCodable?, + execask: AnyCodable?, + execnode: AnyCodable?, + model: AnyCodable?, + spawnedby: AnyCodable?, + sendpolicy: AnyCodable?, + groupactivation: AnyCodable? + ) { + self.key = key + self.label = label + self.thinkinglevel = thinkinglevel + self.verboselevel = verboselevel + self.reasoninglevel = reasoninglevel + self.responseusage = responseusage + self.elevatedlevel = elevatedlevel + self.exechost = exechost + self.execsecurity = execsecurity + self.execask = execask + self.execnode = execnode + self.model = model + self.spawnedby = spawnedby + self.sendpolicy = sendpolicy + self.groupactivation = groupactivation + } + private enum CodingKeys: String, CodingKey { + case key + case label + case thinkinglevel = "thinkingLevel" + case verboselevel = "verboseLevel" + case reasoninglevel = "reasoningLevel" + case responseusage = "responseUsage" + case elevatedlevel = "elevatedLevel" + case exechost = "execHost" + case execsecurity = "execSecurity" + case execask = "execAsk" + case execnode = "execNode" + case model + case spawnedby = "spawnedBy" + case sendpolicy = "sendPolicy" + case groupactivation = "groupActivation" + } +} + +public struct SessionsResetParams: Codable, Sendable { + public let key: String + + public init( + key: String + ) { + self.key = key + } + private enum CodingKeys: String, CodingKey { + case key + } +} + +public struct SessionsDeleteParams: Codable, Sendable { + public let key: String + public let deletetranscript: Bool? + + public init( + key: String, + deletetranscript: Bool? + ) { + self.key = key + self.deletetranscript = deletetranscript + } + private enum CodingKeys: String, CodingKey { + case key + case deletetranscript = "deleteTranscript" + } +} + +public struct SessionsCompactParams: Codable, Sendable { + public let key: String + public let maxlines: Int? + + public init( + key: String, + maxlines: Int? + ) { + self.key = key + self.maxlines = maxlines + } + private enum CodingKeys: String, CodingKey { + case key + case maxlines = "maxLines" + } +} + +public struct ConfigGetParams: Codable, Sendable { +} + +public struct ConfigSetParams: Codable, Sendable { + public let raw: String + public let basehash: String? + + public init( + raw: String, + basehash: String? + ) { + self.raw = raw + self.basehash = basehash + } + private enum CodingKeys: String, CodingKey { + case raw + case basehash = "baseHash" + } +} + +public struct ConfigApplyParams: Codable, Sendable { + public let raw: String + public let basehash: String? + public let sessionkey: String? + public let note: String? + public let restartdelayms: Int? + + public init( + raw: String, + basehash: String?, + sessionkey: String?, + note: String?, + restartdelayms: Int? + ) { + self.raw = raw + self.basehash = basehash + self.sessionkey = sessionkey + self.note = note + self.restartdelayms = restartdelayms + } + private enum CodingKeys: String, CodingKey { + case raw + case basehash = "baseHash" + case sessionkey = "sessionKey" + case note + case restartdelayms = "restartDelayMs" + } +} + +public struct ConfigPatchParams: Codable, Sendable { + public let raw: String + public let basehash: String? + + public init( + raw: String, + basehash: String? + ) { + self.raw = raw + self.basehash = basehash + } + private enum CodingKeys: String, CodingKey { + case raw + case basehash = "baseHash" + } +} + +public struct ConfigSchemaParams: Codable, Sendable { +} + +public struct ConfigSchemaResponse: Codable, Sendable { + public let schema: AnyCodable + public let uihints: [String: AnyCodable] + public let version: String + public let generatedat: String + + public init( + schema: AnyCodable, + uihints: [String: AnyCodable], + version: String, + generatedat: String + ) { + self.schema = schema + self.uihints = uihints + self.version = version + self.generatedat = generatedat + } + private enum CodingKeys: String, CodingKey { + case schema + case uihints = "uiHints" + case version + case generatedat = "generatedAt" + } +} + +public struct WizardStartParams: Codable, Sendable { + public let mode: AnyCodable? + public let workspace: String? + + public init( + mode: AnyCodable?, + workspace: String? + ) { + self.mode = mode + self.workspace = workspace + } + private enum CodingKeys: String, CodingKey { + case mode + case workspace + } +} + +public struct WizardNextParams: Codable, Sendable { + public let sessionid: String + public let answer: [String: AnyCodable]? + + public init( + sessionid: String, + answer: [String: AnyCodable]? + ) { + self.sessionid = sessionid + self.answer = answer + } + private enum CodingKeys: String, CodingKey { + case sessionid = "sessionId" + case answer + } +} + +public struct WizardCancelParams: Codable, Sendable { + public let sessionid: String + + public init( + sessionid: String + ) { + self.sessionid = sessionid + } + private enum CodingKeys: String, CodingKey { + case sessionid = "sessionId" + } +} + +public struct WizardStatusParams: Codable, Sendable { + public let sessionid: String + + public init( + sessionid: String + ) { + self.sessionid = sessionid + } + private enum CodingKeys: String, CodingKey { + case sessionid = "sessionId" + } +} + +public struct WizardStep: Codable, Sendable { + public let id: String + public let type: AnyCodable + public let title: String? + public let message: String? + public let options: [[String: AnyCodable]]? + public let initialvalue: AnyCodable? + public let placeholder: String? + public let sensitive: Bool? + public let executor: AnyCodable? + + public init( + id: String, + type: AnyCodable, + title: String?, + message: String?, + options: [[String: AnyCodable]]?, + initialvalue: AnyCodable?, + placeholder: String?, + sensitive: Bool?, + executor: AnyCodable? + ) { + self.id = id + self.type = type + self.title = title + self.message = message + self.options = options + self.initialvalue = initialvalue + self.placeholder = placeholder + self.sensitive = sensitive + self.executor = executor + } + private enum CodingKeys: String, CodingKey { + case id + case type + case title + case message + case options + case initialvalue = "initialValue" + case placeholder + case sensitive + case executor + } +} + +public struct WizardNextResult: Codable, Sendable { + public let done: Bool + public let step: [String: AnyCodable]? + public let status: AnyCodable? + public let error: String? + + public init( + done: Bool, + step: [String: AnyCodable]?, + status: AnyCodable?, + error: String? + ) { + self.done = done + self.step = step + self.status = status + self.error = error + } + private enum CodingKeys: String, CodingKey { + case done + case step + case status + case error + } +} + +public struct WizardStartResult: Codable, Sendable { + public let sessionid: String + public let done: Bool + public let step: [String: AnyCodable]? + public let status: AnyCodable? + public let error: String? + + public init( + sessionid: String, + done: Bool, + step: [String: AnyCodable]?, + status: AnyCodable?, + error: String? + ) { + self.sessionid = sessionid + self.done = done + self.step = step + self.status = status + self.error = error + } + private enum CodingKeys: String, CodingKey { + case sessionid = "sessionId" + case done + case step + case status + case error + } +} + +public struct WizardStatusResult: Codable, Sendable { + public let status: AnyCodable + public let error: String? + + public init( + status: AnyCodable, + error: String? + ) { + self.status = status + self.error = error + } + private enum CodingKeys: String, CodingKey { + case status + case error + } +} + +public struct TalkModeParams: Codable, Sendable { + public let enabled: Bool + public let phase: String? + + public init( + enabled: Bool, + phase: String? + ) { + self.enabled = enabled + self.phase = phase + } + private enum CodingKeys: String, CodingKey { + case enabled + case phase + } +} + +public struct ChannelsStatusParams: Codable, Sendable { + public let probe: Bool? + public let timeoutms: Int? + + public init( + probe: Bool?, + timeoutms: Int? + ) { + self.probe = probe + self.timeoutms = timeoutms + } + private enum CodingKeys: String, CodingKey { + case probe + case timeoutms = "timeoutMs" + } +} + +public struct ChannelsStatusResult: Codable, Sendable { + public let ts: Int + public let channelorder: [String] + public let channellabels: [String: AnyCodable] + public let channeldetaillabels: [String: AnyCodable]? + public let channelsystemimages: [String: AnyCodable]? + public let channelmeta: [[String: AnyCodable]]? + public let channels: [String: AnyCodable] + public let channelaccounts: [String: AnyCodable] + public let channeldefaultaccountid: [String: AnyCodable] + + public init( + ts: Int, + channelorder: [String], + channellabels: [String: AnyCodable], + channeldetaillabels: [String: AnyCodable]?, + channelsystemimages: [String: AnyCodable]?, + channelmeta: [[String: AnyCodable]]?, + channels: [String: AnyCodable], + channelaccounts: [String: AnyCodable], + channeldefaultaccountid: [String: AnyCodable] + ) { + self.ts = ts + self.channelorder = channelorder + self.channellabels = channellabels + self.channeldetaillabels = channeldetaillabels + self.channelsystemimages = channelsystemimages + self.channelmeta = channelmeta + self.channels = channels + self.channelaccounts = channelaccounts + self.channeldefaultaccountid = channeldefaultaccountid + } + private enum CodingKeys: String, CodingKey { + case ts + case channelorder = "channelOrder" + case channellabels = "channelLabels" + case channeldetaillabels = "channelDetailLabels" + case channelsystemimages = "channelSystemImages" + case channelmeta = "channelMeta" + case channels + case channelaccounts = "channelAccounts" + case channeldefaultaccountid = "channelDefaultAccountId" + } +} + +public struct ChannelsLogoutParams: Codable, Sendable { + public let channel: String + public let accountid: String? + + public init( + channel: String, + accountid: String? + ) { + self.channel = channel + self.accountid = accountid + } + private enum CodingKeys: String, CodingKey { + case channel + case accountid = "accountId" + } +} + +public struct WebLoginStartParams: Codable, Sendable { + public let force: Bool? + public let timeoutms: Int? + public let verbose: Bool? + public let accountid: String? + + public init( + force: Bool?, + timeoutms: Int?, + verbose: Bool?, + accountid: String? + ) { + self.force = force + self.timeoutms = timeoutms + self.verbose = verbose + self.accountid = accountid + } + private enum CodingKeys: String, CodingKey { + case force + case timeoutms = "timeoutMs" + case verbose + case accountid = "accountId" + } +} + +public struct WebLoginWaitParams: Codable, Sendable { + public let timeoutms: Int? + public let accountid: String? + + public init( + timeoutms: Int?, + accountid: String? + ) { + self.timeoutms = timeoutms + self.accountid = accountid + } + private enum CodingKeys: String, CodingKey { + case timeoutms = "timeoutMs" + case accountid = "accountId" + } +} + +public struct AgentSummary: Codable, Sendable { + public let id: String + public let name: String? + public let identity: [String: AnyCodable]? + + public init( + id: String, + name: String?, + identity: [String: AnyCodable]? + ) { + self.id = id + self.name = name + self.identity = identity + } + private enum CodingKeys: String, CodingKey { + case id + case name + case identity + } +} + +public struct AgentsListParams: Codable, Sendable { +} + +public struct AgentsListResult: Codable, Sendable { + public let defaultid: String + public let mainkey: String + public let scope: AnyCodable + public let agents: [AgentSummary] + + public init( + defaultid: String, + mainkey: String, + scope: AnyCodable, + agents: [AgentSummary] + ) { + self.defaultid = defaultid + self.mainkey = mainkey + self.scope = scope + self.agents = agents + } + private enum CodingKeys: String, CodingKey { + case defaultid = "defaultId" + case mainkey = "mainKey" + case scope + case agents + } +} + +public struct ModelChoice: Codable, Sendable { + public let id: String + public let name: String + public let provider: String + public let contextwindow: Int? + public let reasoning: Bool? + + public init( + id: String, + name: String, + provider: String, + contextwindow: Int?, + reasoning: Bool? + ) { + self.id = id + self.name = name + self.provider = provider + self.contextwindow = contextwindow + self.reasoning = reasoning + } + private enum CodingKeys: String, CodingKey { + case id + case name + case provider + case contextwindow = "contextWindow" + case reasoning + } +} + +public struct ModelsListParams: Codable, Sendable { +} + +public struct ModelsListResult: Codable, Sendable { + public let models: [ModelChoice] + + public init( + models: [ModelChoice] + ) { + self.models = models + } + private enum CodingKeys: String, CodingKey { + case models + } +} + +public struct SkillsStatusParams: Codable, Sendable { +} + +public struct SkillsBinsParams: Codable, Sendable { +} + +public struct SkillsBinsResult: Codable, Sendable { + public let bins: [String] + + public init( + bins: [String] + ) { + self.bins = bins + } + private enum CodingKeys: String, CodingKey { + case bins + } +} + +public struct SkillsInstallParams: Codable, Sendable { + public let name: String + public let installid: String + public let timeoutms: Int? + + public init( + name: String, + installid: String, + timeoutms: Int? + ) { + self.name = name + self.installid = installid + self.timeoutms = timeoutms + } + private enum CodingKeys: String, CodingKey { + case name + case installid = "installId" + case timeoutms = "timeoutMs" + } +} + +public struct SkillsUpdateParams: Codable, Sendable { + public let skillkey: String + public let enabled: Bool? + public let apikey: String? + public let env: [String: AnyCodable]? + + public init( + skillkey: String, + enabled: Bool?, + apikey: String?, + env: [String: AnyCodable]? + ) { + self.skillkey = skillkey + self.enabled = enabled + self.apikey = apikey + self.env = env + } + private enum CodingKeys: String, CodingKey { + case skillkey = "skillKey" + case enabled + case apikey = "apiKey" + case env + } +} + +public struct CronJob: Codable, Sendable { + public let id: String + public let agentid: String? + public let name: String + public let description: String? + public let enabled: Bool + public let deleteafterrun: Bool? + public let createdatms: Int + public let updatedatms: Int + public let schedule: AnyCodable + public let sessiontarget: AnyCodable + public let wakemode: AnyCodable + public let payload: AnyCodable + public let isolation: [String: AnyCodable]? + public let state: [String: AnyCodable] + + public init( + id: String, + agentid: String?, + name: String, + description: String?, + enabled: Bool, + deleteafterrun: Bool?, + createdatms: Int, + updatedatms: Int, + schedule: AnyCodable, + sessiontarget: AnyCodable, + wakemode: AnyCodable, + payload: AnyCodable, + isolation: [String: AnyCodable]?, + state: [String: AnyCodable] + ) { + self.id = id + self.agentid = agentid + self.name = name + self.description = description + self.enabled = enabled + self.deleteafterrun = deleteafterrun + self.createdatms = createdatms + self.updatedatms = updatedatms + self.schedule = schedule + self.sessiontarget = sessiontarget + self.wakemode = wakemode + self.payload = payload + self.isolation = isolation + self.state = state + } + private enum CodingKeys: String, CodingKey { + case id + case agentid = "agentId" + case name + case description + case enabled + case deleteafterrun = "deleteAfterRun" + case createdatms = "createdAtMs" + case updatedatms = "updatedAtMs" + case schedule + case sessiontarget = "sessionTarget" + case wakemode = "wakeMode" + case payload + case isolation + case state + } +} + +public struct CronListParams: Codable, Sendable { + public let includedisabled: Bool? + + public init( + includedisabled: Bool? + ) { + self.includedisabled = includedisabled + } + private enum CodingKeys: String, CodingKey { + case includedisabled = "includeDisabled" + } +} + +public struct CronStatusParams: Codable, Sendable { +} + +public struct CronAddParams: Codable, Sendable { + public let name: String + public let agentid: AnyCodable? + public let description: String? + public let enabled: Bool? + public let deleteafterrun: Bool? + public let schedule: AnyCodable + public let sessiontarget: AnyCodable + public let wakemode: AnyCodable + public let payload: AnyCodable + public let isolation: [String: AnyCodable]? + + public init( + name: String, + agentid: AnyCodable?, + description: String?, + enabled: Bool?, + deleteafterrun: Bool?, + schedule: AnyCodable, + sessiontarget: AnyCodable, + wakemode: AnyCodable, + payload: AnyCodable, + isolation: [String: AnyCodable]? + ) { + self.name = name + self.agentid = agentid + self.description = description + self.enabled = enabled + self.deleteafterrun = deleteafterrun + self.schedule = schedule + self.sessiontarget = sessiontarget + self.wakemode = wakemode + self.payload = payload + self.isolation = isolation + } + private enum CodingKeys: String, CodingKey { + case name + case agentid = "agentId" + case description + case enabled + case deleteafterrun = "deleteAfterRun" + case schedule + case sessiontarget = "sessionTarget" + case wakemode = "wakeMode" + case payload + case isolation + } +} + +public struct CronRunLogEntry: Codable, Sendable { + public let ts: Int + public let jobid: String + public let action: String + public let status: AnyCodable? + public let error: String? + public let summary: String? + public let runatms: Int? + public let durationms: Int? + public let nextrunatms: Int? + + public init( + ts: Int, + jobid: String, + action: String, + status: AnyCodable?, + error: String?, + summary: String?, + runatms: Int?, + durationms: Int?, + nextrunatms: Int? + ) { + self.ts = ts + self.jobid = jobid + self.action = action + self.status = status + self.error = error + self.summary = summary + self.runatms = runatms + self.durationms = durationms + self.nextrunatms = nextrunatms + } + private enum CodingKeys: String, CodingKey { + case ts + case jobid = "jobId" + case action + case status + case error + case summary + case runatms = "runAtMs" + case durationms = "durationMs" + case nextrunatms = "nextRunAtMs" + } +} + +public struct LogsTailParams: Codable, Sendable { + public let cursor: Int? + public let limit: Int? + public let maxbytes: Int? + + public init( + cursor: Int?, + limit: Int?, + maxbytes: Int? + ) { + self.cursor = cursor + self.limit = limit + self.maxbytes = maxbytes + } + private enum CodingKeys: String, CodingKey { + case cursor + case limit + case maxbytes = "maxBytes" + } +} + +public struct LogsTailResult: Codable, Sendable { + public let file: String + public let cursor: Int + public let size: Int + public let lines: [String] + public let truncated: Bool? + public let reset: Bool? + + public init( + file: String, + cursor: Int, + size: Int, + lines: [String], + truncated: Bool?, + reset: Bool? + ) { + self.file = file + self.cursor = cursor + self.size = size + self.lines = lines + self.truncated = truncated + self.reset = reset + } + private enum CodingKeys: String, CodingKey { + case file + case cursor + case size + case lines + case truncated + case reset + } +} + +public struct ExecApprovalsGetParams: Codable, Sendable { +} + +public struct ExecApprovalsSetParams: Codable, Sendable { + public let file: [String: AnyCodable] + public let basehash: String? + + public init( + file: [String: AnyCodable], + basehash: String? + ) { + self.file = file + self.basehash = basehash + } + private enum CodingKeys: String, CodingKey { + case file + case basehash = "baseHash" + } +} + +public struct ExecApprovalsNodeGetParams: Codable, Sendable { + public let nodeid: String + + public init( + nodeid: String + ) { + self.nodeid = nodeid + } + private enum CodingKeys: String, CodingKey { + case nodeid = "nodeId" + } +} + +public struct ExecApprovalsNodeSetParams: Codable, Sendable { + public let nodeid: String + public let file: [String: AnyCodable] + public let basehash: String? + + public init( + nodeid: String, + file: [String: AnyCodable], + basehash: String? + ) { + self.nodeid = nodeid + self.file = file + self.basehash = basehash + } + private enum CodingKeys: String, CodingKey { + case nodeid = "nodeId" + case file + case basehash = "baseHash" + } +} + +public struct ExecApprovalsSnapshot: Codable, Sendable { + public let path: String + public let exists: Bool + public let hash: String + public let file: [String: AnyCodable] + + public init( + path: String, + exists: Bool, + hash: String, + file: [String: AnyCodable] + ) { + self.path = path + self.exists = exists + self.hash = hash + self.file = file + } + private enum CodingKeys: String, CodingKey { + case path + case exists + case hash + case file + } +} + +public struct ExecApprovalRequestParams: Codable, Sendable { + public let id: String? + public let command: String + public let cwd: AnyCodable? + public let host: AnyCodable? + public let security: AnyCodable? + public let ask: AnyCodable? + public let agentid: AnyCodable? + public let resolvedpath: AnyCodable? + public let sessionkey: AnyCodable? + public let timeoutms: Int? + + public init( + id: String?, + command: String, + cwd: AnyCodable?, + host: AnyCodable?, + security: AnyCodable?, + ask: AnyCodable?, + agentid: AnyCodable?, + resolvedpath: AnyCodable?, + sessionkey: AnyCodable?, + timeoutms: Int? + ) { + self.id = id + self.command = command + self.cwd = cwd + self.host = host + self.security = security + self.ask = ask + self.agentid = agentid + self.resolvedpath = resolvedpath + self.sessionkey = sessionkey + self.timeoutms = timeoutms + } + private enum CodingKeys: String, CodingKey { + case id + case command + case cwd + case host + case security + case ask + case agentid = "agentId" + case resolvedpath = "resolvedPath" + case sessionkey = "sessionKey" + case timeoutms = "timeoutMs" + } +} + +public struct ExecApprovalResolveParams: Codable, Sendable { + public let id: String + public let decision: String + + public init( + id: String, + decision: String + ) { + self.id = id + self.decision = decision + } + private enum CodingKeys: String, CodingKey { + case id + case decision + } +} + +public struct DevicePairListParams: Codable, Sendable { +} + +public struct DevicePairApproveParams: Codable, Sendable { + public let requestid: String + + public init( + requestid: String + ) { + self.requestid = requestid + } + private enum CodingKeys: String, CodingKey { + case requestid = "requestId" + } +} + +public struct DevicePairRejectParams: Codable, Sendable { + public let requestid: String + + public init( + requestid: String + ) { + self.requestid = requestid + } + private enum CodingKeys: String, CodingKey { + case requestid = "requestId" + } +} + +public struct DeviceTokenRotateParams: Codable, Sendable { + public let deviceid: String + public let role: String + public let scopes: [String]? + + public init( + deviceid: String, + role: String, + scopes: [String]? + ) { + self.deviceid = deviceid + self.role = role + self.scopes = scopes + } + private enum CodingKeys: String, CodingKey { + case deviceid = "deviceId" + case role + case scopes + } +} + +public struct DeviceTokenRevokeParams: Codable, Sendable { + public let deviceid: String + public let role: String + + public init( + deviceid: String, + role: String + ) { + self.deviceid = deviceid + self.role = role + } + private enum CodingKeys: String, CodingKey { + case deviceid = "deviceId" + case role + } +} + +public struct DevicePairRequestedEvent: Codable, Sendable { + public let requestid: String + public let deviceid: String + public let publickey: String + public let displayname: String? + public let platform: String? + public let clientid: String? + public let clientmode: String? + public let role: String? + public let roles: [String]? + public let scopes: [String]? + public let remoteip: String? + public let silent: Bool? + public let isrepair: Bool? + public let ts: Int + + public init( + requestid: String, + deviceid: String, + publickey: String, + displayname: String?, + platform: String?, + clientid: String?, + clientmode: String?, + role: String?, + roles: [String]?, + scopes: [String]?, + remoteip: String?, + silent: Bool?, + isrepair: Bool?, + ts: Int + ) { + self.requestid = requestid + self.deviceid = deviceid + self.publickey = publickey + self.displayname = displayname + self.platform = platform + self.clientid = clientid + self.clientmode = clientmode + self.role = role + self.roles = roles + self.scopes = scopes + self.remoteip = remoteip + self.silent = silent + self.isrepair = isrepair + self.ts = ts + } + private enum CodingKeys: String, CodingKey { + case requestid = "requestId" + case deviceid = "deviceId" + case publickey = "publicKey" + case displayname = "displayName" + case platform + case clientid = "clientId" + case clientmode = "clientMode" + case role + case roles + case scopes + case remoteip = "remoteIp" + case silent + case isrepair = "isRepair" + case ts + } +} + +public struct DevicePairResolvedEvent: Codable, Sendable { + public let requestid: String + public let deviceid: String + public let decision: String + public let ts: Int + + public init( + requestid: String, + deviceid: String, + decision: String, + ts: Int + ) { + self.requestid = requestid + self.deviceid = deviceid + self.decision = decision + self.ts = ts + } + private enum CodingKeys: String, CodingKey { + case requestid = "requestId" + case deviceid = "deviceId" + case decision + case ts + } +} + +public struct ChatHistoryParams: Codable, Sendable { + public let sessionkey: String + public let limit: Int? + + public init( + sessionkey: String, + limit: Int? + ) { + self.sessionkey = sessionkey + self.limit = limit + } + private enum CodingKeys: String, CodingKey { + case sessionkey = "sessionKey" + case limit + } +} + +public struct ChatSendParams: Codable, Sendable { + public let sessionkey: String + public let message: String + public let thinking: String? + public let deliver: Bool? + public let attachments: [AnyCodable]? + public let timeoutms: Int? + public let idempotencykey: String + + public init( + sessionkey: String, + message: String, + thinking: String?, + deliver: Bool?, + attachments: [AnyCodable]?, + timeoutms: Int?, + idempotencykey: String + ) { + self.sessionkey = sessionkey + self.message = message + self.thinking = thinking + self.deliver = deliver + self.attachments = attachments + self.timeoutms = timeoutms + self.idempotencykey = idempotencykey + } + private enum CodingKeys: String, CodingKey { + case sessionkey = "sessionKey" + case message + case thinking + case deliver + case attachments + case timeoutms = "timeoutMs" + case idempotencykey = "idempotencyKey" + } +} + +public struct ChatAbortParams: Codable, Sendable { + public let sessionkey: String + public let runid: String? + + public init( + sessionkey: String, + runid: String? + ) { + self.sessionkey = sessionkey + self.runid = runid + } + private enum CodingKeys: String, CodingKey { + case sessionkey = "sessionKey" + case runid = "runId" + } +} + +public struct ChatInjectParams: Codable, Sendable { + public let sessionkey: String + public let message: String + public let label: String? + + public init( + sessionkey: String, + message: String, + label: String? + ) { + self.sessionkey = sessionkey + self.message = message + self.label = label + } + private enum CodingKeys: String, CodingKey { + case sessionkey = "sessionKey" + case message + case label + } +} + +public struct ChatEvent: Codable, Sendable { + public let runid: String + public let sessionkey: String + public let seq: Int + public let state: AnyCodable + public let message: AnyCodable? + public let errormessage: String? + public let usage: AnyCodable? + public let stopreason: String? + + public init( + runid: String, + sessionkey: String, + seq: Int, + state: AnyCodable, + message: AnyCodable?, + errormessage: String?, + usage: AnyCodable?, + stopreason: String? + ) { + self.runid = runid + self.sessionkey = sessionkey + self.seq = seq + self.state = state + self.message = message + self.errormessage = errormessage + self.usage = usage + self.stopreason = stopreason + } + private enum CodingKeys: String, CodingKey { + case runid = "runId" + case sessionkey = "sessionKey" + case seq + case state + case message + case errormessage = "errorMessage" + case usage + case stopreason = "stopReason" + } +} + +public struct UpdateRunParams: Codable, Sendable { + public let sessionkey: String? + public let note: String? + public let restartdelayms: Int? + public let timeoutms: Int? + + public init( + sessionkey: String?, + note: String?, + restartdelayms: Int?, + timeoutms: Int? + ) { + self.sessionkey = sessionkey + self.note = note + self.restartdelayms = restartdelayms + self.timeoutms = timeoutms + } + private enum CodingKeys: String, CodingKey { + case sessionkey = "sessionKey" + case note + case restartdelayms = "restartDelayMs" + case timeoutms = "timeoutMs" + } +} + +public struct TickEvent: Codable, Sendable { + public let ts: Int + + public init( + ts: Int + ) { + self.ts = ts + } + private enum CodingKeys: String, CodingKey { + case ts + } +} + +public struct ShutdownEvent: Codable, Sendable { + public let reason: String + public let restartexpectedms: Int? + + public init( + reason: String, + restartexpectedms: Int? + ) { + self.reason = reason + self.restartexpectedms = restartexpectedms + } + private enum CodingKeys: String, CodingKey { + case reason + case restartexpectedms = "restartExpectedMs" + } +} + +public enum GatewayFrame: Codable, Sendable { + case req(RequestFrame) + case res(ResponseFrame) + case event(EventFrame) + case unknown(type: String, raw: [String: AnyCodable]) + + private enum CodingKeys: String, CodingKey { + case type + } + + public init(from decoder: Decoder) throws { + let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + let type = try typeContainer.decode(String.self, forKey: .type) + switch type { + case "req": + self = .req(try RequestFrame(from: decoder)) + case "res": + self = .res(try ResponseFrame(from: decoder)) + case "event": + self = .event(try EventFrame(from: decoder)) + default: + let container = try decoder.singleValueContainer() + let raw = try container.decode([String: AnyCodable].self) + self = .unknown(type: type, raw: raw) + } + } + + public func encode(to encoder: Encoder) throws { + switch self { + case .req(let v): try v.encode(to: encoder) + case .res(let v): try v.encode(to: encoder) + case .event(let v): try v.encode(to: encoder) + case .unknown(_, let raw): + var container = encoder.singleValueContainer() + try container.encode(raw) + } + } + +} diff --git a/apps/macos/Sources/ClawdbotProtocol/WizardHelpers.swift b/apps/shared/ClawdbotKit/Sources/ClawdbotProtocol/WizardHelpers.swift similarity index 100% rename from apps/macos/Sources/ClawdbotProtocol/WizardHelpers.swift rename to apps/shared/ClawdbotKit/Sources/ClawdbotProtocol/WizardHelpers.swift diff --git a/apps/shared/ClawdbotKit/Tests/ClawdbotKitTests/GatewayNodeSessionTests.swift b/apps/shared/ClawdbotKit/Tests/ClawdbotKitTests/GatewayNodeSessionTests.swift new file mode 100644 index 000000000..0fc688f63 --- /dev/null +++ b/apps/shared/ClawdbotKit/Tests/ClawdbotKitTests/GatewayNodeSessionTests.swift @@ -0,0 +1,56 @@ +import Foundation +import Testing +@testable import ClawdbotKit +import ClawdbotProtocol + +struct GatewayNodeSessionTests { + @Test + func invokeWithTimeoutReturnsUnderlyingResponseBeforeTimeout() async { + let request = BridgeInvokeRequest(id: "1", command: "x", paramsJSON: nil) + let response = await GatewayNodeSession.invokeWithTimeout( + request: request, + timeoutMs: 50, + onInvoke: { req in + #expect(req.id == "1") + return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: "{}", error: nil) + } + ) + + #expect(response.ok == true) + #expect(response.error == nil) + #expect(response.payloadJSON == "{}") + } + + @Test + func invokeWithTimeoutReturnsTimeoutError() async { + let request = BridgeInvokeRequest(id: "abc", command: "x", paramsJSON: nil) + let response = await GatewayNodeSession.invokeWithTimeout( + request: request, + timeoutMs: 10, + onInvoke: { _ in + try? await Task.sleep(nanoseconds: 200_000_000) // 200ms + return BridgeInvokeResponse(id: "abc", ok: true, payloadJSON: "{}", error: nil) + } + ) + + #expect(response.ok == false) + #expect(response.error?.code == .unavailable) + #expect(response.error?.message.contains("timed out") == true) + } + + @Test + func invokeWithTimeoutZeroDisablesTimeout() async { + let request = BridgeInvokeRequest(id: "1", command: "x", paramsJSON: nil) + let response = await GatewayNodeSession.invokeWithTimeout( + request: request, + timeoutMs: 0, + onInvoke: { req in + try? await Task.sleep(nanoseconds: 5_000_000) + return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: nil, error: nil) + } + ) + + #expect(response.ok == true) + #expect(response.error == nil) + } +} diff --git a/dist/control-ui/assets/index-BvhR9FCb.css b/dist/control-ui/assets/index-BvhR9FCb.css new file mode 100644 index 000000000..d9800ad6b --- /dev/null +++ b/dist/control-ui/assets/index-BvhR9FCb.css @@ -0,0 +1 @@ +@import"https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Unbounded:wght@400;500;600&family=Work+Sans:wght@400;500;600;700&display=swap";:root{--bg: #0a0f14;--bg-accent: #111826;--bg-grad-1: #162031;--bg-grad-2: #1f2a22;--bg-overlay: rgba(255, 255, 255, .05);--bg-glow: rgba(245, 159, 74, .12);--panel: rgba(14, 20, 30, .88);--panel-strong: rgba(18, 26, 38, .96);--chrome: rgba(9, 14, 20, .72);--chrome-strong: rgba(9, 14, 20, .86);--text: rgba(244, 246, 251, .96);--chat-text: rgba(231, 237, 244, .92);--muted: rgba(156, 169, 189, .72);--border: rgba(255, 255, 255, .09);--border-strong: rgba(255, 255, 255, .16);--accent: #f59f4a;--accent-2: #34c7b7;--ok: #2bd97f;--warn: #f2c94c;--danger: #ff6b6b;--focus: rgba(245, 159, 74, .35);--grid-line: rgba(255, 255, 255, .04);--theme-switch-x: 50%;--theme-switch-y: 50%;--mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--font-body: "Work Sans", system-ui, sans-serif;--font-display: "Unbounded", "Times New Roman", serif;color-scheme:dark}:root[data-theme=light]{--bg: #f5f1ea;--bg-accent: #ffffff;--bg-grad-1: #f1e6d6;--bg-grad-2: #e5eef4;--bg-overlay: rgba(28, 32, 46, .05);--bg-glow: rgba(52, 199, 183, .14);--panel: rgba(255, 255, 255, .9);--panel-strong: rgba(255, 255, 255, .97);--chrome: rgba(255, 255, 255, .75);--chrome-strong: rgba(255, 255, 255, .88);--text: rgba(27, 36, 50, .98);--chat-text: rgba(36, 48, 66, .9);--muted: rgba(80, 94, 114, .7);--border: rgba(18, 24, 40, .12);--border-strong: rgba(18, 24, 40, .2);--accent: #e28a3f;--accent-2: #1ba99d;--ok: #1aa86c;--warn: #b3771c;--danger: #d44848;--focus: rgba(226, 138, 63, .35);--grid-line: rgba(18, 24, 40, .06);color-scheme:light}*{box-sizing:border-box}html,body{height:100%}body{margin:0;font:15px/1.5 var(--font-body);background:radial-gradient(1200px 900px at 15% -10%,var(--bg-grad-1) 0%,transparent 55%) fixed,radial-gradient(900px 700px at 80% 10%,var(--bg-grad-2) 0%,transparent 60%) fixed,linear-gradient(160deg,var(--bg) 0%,var(--bg-accent) 100%) fixed;color:var(--text)}body:before{content:"";position:fixed;inset:0;background:linear-gradient(140deg,var(--bg-overlay) 0%,rgba(255,255,255,0) 40%),radial-gradient(620px 420px at 75% 75%,var(--bg-glow),transparent 60%);pointer-events:none;z-index:0}@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 .45s 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:inherit}button,input,textarea,select{font:inherit;color:inherit}@keyframes rise{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}@keyframes dashboard-enter{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}.shell{--shell-pad: 16px;--shell-gap: 16px;--shell-nav-width: 220px;--shell-topbar-height: 56px;--shell-focus-duration: .22s;--shell-focus-ease: cubic-bezier(.2, .85, .25, 1);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 .6s 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(--panel);-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px)}.topbar-left{display:flex;align-items:center;gap:12px}.topbar .nav-collapse-toggle{width:44px;height:44px;margin-bottom:0}.topbar .nav-collapse-toggle__icon{font-size:22px}.brand{display:flex;flex-direction:column;gap:2px}.brand-title{font-family:var(--font-display);font-size:16px;letter-spacing:1px;text-transform:uppercase;font-weight:600;line-height:1.1}.brand-sub{font-size:10px;color:var(--muted);letter-spacing:.8px;text-transform:uppercase;line-height:1}.topbar-status{display:flex;align-items:center;gap:8px}.topbar-status .pill{padding:4px 10px;gap:6px;font-size:11px}.topbar-status .statusDot{width:6px;height:6px}.topbar-status .theme-toggle{--theme-item: 22px;--theme-gap: 4px;--theme-pad: 4px}.topbar-status .theme-icon{width:12px;height:12px}.nav{grid-area:nav;overflow-y:auto;overflow-x:hidden;padding:16px;border-right:1px solid var(--border);background:var(--panel);-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px);transition:width var(--shell-focus-duration) var(--shell-focus-ease),padding 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}.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:6px;cursor:pointer;transition:background .15s ease,border-color .15s ease;margin-bottom:16px}.nav-collapse-toggle:hover{background:#ffffff14;border-color:var(--border)}:root[data-theme=light] .nav-collapse-toggle:hover{background:#0000000f}.nav-collapse-toggle__icon{font-size:16px;color:var(--muted)}.nav-group{margin-bottom:18px;display:grid;gap:6px;padding-bottom:12px;border-bottom:1px dashed rgba(255,255,255,.08)}.nav-group:last-child{margin-bottom:0;padding-bottom:0;border-bottom:none}.nav-group__items{display:grid;gap:4px}.nav-group--collapsed .nav-group__items{display:none}.nav-label{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;padding:4px 0;font-size:11px;font-weight:500;text-transform:uppercase;letter-spacing:1.4px;color:var(--text);opacity:.7;margin-bottom:4px;background:transparent;border:none;cursor:pointer;text-align:left}.nav-label:hover{opacity:1}.nav-label--static{cursor:default}.nav-label--static:hover{opacity:.7}.nav-label__text{flex:1}.nav-label__chevron{font-size:12px;opacity:.6}.nav-item{position:relative;display:flex;align-items:center;justify-content:flex-start;gap:8px;padding:10px 12px 10px 14px;border-radius:12px;border:1px solid transparent;background:transparent;color:var(--muted);cursor:pointer;text-decoration:none;transition:border-color .16s ease,background .16s ease,color .16s ease}.nav-item__icon{font-size:16px;width:18px;height:18px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.nav-item__text{font-size:13px;white-space:nowrap}.nav-item:hover{color:var(--text);border-color:#ffffff1f;background:#ffffff0f}.nav-item:before{content:"";position:absolute;left:0;top:50%;width:4px;height:60%;border-radius:0 999px 999px 0;transform:translateY(-50%);background:transparent}.nav-item.active{color:var(--text);border-color:#f59f4a73;background:#f59f4a1f}.nav-item.active:before{background:var(--accent);box-shadow:0 0 12px #f59f4a66}.content{grid-area:content;padding:8px 6px 20px;display:flex;flex-direction:column;gap:20px;min-height:0;overflow-y:auto;overflow-x:hidden}.content--chat{overflow:hidden}.content-header{display:flex;align-items:flex-end;justify-content:space-between;gap:12px;padding:0 6px;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:90px}.shell--chat-focus .content-header{opacity:0;transform:translateY(-10px);max-height:0px;padding:0;pointer-events:none}.page-title{font-family:var(--font-display);font-size:26px;letter-spacing:.6px}.page-sub{color:var(--muted);font-size:12px;letter-spacing:.4px}.page-meta{display:flex;gap:10px}.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:18px}.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:14px;grid-template-columns:repeat(auto-fit,minmax(140px,1fr))}.note-grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}.row{display:flex;gap:12px;align-items:center}.stack{display:grid;gap:14px}.filters{display:flex;flex-wrap:wrap;gap:10px;align-items:center}@media(max-width:1100px){.shell{--shell-pad: 12px;--shell-gap: 12px;--shell-nav-col: 1fr;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:16px;overflow-x:auto;border-right:none;padding:12px}.nav-group{grid-auto-flow:column;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));border-bottom:none;padding-bottom:0}.grid-cols-2,.grid-cols-3{grid-template-columns:1fr}.topbar{position:static;flex-direction:column;align-items:flex-start;gap:12px}.topbar-status{width:100%;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:6px;padding:10px 12px;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:10px;white-space:nowrap;flex-shrink:0}.nav-item:before{display:none}}@media(max-width:600px){.shell{--shell-pad: 8px;--shell-gap: 8px}.topbar{padding:10px 12px;border-radius: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:15px;letter-spacing:.3px}.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;border-radius:12px;gap:8px;-webkit-overflow-scrolling:touch;scrollbar-width:none}.nav::-webkit-scrollbar{display:none}.nav-group{display:contents}.nav-label{display:none}.nav-item{padding:7px 10px;font-size:12px;border-radius:8px;white-space:nowrap;flex-shrink:0}.nav-item:before{display:none}.content-header{display:none}.content{padding:4px 4px 16px;gap:12px}.card{padding:12px;border-radius:12px}.card-title{font-size:14px}.stat-grid{gap:8px;grid-template-columns:repeat(2,1fr)}.stat{padding:10px;border-radius:10px}.stat-label{font-size:10px}.stat-value{font-size:16px}.note-grid,.form-grid{grid-template-columns:1fr;gap:10px}.field input,.field textarea,.field select{padding:8px 10px;border-radius:10px;font-size:14px}.btn{padding:8px 12px;font-size:13px}.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:10px 8px;border-radius:12px}.chat-msg{max-width:92%}.chat-bubble{padding:8px 10px;border-radius:12px}.chat-compose{gap:8px}.chat-compose__field textarea{min-height:60px;padding:8px 10px;border-radius:12px;font-size:14px}.log-stream{border-radius:10px;max-height:400px}.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:10px}.list-title{font-size:14px}.list-sub{font-size:11px}.code-block{padding:8px;border-radius:10px;font-size:11px}.theme-toggle{--theme-item: 24px;--theme-gap: 4px;--theme-pad: 4px}.theme-icon{width:14px;height:14px}}.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-header__right{display:flex;align-items:center;gap:8px}.chat-session{min-width:180px}.chat-thread{flex:1 1 0;overflow-y:auto;overflow-x:hidden;padding:12px;margin:0 -12px;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-compose{position:sticky;bottom:0;flex-shrink:0;display:flex;align-items:flex-end;gap:12px;margin-top:auto;padding:16px 0 4px;background:linear-gradient(to bottom,transparent,var(--bg) 20%);z-index:10}.chat-compose__field{flex:1 1 auto;min-width:0}.chat-compose__field>span{display:none}.chat-compose .chat-compose__field textarea{width:100%;min-height:36px;max-height:150px;padding:8px 12px;border-radius:10px;resize:vertical;white-space:pre-wrap;font-family:var(--font-body);font-size:14px;line-height:1.45}.chat-compose__actions{flex-shrink:0;display:flex;align-items:stretch}.chat-compose .chat-compose__actions .btn{padding:8px 16px;font-size:13px;min-height:36px;white-space:nowrap}.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:#ffffffe6;border-color:#10182833;box-shadow:0 1px 2px #1018280d;color:#101828b3}:root[data-theme=light] .btn--icon:hover{background:#fff;border-color:#1018284d;color:#101828e6}.btn--icon svg{display:block}.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:#1018282e;background:#10182808}.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(code){font-family:var(--mono);font-size:.9em}.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);padding-left:12px;color:var(--muted)}.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:16px;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:#f59f4a33;color:#f59f4a}.chat-avatar.assistant{background:#34c7b733;color:#34c7b7}.chat-avatar.other{background:#96969633;color:#969696}.chat-avatar.tool{background:#868e9633;color:#868e96}img.chat-avatar{display:block;object-fit:cover;object-position:center}.chat-bubble{position:relative;display:inline-block;border:1px solid var(--border);background:#0000001f;border-radius:12px;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:#00000038;color:var(--muted);border-radius:8px;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-block;width:1em;text-align:center}.chat-bubble:hover .chat-copy-btn{opacity:1;pointer-events:auto}.chat-copy-btn:hover{background:#0000004d}.chat-copy-btn[data-copying="1"]{opacity:0;pointer-events:none}.chat-copy-btn[data-error="1"]{opacity:1;pointer-events:auto;border-color:#ff453acc;background:#ff453a2e;color:#ff453a}.chat-copy-btn[data-copied="1"]{opacity:1;pointer-events:auto;border-color:#34c7b7cc;background:#34c7b72e;color:#34c7b7}.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}}.chat-bubble:hover{background:#0000002e}.chat-group.user .chat-bubble{background:#f59f4a26;border-color:#f59f4a4d}.chat-group.user .chat-bubble:hover{background:#f59f4a38}.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;transition:border-color .15s ease-out,background .15s ease-out;max-height:120px;overflow:hidden}.chat-tool-card:hover{border-color:var(--accent);background:#0000000f}.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:18px;height:18px;font-size:14px;line-height:1;font-family:"Apple Color Emoji","Segoe UI Emoji","Noto Color Emoji",sans-serif;vertical-align:middle;flex-shrink:0}.chat-tool-card__action{font-size:12px;color:var(--accent);opacity:.8;transition:opacity .15s ease-out}.chat-tool-card--clickable:hover .chat-tool-card__action{opacity:1}.chat-tool-card__status{font-size:14px;color:var(--ok)}.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:#00000014;border-radius:6px;white-space:pre-wrap;overflow:hidden;max-height:44px;line-height:1.4;border:1px solid rgba(255,255,255,.04)}.chat-tool-card--clickable:hover .chat-tool-card__preview{background:#0000001f;border-color:#ffffff14}.chat-tool-card__inline{font-size:11px;color:var(--text);margin-top:6px;padding:6px 8px;background:#0000000f;border-radius:4px;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:linear-gradient(160deg,rgba(255,255,255,.04),transparent 65%),var(--panel);border-radius:16px;padding:16px;box-shadow:0 18px 36px #00000047;animation:rise .4s ease}.card-title{font-family:var(--font-display);font-size:16px;letter-spacing:.6px;text-transform:uppercase}.card-sub{color:var(--muted);font-size:12px}.stat{background:linear-gradient(140deg,rgba(255,255,255,.04),transparent 70%),var(--panel-strong);border-radius:14px;padding:12px;border:1px solid var(--border-strong)}.stat-label{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:1px}.stat-value{font-size:18px;margin-top:6px}.stat-value.ok{color:var(--ok)}.stat-value.warn{color:var(--warn)}.stat-card{display:grid;gap:6px}.note-title{font-weight:600;letter-spacing:.2px}.status-list{display:grid;gap:8px}.status-list div{display:flex;justify-content:space-between;gap:12px;padding:6px 0;border-bottom:1px dashed rgba(255,255,255,.06)}.status-list div:last-child{border-bottom:none}.account-count{margin-top:8px;font-size:12px;font-weight:600;letter-spacing:.4px;color:var(--muted)}.account-card-list{margin-top:16px;display:grid;gap:10px}.account-card{border:1px solid var(--border);border-radius:10px;padding:12px;background:linear-gradient(160deg,rgba(255,255,255,.06),transparent),#ffffff08}.account-card-header{display:flex;justify-content:space-between;align-items:baseline;gap:12px}.account-card-title{font-weight:600}.account-card-id{font-family:var(--mono);font-size:12px;color:var(--muted)}.account-card-status{margin-top:8px;font-size:13px}.account-card-status div{padding:4px 0}.account-card-error{margin-top:6px;color:var(--danger);font-size:12px}.label{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.9px}.pill{display:inline-flex;align-items:center;gap:8px;border:1px solid var(--border-strong);padding:6px 12px;border-radius:999px;background:linear-gradient(160deg,rgba(255,255,255,.06),transparent),var(--panel)}.theme-toggle{--theme-item: 28px;--theme-gap: 6px;--theme-pad: 6px;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:999px;border:1px solid var(--border-strong);background:#ffffff0a}.theme-toggle__indicator{position:absolute;top:50%;left:var(--theme-pad);width:var(--theme-item);height:var(--theme-item);border-radius:999px;transform:translateY(-50%) translate(calc(var(--theme-index, 0) * (var(--theme-item) + var(--theme-gap))));background:linear-gradient(160deg,rgba(255,255,255,.12),transparent),var(--panel-strong);border:1px solid var(--border-strong);box-shadow:0 8px 16px #00000040;transition:transform .18s ease-out,background .18s ease-out,box-shadow .18s 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:999px;background:transparent;color:var(--muted);cursor:pointer;position:relative;z-index:1;transition:color .15s ease-out,background .15s ease-out}.theme-toggle__button:hover{color:var(--text);background:#ffffff14}.theme-toggle__button.active{color:var(--text)}.theme-icon{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:1.75px;stroke-linecap:round;stroke-linejoin:round}.pill.danger{border-color:#ff5c5c80;color:var(--danger)}.statusDot{width:8px;height:8px;border-radius:999px;background:var(--danger);box-shadow:0 0 0 2px #00000040}.statusDot.ok{background:var(--ok);box-shadow:0 0 0 2px #00000040,0 0 10px #2bd97f66}.btn{border:1px solid var(--border-strong);background:#ffffff0a;padding:8px 14px;border-radius:999px;cursor:pointer;transition:transform .15s ease,border-color .15s ease,background .15s ease}.btn:hover{background:#ffffff1a;transform:translateY(-1px)}.btn.primary{border-color:#f59f4a73;background:#f59f4a33}.btn.active{border-color:#f59f4a8c;background:#f59f4a29}.btn.danger{border-color:#ff6b6b73;background:#ff6b6b2e}.btn--sm{padding:5px 10px;font-size:12px}.btn:disabled{opacity:.5;cursor:not-allowed;transform:none}.field{display:grid;gap:6px}.field.full{grid-column:1 / -1}.field span{color:var(--muted);font-size:11px;letter-spacing:.4px}.field input,.field textarea,.field select{border:1px solid var(--border-strong);background:#00000038;border-radius:12px;padding:9px 11px;outline:none;transition:border-color .15s ease,box-shadow .15s ease,background .15s ease}.field input:focus,.field textarea:focus,.field select:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--focus);background:#00000047}.field select{appearance:none;padding-right:38px;background-color:var(--panel-strong);background-image:linear-gradient(45deg,transparent 50%,var(--muted) 50%),linear-gradient(135deg,var(--muted) 50%,transparent 50%),linear-gradient(to right,transparent,transparent);background-position:calc(100% - 18px) 50%,calc(100% - 12px) 50%,calc(100% - 38px) 50%;background-size:6px 6px,6px 6px,1px 60%;background-repeat:no-repeat;box-shadow:inset 0 1px #ffffff0a}.field textarea{font-family:var(--mono);min-height:180px;resize:vertical;white-space:pre}.field textarea:focus{background:#00000052}.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}.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:#fff;border-color:#10182840;box-shadow:0 1px 2px #1018280f}:root[data-theme=light] .field input:focus,:root[data-theme=light] .field textarea:focus,:root[data-theme=light] .field select:focus{background:#fff}:root[data-theme=light] .btn{background:#ffffffe6;border-color:#10182833;box-shadow:0 1px 2px #1018280d}:root[data-theme=light] .btn:hover{background:#fff;border-color:#1018284d}:root[data-theme=light] .btn.primary{background:#f59f4a26}:root[data-theme=light] .btn.active{background:#f59f4a1f}.muted{color:var(--muted)}.mono{font-family:var(--mono)}.callout{padding:10px 12px;border-radius:14px;background:linear-gradient(160deg,rgba(255,255,255,.06),transparent),#ffffff08;border:1px solid var(--border)}.callout.danger{border-color:#ff5c5c66;color:var(--danger)}.callout.info{border-color:#5c9cff66;color:var(--accent)}.callout.success{border-color:#5cff8066;color:var(--positive, #5cff80)}.compaction-indicator{font-size:13px;padding:8px 12px;margin-bottom:8px;animation:compaction-fade-in .2s ease-out}.compaction-indicator--active{animation:compaction-pulse 1.5s ease-in-out infinite}.compaction-indicator--complete{animation:compaction-fade-in .2s ease-out}@keyframes compaction-fade-in{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}@keyframes compaction-pulse{0%,to{opacity:.7}50%{opacity:1}}.code-block{font-family:var(--mono);font-size:12px;background:#00000059;padding:10px;border-radius:12px;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:#ffffffd9}.list{display:grid;gap:12px;container-type:inline-size}.list-item{display:grid;grid-template-columns:minmax(0,1fr) minmax(220px,260px);gap:14px;align-items:start;border:1px solid var(--border);border-radius:14px;padding:12px;background:#0003}.list-item-clickable{cursor:pointer;transition:border-color .15s ease,box-shadow .15s ease}.list-item-clickable:hover{border-color:var(--border-strong)}.list-item-selected{border-color:var(--accent);box-shadow:0 0 0 1px var(--focus)}.list-main{display:grid;gap:6px;min-width:0}.list-title{font-weight:600}.list-sub{color:var(--muted);font-size:12px}.list-meta{text-align:right;color:var(--muted);font-size:11px;display:grid;gap:4px;min-width:220px}.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:11px;border:1px solid var(--border);border-radius:999px;padding:4px 8px;color:var(--muted);background:#0003}.chip input{margin-right:6px}.chip-ok{color:var(--ok);border-color:#1bd98a66}.chip-warn{color:var(--warn);border-color:#f2c94c66}.table{display:grid;gap:8px}.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:11px;text-transform:uppercase;letter-spacing:.8px;color:var(--muted)}.table-row{border:1px solid var(--border);padding:10px;border-radius:12px;background:#0003}.session-link{text-decoration:none;color:var(--accent)}.session-link:hover{text-decoration:underline}.log-stream{border:1px solid var(--border);border-radius:14px;background:#0003;max-height:520px;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:6px 10px;border-bottom:1px solid var(--border);font-size:12px}.log-row:last-child{border-bottom:none}.log-time{color:var(--muted)}.log-level{text-transform:uppercase;font-size:10px;font-weight:600;border:1px solid var(--border);border-radius:999px;padding:2px 6px;width:fit-content}.log-level.trace,.log-level.debug{color:var(--muted)}.log-level.info{color:var(--info);border-color:#4c96f266}.log-level.warn{color:var(--warn);border-color:#f2c94c66}.log-level.error,.log-level.fatal{color:var(--danger);border-color:#ff5c5c66}.log-chip.trace,.log-chip.debug{color:var(--muted)}.log-chip.info{color:var(--info);border-color:#4c96f266}.log-chip.warn{color:var(--warn);border-color:#f2c94c66}.log-chip.error,.log-chip.fatal{color:var(--danger);border-color:#ff5c5c66}.log-subsystem{color:var(--muted)}.log-message{white-space:pre-wrap;word-break:break-word}@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:12px;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:10px}.chat-session{min-width:240px}.chat-thread{margin-top:12px;display:flex;flex-direction:column;gap:12px;flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;padding:14px 12px;min-width:0;border-radius:0;border:none;background:transparent}:root[data-theme=light] .chat-thread{background:transparent}.chat-queue{margin-top:12px;padding:10px 12px;border-radius:16px;border:1px solid var(--border);background:#0000002e;display:grid;gap:8px}:root[data-theme=light] .chat-queue{background:#1018280a}.chat-queue__title{font-family:var(--font-mono);font-size:12px;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:10px;padding:8px 10px;border-radius:12px;border:1px dashed var(--border);background:#0003}:root[data-theme=light] .chat-queue__item{background:#1018280d}.chat-queue__text{color:var(--chat-text);font-size:13px;line-height:1.4;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(720px,82%)}.chat-line.user .chat-msg{justify-items:end}.chat-bubble{border:1px solid var(--border);background:#0000003d;border-radius:16px;padding:10px 12px;min-width:0;box-shadow:0 12px 22px #0000003d}:root[data-theme=light] .chat-bubble{background:#ffffffd9;box-shadow:0 12px 26px #10182814}.chat-line.user .chat-bubble{border-color:#f59f4a73;background:linear-gradient(135deg,#f59f4a42,#f59f4a1f)}.chat-line.assistant .chat-bubble{border-color:#34c7b733;background:linear-gradient(135deg,#34c7b71f,#0000003d)}:root[data-theme=light] .chat-line.assistant .chat-bubble{background:linear-gradient(135deg,#1bb9b11f,#ffffffd9)}@keyframes chatStreamPulse{0%{box-shadow:0 12px 22px #0000003d,0 0 #34c7b700}60%{box-shadow:0 12px 22px #0000003d,0 0 0 6px #34c7b714}to{box-shadow:0 12px 22px #0000003d,0 0 #34c7b700}}.chat-bubble.streaming{border-color:#34c7b766;animation:chatStreamPulse 1.6s ease-in-out infinite}@media(prefers-reduced-motion:reduce){.chat-bubble.streaming{animation:none}}.chat-bubble.chat-reading-indicator{width:fit-content;padding:10px 14px}.chat-reading-indicator__dots{display:inline-flex;align-items:center;gap:6px;height:10px}.chat-reading-indicator__dots>span{display:inline-block;width:6px;height:6px;border-radius:999px;background:var(--chat-text);opacity:.55;transform:translateY(0);animation:chatReadingDot 1.1s ease-in-out infinite;will-change:transform,opacity}.chat-reading-indicator__dots>span:nth-child(2){animation-delay:.12s}.chat-reading-indicator__dots>span:nth-child(3){animation-delay:.24s}@keyframes chatReadingDot{0%,80%,to{opacity:.38;transform:translateY(0) scale(.92)}40%{opacity:1;transform:translateY(-3px) scale(1.18)}}@media(prefers-reduced-motion:reduce){.chat-reading-indicator__dots>span{animation:none;opacity:.75}}.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.1em}.chat-text :where(li+li){margin-top:.25em}.chat-text :where(a){color:var(--accent);text-decoration-thickness:2px;text-underline-offset:2px}.chat-text :where(a:hover){text-decoration-thickness:3px}.chat-text :where(blockquote){border-left:2px solid rgba(255,255,255,.14);padding-left:12px;color:var(--muted)}:root[data-theme=light] .chat-text :where(blockquote){border-left-color:#10182829}.chat-text :where(hr){border:0;border-top:1px solid var(--border);opacity:.6;margin:.9em 0}.chat-text :where(code){font-family:var(--font-mono);font-size:.92em}.chat-text :where(:not(pre)>code){padding:.15em .35em;border-radius:8px;border:1px solid var(--border);background:#0003}:root[data-theme=light] .chat-text :where(:not(pre)>code){background:#1018280d}.chat-text :where(pre){margin-top:.75em;padding:10px 12px;border-radius:14px;border:1px solid var(--border);background:#00000038;overflow:auto}:root[data-theme=light] .chat-text :where(pre){background:#1018280a}.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:12px}.chat-text :where(th,td){border:1px solid var(--border);padding:6px 8px;vertical-align:top}.chat-text :where(th){font-family:var(--font-mono);font-weight:600;color:var(--muted)}.chat-tool-card{margin-top:8px;padding:8px 10px;border-radius:12px;border:1px solid var(--border);background:#00000038;display:grid;gap:4px}:root[data-theme=light] .chat-tool-card{background:#ffffffb3}.chat-tool-card__title{font-family:var(--font-mono);font-size:12px;color:var(--chat-text)}.chat-tool-card__detail{font-family:var(--font-mono);font-size:11px;color:var(--muted)}.chat-tool-card__details{margin-top:6px}.chat-tool-card__summary{font-family:var(--font-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:.8}.chat-tool-card__details[open] .chat-tool-card__summary{color:var(--chat-text)}.chat-tool-card__output{margin-top:6px;font-family:var(--font-mono);font-size:11px;line-height:1.45;white-space:pre-wrap;color:var(--chat-text);padding:8px;border-radius:10px;border:1px solid var(--border);background:#0003}:root[data-theme=light] .chat-tool-card__output{background:#1018280d}.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,rgba(0,0,0,0) 0%,var(--panel) 35%)}.shell--chat-focus .chat-compose{bottom:calc(var(--shell-pad) + 8px);padding-bottom:calc(14px + env(safe-area-inset-bottom,0px));border-bottom-left-radius:18px;border-bottom-right-radius:18px}.chat-compose__field{gap:4px}.chat-compose__field textarea{min-height:72px;padding:10px 12px;border-radius:16px;resize:vertical;white-space:pre-wrap;font-family:var(--font-body);line-height:1.45}.chat-compose__field textarea:disabled{opacity:.7;cursor:not-allowed}.chat-compose__actions{justify-content:flex-end;align-self:end}@media(max-width:900px){.chat-session{min-width:200px}.chat-compose{grid-template-columns:1fr}}.qr-wrap{margin-top:12px;border-radius:14px;background:#0003;border:1px dashed rgba(255,255,255,.18);padding:12px;display:inline-flex}.qr-wrap img{width:180px;height:180px;border-radius:10px;image-rendering:pixelated}.exec-approval-overlay{position:fixed;inset:0;background:#080c12b3;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;padding:24px;z-index:200}.exec-approval-card{width:min(560px,100%);background:var(--panel-strong);border:1px solid var(--border-strong);border-radius:18px;padding:20px;box-shadow:0 28px 60px #00000059;animation:rise .25s ease}.exec-approval-header{display:flex;align-items:center;justify-content:space-between;gap:12px}.exec-approval-title{font-family:var(--font-display);font-size:14px;letter-spacing:.8px;text-transform:uppercase}.exec-approval-sub{color:var(--muted);font-size:12px}.exec-approval-queue{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--muted);border:1px solid var(--border);border-radius:999px;padding:4px 10px}.exec-approval-command{margin-top:12px;padding:10px 12px;background:#00000040;border:1px solid var(--border);border-radius:12px;word-break:break-word;white-space:pre-wrap}.exec-approval-meta{margin-top:12px;display:grid;gap:6px;font-size:12px;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:12px;color:var(--danger)}.exec-approval-actions{margin-top:16px;display:flex;flex-wrap:wrap;gap:10px}.config-layout{display:grid;grid-template-columns:240px minmax(0,1fr);gap:0;min-height:calc(100vh - 140px);margin:-16px;border-radius:16px;overflow:hidden;border:1px solid var(--border);background:var(--panel)}.config-sidebar{display:flex;flex-direction:column;background:#0003;border-right:1px solid var(--border)}:root[data-theme=light] .config-sidebar{background:#00000008}.config-sidebar__header{display:flex;align-items:center;justify-content:space-between;padding:16px;border-bottom:1px solid var(--border)}.config-sidebar__title{font-weight:600;font-size:14px;letter-spacing:.3px}.config-sidebar__footer{margin-top:auto;padding:12px;border-top:1px solid var(--border)}.config-search{position:relative;padding:12px;border-bottom:1px solid var(--border)}.config-search__icon{position:absolute;left:24px;top:50%;transform:translateY(-50%);width:16px;height:16px;color:var(--muted);pointer-events:none}.config-search__input{width:100%;padding:10px 32px 10px 40px;border:1px solid var(--border);border-radius:8px;background:#00000026;font-size:13px;outline:none;transition:border-color .15s ease,box-shadow .15s ease,background .15s ease}.config-search__input::placeholder{color:var(--muted)}.config-search__input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--focus);background:#0003}:root[data-theme=light] .config-search__input{background:#fffc}:root[data-theme=light] .config-search__input:focus{background:#fff}.config-search__clear{position:absolute;right:20px;top:50%;transform:translateY(-50%);width:20px;height:20px;border:none;border-radius:50%;background:#ffffff1a;color:var(--muted);font-size:16px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .15s ease,color .15s ease}.config-search__clear:hover{background:#fff3;color:var(--text)}.config-nav{flex:1;overflow-y:auto;padding:8px}.config-nav__item{display:flex;align-items:center;gap:12px;width:100%;padding:10px 12px;border:none;border-radius:8px;background:transparent;color:var(--muted);font-size:13px;font-weight:500;text-align:left;cursor:pointer;transition:background .15s ease,color .15s ease}.config-nav__item:hover{background:#ffffff0d;color:var(--text)}:root[data-theme=light] .config-nav__item:hover{background:#0000000d}.config-nav__item.active{background:#f59f4a1f;color:var(--accent)}.config-nav__icon{width:20px;height:20px;display:flex;align-items:center;justify-content:center;font-size:14px}.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:3px;background:#0003;border-radius:8px;border:1px solid var(--border)}:root[data-theme=light] .config-mode-toggle{background:#0000000f}.config-mode-toggle__btn{flex:1;padding:8px 12px;border:none;border-radius:6px;background:transparent;color:var(--muted);font-size:12px;font-weight:600;cursor:pointer;transition:background .15s ease,color .15s ease,box-shadow .15s ease}.config-mode-toggle__btn:hover{color:var(--text)}.config-mode-toggle__btn.active{background:#ffffff1a;color:var(--text);box-shadow:0 1px 3px #0003}:root[data-theme=light] .config-mode-toggle__btn.active{background:#fff;box-shadow:0 1px 3px #0000001a}.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:12px;padding:12px 20px;background:#00000014;border-bottom:1px solid var(--border)}:root[data-theme=light] .config-actions{background:#00000005}.config-actions__left,.config-actions__right{display:flex;align-items:center;gap:8px}.config-changes-badge{padding:5px 12px;border-radius:999px;background:#f59f4a26;border:1px solid rgba(245,159,74,.3);color:var(--accent);font-size:12px;font-weight:600}.config-status{font-size:13px;color:var(--muted)}.config-diff{margin:16px 20px 0;border:1px solid rgba(245,159,74,.3);border-radius:10px;background:#f59f4a0d;overflow:hidden}.config-diff__summary{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;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 .2s ease}.config-diff__chevron svg{width:100%;height:100%}.config-diff[open] .config-diff__chevron{transform:rotate(180deg)}.config-diff__content{padding:0 16px 16px;display:grid;gap:8px}.config-diff__item{display:flex;align-items:baseline;gap:12px;padding:8px 12px;border-radius:6px;background:#0000001a;font-size:12px;font-family:var(--mono)}:root[data-theme=light] .config-diff__item{background:#fff9}.config-diff__path{font-weight:600;color:var(--text);flex-shrink:0}.config-diff__values{display:flex;align-items:baseline;gap:8px;min-width:0;flex-wrap:wrap}.config-diff__from{color:var(--danger);opacity:.8}.config-diff__arrow{color:var(--muted)}.config-diff__to{color:var(--ok)}.config-section-hero{display:flex;align-items:center;gap:14px;padding:14px 20px;border-bottom:1px solid var(--border);background:#0000000a}:root[data-theme=light] .config-section-hero{background:#00000004}.config-section-hero__icon{width:28px;height:28px;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:2px;min-width:0}.config-section-hero__title{font-size:15px;font-weight:600}.config-section-hero__desc{font-size:12px;color:var(--muted)}.config-subnav{display:flex;gap:8px;padding:10px 20px 12px;border-bottom:1px solid var(--border);background:#00000008;overflow-x:auto}:root[data-theme=light] .config-subnav{background:#00000005}.config-subnav__item{border:1px solid transparent;border-radius:999px;padding:6px 12px;font-size:12px;font-weight:600;color:var(--muted);background:#0000001f;cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease;white-space:nowrap}:root[data-theme=light] .config-subnav__item{background:#0000000f}.config-subnav__item:hover{color:var(--text);background:#ffffff14}:root[data-theme=light] .config-subnav__item:hover{background:#00000014}.config-subnav__item.active{color:var(--accent);border-color:#f59f4a66;background:#f59f4a1f}.config-content{flex:1;overflow-y:auto;padding:20px}.config-raw-field textarea{min-height:500px;font-family:var(--mono);font-size:13px;line-height:1.5}.config-loading{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;padding:80px 20px;color:var(--muted)}.config-loading__spinner{width:36px;height:36px;border:3px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.config-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;padding:80px 20px;text-align:center}.config-empty__icon{font-size:56px;opacity:.4}.config-empty__text{color:var(--muted);font-size:15px}.config-form--modern{display:grid;gap:24px}.config-section-card{border:1px solid var(--border);border-radius:12px;background:#ffffff05;overflow:hidden}:root[data-theme=light] .config-section-card{background:#ffffff80}.config-section-card__header{display:flex;align-items:flex-start;gap:14px;padding:18px 20px;background:#0000000f;border-bottom:1px solid var(--border)}:root[data-theme=light] .config-section-card__header{background:#00000005}.config-section-card__icon{width:32px;height:32px;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}.config-section-card__desc{margin:4px 0 0;font-size:13px;color:var(--muted);line-height:1.4}.config-section-card__content{padding:20px}.cfg-fields{display:grid;gap:20px}.cfg-field{display:grid;gap:6px}.cfg-field--error{padding:12px;border-radius:8px;background:#ff5c5c1a;border:1px solid rgba(255,92,92,.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.4}.cfg-field__error{font-size:12px;color:var(--danger)}.cfg-input-wrap{display:flex;gap:8px}.cfg-input{flex:1;padding:10px 12px;border:1px solid var(--border);border-radius:8px;background:#0000001f;font-size:14px;outline:none;transition:border-color .15s ease,box-shadow .15s ease,background .15s ease}.cfg-input::placeholder{color:var(--muted);opacity:.7}.cfg-input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--focus);background:#0000002e}:root[data-theme=light] .cfg-input{background:#fff}:root[data-theme=light] .cfg-input:focus{background:#fff}.cfg-input--sm{padding:8px 10px;font-size:13px}.cfg-input__reset{padding:8px 12px;border:1px solid var(--border);border-radius:8px;background:#ffffff0d;color:var(--muted);font-size:14px;cursor:pointer;transition:background .15s ease,color .15s ease}.cfg-input__reset:hover:not(:disabled){background:#ffffff1a;color:var(--text)}.cfg-input__reset:disabled{opacity:.5;cursor:not-allowed}.cfg-textarea{width:100%;padding:10px 12px;border:1px solid var(--border);border-radius:8px;background:#0000001f;font-family:var(--mono);font-size:13px;line-height:1.5;resize:vertical;outline:none;transition:border-color .15s ease,box-shadow .15s ease}.cfg-textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--focus)}:root[data-theme=light] .cfg-textarea{background:#fff}.cfg-textarea--sm{padding:8px 10px;font-size:12px}.cfg-number{display:inline-flex;border:1px solid var(--border);border-radius:8px;overflow:hidden;background:#0000001f}:root[data-theme=light] .cfg-number{background:#fff}.cfg-number__btn{width:40px;border:none;background:#ffffff0d;color:var(--text);font-size:18px;font-weight:300;cursor:pointer;transition:background .15s ease}.cfg-number__btn:hover:not(:disabled){background:#ffffff1a}.cfg-number__btn:disabled{opacity:.4;cursor:not-allowed}:root[data-theme=light] .cfg-number__btn{background:#00000008}:root[data-theme=light] .cfg-number__btn:hover:not(:disabled){background:#0000000f}.cfg-number__input{width:80px;padding:10px;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:10px 36px 10px 12px;border:1px solid var(--border);border-radius:8px;background-color:#0000001f;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 10px center;font-size:14px;cursor:pointer;outline:none;appearance:none;transition:border-color .15s ease,box-shadow .15s ease}.cfg-select:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--focus)}:root[data-theme=light] .cfg-select{background-color:#fff}.cfg-segmented{display:inline-flex;padding:3px;border:1px solid var(--border);border-radius:8px;background:#0000001f}:root[data-theme=light] .cfg-segmented{background:#0000000a}.cfg-segmented__btn{padding:8px 16px;border:none;border-radius:6px;background:transparent;color:var(--muted);font-size:13px;font-weight:500;cursor:pointer;transition:background .15s ease,color .15s ease,box-shadow .15s ease}.cfg-segmented__btn:hover:not(:disabled):not(.active){color:var(--text)}.cfg-segmented__btn.active{background:#ffffff1f;color:var(--text);box-shadow:0 1px 3px #0003}:root[data-theme=light] .cfg-segmented__btn.active{background:#fff;box-shadow:0 1px 3px #0000001a}.cfg-segmented__btn:disabled{opacity:.5;cursor:not-allowed}.cfg-toggle-row{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:14px 16px;border:1px solid var(--border);border-radius:10px;background:#0000000f;cursor:pointer;transition:background .15s ease,border-color .15s ease}.cfg-toggle-row:hover:not(.disabled){background:#0000001a;border-color:var(--border-strong)}.cfg-toggle-row.disabled{opacity:.6;cursor:not-allowed}:root[data-theme=light] .cfg-toggle-row{background:#ffffff80}:root[data-theme=light] .cfg-toggle-row:hover:not(.disabled){background:#fffc}.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:2px;font-size:12px;color:var(--muted);line-height:1.4}.cfg-toggle{position:relative;flex-shrink:0}.cfg-toggle input{position:absolute;opacity:0;width:0;height:0}.cfg-toggle__track{display:block;width:48px;height:28px;background:#ffffff1f;border:1px solid var(--border);border-radius:999px;position:relative;transition:background .2s ease,border-color .2s ease}:root[data-theme=light] .cfg-toggle__track{background:#0000001a}.cfg-toggle__track:after{content:"";position:absolute;top:3px;left:3px;width:20px;height:20px;background:var(--text);border-radius:50%;box-shadow:0 2px 4px #0000004d;transition:transform .2s ease,background .2s ease}.cfg-toggle input:checked+.cfg-toggle__track{background:#2bd97f40;border-color:#2bd97f80}.cfg-toggle input:checked+.cfg-toggle__track:after{transform:translate(20px);background:var(--ok)}.cfg-toggle input:focus+.cfg-toggle__track{box-shadow:0 0 0 3px var(--focus)}.cfg-object{border:1px solid var(--border);border-radius:10px;background:#0000000a;overflow:hidden}:root[data-theme=light] .cfg-object{background:#fff6}.cfg-object__header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;cursor:pointer;list-style:none;transition:background .15s ease}.cfg-object__header:hover{background:#ffffff08}:root[data-theme=light] .cfg-object__header:hover{background:#00000005}.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 .2s ease}.cfg-object__chevron svg{width:100%;height:100%}.cfg-object[open] .cfg-object__chevron{transform:rotate(180deg)}.cfg-object__help{padding:0 16px 12px;font-size:12px;color:var(--muted);border-bottom:1px solid var(--border)}.cfg-object__content{padding:16px;display:grid;gap:16px}.cfg-array{border:1px solid var(--border);border-radius:10px;overflow:hidden}.cfg-array__header{display:flex;align-items:center;gap:12px;padding:12px 16px;background:#0000000f;border-bottom:1px solid var(--border)}:root[data-theme=light] .cfg-array__header{background:#00000005}.cfg-array__label{flex:1;font-size:14px;font-weight:600;color:var(--text)}.cfg-array__count{font-size:12px;color:var(--muted);padding:3px 8px;background:#ffffff0f;border-radius:999px}:root[data-theme=light] .cfg-array__count{background:#0000000f}.cfg-array__add{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border:1px solid var(--border);border-radius:6px;background:#ffffff0d;color:var(--text);font-size:12px;font-weight:500;cursor:pointer;transition:background .15s ease}.cfg-array__add:hover:not(:disabled){background:#ffffff1a}.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:10px 16px;font-size:12px;color:var(--muted);border-bottom:1px solid var(--border)}.cfg-array__empty{padding:32px 16px;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:10px 16px;background:#0000000a;border-bottom:1px solid var(--border)}:root[data-theme=light] .cfg-array__item-header{background:#00000005}.cfg-array__item-index{font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}.cfg-array__item-remove{width:28px;height:28px;display:flex;align-items:center;justify-content:center;border:none;border-radius:6px;background:transparent;color:var(--muted);cursor:pointer;transition:background .15s ease,color .15s ease}.cfg-array__item-remove svg{width:16px;height:16px}.cfg-array__item-remove:hover:not(:disabled){background:#ff5c5c26;color:var(--danger)}.cfg-array__item-remove:disabled{opacity:.4;cursor:not-allowed}.cfg-array__item-content{padding:16px}.cfg-map{border:1px solid var(--border);border-radius:10px;overflow:hidden}.cfg-map__header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 16px;background:#0000000f;border-bottom:1px solid var(--border)}:root[data-theme=light] .cfg-map__header{background:#00000005}.cfg-map__label{font-size:13px;font-weight:600;color:var(--muted)}.cfg-map__add{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border:1px solid var(--border);border-radius:6px;background:#ffffff0d;color:var(--text);font-size:12px;font-weight:500;cursor:pointer;transition:background .15s ease}.cfg-map__add:hover:not(:disabled){background:#ffffff1a}.cfg-map__add-icon{width:14px;height:14px}.cfg-map__add-icon svg{width:100%;height:100%}.cfg-map__empty{padding:24px 16px;text-align:center;color:var(--muted);font-size:13px}.cfg-map__items{display:grid;gap:8px;padding:12px}.cfg-map__item{display:grid;grid-template-columns:140px 1fr auto;gap:8px;align-items:start}.cfg-map__item-key,.cfg-map__item-value{min-width:0}.cfg-map__item-remove{width:32px;height:32px;display:flex;align-items:center;justify-content:center;border:none;border-radius:6px;background:transparent;color:var(--muted);cursor:pointer;transition:background .15s ease,color .15s ease}.cfg-map__item-remove svg{width:16px;height:16px}.cfg-map__item-remove:hover:not(:disabled){background:#ff5c5c26;color:var(--danger)}.pill--sm{padding:4px 10px;font-size:11px}.pill--ok{border-color:#2bd97f66;color:var(--ok)}.pill--danger{border-color:#ff5c5c66;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:12px 16px}.config-nav{display:flex;flex-wrap:nowrap;gap:4px;padding:8px 12px;overflow-x:auto;-webkit-overflow-scrolling:touch}.config-nav__item{flex:0 0 auto;padding:8px 12px;white-space:nowrap}.config-nav__label{display:inline}.config-sidebar__footer{display:none}.config-actions{flex-wrap:wrap;padding:12px 16px}.config-actions__left,.config-actions__right{width:100%;justify-content:center}.config-section-hero{padding:12px 16px}.config-subnav{padding:8px 16px 10px}.config-content{padding:16px}.config-section-card__header{padding:14px 16px}.config-section-card__content{padding:16px}.cfg-toggle-row{padding:12px 14px}.cfg-map__item{grid-template-columns:1fr;gap:8px}.cfg-map__item-remove{justify-self:end}}@media(max-width:480px){.config-nav__icon{width:24px;height:24px;font-size:16px}.config-nav__label{display:none}.config-section-card__icon{width:28px;height:28px}.config-section-card__title{font-size:15px}.cfg-segmented{flex-wrap:wrap}.cfg-segmented__btn{flex:1 0 auto;min-width:60px}} diff --git a/dist/control-ui/assets/index-DsXRcnEw.js b/dist/control-ui/assets/index-DsXRcnEw.js new file mode 100644 index 000000000..1b0c3042c --- /dev/null +++ b/dist/control-ui/assets/index-DsXRcnEw.js @@ -0,0 +1,3059 @@ +(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 o of i)if(o.type==="childList")for(const a of o.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&s(a)}).observe(document,{childList:!0,subtree:!0});function n(i){const o={};return i.integrity&&(o.integrity=i.integrity),i.referrerPolicy&&(o.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?o.credentials="include":i.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function s(i){if(i.ep)return;i.ep=!0;const o=n(i);fetch(i.href,o)}})();const jt=globalThis,Cs=jt.ShadowRoot&&(jt.ShadyCSS===void 0||jt.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,Es=Symbol(),Ni=new WeakMap;let Go=class{constructor(t,n,s){if(this._$cssResult$=!0,s!==Es)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(Cs&&t===void 0){const s=n!==void 0&&n.length===1;s&&(t=Ni.get(n)),t===void 0&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),s&&Ni.set(n,t))}return t}toString(){return this.cssText}};const Br=e=>new Go(typeof e=="string"?e:e+"",void 0,Es),Fr=(e,...t)=>{const n=e.length===1?e[0]:t.reduce((s,i,o)=>s+(a=>{if(a._$cssResult$===!0)return a.cssText;if(typeof a=="number")return a;throw Error("Value passed to 'css' function must be a 'css' function result: "+a+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+e[o+1],e[0]);return new Go(n,e,Es)},Ur=(e,t)=>{if(Cs)e.adoptedStyleSheets=t.map(n=>n instanceof CSSStyleSheet?n:n.styleSheet);else for(const n of t){const s=document.createElement("style"),i=jt.litNonce;i!==void 0&&s.setAttribute("nonce",i),s.textContent=n.cssText,e.appendChild(s)}},Oi=Cs?e=>e:e=>e instanceof CSSStyleSheet?(t=>{let n="";for(const s of t.cssRules)n+=s.cssText;return Br(n)})(e):e;const{is:Kr,defineProperty:Hr,getOwnPropertyDescriptor:zr,getOwnPropertyNames:jr,getOwnPropertySymbols:qr,getPrototypeOf:Wr}=Object,tn=globalThis,Di=tn.trustedTypes,Vr=Di?Di.emptyScript:"",Gr=tn.reactiveElementPolyfillSupport,mt=(e,t)=>e,Vt={toAttribute(e,t){switch(t){case Boolean:e=e?Vr: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}},Is=(e,t)=>!Kr(e,t),Bi={attribute:!0,type:String,converter:Vt,reflect:!1,useDefault:!1,hasChanged:Is};Symbol.metadata??=Symbol("metadata"),tn.litPropertyMetadata??=new WeakMap;let Ge=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=Bi){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&&Hr(this.prototype,t,i)}}static getPropertyDescriptor(t,n,s){const{get:i,set:o}=zr(this.prototype,t)??{get(){return this[n]},set(a){this[n]=a}};return{get:i,set(a){const l=i?.call(this);o?.call(this,a),this.requestUpdate(t,l,s)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)??Bi}static _$Ei(){if(this.hasOwnProperty(mt("elementProperties")))return;const t=Wr(this);t.finalize(),t.l!==void 0&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties)}static finalize(){if(this.hasOwnProperty(mt("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(mt("properties"))){const n=this.properties,s=[...jr(n),...qr(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(Oi(i))}else t!==void 0&&n.push(Oi(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 Ur(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 o=(s.converter?.toAttribute!==void 0?s.converter:Vt).toAttribute(n,s.type);this._$Em=t,o==null?this.removeAttribute(i):this.setAttribute(i,o),this._$Em=null}}_$AK(t,n){const s=this.constructor,i=s._$Eh.get(t);if(i!==void 0&&this._$Em!==i){const o=s.getPropertyOptions(i),a=typeof o.converter=="function"?{fromAttribute:o.converter}:o.converter?.fromAttribute!==void 0?o.converter:Vt;this._$Em=i;const l=a.fromAttribute(n,o.type);this[i]=l??this._$Ej?.get(i)??l,this._$Em=null}}requestUpdate(t,n,s,i=!1,o){if(t!==void 0){const a=this.constructor;if(i===!1&&(o=this[t]),s??=a.getPropertyOptions(t),!((s.hasChanged??Is)(o,n)||s.useDefault&&s.reflect&&o===this._$Ej?.get(t)&&!this.hasAttribute(a._$Eu(t,s))))return;this.C(t,n,s)}this.isUpdatePending===!1&&(this._$ES=this._$EP())}C(t,n,{useDefault:s,reflect:i,wrapped:o},a){s&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,a??n??this[t]),o!==!0||a!==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,o]of this._$Ep)this[i]=o;this._$Ep=void 0}const s=this.constructor.elementProperties;if(s.size>0)for(const[i,o]of s){const{wrapped:a}=o,l=this[i];a!==!0||this._$AL.has(i)||l===void 0||this.C(i,void 0,o,l)}}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){}};Ge.elementStyles=[],Ge.shadowRootOptions={mode:"open"},Ge[mt("elementProperties")]=new Map,Ge[mt("finalized")]=new Map,Gr?.({ReactiveElement:Ge}),(tn.reactiveElementVersions??=[]).push("2.1.2");const Ls=globalThis,Fi=e=>e,Gt=Ls.trustedTypes,Ui=Gt?Gt.createPolicy("lit-html",{createHTML:e=>e}):void 0,Yo="$lit$",we=`lit$${Math.random().toFixed(9).slice(2)}$`,Qo="?"+we,Yr=`<${Qo}>`,Ne=document,wt=()=>Ne.createComment(""),$t=e=>e===null||typeof e!="object"&&typeof e!="function",Rs=Array.isArray,Qr=e=>Rs(e)||typeof e?.[Symbol.iterator]=="function",Nn=`[ +\f\r]`,at=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,Ki=/-->/g,Hi=/>/g,Ee=RegExp(`>|${Nn}(?:([^\\s"'>=/]+)(${Nn}*=${Nn}*(?:[^ +\f\r"'\`<>=]|("|')|))|$)`,"g"),zi=/'/g,ji=/"/g,Jo=/^(?:script|style|textarea|title)$/i,Jr=e=>(t,...n)=>({_$litType$:e,strings:t,values:n}),c=Jr(1),xe=Symbol.for("lit-noChange"),g=Symbol.for("lit-nothing"),qi=new WeakMap,Me=Ne.createTreeWalker(Ne,129);function Zo(e,t){if(!Rs(e)||!e.hasOwnProperty("raw"))throw Error("invalid template strings array");return Ui!==void 0?Ui.createHTML(t):t}const Zr=(e,t)=>{const n=e.length-1,s=[];let i,o=t===2?"":t===3?"":"",a=at;for(let l=0;l"?(a=i??at,u=-1):d[1]===void 0?u=-2:(u=a.lastIndex-d[2].length,p=d[1],a=d[3]===void 0?Ee:d[3]==='"'?ji:zi):a===ji||a===zi?a=Ee:a===Ki||a===Hi?a=at:(a=Ee,i=void 0);const v=a===Ee&&e[l+1].startsWith("/>")?" ":"";o+=a===at?r+Yr:u>=0?(s.push(p),r.slice(0,u)+Yo+r.slice(u)+we+v):r+we+(u===-2?l:v)}return[Zo(e,o+(e[n]||"")+(t===2?"":t===3?"":"")),s]};let ns=class Xo{constructor({strings:t,_$litType$:n},s){let i;this.parts=[];let o=0,a=0;const l=t.length-1,r=this.parts,[p,d]=Zr(t,n);if(this.el=Xo.createElement(p,s),Me.currentNode=this.el.content,n===2||n===3){const u=this.el.content.firstChild;u.replaceWith(...u.childNodes)}for(;(i=Me.nextNode())!==null&&r.length0){i.textContent=Gt?Gt.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 o=this.strings;let a=!1;if(o===void 0)t=Je(this,t,n,0),a=!$t(t)||t!==this._$AH&&t!==xe,a&&(this._$AH=t);else{const l=t;let r,p;for(t=o[0],r=0;r{const s=n?.renderBefore??t;let i=s._$litPart$;if(i===void 0){const o=n?.renderBefore??null;s._$litPart$=i=new nn(t.insertBefore(wt(),o),o,void 0,n??{})}return i._$AI(e),i};const Ms=globalThis;let Qe=class extends Ge{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=al(n,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return xe}};Qe._$litElement$=!0,Qe.finalized=!0,Ms.litElementHydrateSupport?.({LitElement:Qe});const rl=Ms.litElementPolyfillSupport;rl?.({LitElement:Qe});(Ms.litElementVersions??=[]).push("4.2.2");const ta=e=>(t,n)=>{n!==void 0?n.addInitializer(()=>{customElements.define(e,t)}):customElements.define(e,t)};const ll={attribute:!0,type:String,converter:Vt,reflect:!1,hasChanged:Is},cl=(e=ll,t,n)=>{const{kind:s,metadata:i}=n;let o=globalThis.litPropertyMetadata.get(i);if(o===void 0&&globalThis.litPropertyMetadata.set(i,o=new Map),s==="setter"&&((e=Object.create(e)).wrapped=!0),o.set(n.name,e),s==="accessor"){const{name:a}=n;return{set(l){const r=t.get.call(this);t.set.call(this,l),this.requestUpdate(a,r,e,!0,l)},init(l){return l!==void 0&&this.C(a,void 0,e,l),l}}}if(s==="setter"){const{name:a}=n;return function(l){const r=this[a];t.call(this,l),this.requestUpdate(a,r,e,!0,l)}}throw Error("Unsupported decorator location: "+s)};function on(e){return(t,n)=>typeof n=="object"?cl(e,t,n):((s,i,o)=>{const a=i.hasOwnProperty(o);return i.constructor.createProperty(o,s),a?Object.getOwnPropertyDescriptor(i,o):void 0})(e,t,n)}function y(e){return on({...e,state:!0,attribute:!1})}const dl=50,ul=200,pl="Assistant";function Wi(e,t){if(typeof e!="string")return;const n=e.trim();if(n)return n.length<=t?n:n.slice(0,t)}function ss(e){const t=Wi(e?.name,dl)??pl,n=Wi(e?.avatar??void 0,ul)??null;return{agentId:typeof e?.agentId=="string"&&e.agentId.trim()?e.agentId.trim():null,name:t,avatar:n}}function fl(){return ss(typeof window>"u"?{}:{name:window.__CLAWDBOT_ASSISTANT_NAME__,avatar:window.__CLAWDBOT_ASSISTANT_AVATAR__})}const na="clawdbot.control.settings.v1";function hl(){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(na);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 gl(e){localStorage.setItem(na,JSON.stringify(e))}function sa(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 vl=[{label:"Chat",tabs:["chat"]},{label:"Control",tabs:["overview","channels","instances","sessions","cron"]},{label:"Agent",tabs:["skills","nodes"]},{label:"Settings",tabs:["config","debug","logs"]}],ia={overview:"/overview",channels:"/channels",instances:"/instances",sessions:"/sessions",cron:"/cron",skills:"/skills",nodes:"/nodes",chat:"/chat",config:"/config",debug:"/debug",logs:"/logs"},oa=new Map(Object.entries(ia).map(([e,t])=>[t,e]));function an(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 Ps(e,t=""){const n=an(t),s=ia[e];return n?`${n}${s}`:s}function aa(e,t=""){const n=an(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":oa.get(i)??null}function ml(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!!(t&&t.trim())).join(", ")}function as(e,t=120){return e.length<=t?e:`${e.slice(0,Math.max(0,t-1))}…`}function la(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 Yt(e,t){const n=Number(e);return Number.isFinite(n)?n:t}const On=/<\s*\/?\s*think(?:ing)?\s*>/gi,Vi=/<\s*think(?:ing)?\s*>/i,Gi=/<\s*\/\s*think(?:ing)?\s*>/i;function Dn(e){if(!e)return e;const t=Vi.test(e),n=Gi.test(e);if(!t&&!n)return e;if(t!==n)return t?e.replace(Vi,"").trimStart():e.replace(Gi,"").trimStart();if(!On.test(e))return e;On.lastIndex=0;let s="",i=0,o=!1;for(const a of e.matchAll(On)){const l=a.index??0;o||(s+=e.slice(i,l)),o=!a[0].toLowerCase().includes("/"),i=l+a[0].length}return o||(s+=e.slice(i)),s.trimStart()}const wl=/^\[([^\]]+)\]\s*/,$l=["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:$l.some(t=>e.startsWith(`${t} `))}function Un(e){const t=e.match(wl);if(!t)return e;const n=t[1]??"";return kl(n)?e.slice(t[0].length):e}function rs(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(o=>{const a=o;return a.type==="text"&&typeof a.text=="string"?a.text:null}).filter(o=>typeof o=="string");if(i.length>0){const o=i.join(` +`);return n==="assistant"?Dn(o):Un(o)}}return typeof t.text=="string"?n==="assistant"?Dn(t.text):Un(t.text):null}function ca(e){if(!e||typeof e!="object")return rs(e);const t=e;if(Bn.has(t))return Bn.get(t)??null;const n=rs(e);return Bn.set(t,n),n}function Yi(e){const n=e.content,s=[];if(Array.isArray(n))for(const l of n){const r=l;if(r.type==="thinking"&&typeof r.thinking=="string"){const p=r.thinking.trim();p&&s.push(p)}}if(s.length>0)return s.join(` +`);const i=Al(e);if(!i)return null;const a=[...i.matchAll(/<\s*think(?:ing)?\s*>([\s\S]*?)<\s*\/\s*think(?:ing)?\s*>/gi)].map(l=>(l[1]??"").trim()).filter(Boolean);return a.length>0?a.join(` +`):null}function xl(e){if(!e||typeof e!="object")return Yi(e);const t=e;if(Fn.has(t))return Fn.get(t)??null;const n=Yi(e);return Fn.set(t,n),n}function Al(e){const t=e,n=t.content;if(typeof n=="string")return n;if(Array.isArray(n)){const s=n.map(i=>{const o=i;return o.type==="text"&&typeof o.text=="string"?o.text:null}).filter(i=>typeof i=="string");if(s.length>0)return s.join(` +`)}return typeof t.text=="string"?t.text:null}function Sl(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 Qi(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 Ns(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),Qi(t)}return Qi(_l())}async function Ze(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 Tl(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=Ns();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(o){const a=String(o);return e.chatRunId=null,e.chatStream=null,e.chatStreamStartedAt=null,e.lastError=a,e.chatMessages=[...e.chatMessages,{role:"assistant",content:[{type:"text",text:"Error: "+a}],timestamp:Date.now()}],!1}finally{e.chatSending=!1}}async function Cl(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 El(e,t){if(!t||t.sessionKey!==e.sessionKey||t.runId&&e.chatRunId&&t.runId!==e.chatRunId)return null;if(t.state==="delta"){const n=rs(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 nt(e){if(!(!e.client||!e.connected)&&!e.sessionsLoading){e.sessionsLoading=!0,e.sessionsError=null;try{const t={includeGlobal:e.sessionsIncludeGlobal,includeUnknown:e.sessionsIncludeUnknown},n=Yt(e.sessionsFilterActive,0),s=Yt(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 Il(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 nt(e)}catch(i){e.sessionsError=String(i)}}async function Ll(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 nt(e)}catch(s){e.sessionsError=String(s)}finally{e.sessionsLoading=!1}}}const Ji=50,Rl=80,Ml=12e4;function Pl(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 o=i;return o.type==="text"&&typeof o.text=="string"?o.text:null}).filter(i=>!!i);return s.length===0?null:s.join(` +`)}function Zi(e){if(e==null)return null;if(typeof e=="number"||typeof e=="boolean")return String(e);const t=Pl(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=la(n,Ml);return s.truncated?`${s.text} + +… truncated (${s.total} chars, showing first ${s.text.length}).`:s.text}function Nl(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 Ol(e){if(e.toolStreamOrder.length<=Ji)return;const t=e.toolStreamOrder.length-Ji,n=e.toolStreamOrder.splice(0,t);for(const s of n)e.toolStreamById.delete(s)}function Dl(e){e.chatToolMessages=e.toolStreamOrder.map(t=>e.toolStreamById.get(t)?.message).filter(t=>!!t)}function ls(e){e.toolStreamSyncTimer!=null&&(clearTimeout(e.toolStreamSyncTimer),e.toolStreamSyncTimer=null),Dl(e)}function Bl(e,t=!1){if(t){ls(e);return}e.toolStreamSyncTimer==null&&(e.toolStreamSyncTimer=window.setTimeout(()=>ls(e),Rl))}function Os(e){e.toolStreamById.clear(),e.toolStreamOrder=[],e.chatToolMessages=[],ls(e)}const Fl=5e3;function Ul(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},Fl))}function Kl(e,t){if(!t)return;if(t.stream==="compaction"){Ul(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 o=typeof s.name=="string"?s.name:"tool",a=typeof s.phase=="string"?s.phase:"",l=a==="start"?s.args:void 0,r=a==="update"?Zi(s.partialResult):a==="result"?Zi(s.result):void 0,p=Date.now();let d=e.toolStreamById.get(i);d?(d.name=o,l!==void 0&&(d.args=l),r!==void 0&&(d.output=r),d.updatedAt=p):(d={toolCallId:i,runId:t.runId,sessionKey:n,name:o,args:l,output:r,startedAt:typeof t.ts=="number"?t.ts:p,updatedAt:p,message:{}},e.toolStreamById.set(i,d),e.toolStreamOrder.push(i)),d.message=Nl(d),Ol(e),Bl(e,a==="result")}function rn(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 a=t?150:120;e.chatScrollTimeout=window.setTimeout(()=>{e.chatScrollTimeout=null;const l=n();if(!l)return;const r=l.scrollHeight-l.scrollTop-l.clientHeight;(t||e.chatUserNearBottom||r<200)&&(l.scrollTop=l.scrollHeight,e.chatUserNearBottom=!0)},a)})})}function da(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 Hl(e,t){const n=t.currentTarget;if(!n)return;const s=n.scrollHeight-n.scrollTop-n.clientHeight;e.chatUserNearBottom=s<200}function zl(e,t){const n=t.currentTarget;if(!n)return;const s=n.scrollHeight-n.scrollTop-n.clientHeight;e.logsAtBottom=s<80}function jl(e){e.chatHasAutoScrolled=!1,e.chatUserNearBottom=!0}function ql(e,t){if(e.length===0)return;const n=new Blob([`${e.join(` +`)} +`],{type:"text/plain"}),s=URL.createObjectURL(n),i=document.createElement("a"),o=new Date().toISOString().slice(0,19).replace(/[:T]/g,"-");i.href=s,i.download=`clawdbot-logs-${t}-${o}.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 Oe(e){return typeof structuredClone=="function"?structuredClone(e):JSON.parse(JSON.stringify(e))}function Xe(e){return`${JSON.stringify(e,null,2).trimEnd()} +`}function ua(e,t,n){if(t.length===0)return;let s=e;for(let o=0;o0&&(n.timeoutSeconds=s),n}async function Xl(e){if(!(!e.client||!e.connected||e.cronBusy)){e.cronBusy=!0,e.cronError=null;try{const t=Jl(e.cronForm),n=Zl(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 ln(e),await _t(e)}catch(t){e.cronError=String(t)}finally{e.cronBusy=!1}}}async function ec(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 ln(e),await _t(e)}catch(s){e.cronError=String(s)}finally{e.cronBusy=!1}}}async function tc(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 ha(e,t.id)}catch(n){e.cronError=String(n)}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.remove",{id:t.id}),e.cronRunsJobId===t.id&&(e.cronRunsJobId=null,e.cronRuns=[]),await ln(e),await _t(e)}catch(n){e.cronError=String(n)}finally{e.cronBusy=!1}}}async function ha(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 sc(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 ic(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 cn(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 o=s;e.debugModels=Array.isArray(o?.models)?o?.models:[],e.debugHeartbeat=i}catch(t){e.debugCallError=String(t)}finally{e.debugLoading=!1}}}async function ac(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 rc=2e3,lc=new Set(["trace","debug","info","warn","error","fatal"]);function cc(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 dc(e){if(typeof e!="string")return null;const t=e.toLowerCase();return lc.has(t)?t:null}function uc(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=dc(n?.logLevelName??n?.level),o=typeof t[0]=="string"?t[0]:typeof n?.name=="string"?n?.name:null,a=cc(o);let l=null;a&&(typeof a.subsystem=="string"?l=a.subsystem:typeof a.module=="string"&&(l=a.module)),!l&&o&&o.length<120&&(l=o);let r=null;return typeof t[1]=="string"?r=t[1]:!a&&typeof t[0]=="string"?r=t[0]:typeof t.message=="string"&&(r=t.message),{raw:e,time:s,level:i,subsystem:l,message:r??e,meta:n??void 0}}catch{return{raw:e,message:e}}}async function Ds(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}),o=(Array.isArray(s.lines)?s.lines.filter(l=>typeof l=="string"):[]).map(uc),a=!!(t?.reset||s.reset||e.logsCursor==null);e.logsEntries=a?o:[...e.logsEntries,...o].slice(-rc),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 ga={p:0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffedn,n:0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3edn,h:8n,a:0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffecn,d:0x52036cee2b6ffe738cc740797779e89800700a4d4141d8ab75eb4dca135978a3n,Gx:0x216936d3cd6e53fec0a4e231fdd6dc5c692cc7609525a7b2c9562d608f25d51an,Gy:0x6666666666666666666666666666666666666666666666666666666666666658n},{p:W,n:qt,Gx:eo,Gy:to,a:Kn,d:Hn,h:pc}=ga,De=32,Bs=64,fc=(...e)=>{"captureStackTrace"in Error&&typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(...e)},H=(e="")=>{const t=new Error(e);throw fc(t,H),t},hc=e=>typeof e=="bigint",gc=e=>typeof e=="string",vc=e=>e instanceof Uint8Array||ArrayBuffer.isView(e)&&e.constructor.name==="Uint8Array",Ae=(e,t,n="")=>{const s=vc(e),i=e?.length,o=t!==void 0;if(!s||o&&i!==t){const a=n&&`"${n}" `,l=o?` of length ${t}`:"",r=s?`length=${i}`:`type=${typeof e}`;H(a+"expected Uint8Array"+l+", got "+r)}return e},dn=e=>new Uint8Array(e),va=e=>Uint8Array.from(e),ma=(e,t)=>e.toString(16).padStart(t,"0"),ba=e=>Array.from(Ae(e)).map(t=>ma(t,2)).join(""),ge={_0:48,_9:57,A:65,F:70,a:97,f:102},no=e=>{if(e>=ge._0&&e<=ge._9)return e-ge._0;if(e>=ge.A&&e<=ge.F)return e-(ge.A-10);if(e>=ge.a&&e<=ge.f)return e-(ge.a-10)},ya=e=>{const t="hex invalid";if(!gc(e))return H(t);const n=e.length,s=n/2;if(n%2)return H(t);const i=dn(s);for(let o=0,a=0;oglobalThis?.crypto,mc=()=>wa()?.subtle??H("crypto.subtle must be defined, consider polyfill"),At=(...e)=>{const t=dn(e.reduce((s,i)=>s+Ae(i).length,0));let n=0;return e.forEach(s=>{t.set(s,n),n+=s.length}),t},bc=(e=De)=>wa().getRandomValues(dn(e)),Qt=BigInt,Re=(e,t,n,s="bad number: out of range")=>hc(e)&&t<=e&&e{const n=e%t;return n>=0n?n:t+n},$a=e=>S(e,qt),yc=(e,t)=>{(e===0n||t<=0n)&&H("no inverse n="+e+" mod="+t);let n=S(e,t),s=t,i=0n,o=1n;for(;n!==0n;){const a=s/n,l=s%n,r=i-o*a;s=n,n=l,i=o,o=r}return s===1n?S(i,t):H("no inverse")},wc=e=>{const t=Sa[e];return typeof t!="function"&&H("hashes."+e+" not set"),t},zn=e=>e instanceof X?e:H("Point expected"),ds=2n**256n;class X{static BASE;static ZERO;X;Y;Z;T;constructor(t,n,s,i){const o=ds;this.X=Re(t,0n,o),this.Y=Re(n,0n,o),this.Z=Re(s,1n,o),this.T=Re(i,0n,o),Object.freeze(this)}static CURVE(){return ga}static fromAffine(t){return new X(t.x,t.y,1n,S(t.x*t.y))}static fromBytes(t,n=!1){const s=Hn,i=va(Ae(t,De)),o=t[31];i[31]=o&-129;const a=xa(i);Re(a,0n,n?ds:W);const r=S(a*a),p=S(r-1n),d=S(s*r+1n);let{isValid:u,value:h}=kc(p,d);u||H("bad point: y not sqrt");const v=(h&1n)===1n,w=(o&128)!==0;return!n&&h===0n&&w&&H("bad point: x==0, isLastByteOdd"),w!==v&&(h=S(-h)),new X(h,a,1n,S(h*a))}static fromHex(t,n){return X.fromBytes(ya(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:o,Z:a,T:l}=s,r=S(i*i),p=S(o*o),d=S(a*a),u=S(d*d),h=S(r*t),v=S(d*S(h+p)),w=S(u+S(n*S(r*p)));if(v!==w)return H("bad point: equation left != right (1)");const $=S(i*o),x=S(a*l);return $!==x?H("bad point: equation left != right (2)"):this}equals(t){const{X:n,Y:s,Z:i}=this,{X:o,Y:a,Z:l}=zn(t),r=S(n*l),p=S(o*i),d=S(s*l),u=S(a*i);return r===p&&d===u}is0(){return this.equals(Ye)}negate(){return new X(S(-this.X),this.Y,this.Z,S(-this.T))}double(){const{X:t,Y:n,Z:s}=this,i=Kn,o=S(t*t),a=S(n*n),l=S(2n*S(s*s)),r=S(i*o),p=t+n,d=S(S(p*p)-o-a),u=r+a,h=u-l,v=r-a,w=S(d*h),$=S(u*v),x=S(d*v),C=S(h*u);return new X(w,$,C,x)}add(t){const{X:n,Y:s,Z:i,T:o}=this,{X:a,Y:l,Z:r,T:p}=zn(t),d=Kn,u=Hn,h=S(n*a),v=S(s*l),w=S(o*u*p),$=S(i*r),x=S((n+s)*(a+l)-h-v),C=S($-w),I=S($+w),R=S(v-d*h),E=S(x*C),A=S(I*R),B=S(x*R),ue=S(C*I);return new X(E,A,ue,B)}subtract(t){return this.add(zn(t).negate())}multiply(t,n=!0){if(!n&&(t===0n||this.is0()))return Ye;if(Re(t,1n,qt),t===1n)return this;if(this.equals(Be))return Mc(t).p;let s=Ye,i=Be;for(let o=this;t>0n;o=o.double(),t>>=1n)t&1n?s=s.add(o):n&&(i=i.add(o));return s}multiplyUnsafe(t){return this.multiply(t,!1)}toAffine(){const{X:t,Y:n,Z:s}=this;if(this.equals(Ye))return{x:0n,y:1n};const i=yc(s,W);S(s*i)!==1n&&H("invalid inverse");const o=S(t*i),a=S(n*i);return{x:o,y:a}}toBytes(){const{x:t,y:n}=this.assertValidity().toAffine(),s=ka(n);return s[31]|=t&1n?128:0,s}toHex(){return ba(this.toBytes())}clearCofactor(){return this.multiply(Qt(pc),!1)}isSmallOrder(){return this.clearCofactor().is0()}isTorsionFree(){let t=this.multiply(qt/2n,!1).double();return qt%2n&&(t=t.add(this)),t.is0()}}const Be=new X(eo,to,1n,S(eo*to)),Ye=new X(0n,1n,1n,0n);X.BASE=Be;X.ZERO=Ye;const ka=e=>ya(ma(Re(e,0n,ds),Bs)).reverse(),xa=e=>Qt("0x"+ba(va(Ae(e)).reverse())),le=(e,t)=>{let n=e;for(;t-- >0n;)n*=n,n%=W;return n},$c=e=>{const n=e*e%W*e%W,s=le(n,2n)*n%W,i=le(s,1n)*e%W,o=le(i,5n)*i%W,a=le(o,10n)*o%W,l=le(a,20n)*a%W,r=le(l,40n)*l%W,p=le(r,80n)*r%W,d=le(p,80n)*r%W,u=le(d,10n)*o%W;return{pow_p_5_8:le(u,2n)*e%W,b2:n}},so=0x2b8324804fc1df0b2b4d00993dfbd7a72f431806ad2fe478c4ee1b274a0ea0b0n,kc=(e,t)=>{const n=S(t*t*t),s=S(n*n*t),i=$c(e*s).pow_p_5_8;let o=S(e*n*i);const a=S(t*o*o),l=o,r=S(o*so),p=a===e,d=a===S(-e),u=a===S(-e*so);return p&&(o=l),(d||u)&&(o=r),(S(o)&1n)===1n&&(o=S(-o)),{isValid:p||d,value:o}},us=e=>$a(xa(e)),Fs=(...e)=>Sa.sha512Async(At(...e)),xc=(...e)=>wc("sha512")(At(...e)),Aa=e=>{const t=e.slice(0,De);t[0]&=248,t[31]&=127,t[31]|=64;const n=e.slice(De,Bs),s=us(t),i=Be.multiply(s),o=i.toBytes();return{head:t,prefix:n,scalar:s,point:i,pointBytes:o}},Us=e=>Fs(Ae(e,De)).then(Aa),Ac=e=>Aa(xc(Ae(e,De))),Sc=e=>Us(e).then(t=>t.pointBytes),_c=e=>Fs(e.hashable).then(e.finish),Tc=(e,t,n)=>{const{pointBytes:s,scalar:i}=e,o=us(t),a=Be.multiply(o).toBytes();return{hashable:At(a,s,n),finish:p=>{const d=$a(o+us(p)*i);return Ae(At(a,ka(d)),Bs)}}},Cc=async(e,t)=>{const n=Ae(e),s=await Us(t),i=await Fs(s.prefix,n);return _c(Tc(s,i,n))},Sa={sha512Async:async e=>{const t=mc(),n=At(e);return dn(await t.digest("SHA-512",n.buffer))},sha512:void 0},Ec=(e=bc(De))=>e,Ic={getExtendedPublicKeyAsync:Us,getExtendedPublicKey:Ac,randomSecretKey:Ec},Jt=8,Lc=256,_a=Math.ceil(Lc/Jt)+1,ps=2**(Jt-1),Rc=()=>{const e=[];let t=Be,n=t;for(let s=0;s<_a;s++){n=t,e.push(n);for(let i=1;i{const n=t.negate();return e?n:t},Mc=e=>{const t=io||(io=Rc());let n=Ye,s=Be;const i=2**Jt,o=i,a=Qt(i-1),l=Qt(Jt);for(let r=0;r<_a;r++){let p=Number(e&a);e>>=l,p>ps&&(p-=o,e+=1n);const d=r*ps,u=d,h=d+Math.abs(p)-1,v=r%2!==0,w=p<0;p===0?s=s.add(oo(v,t[u])):n=n.add(oo(w,t[h]))}return e!==0n&&H("invalid wnaf"),{p:n,f:s}},jn="clawdbot-device-identity-v1";function fs(e){let t="";for(const n of e)t+=String.fromCharCode(n);return btoa(t).replaceAll("+","-").replaceAll("/","_").replace(/=+$/g,"")}function Ta(e){const t=e.replaceAll("-","+").replaceAll("_","/"),n=t+"=".repeat((4-t.length%4)%4),s=atob(n),i=new Uint8Array(s.length);for(let o=0;ot.toString(16).padStart(2,"0")).join("")}async function Ca(e){const t=await crypto.subtle.digest("SHA-256",e);return Pc(new Uint8Array(t))}async function Nc(){const e=Ic.randomSecretKey(),t=await Sc(e);return{deviceId:await Ca(t),publicKey:fs(t),privateKey:fs(e)}}async function Ks(){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 Ca(Ta(s.publicKey));if(i!==s.deviceId){const o={...s,deviceId:i};return localStorage.setItem(jn,JSON.stringify(o)),{deviceId:i,publicKey:s.publicKey,privateKey:s.privateKey}}return{deviceId:s.deviceId,publicKey:s.publicKey,privateKey:s.privateKey}}}}catch{}const e=await Nc(),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 Oc(e,t){const n=Ta(e),s=new TextEncoder().encode(t),i=await Cc(s,n);return fs(i)}const Ea="clawdbot.device.auth.v1";function Hs(e){return e.trim()}function Dc(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 zs(){try{const e=window.localStorage.getItem(Ea);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 Ia(e){try{window.localStorage.setItem(Ea,JSON.stringify(e))}catch{}}function Bc(e){const t=zs();if(!t||t.deviceId!==e.deviceId)return null;const n=Hs(e.role),s=t.tokens[n];return!s||typeof s.token!="string"?null:s}function La(e){const t=Hs(e.role),n={version:1,deviceId:e.deviceId,tokens:{}},s=zs();s&&s.deviceId===e.deviceId&&(n.tokens={...s.tokens});const i={token:e.token,role:t,scopes:Dc(e.scopes),updatedAtMs:Date.now()};return n.tokens[t]=i,Ia(n),i}function Ra(e){const t=zs();if(!t||t.deviceId!==e.deviceId)return;const n=Hs(e.role);if(!t.tokens[n])return;const s={...t,tokens:{...t.tokens}};delete s.tokens[n],Ia(s)}async function Se(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 Fc(e,t){if(!(!e.client||!e.connected))try{await e.client.request("device.pair.approve",{requestId:t}),await Se(e)}catch(n){e.devicesError=String(n)}}async function Uc(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 Se(e)}catch(s){e.devicesError=String(s)}}async function Kc(e,t){if(!(!e.client||!e.connected))try{const n=await e.client.request("device.token.rotate",t);if(n?.token){const s=await Ks(),i=n.role??t.role;(n.deviceId===s.deviceId||t.deviceId===s.deviceId)&&La({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 Se(e)}catch(n){e.devicesError=String(n)}}async function Hc(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 Ks();t.deviceId===s.deviceId&&Ra({deviceId:s.deviceId,role:t.role}),await Se(e)}catch(s){e.devicesError=String(s)}}async function un(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 zc(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 jc(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 js(e,t){if(!(!e.client||!e.connected)&&!e.execApprovalsLoading){e.execApprovalsLoading=!0,e.lastError=null;try{const n=zc(t);if(!n){e.lastError="Select a node before loading exec approvals.";return}const s=await e.client.request(n.method,n.params);qc(e,s)}catch(n){e.lastError=String(n)}finally{e.execApprovalsLoading=!1}}}function qc(e,t){e.execApprovalsSnapshot=t,e.execApprovalsDirty||(e.execApprovalsForm=Oe(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=jc(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 js(e,t)}catch(n){e.lastError=String(n)}finally{e.execApprovalsSaving=!1}}}function Vc(e,t,n){const s=Oe(e.execApprovalsForm??e.execApprovalsSnapshot?.file??{});ua(s,t,n),e.execApprovalsForm=s,e.execApprovalsDirty=!0}function Gc(e,t){const n=Oe(e.execApprovalsForm??e.execApprovalsSnapshot?.file??{});pa(n,t),e.execApprovalsForm=n,e.execApprovalsDirty=!0}async function qs(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 et(e,t,n){if(!t.trim())return;const s={...e.skillMessages};n?s[t]=n:delete s[t],e.skillMessages=s}function pn(e){return e instanceof Error?e.message:String(e)}async function Tt(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=pn(n)}finally{e.skillsLoading=!1}}}function Yc(e,t,n){e.skillEdits={...e.skillEdits,[t]:n}}async function Qc(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 Tt(e),et(e,t,{kind:"success",message:n?"Skill enabled":"Skill disabled"})}catch(s){const i=pn(s);e.skillsError=i,et(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 Tt(e),et(e,t,{kind:"success",message:"API key saved"})}catch(n){const s=pn(n);e.skillsError=s,et(e,t,{kind:"error",message:s})}finally{e.skillsBusyKey=null}}}async function Zc(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 Tt(e),et(e,t,{kind:"success",message:i?.message??"Installed"})}catch(i){const o=pn(i);e.skillsError=o,et(e,t,{kind:"error",message:o})}finally{e.skillsBusyKey=null}}}function Xc(){return typeof window>"u"||typeof window.matchMedia!="function"||window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function Ws(e){return e==="system"?Xc():e}const Dt=e=>Number.isNaN(e)?.5:e<=0?0:e>=1?1:e,ed=()=>typeof window>"u"||typeof window.matchMedia!="function"?!1:window.matchMedia("(prefers-reduced-motion: reduce)").matches??!1,Bt=e=>{e.classList.remove("theme-transition"),e.style.removeProperty("--theme-switch-x"),e.style.removeProperty("--theme-switch-y")},td=({nextTheme:e,applyTheme:t,context:n,currentTheme:s})=>{if(s===e)return;const i=globalThis.document??null;if(!i){t();return}const o=i.documentElement,a=i,l=ed();if(!!a.startViewTransition&&!l){let p=.5,d=.5;if(n?.pointerClientX!==void 0&&n?.pointerClientY!==void 0&&typeof window<"u")p=Dt(n.pointerClientX/window.innerWidth),d=Dt(n.pointerClientY/window.innerHeight);else if(n?.element){const u=n.element.getBoundingClientRect();u.width>0&&u.height>0&&typeof window<"u"&&(p=Dt((u.left+u.width/2)/window.innerWidth),d=Dt((u.top+u.height/2)/window.innerHeight))}o.style.setProperty("--theme-switch-x",`${p*100}%`),o.style.setProperty("--theme-switch-y",`${d*100}%`),o.classList.add("theme-transition");try{const u=a.startViewTransition?.(()=>{t()});u?.finished?u.finished.finally(()=>Bt(o)):Bt(o)}catch{Bt(o),t()}return}t(),Bt(o)};function nd(e){e.nodesPollInterval==null&&(e.nodesPollInterval=window.setInterval(()=>{un(e,{quiet:!0})},5e3))}function sd(e){e.nodesPollInterval!=null&&(clearInterval(e.nodesPollInterval),e.nodesPollInterval=null)}function Vs(e){e.logsPollInterval==null&&(e.logsPollInterval=window.setInterval(()=>{e.tab==="logs"&&Ds(e,{quiet:!0})},2e3))}function Gs(e){e.logsPollInterval!=null&&(clearInterval(e.logsPollInterval),e.logsPollInterval=null)}function Ys(e){e.debugPollInterval==null&&(e.debugPollInterval=window.setInterval(()=>{e.tab==="debug"&&cn(e)},3e3))}function Qs(e){e.debugPollInterval!=null&&(clearInterval(e.debugPollInterval),e.debugPollInterval=null)}function $e(e,t){const n={...t,lastActiveSessionKey:t.lastActiveSessionKey?.trim()||t.sessionKey.trim()||"main"};e.settings=n,gl(n),t.theme!==e.theme&&(e.theme=t.theme,fn(e,Ws(t.theme))),e.applySessionKey=e.settings.lastActiveSessionKey}function Ma(e,t){const n=t.trim();n&&e.settings.lastActiveSessionKey!==n&&$e(e,{...e.settings,lastActiveSessionKey:n})}function id(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"),o=t.get("gatewayUrl");let a=!1;if(n!=null){const r=n.trim();r&&r!==e.settings.token&&$e(e,{...e.settings,token:r}),t.delete("token"),a=!0}if(s!=null){const r=s.trim();r&&(e.password=r),t.delete("password"),a=!0}if(i!=null){const r=i.trim();r&&(e.sessionKey=r,$e(e,{...e.settings,sessionKey:r,lastActiveSessionKey:r}))}if(o!=null){const r=o.trim();r&&r!==e.settings.gatewayUrl&&$e(e,{...e.settings,gatewayUrl:r}),t.delete("gatewayUrl"),a=!0}if(!a)return;const l=new URL(window.location.href);l.search=t.toString(),window.history.replaceState({},"",l.toString())}function od(e,t){e.tab!==t&&(e.tab=t),t==="chat"&&(e.chatHasAutoScrolled=!1),t==="logs"?Vs(e):Gs(e),t==="debug"?Ys(e):Qs(e),Js(e),Na(e,t,!1)}function ad(e,t,n){td({nextTheme:t,applyTheme:()=>{e.theme=t,$e(e,{...e.settings,theme:t}),fn(e,Ws(t))},context:n,currentTheme:e.theme})}async function Js(e){e.tab==="overview"&&await Oa(e),e.tab==="channels"&&await hd(e),e.tab==="instances"&&await qs(e),e.tab==="sessions"&&await nt(e),e.tab==="cron"&&await Zs(e),e.tab==="skills"&&await Tt(e),e.tab==="nodes"&&(await un(e),await Se(e),await me(e),await js(e)),e.tab==="chat"&&(await yd(e),rn(e,!e.chatHasAutoScrolled)),e.tab==="config"&&(await fa(e),await me(e)),e.tab==="debug"&&(await cn(e),e.eventLog=e.eventLogBuffer),e.tab==="logs"&&(e.logsAtBottom=!0,await Ds(e,{reset:!0}),da(e,!0))}function rd(){if(typeof window>"u")return"";const e=window.__CLAWDBOT_CONTROL_UI_BASE_PATH__;return typeof e=="string"&&e.trim()?an(e):ml(window.location.pathname)}function ld(e){e.theme=e.settings.theme??"system",fn(e,Ws(e.theme))}function fn(e,t){if(e.themeResolved=t,typeof document>"u")return;const n=document.documentElement;n.dataset.theme=t,n.style.colorScheme=t}function cd(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"&&fn(e,n.matches?"dark":"light")},typeof e.themeMedia.addEventListener=="function"){e.themeMedia.addEventListener("change",e.themeMediaHandler);return}e.themeMedia.addListener(e.themeMediaHandler)}function dd(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 ud(e,t){if(typeof window>"u")return;const n=aa(window.location.pathname,e.basePath)??"chat";Pa(e,n),Na(e,n,t)}function pd(e){if(typeof window>"u")return;const t=aa(window.location.pathname,e.basePath);if(!t)return;const s=new URL(window.location.href).searchParams.get("session")?.trim();s&&(e.sessionKey=s,$e(e,{...e.settings,sessionKey:s,lastActiveSessionKey:s})),Pa(e,t)}function Pa(e,t){e.tab!==t&&(e.tab=t),t==="chat"&&(e.chatHasAutoScrolled=!1),t==="logs"?Vs(e):Gs(e),t==="debug"?Ys(e):Qs(e),e.connected&&Js(e)}function Na(e,t,n){if(typeof window>"u")return;const s=kt(Ps(t,e.basePath)),i=kt(window.location.pathname),o=new URL(window.location.href);t==="chat"&&e.sessionKey?o.searchParams.set("session",e.sessionKey):o.searchParams.delete("session"),i!==s&&(o.pathname=s),n?window.history.replaceState({},"",o.toString()):window.history.pushState({},"",o.toString())}function fd(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 Oa(e){await Promise.all([oe(e,!1),qs(e),nt(e),_t(e),cn(e)])}async function hd(e){await Promise.all([oe(e,!0),fa(e),me(e)])}async function Zs(e){await Promise.all([oe(e,!1),_t(e),ln(e)])}function Da(e){return e.chatSending||!!e.chatRunId}function gd(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 Ba(e){e.connected&&(e.chatMessage="",await Cl(e))}function vd(e,t){const n=t.trim();n&&(e.chatQueue=[...e.chatQueue,{id:Ns(),text:n,createdAt:Date.now()}])}async function Fa(e,t,n){Os(e);const s=await Tl(e,t);return!s&&n?.previousDraft!=null&&(e.chatMessage=n.previousDraft),s&&Ma(e,e.sessionKey),s&&n?.restoreDraft&&n.previousDraft?.trim()&&(e.chatMessage=n.previousDraft),rn(e),s&&!e.chatRunId&&Ua(e),s}async function Ua(e){if(!e.connected||Da(e))return;const[t,...n]=e.chatQueue;if(!t)return;e.chatQueue=n,await Fa(e,t.text)||(e.chatQueue=[t,...e.chatQueue])}function md(e,t){e.chatQueue=e.chatQueue.filter(n=>n.id!==t)}async function bd(e,t,n){if(!e.connected)return;const s=e.chatMessage,i=(t??e.chatMessage).trim();if(i){if(gd(i)){await Ba(e);return}if(t==null&&(e.chatMessage=""),Da(e)){vd(e,i);return}await Fa(e,i,{previousDraft:t==null?s:void 0,restoreDraft:!!(t&&n?.restoreDraft)})}}async function yd(e){await Promise.all([Ze(e),nt(e),hs(e)]),rn(e,!0)}const wd=Ua;function $d(e){const t=sa(e.sessionKey);return t?.agentId?t.agentId:e.hello?.snapshot?.sessionDefaults?.defaultAgentId?.trim()||"main"}function kd(e,t){const n=an(e),s=encodeURIComponent(t);return n?`${n}/avatar/${s}?meta=1`:`/avatar/${s}?meta=1`}async function hs(e){if(!e.connected){e.chatAvatarUrl=null;return}const t=$d(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(),o=typeof i.avatarUrl=="string"?i.avatarUrl.trim():"";e.chatAvatarUrl=o||null}catch{e.chatAvatarUrl=null}}const Ka={CHILD:2},Ha=e=>(...t)=>({_$litDirective$:e,values:t});let za=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:xd}=il,ao=e=>e,ro=()=>document.createComment(""),rt=(e,t,n)=>{const s=e._$AA.parentNode,i=t===void 0?e._$AB:t._$AA;if(n===void 0){const o=s.insertBefore(ro(),i),a=s.insertBefore(ro(),i);n=new xd(o,a,e,e.options)}else{const o=n._$AB.nextSibling,a=n._$AM,l=a!==e;if(l){let r;n._$AQ?.(e),n._$AM=e,n._$AP!==void 0&&(r=e._$AU)!==a._$AU&&n._$AP(r)}if(o!==i||l){let r=n._$AA;for(;r!==o;){const p=ao(r).nextSibling;ao(s).insertBefore(r,i),r=p}}}return n},Ie=(e,t,n=e)=>(e._$AI(t,n),e),Ad={},Sd=(e,t=Ad)=>e._$AH=t,_d=e=>e._$AH,qn=e=>{e._$AR(),e._$AA.remove()};const lo=(e,t,n)=>{const s=new Map;for(let i=t;i<=n;i++)s.set(e[i],i);return s},ja=Ha(class extends za{constructor(e){if(super(e),e.type!==Ka.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=[],o=[];let a=0;for(const l of e)i[a]=s?s(l,a):a,o[a]=n(l,a),a++;return{values:o,keys:i}}render(e,t,n){return this.dt(e,t,n).values}update(e,[t,n,s]){const i=_d(e),{values:o,keys:a}=this.dt(t,n,s);if(!Array.isArray(i))return this.ut=a,o;const l=this.ut??=[],r=[];let p,d,u=0,h=i.length-1,v=0,w=o.length-1;for(;u<=h&&v<=w;)if(i[u]===null)u++;else if(i[h]===null)h--;else if(l[u]===a[v])r[v]=Ie(i[u],o[v]),u++,v++;else if(l[h]===a[w])r[w]=Ie(i[h],o[w]),h--,w--;else if(l[u]===a[w])r[w]=Ie(i[u],o[w]),rt(e,r[w+1],i[u]),u++,w--;else if(l[h]===a[v])r[v]=Ie(i[h],o[v]),rt(e,i[u],i[h]),h--,v++;else if(p===void 0&&(p=lo(a,v,w),d=lo(l,u,h)),p.has(l[u]))if(p.has(l[h])){const $=d.get(a[v]),x=$!==void 0?i[$]:null;if(x===null){const C=rt(e,i[u]);Ie(C,o[v]),r[v]=C}else r[v]=Ie(x,o[v]),rt(e,i[u],x),i[$]=null;v++}else qn(i[h]),h--;else qn(i[u]),u++;for(;v<=w;){const $=rt(e,r[w+1]);Ie($,o[v]),r[v++]=$}for(;u<=h;){const $=i[u++];$!==null&&qn($)}return this.ut=a,Sd(e,r),xe}});function qa(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,o=Array.isArray(i)?i:null,a=Array.isArray(o)&&o.some(u=>{const v=String(u.type??"").toLowerCase();return v==="toolresult"||v==="tool_result"}),l=typeof t.toolName=="string"||typeof t.tool_name=="string";(s||a||l)&&(n="toolResult");let r=[];typeof t.content=="string"?r=[{type:"text",text:t.content}]:Array.isArray(t.content)?r=t.content.map(u=>({type:u.type||"text",text:u.text,name:u.name,args:u.args||u.arguments})):typeof t.text=="string"&&(r=[{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:r,timestamp:p,id:d}}function Xs(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 Wa(e){const t=e,n=typeof t.role=="string"?t.role.toLowerCase():"";return n==="toolresult"||n==="tool_result"}class gs extends za{constructor(t){if(super(t),this.it=g,t.type!==Ka.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===xe)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:[]}}}gs.directiveName="unsafeHTML",gs.resultType=1;const vs=Ha(gs);const{entries:Va,setPrototypeOf:co,isFrozen:Td,getPrototypeOf:Cd,getOwnPropertyDescriptor:Ed}=Object;let{freeze:Q,seal:te,create:ms}=Object,{apply:bs,construct:ys}=typeof Reflect<"u"&&Reflect;Q||(Q=function(t){return t});te||(te=function(t){return t});bs||(bs=function(t,n){for(var s=arguments.length,i=new Array(s>2?s-2:0),o=2;o1?n-1:0),i=1;i1?n-1:0),i=1;i2&&arguments[2]!==void 0?arguments[2]:Wt;co&&co(e,null);let s=t.length;for(;s--;){let i=t[s];if(typeof i=="string"){const o=n(i);o!==i&&(Td(t)||(t[s]=o),i=o)}e[i]=!0}return e}function Nd(e){for(let t=0;t/gm),Ud=te(/\$\{[\w\W]*/gm),Kd=te(/^data-[\-\w.\u00B7-\uFFFF]+$/),Hd=te(/^aria-[\-\w]+$/),Ga=te(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),zd=te(/^(?:\w+script|data):/i),jd=te(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),Ya=te(/^html$/i),qd=te(/^[a-z][.\w]*(-[.\w]+)+$/i);var vo=Object.freeze({__proto__:null,ARIA_ATTR:Hd,ATTR_WHITESPACE:jd,CUSTOM_ELEMENT:qd,DATA_ATTR:Kd,DOCTYPE_NAME:Ya,ERB_EXPR:Fd,IS_ALLOWED_URI:Ga,IS_SCRIPT_OR_DATA:zd,MUSTACHE_EXPR:Bd,TMPLIT_EXPR:Ud});const pt={element:1,text:3,progressingInstruction:7,comment:8,document:9},Wd=function(){return typeof window>"u"?null:window},Vd=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 o="dompurify"+(s?"#"+s:"");try{return t.createPolicy(o,{createHTML(a){return a},createScriptURL(a){return a}})}catch{return console.warn("TrustedTypes policy "+o+" could not be created."),null}},mo=function(){return{afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}};function Qa(){let e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:Wd();const t=T=>Qa(T);if(t.version="3.3.1",t.removed=[],!e||!e.document||e.document.nodeType!==pt.document||!e.Element)return t.isSupported=!1,t;let{document:n}=e;const s=n,i=s.currentScript,{DocumentFragment:o,HTMLTemplateElement:a,Node:l,Element:r,NodeFilter:p,NamedNodeMap:d=e.NamedNodeMap||e.MozNamedAttrMap,HTMLFormElement:u,DOMParser:h,trustedTypes:v}=e,w=r.prototype,$=ut(w,"cloneNode"),x=ut(w,"remove"),C=ut(w,"nextSibling"),I=ut(w,"childNodes"),R=ut(w,"parentNode");if(typeof a=="function"){const T=n.createElement("template");T.content&&T.content.ownerDocument&&(n=T.content.ownerDocument)}let E,A="";const{implementation:B,createNodeIterator:ue,createDocumentFragment:bn,getElementsByTagName:yn}=n,{importNode:Sr}=s;let V=mo();t.isSupported=typeof Va=="function"&&typeof R=="function"&&B&&B.createHTMLDocument!==void 0;const{MUSTACHE_EXPR:wn,ERB_EXPR:$n,TMPLIT_EXPR:kn,DATA_ATTR:_r,ARIA_ATTR:Tr,IS_SCRIPT_OR_DATA:Cr,ATTR_WHITESPACE:ui,CUSTOM_ELEMENT:Er}=vo;let{IS_ALLOWED_URI:pi}=vo,K=null;const fi=L({},[...po,...Gn,...Yn,...Qn,...fo]);let z=null;const hi=L({},[...ho,...Jn,...go,...Ut]);let D=Object.seal(ms(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}})),st=null,xn=null;const Ke=Object.seal(ms(null,{tagCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeCheck:{writable:!0,configurable:!1,enumerable:!0,value:null}}));let gi=!0,An=!0,vi=!1,mi=!0,He=!1,Et=!0,Te=!1,Sn=!1,_n=!1,ze=!1,It=!1,Lt=!1,bi=!0,yi=!1;const Ir="user-content-";let Tn=!0,it=!1,je={},ae=null;const Cn=L({},["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 wi=null;const $i=L({},["audio","video","img","source","image","track"]);let En=null;const ki=L({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Rt="http://www.w3.org/1998/Math/MathML",Mt="http://www.w3.org/2000/svg",pe="http://www.w3.org/1999/xhtml";let qe=pe,In=!1,Ln=null;const Lr=L({},[Rt,Mt,pe],Wn);let Pt=L({},["mi","mo","mn","ms","mtext"]),Nt=L({},["annotation-xml"]);const Rr=L({},["title","style","font","a","script"]);let ot=null;const Mr=["application/xhtml+xml","text/html"],Pr="text/html";let U=null,We=null;const Nr=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=ce(f),ot=Mr.indexOf(f.PARSER_MEDIA_TYPE)===-1?Pr:f.PARSER_MEDIA_TYPE,U=ot==="application/xhtml+xml"?Wn:Wt,K=ne(f,"ALLOWED_TAGS")?L({},f.ALLOWED_TAGS,U):fi,z=ne(f,"ALLOWED_ATTR")?L({},f.ALLOWED_ATTR,U):hi,Ln=ne(f,"ALLOWED_NAMESPACES")?L({},f.ALLOWED_NAMESPACES,Wn):Lr,En=ne(f,"ADD_URI_SAFE_ATTR")?L(ce(ki),f.ADD_URI_SAFE_ATTR,U):ki,wi=ne(f,"ADD_DATA_URI_TAGS")?L(ce($i),f.ADD_DATA_URI_TAGS,U):$i,ae=ne(f,"FORBID_CONTENTS")?L({},f.FORBID_CONTENTS,U):Cn,st=ne(f,"FORBID_TAGS")?L({},f.FORBID_TAGS,U):ce({}),xn=ne(f,"FORBID_ATTR")?L({},f.FORBID_ATTR,U):ce({}),je=ne(f,"USE_PROFILES")?f.USE_PROFILES:!1,gi=f.ALLOW_ARIA_ATTR!==!1,An=f.ALLOW_DATA_ATTR!==!1,vi=f.ALLOW_UNKNOWN_PROTOCOLS||!1,mi=f.ALLOW_SELF_CLOSE_IN_ATTR!==!1,He=f.SAFE_FOR_TEMPLATES||!1,Et=f.SAFE_FOR_XML!==!1,Te=f.WHOLE_DOCUMENT||!1,ze=f.RETURN_DOM||!1,It=f.RETURN_DOM_FRAGMENT||!1,Lt=f.RETURN_TRUSTED_TYPE||!1,_n=f.FORCE_BODY||!1,bi=f.SANITIZE_DOM!==!1,yi=f.SANITIZE_NAMED_PROPS||!1,Tn=f.KEEP_CONTENT!==!1,it=f.IN_PLACE||!1,pi=f.ALLOWED_URI_REGEXP||Ga,qe=f.NAMESPACE||pe,Pt=f.MATHML_TEXT_INTEGRATION_POINTS||Pt,Nt=f.HTML_INTEGRATION_POINTS||Nt,D=f.CUSTOM_ELEMENT_HANDLING||{},f.CUSTOM_ELEMENT_HANDLING&&xi(f.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(D.tagNameCheck=f.CUSTOM_ELEMENT_HANDLING.tagNameCheck),f.CUSTOM_ELEMENT_HANDLING&&xi(f.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(D.attributeNameCheck=f.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),f.CUSTOM_ELEMENT_HANDLING&&typeof f.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements=="boolean"&&(D.allowCustomizedBuiltInElements=f.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),He&&(An=!1),It&&(ze=!0),je&&(K=L({},fo),z=[],je.html===!0&&(L(K,po),L(z,ho)),je.svg===!0&&(L(K,Gn),L(z,Jn),L(z,Ut)),je.svgFilters===!0&&(L(K,Yn),L(z,Jn),L(z,Ut)),je.mathMl===!0&&(L(K,Qn),L(z,go),L(z,Ut))),f.ADD_TAGS&&(typeof f.ADD_TAGS=="function"?Ke.tagCheck=f.ADD_TAGS:(K===fi&&(K=ce(K)),L(K,f.ADD_TAGS,U))),f.ADD_ATTR&&(typeof f.ADD_ATTR=="function"?Ke.attributeCheck=f.ADD_ATTR:(z===hi&&(z=ce(z)),L(z,f.ADD_ATTR,U))),f.ADD_URI_SAFE_ATTR&&L(En,f.ADD_URI_SAFE_ATTR,U),f.FORBID_CONTENTS&&(ae===Cn&&(ae=ce(ae)),L(ae,f.FORBID_CONTENTS,U)),f.ADD_FORBID_CONTENTS&&(ae===Cn&&(ae=ce(ae)),L(ae,f.ADD_FORBID_CONTENTS,U)),Tn&&(K["#text"]=!0),Te&&L(K,["html","head","body"]),K.table&&(L(K,["tbody"]),delete st.tbody),f.TRUSTED_TYPES_POLICY){if(typeof f.TRUSTED_TYPES_POLICY.createHTML!="function")throw dt('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if(typeof f.TRUSTED_TYPES_POLICY.createScriptURL!="function")throw dt('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');E=f.TRUSTED_TYPES_POLICY,A=E.createHTML("")}else E===void 0&&(E=Vd(v,i)),E!==null&&typeof A=="string"&&(A=E.createHTML(""));Q&&Q(f),We=f}},Ai=L({},[...Gn,...Yn,...Od]),Si=L({},[...Qn,...Dd]),Or=function(f){let k=R(f);(!k||!k.tagName)&&(k={namespaceURI:qe,tagName:"template"});const _=Wt(f.tagName),N=Wt(k.tagName);return Ln[f.namespaceURI]?f.namespaceURI===Mt?k.namespaceURI===pe?_==="svg":k.namespaceURI===Rt?_==="svg"&&(N==="annotation-xml"||Pt[N]):!!Ai[_]:f.namespaceURI===Rt?k.namespaceURI===pe?_==="math":k.namespaceURI===Mt?_==="math"&&Nt[N]:!!Si[_]:f.namespaceURI===pe?k.namespaceURI===Mt&&!Nt[N]||k.namespaceURI===Rt&&!Pt[N]?!1:!Si[_]&&(Rr[_]||!Ai[_]):!!(ot==="application/xhtml+xml"&&Ln[f.namespaceURI]):!1},re=function(f){lt(t.removed,{element:f});try{R(f).removeChild(f)}catch{x(f)}},Ce=function(f,k){try{lt(t.removed,{attribute:k.getAttributeNode(f),from:k})}catch{lt(t.removed,{attribute:null,from:k})}if(k.removeAttribute(f),f==="is")if(ze||It)try{re(k)}catch{}else try{k.setAttribute(f,"")}catch{}},_i=function(f){let k=null,_=null;if(_n)f=""+f;else{const F=Vn(f,/^[\r\n\t ]+/);_=F&&F[0]}ot==="application/xhtml+xml"&&qe===pe&&(f=''+f+"");const N=E?E.createHTML(f):f;if(qe===pe)try{k=new h().parseFromString(N,ot)}catch{}if(!k||!k.documentElement){k=B.createDocument(qe,"template",null);try{k.documentElement.innerHTML=In?A:N}catch{}}const q=k.body||k.documentElement;return f&&_&&q.insertBefore(n.createTextNode(_),q.childNodes[0]||null),qe===pe?yn.call(k,Te?"html":"body")[0]:Te?k.documentElement:q},Ti=function(f){return ue.call(f.ownerDocument||f,f,p.SHOW_ELEMENT|p.SHOW_COMMENT|p.SHOW_TEXT|p.SHOW_PROCESSING_INSTRUCTION|p.SHOW_CDATA_SECTION,null)},Mn=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")},Ci=function(f){return typeof l=="function"&&f instanceof l};function fe(T,f,k){Ft(T,_=>{_.call(t,f,k,We)})}const Ei=function(f){let k=null;if(fe(V.beforeSanitizeElements,f,null),Mn(f))return re(f),!0;const _=U(f.nodeName);if(fe(V.uponSanitizeElement,f,{tagName:_,allowedTags:K}),Et&&f.hasChildNodes()&&!Ci(f.firstElementChild)&&G(/<[/\w!]/g,f.innerHTML)&&G(/<[/\w!]/g,f.textContent)||f.nodeType===pt.progressingInstruction||Et&&f.nodeType===pt.comment&&G(/<[/\w]/g,f.data))return re(f),!0;if(!(Ke.tagCheck instanceof Function&&Ke.tagCheck(_))&&(!K[_]||st[_])){if(!st[_]&&Li(_)&&(D.tagNameCheck instanceof RegExp&&G(D.tagNameCheck,_)||D.tagNameCheck instanceof Function&&D.tagNameCheck(_)))return!1;if(Tn&&!ae[_]){const N=R(f)||f.parentNode,q=I(f)||f.childNodes;if(q&&N){const F=q.length;for(let Z=F-1;Z>=0;--Z){const he=$(q[Z],!0);he.__removalCount=(f.__removalCount||0)+1,N.insertBefore(he,C(f))}}}return re(f),!0}return f instanceof r&&!Or(f)||(_==="noscript"||_==="noembed"||_==="noframes")&&G(/<\/no(script|embed|frames)/i,f.innerHTML)?(re(f),!0):(He&&f.nodeType===pt.text&&(k=f.textContent,Ft([wn,$n,kn],N=>{k=ct(k,N," ")}),f.textContent!==k&&(lt(t.removed,{element:f.cloneNode()}),f.textContent=k)),fe(V.afterSanitizeElements,f,null),!1)},Ii=function(f,k,_){if(bi&&(k==="id"||k==="name")&&(_ in n||_ in Nr))return!1;if(!(An&&!xn[k]&&G(_r,k))){if(!(gi&&G(Tr,k))){if(!(Ke.attributeCheck instanceof Function&&Ke.attributeCheck(k,f))){if(!z[k]||xn[k]){if(!(Li(f)&&(D.tagNameCheck instanceof RegExp&&G(D.tagNameCheck,f)||D.tagNameCheck instanceof Function&&D.tagNameCheck(f))&&(D.attributeNameCheck instanceof RegExp&&G(D.attributeNameCheck,k)||D.attributeNameCheck instanceof Function&&D.attributeNameCheck(k,f))||k==="is"&&D.allowCustomizedBuiltInElements&&(D.tagNameCheck instanceof RegExp&&G(D.tagNameCheck,_)||D.tagNameCheck instanceof Function&&D.tagNameCheck(_))))return!1}else if(!En[k]){if(!G(pi,ct(_,ui,""))){if(!((k==="src"||k==="xlink:href"||k==="href")&&f!=="script"&&Rd(_,"data:")===0&&wi[f])){if(!(vi&&!G(Cr,ct(_,ui,"")))){if(_)return!1}}}}}}}return!0},Li=function(f){return f!=="annotation-xml"&&Vn(f,Er)},Ri=function(f){fe(V.beforeSanitizeAttributes,f,null);const{attributes:k}=f;if(!k||Mn(f))return;const _={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:z,forceKeepAttr:void 0};let N=k.length;for(;N--;){const q=k[N],{name:F,namespaceURI:Z,value:he}=q,Ve=U(F),Pn=he;let j=F==="value"?Pn:Md(Pn);if(_.attrName=Ve,_.attrValue=j,_.keepAttr=!0,_.forceKeepAttr=void 0,fe(V.uponSanitizeAttribute,f,_),j=_.attrValue,yi&&(Ve==="id"||Ve==="name")&&(Ce(F,f),j=Ir+j),Et&&G(/((--!?|])>)|<\/(style|title|textarea)/i,j)){Ce(F,f);continue}if(Ve==="attributename"&&Vn(j,"href")){Ce(F,f);continue}if(_.forceKeepAttr)continue;if(!_.keepAttr){Ce(F,f);continue}if(!mi&&G(/\/>/i,j)){Ce(F,f);continue}He&&Ft([wn,$n,kn],Pi=>{j=ct(j,Pi," ")});const Mi=U(f.nodeName);if(!Ii(Mi,Ve,j)){Ce(F,f);continue}if(E&&typeof v=="object"&&typeof v.getAttributeType=="function"&&!Z)switch(v.getAttributeType(Mi,Ve)){case"TrustedHTML":{j=E.createHTML(j);break}case"TrustedScriptURL":{j=E.createScriptURL(j);break}}if(j!==Pn)try{Z?f.setAttributeNS(Z,F,j):f.setAttribute(F,j),Mn(f)?re(f):uo(t.removed)}catch{Ce(F,f)}}fe(V.afterSanitizeAttributes,f,null)},Dr=function T(f){let k=null;const _=Ti(f);for(fe(V.beforeSanitizeShadowDOM,f,null);k=_.nextNode();)fe(V.uponSanitizeShadowNode,k,null),Ei(k),Ri(k),k.content instanceof o&&T(k.content);fe(V.afterSanitizeShadowDOM,f,null)};return t.sanitize=function(T){let f=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},k=null,_=null,N=null,q=null;if(In=!T,In&&(T=""),typeof T!="string"&&!Ci(T))if(typeof T.toString=="function"){if(T=T.toString(),typeof T!="string")throw dt("dirty is not a string, aborting")}else throw dt("toString is not a function");if(!t.isSupported)return T;if(Sn||Rn(f),t.removed=[],typeof T=="string"&&(it=!1),it){if(T.nodeName){const he=U(T.nodeName);if(!K[he]||st[he])throw dt("root node is forbidden and cannot be sanitized in-place")}}else if(T instanceof l)k=_i(""),_=k.ownerDocument.importNode(T,!0),_.nodeType===pt.element&&_.nodeName==="BODY"||_.nodeName==="HTML"?k=_:k.appendChild(_);else{if(!ze&&!He&&!Te&&T.indexOf("<")===-1)return E&&Lt?E.createHTML(T):T;if(k=_i(T),!k)return ze?null:Lt?A:""}k&&_n&&re(k.firstChild);const F=Ti(it?T:k);for(;N=F.nextNode();)Ei(N),Ri(N),N.content instanceof o&&Dr(N.content);if(it)return T;if(ze){if(It)for(q=bn.call(k.ownerDocument);k.firstChild;)q.appendChild(k.firstChild);else q=k;return(z.shadowroot||z.shadowrootmode)&&(q=Sr.call(s,q,!0)),q}let Z=Te?k.outerHTML:k.innerHTML;return Te&&K["!doctype"]&&k.ownerDocument&&k.ownerDocument.doctype&&k.ownerDocument.doctype.name&&G(Ya,k.ownerDocument.doctype.name)&&(Z=" +`+Z),He&&Ft([wn,$n,kn],he=>{Z=ct(Z,he," ")}),E&&Lt?E.createHTML(Z):Z},t.setConfig=function(){let T=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};Rn(T),Sn=!0},t.clearConfig=function(){We=null,Sn=!1},t.isValidAttribute=function(T,f,k){We||Rn({});const _=U(T),N=U(f);return Ii(_,N,k)},t.addHook=function(T,f){typeof f=="function"&<(V[T],f)},t.removeHook=function(T,f){if(f!==void 0){const k=Id(V[T],f);return k===-1?void 0:Ld(V[T],k,1)[0]}return uo(V[T])},t.removeHooks=function(T){V[T]=[]},t.removeAllHooks=function(){V=mo()},t}var ws=Qa();function ei(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}var Ue=ei();function Ja(e){Ue=e}var bt={exec:()=>null};function M(e,t=""){let n=typeof e=="string"?e:e.source,s={replace:(i,o)=>{let a=typeof o=="string"?o:o.source;return a=a.replace(Y.caret,"$1"),n=n.replace(i,a),s},getRegex:()=>new RegExp(n,t)};return s}var Gd=(()=>{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")},Yd=/^(?:[ \t]*(?:\n|$))+/,Qd=/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,Jd=/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,Ct=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,Zd=/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,ti=/(?:[*+-]|\d{1,9}[.)])/,Za=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,Xa=M(Za).replace(/bull/g,ti).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(),Xd=M(Za).replace(/bull/g,ti).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(),ni=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,eu=/^[^\n]+/,si=/(?!\s*\])(?:\\[\s\S]|[^\[\]\\])+/,tu=M(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",si).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),nu=M(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,ti).getRegex(),hn="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",ii=/|$))/,su=M("^ {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",ii).replace("tag",hn).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),er=M(ni).replace("hr",Ct).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",hn).getRegex(),iu=M(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",er).getRegex(),oi={blockquote:iu,code:Qd,def:tu,fences:Jd,heading:Zd,hr:Ct,html:su,lheading:Xa,list:nu,newline:Yd,paragraph:er,table:bt,text:eu},bo=M("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",Ct).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",hn).getRegex(),ou={...oi,lheading:Xd,table:bo,paragraph:M(ni).replace("hr",Ct).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",bo).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",hn).getRegex()},au={...oi,html:M(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment",ii).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:bt,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:M(ni).replace("hr",Ct).replace("heading",` *#{1,6} *[^ +]`).replace("lheading",Xa).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},ru=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,lu=/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,tr=/^( {2,}|\\)\n(?!\s*$)/,cu=/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\`+)[^`]+\k(?!`))*?\]\((?:\\[\s\S]|[^\\\(\)]|\((?:\\[\s\S]|[^\\\(\)])*\))*\)/).replace("precode-",Gd?"(?`+)[^`]+\k(?!`)/).replace("html",/<(?! )[^<>]*?>/).getRegex(),ir=/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/,hu=M(ir,"u").replace(/punct/g,gn).getRegex(),gu=M(ir,"u").replace(/punct/g,sr).getRegex(),or="^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)",vu=M(or,"gu").replace(/notPunctSpace/g,nr).replace(/punctSpace/g,ai).replace(/punct/g,gn).getRegex(),mu=M(or,"gu").replace(/notPunctSpace/g,pu).replace(/punctSpace/g,uu).replace(/punct/g,sr).getRegex(),bu=M("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,nr).replace(/punctSpace/g,ai).replace(/punct/g,gn).getRegex(),yu=M(/\\(punct)/,"gu").replace(/punct/g,gn).getRegex(),wu=M(/^<(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(),$u=M(ii).replace("(?:-->|$)","-->").getRegex(),ku=M("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",$u).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),Zt=/(?:\[(?:\\[\s\S]|[^\[\]\\])*\]|\\[\s\S]|`+[^`]*?`+(?!`)|[^\[\]\\`])*?/,xu=M(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]*(?:\n[ \t]*)?)(title))?\s*\)/).replace("label",Zt).replace("href",/<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),ar=M(/^!?\[(label)\]\[(ref)\]/).replace("label",Zt).replace("ref",si).getRegex(),rr=M(/^!?\[(ref)\](?:\[\])?/).replace("ref",si).getRegex(),Au=M("reflink|nolink(?!\\()","g").replace("reflink",ar).replace("nolink",rr).getRegex(),yo=/[hH][tT][tT][pP][sS]?|[fF][tT][pP]/,ri={_backpedal:bt,anyPunctuation:yu,autolink:wu,blockSkip:fu,br:tr,code:lu,del:bt,emStrongLDelim:hu,emStrongRDelimAst:vu,emStrongRDelimUnd:bu,escape:ru,link:xu,nolink:rr,punctuation:du,reflink:ar,reflinkSearch:Au,tag:ku,text:cu,url:bt},Su={...ri,link:M(/^!?\[(label)\]\((.*?)\)/).replace("label",Zt).getRegex(),reflink:M(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",Zt).getRegex()},$s={...ri,emStrongRDelimAst:mu,emStrongLDelim:gu,url:M(/^((?:protocol):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/).replace("protocol",yo).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:M(/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\":">",'"':""","'":"'"},wo=e=>Tu[e];function ve(e,t){if(t){if(Y.escapeTest.test(e))return e.replace(Y.escapeReplace,wo)}else if(Y.escapeTestNoEncode.test(e))return e.replace(Y.escapeReplaceNoEncode,wo);return e}function $o(e){try{e=encodeURI(e).replace(Y.percentDecode,"%")}catch{return null}return e}function ko(e,t){let n=e.replace(Y.findPipe,(o,a,l)=>{let r=!1,p=a;for(;--p>=0&&l[p]==="\\";)r=!r;return r?"|":" |"}),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 xo(e,t,n,s,i){let o=t.href,a=t.title||null,l=e[1].replace(i.other.outputLinkReplace,"$1");s.state.inLink=!0;let r={type:e[0].charAt(0)==="!"?"image":"link",raw:n,href:o,title:a,text:l,tokens:s.inlineTokens(l)};return s.state.inLink=!1,r}function Eu(e,t,n){let s=e.match(n.other.indentCodeCompensation);if(s===null)return t;let i=s[1];return t.split(` +`).map(o=>{let a=o.match(n.other.beginningSpace);if(a===null)return o;let[l]=a;return l.length>=i.length?o.slice(i.length):o}).join(` +`)}var Xt=class{options;rules;lexer;constructor(e){this.options=e||Ue}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:ht(n,` +`)}}}fences(e){let t=this.rules.block.fences.exec(e);if(t){let n=t[0],s=Eu(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=ht(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:ht(t[0],` +`)}}blockquote(e){let t=this.rules.block.blockquote.exec(e);if(t){let n=ht(t[0],` +`).split(` +`),s="",i="",o=[];for(;n.length>0;){let a=!1,l=[],r;for(r=0;r1,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 o=this.rules.other.listItemRegex(n),a=!1;for(;e;){let r=!1,p="",d="";if(!(t=o.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),r=!0),!r){let $=this.rules.other.nextBulletRegex(w),x=this.rules.other.hrRegex(w),C=this.rules.other.fencesBeginRegex(w),I=this.rules.other.headingBeginRegex(w),R=this.rules.other.htmlBeginRegex(w);for(;e;){let E=e.split(` +`,1)[0],A;if(h=E,this.options.pedantic?(h=h.replace(this.rules.other.listReplaceNesting," "),A=h):A=h.replace(this.rules.other.tabCharGlobal," "),C.test(h)||I.test(h)||R.test(h)||$.test(h)||x.test(h))break;if(A.search(this.rules.other.nonSpaceChar)>=w||!h.trim())d+=` +`+A.slice(w);else{if(v||u.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4||C.test(u)||I.test(u)||x.test(u))break;d+=` +`+h}!v&&!h.trim()&&(v=!0),p+=E+` +`,e=e.substring(E.length+1),u=A.slice(w)}}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(d),loose:!1,text:d,tokens:[]}),i.raw+=p}let l=i.items.at(-1);if(l)l.raw=l.raw.trimEnd(),l.text=l.text.trimEnd();else return;i.raw=i.raw.trimEnd();for(let r of i.items){if(this.lexer.state.top=!1,r.tokens=this.lexer.blockTokens(r.text,[]),r.task){if(r.text=r.text.replace(this.rules.other.listReplaceTask,""),r.tokens[0]?.type==="text"||r.tokens[0]?.type==="paragraph"){r.tokens[0].raw=r.tokens[0].raw.replace(this.rules.other.listReplaceTask,""),r.tokens[0].text=r.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(r.raw);if(p){let d={type:"checkbox",raw:p[0]+" ",checked:p[0]!=="[ ]"};r.checked=d.checked,i.loose?r.tokens[0]&&["paragraph","text"].includes(r.tokens[0].type)&&"tokens"in r.tokens[0]&&r.tokens[0].tokens?(r.tokens[0].raw=d.raw+r.tokens[0].raw,r.tokens[0].text=d.raw+r.tokens[0].text,r.tokens[0].tokens.unshift(d)):r.tokens.unshift({type:"paragraph",raw:d.raw,text:d.raw,tokens:[d]}):r.tokens.unshift(d)}}if(!i.loose){let p=r.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 r of i.items){r.loose=!0;for(let p of r.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=ko(t[1]),s=t[2].replace(this.rules.other.tableAlignChars,"").split("|"),i=t[3]?.trim()?t[3].replace(this.rules.other.tableRowBlankLine,"").split(` +`):[],o={type:"table",raw:t[0],header:[],align:[],rows:[]};if(n.length===s.length){for(let a of s)this.rules.other.tableAlignRight.test(a)?o.align.push("right"):this.rules.other.tableAlignCenter.test(a)?o.align.push("center"):this.rules.other.tableAlignLeft.test(a)?o.align.push("left"):o.align.push(null);for(let a=0;a({text:l,tokens:this.lexer.inline(l),header:!1,align:o.align[r]})));return o}}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 o=ht(n.slice(0,-1),"\\");if((n.length-o.length)%2===0)return}else{let o=Cu(t[2],"()");if(o===-2)return;if(o>-1){let a=(t[0].indexOf("!")===0?5:4)+t[1].length+o;t[2]=t[2].substring(0,o),t[0]=t[0].substring(0,a).trim(),t[3]=""}}let s=t[2],i="";if(this.options.pedantic){let o=this.rules.other.pedanticHrefTitle.exec(s);o&&(s=o[1],i=o[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)),xo(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 o=n[0].charAt(0);return{type:"text",raw:o,text:o}}return xo(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,o,a,l=i,r=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(o=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!o)continue;if(a=[...o].length,s[3]||s[4]){l+=a;continue}else if((s[5]||s[6])&&i%3&&!((i+a)%3)){r+=a;continue}if(l-=a,l>0)continue;a=Math.min(a,a+l+r);let d=[...s[0]][0].length,u=e.slice(0,i+s.index+d+a);if(Math.min(i,a)%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}}}},se=class ks{tokens;options;state;inlineQueue;tokenizer;constructor(t){this.tokens=[],this.tokens.links=Object.create(null),this.options=t||Ue,this.options.tokenizer=this.options.tokenizer||new Xt,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:Kt.normal,inline:ft.normal};this.options.pedantic?(n.block=Kt.pedantic,n.inline=ft.pedantic):this.options.gfm&&(n.block=Kt.gfm,this.options.breaks?n.inline=ft.breaks:n.inline=ft.gfm),this.tokenizer.rules=n}static get rules(){return{block:Kt,inline:ft}}static lex(t,n){return new ks(n).lex(t)}static lexInline(t,n){return new ks(n).inlineTokens(t)}lex(t){t=t.replace(Y.carriageReturn,` +`),this.blockTokens(t,this.tokens);for(let n=0;n(i=a.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 a=n.at(-1);i.raw.length===1&&a!==void 0?a.raw+=` +`:n.push(i);continue}if(i=this.tokenizer.code(t)){t=t.substring(i.raw.length);let a=n.at(-1);a?.type==="paragraph"||a?.type==="text"?(a.raw+=(a.raw.endsWith(` +`)?"":` +`)+i.raw,a.text+=` +`+i.text,this.inlineQueue.at(-1).src=a.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 a=n.at(-1);a?.type==="paragraph"||a?.type==="text"?(a.raw+=(a.raw.endsWith(` +`)?"":` +`)+i.raw,a.text+=` +`+i.raw,this.inlineQueue.at(-1).src=a.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 o=t;if(this.options.extensions?.startBlock){let a=1/0,l=t.slice(1),r;this.options.extensions.startBlock.forEach(p=>{r=p.call({lexer:this},l),typeof r=="number"&&r>=0&&(a=Math.min(a,r))}),a<1/0&&a>=0&&(o=t.substring(0,a+1))}if(this.state.top&&(i=this.tokenizer.paragraph(o))){let a=n.at(-1);s&&a?.type==="paragraph"?(a.raw+=(a.raw.endsWith(` +`)?"":` +`)+i.raw,a.text+=` +`+i.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=a.text):n.push(i),s=o.length!==t.length,t=t.substring(i.raw.length);continue}if(i=this.tokenizer.text(t)){t=t.substring(i.raw.length);let a=n.at(-1);a?.type==="text"?(a.raw+=(a.raw.endsWith(` +`)?"":` +`)+i.raw,a.text+=` +`+i.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=a.text):n.push(i);continue}if(t){let a="Infinite loop on byte: "+t.charCodeAt(0);if(this.options.silent){console.error(a);break}else throw new Error(a)}}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 r=Object.keys(this.tokens.links);if(r.length>0)for(;(i=this.tokenizer.rules.inline.reflinkSearch.exec(s))!=null;)r.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 o;for(;(i=this.tokenizer.rules.inline.blockSkip.exec(s))!=null;)o=i[2]?i[2].length:0,s=s.slice(0,i.index+o)+"["+"a".repeat(i[0].length-o-2)+"]"+s.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);s=this.options.hooks?.emStrongMask?.call({lexer:this},s)??s;let a=!1,l="";for(;t;){a||(l=""),a=!1;let r;if(this.options.extensions?.inline?.some(d=>(r=d.call({lexer:this},t,n))?(t=t.substring(r.raw.length),n.push(r),!0):!1))continue;if(r=this.tokenizer.escape(t)){t=t.substring(r.raw.length),n.push(r);continue}if(r=this.tokenizer.tag(t)){t=t.substring(r.raw.length),n.push(r);continue}if(r=this.tokenizer.link(t)){t=t.substring(r.raw.length),n.push(r);continue}if(r=this.tokenizer.reflink(t,this.tokens.links)){t=t.substring(r.raw.length);let d=n.at(-1);r.type==="text"&&d?.type==="text"?(d.raw+=r.raw,d.text+=r.text):n.push(r);continue}if(r=this.tokenizer.emStrong(t,s,l)){t=t.substring(r.raw.length),n.push(r);continue}if(r=this.tokenizer.codespan(t)){t=t.substring(r.raw.length),n.push(r);continue}if(r=this.tokenizer.br(t)){t=t.substring(r.raw.length),n.push(r);continue}if(r=this.tokenizer.del(t)){t=t.substring(r.raw.length),n.push(r);continue}if(r=this.tokenizer.autolink(t)){t=t.substring(r.raw.length),n.push(r);continue}if(!this.state.inLink&&(r=this.tokenizer.url(t))){t=t.substring(r.raw.length),n.push(r);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(r=this.tokenizer.inlineText(p)){t=t.substring(r.raw.length),r.raw.slice(-1)!=="_"&&(l=r.raw.slice(-1)),a=!0;let d=n.at(-1);d?.type==="text"?(d.raw+=r.raw,d.text+=r.text):n.push(r);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}},en=class{options;parser;constructor(e){this.options=e||Ue}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:ve(i,!0))+`
+`:"
"+(n?i:ve(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 a=0;a +`+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`${ve(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=$o(e);if(i===null)return s;e=i;let o='
    ",o}image({href:e,title:t,text:n,tokens:s}){s&&(n=this.parser.parseInline(s,this.parser.textRenderer));let i=$o(e);if(i===null)return ve(n);e=i;let o=`${n}{let a=i[o].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 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 o=t.renderers[i.name];o?t.renderers[i.name]=function(...a){let l=i.renderer.apply(this,a);return l===!1&&(l=o.apply(this,a)),l}: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 o=t[i.level];o?o.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 en(this.defaults);for(let o in n.renderer){if(!(o in i))throw new Error(`renderer '${o}' does not exist`);if(["options","parser"].includes(o))continue;let a=o,l=n.renderer[a],r=i[a];i[a]=(...p)=>{let d=l.apply(i,p);return d===!1&&(d=r.apply(i,p)),d||""}}s.renderer=i}if(n.tokenizer){let i=this.defaults.tokenizer||new Xt(this.defaults);for(let o in n.tokenizer){if(!(o in i))throw new Error(`tokenizer '${o}' does not exist`);if(["options","rules","lexer"].includes(o))continue;let a=o,l=n.tokenizer[a],r=i[a];i[a]=(...p)=>{let d=l.apply(i,p);return d===!1&&(d=r.apply(i,p)),d}}s.tokenizer=i}if(n.hooks){let i=this.defaults.hooks||new gt;for(let o in n.hooks){if(!(o in i))throw new Error(`hook '${o}' does not exist`);if(["options","block"].includes(o))continue;let a=o,l=n.hooks[a],r=i[a];gt.passThroughHooks.has(o)?i[a]=p=>{if(this.defaults.async&>.passThroughHooksRespectAsync.has(o))return(async()=>{let u=await l.call(i,p);return r.call(i,u)})();let d=l.call(i,p);return r.call(i,d)}:i[a]=(...p)=>{if(this.defaults.async)return(async()=>{let u=await l.apply(i,p);return u===!1&&(u=await r.apply(i,p)),u})();let d=l.apply(i,p);return d===!1&&(d=r.apply(i,p)),d}}s.hooks=i}if(n.walkTokens){let i=this.defaults.walkTokens,o=n.walkTokens;s.walkTokens=function(a){let l=[];return l.push(o.call(this,a)),i&&(l=l.concat(i.call(this,a))),l}}this.defaults={...this.defaults,...s}}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return se.lex(e,t??this.defaults)}parser(e,t){return ie.parse(e,t??this.defaults)}parseMarkdown(e){return(t,n)=>{let s={...n},i={...this.defaults,...s},o=this.onError(!!i.silent,!!i.async);if(this.defaults.async===!0&&s.async===!1)return o(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 o(new Error("marked(): input parameter is undefined or null"));if(typeof t!="string")return o(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 a=i.hooks?await i.hooks.preprocess(t):t,l=await(i.hooks?await i.hooks.provideLexer():e?se.lex:se.lexInline)(a,i),r=i.hooks?await i.hooks.processAllTokens(l):l;i.walkTokens&&await Promise.all(this.walkTokens(r,i.walkTokens));let p=await(i.hooks?await i.hooks.provideParser():e?ie.parse:ie.parseInline)(r,i);return i.hooks?await i.hooks.postprocess(p):p})().catch(o);try{i.hooks&&(t=i.hooks.preprocess(t));let a=(i.hooks?i.hooks.provideLexer():e?se.lex:se.lexInline)(t,i);i.hooks&&(a=i.hooks.processAllTokens(a)),i.walkTokens&&this.walkTokens(a,i.walkTokens);let l=(i.hooks?i.hooks.provideParser():e?ie.parse:ie.parseInline)(a,i);return i.hooks&&(l=i.hooks.postprocess(l)),l}catch(a){return o(a)}}}onError(e,t){return n=>{if(n.message+=` +Please report this to https://github.com/markedjs/marked.`,e){let s="

    An error occurred:

    "+ve(n.message+"",!0)+"
    ";return t?Promise.resolve(s):s}if(t)return Promise.reject(n);throw n}}},Fe=new Iu;function P(e,t){return Fe.parse(e,t)}P.options=P.setOptions=function(e){return Fe.setOptions(e),P.defaults=Fe.defaults,Ja(P.defaults),P};P.getDefaults=ei;P.defaults=Ue;P.use=function(...e){return Fe.use(...e),P.defaults=Fe.defaults,Ja(P.defaults),P};P.walkTokens=function(e,t){return Fe.walkTokens(e,t)};P.parseInline=Fe.parseInline;P.Parser=ie;P.parser=ie.parse;P.Renderer=en;P.TextRenderer=li;P.Lexer=se;P.lexer=se.lex;P.Tokenizer=Xt;P.Hooks=gt;P.parse=P;P.options;P.setOptions;P.use;P.walkTokens;P.parseInline;ie.parse;se.lex;P.setOptions({gfm:!0,breaks:!0,mangle:!1});const Ao=["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"],So=["class","href","rel","target","title","start"];let _o=!1;const Lu=14e4,Ru=4e4,Mu=200,Zn=5e4,Pe=new Map;function Pu(e){const t=Pe.get(e);return t===void 0?null:(Pe.delete(e),Pe.set(e,t),t)}function To(e,t){if(Pe.set(e,t),Pe.size<=Mu)return;const n=Pe.keys().next().value;n&&Pe.delete(n)}function Nu(){_o||(_o=!0,ws.addHook("afterSanitizeAttributes",e=>{!(e instanceof HTMLAnchorElement)||!e.getAttribute("href")||(e.setAttribute("rel","noreferrer noopener"),e.setAttribute("target","_blank"))}))}function As(e){const t=e.trim();if(!t)return"";if(Nu(),t.length<=Zn){const a=Pu(t);if(a!==null)return a}const n=la(t,Lu),s=n.truncated?` + +… truncated (${n.total} chars, showing first ${n.text.length}).`:"";if(n.text.length>Ru){const l=`
    ${Ou(`${n.text}${s}`)}
    `,r=ws.sanitize(l,{ALLOWED_TAGS:Ao,ALLOWED_ATTR:So});return t.length<=Zn&&To(t,r),r}const i=P.parse(`${n.text}${s}`),o=ws.sanitize(i,{ALLOWED_TAGS:Ao,ALLOWED_ATTR:So});return t.length<=Zn&&To(t,o),o}function Ou(e){return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function Du(e,t){return c``}function Ht(e,t){e&&(e.textContent=t)}const Bu=1500,Fu=2e3,lr="Copy as markdown",Uu="Copied",Ku="Copy failed",Xn="📋",Hu="✓",zu="!";async function ju(e){if(!e)return!1;try{return await navigator.clipboard.writeText(e),!0}catch{return!1}}function zt(e,t){e.title=t,e.setAttribute("aria-label",t)}function qu(e){const t=e.label??lr;return c` + + `}function Wu(e){return qu({text:()=>e,label:lr})}const Vu={emoji:"🧩",detailKeys:["command","path","url","targetUrl","targetId","ref","element","node","nodeId","id","requestId","to","channelId","guildId","userId","name","query","pattern","messageId"]},Gu={bash:{emoji:"🛠️",title:"Bash",detailKeys:["command"]},process:{emoji:"🧰",title:"Process",detailKeys:["sessionId"]},read:{emoji:"📖",title:"Read",detailKeys:["path"]},write:{emoji:"✍️",title:"Write",detailKeys:["path"]},edit:{emoji:"📝",title:"Edit",detailKeys:["path"]},attach:{emoji:"📎",title:"Attach",detailKeys:["path","url","fileName"]},browser:{emoji:"🌐",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:{emoji:"🖼️",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:{emoji:"📱",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:{emoji:"⏰",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:{emoji:"🔌",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:{emoji:"🟢",title:"WhatsApp Login",actions:{start:{label:"start"},wait:{label:"wait"}}},discord:{emoji:"💬",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:{emoji:"💬",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"}}}},Yu={fallback:Vu,tools:Gu},cr=Yu,Co=cr.fallback??{emoji:"🧩"},Qu=cr.tools??{};function Ju(e){return(e??"tool").trim()}function Zu(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 Xu(e){const t=e?.trim();if(t)return t.replace(/_/g," ")}function dr(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=>dr(s)).filter(s=>!!s);if(t.length===0)return;const n=t.slice(0,3).join(", ");return t.length>3?`${n}…`:n}}}function ep(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 tp(e,t){for(const n of t){const s=ep(e,n),i=dr(s);if(i)return i}}function np(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 sp(e){if(!e||typeof e!="object")return;const t=e;return typeof t.path=="string"?t.path:void 0}function ip(e,t){if(!(!e||!t))return e.actions?.[t]??void 0}function op(e){const t=Ju(e.name),n=t.toLowerCase(),s=Qu[n],i=s?.emoji??Co.emoji??"🧩",o=s?.title??Zu(t),a=s?.label??t,l=e.args&&typeof e.args=="object"?e.args.action:void 0,r=typeof l=="string"?l.trim():void 0,p=ip(s,r),d=Xu(p?.label??r);let u;n==="read"&&(u=np(e.args)),!u&&(n==="write"||n==="edit"||n==="attach")&&(u=sp(e.args));const h=p?.detailKeys??s?.detailKeys??Co.detailKeys??[];return!u&&h.length>0&&(u=tp(e.args,h)),!u&&e.meta&&(u=e.meta),u&&(u=rp(u)),{name:t,emoji:i,title:o,label:a,verb:d,detail:u}}function ap(e){const t=[];if(e.verb&&t.push(e.verb),e.detail&&t.push(e.detail),t.length!==0)return t.join(" · ")}function rp(e){return e&&e.replace(/\/Users\/[^/]+/g,"~").replace(/\/home\/[^/]+/g,"~")}const lp=80,cp=2,Eo=100;function dp(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 up(e){const t=e.split(` +`),n=t.slice(0,cp),s=n.join(` +`);return s.length>Eo?s.slice(0,Eo)+"…":n.lengthi.kind==="result")){const i=typeof t.toolName=="string"&&t.toolName||typeof t.tool_name=="string"&&t.tool_name||"tool",o=ca(e)??void 0;s.push({kind:"result",name:i,text:o})}return s}function Io(e,t){const n=op({name:e.name,args:e.args}),s=ap(n),i=!!e.text?.trim(),o=!!t,a=o?()=>{if(i){t(dp(e.text));return}const u=`## ${n.label} + +${s?`**Command:** \`${s}\` + +`:""}*No output — tool completed successfully.*`;t(u)}:void 0,l=i&&(e.text?.length??0)<=lp,r=i&&!l,p=i&&l,d=!i;return c` +
    {u.key!=="Enter"&&u.key!==" "||(u.preventDefault(),a?.())}:g} + > +
    +
    + ${n.emoji} + ${n.label} +
    + ${o?c`${i?"View ›":"›"}`:g} + ${d&&!o?c``:g} +
    + ${s?c`
    ${s}
    `:g} + ${d?c`
    Completed
    `:g} + ${r?c`
    ${up(e.text)}
    `:g} + ${p?c`
    ${e.text}
    `:g} +
    + `}function fp(e){return Array.isArray(e)?e.filter(Boolean):[]}function hp(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 gp(e){if(typeof e.text=="string")return e.text;if(typeof e.content=="string")return e.content}function vp(e){return c` +
    + ${ci("assistant",e)} +
    + +
    +
    + `}function mp(e,t,n,s){const i=new Date(t).toLocaleTimeString([],{hour:"numeric",minute:"2-digit"}),o=s?.name??"Assistant";return c` +
    + ${ci("assistant",s)} +
    + ${ur({role:"assistant",content:[{type:"text",text:e}],timestamp:t},{isStreaming:!0,showReasoning:!1},n)} + +
    +
    + `}function bp(e,t){const n=Xs(e.role),s=t.assistantName??"Assistant",i=n==="user"?"You":n==="assistant"?s:n,o=n==="user"?"user":n==="assistant"?"assistant":"other",a=new Date(e.timestamp).toLocaleTimeString([],{hour:"numeric",minute:"2-digit"});return c` +
    + ${ci(e.role,{name:s,avatar:t.assistantAvatar??null})} +
    + ${e.messages.map((l,r)=>ur(l.message,{isStreaming:e.isStreaming&&r===e.messages.length-1,showReasoning:t.showReasoning},t.onOpenSidebar))} + +
    +
    + `}function ci(e,t){const n=Xs(e),s=t?.name?.trim()||"Assistant",i=t?.avatar?.trim()||"",o=n==="user"?"U":n==="assistant"?s.charAt(0).toUpperCase()||"A":n==="tool"?"⚙":"?",a=n==="user"?"user":n==="assistant"?"assistant":n==="tool"?"tool":"other";return i&&n==="assistant"?yp(i)?c`${s}`:c`
    ${i}
    `:c`
    ${o}
    `}function yp(e){return/^https?:\/\//i.test(e)||/^data:image\//i.test(e)||/^\//.test(e)}function ur(e,t,n){const s=e,i=typeof s.role=="string"?s.role:"unknown",o=Wa(e)||i.toLowerCase()==="toolresult"||i.toLowerCase()==="tool_result"||typeof s.toolCallId=="string"||typeof s.tool_call_id=="string",a=pp(e),l=a.length>0,r=ca(e),p=t.showReasoning&&i==="assistant"?xl(e):null,d=r?.trim()?r:null,u=p?Sl(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&&l&&o?c`${a.map($=>Io($,n))}`:!h&&!l?g:c` +
    + ${v?Wu(h):g} + ${u?c`
    ${vs(As(u))}
    `:g} + ${h?c`
    ${vs(As(h))}
    `:g} + ${a.map($=>Io($,n))} +
    + `}function wp(e){return c` + + `}var $p=Object.defineProperty,kp=Object.getOwnPropertyDescriptor,vn=(e,t,n,s)=>{for(var i=s>1?void 0:s?kp(t,n):t,o=e.length-1,a;o>=0;o--)(a=e[o])&&(i=(s?a(t,n,i):a(i))||i);return s&&i&&$p(t,n,i),i};let tt=class extends Qe{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 o=this.startRatio+i;o=Math.max(this.minRatio,Math.min(this.maxRatio,o)),this.dispatchEvent(new CustomEvent("resize",{detail:{splitRatio:o},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 c``}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)}};tt.styles=Fr` + :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); + } + `;vn([on({type:Number})],tt.prototype,"splitRatio",2);vn([on({type:Number})],tt.prototype,"minRatio",2);vn([on({type:Number})],tt.prototype,"maxRatio",2);tt=vn([ta("resizable-divider")],tt);const xp=5e3;function Ap(e){return e?e.active?c` +
    + 🧹 Compacting context... +
    + `:e.completedAt&&Date.now()-e.completedAt + 🧹 Context compacted + + `:g:g}function Sp(e){const t=e.connected,n=e.sending||e.stream!==null,i=e.sessions?.sessions?.find(u=>u.key===e.sessionKey)?.reasoningLevel??"off",o=e.showThinking&&i!=="off",a={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…",r=e.splitRatio??.6,p=!!(e.sidebarOpen&&e.onCloseSidebar),d=c` +
    + ${e.loading?c`
    Loading chat…
    `:g} + ${ja(Tp(e),u=>u.key,u=>u.kind==="reading-indicator"?vp(a):u.kind==="stream"?mp(u.text,u.startedAt,e.onOpenSidebar,a):u.kind==="group"?bp(u,{onOpenSidebar:e.onOpenSidebar,showReasoning:o,assistantName:e.assistantName,assistantAvatar:a.avatar}):g)} +
    + `;return c` +
    + ${e.disabledReason?c`
    ${e.disabledReason}
    `:g} + + ${e.error?c`
    ${e.error}
    `:g} + + ${Ap(e.compactionStatus)} + + ${e.focusMode?c` + + `:g} + +
    +
    + ${d} +
    + + ${p?c` + e.onSplitRatioChange?.(u.detail.splitRatio)} + > +
    + ${wp({content:e.sidebarContent??null,error:e.sidebarError??null,onClose:e.onCloseSidebar,onViewRawText:()=>{!e.sidebarContent||!e.onOpenSidebar||e.onOpenSidebar(`\`\`\` +${e.sidebarContent} +\`\`\``)}})} +
    + `:g} +
    + + ${e.queue.length?c` +
    +
    Queued (${e.queue.length})
    +
    + ${e.queue.map(u=>c` +
    +
    ${u.text}
    + +
    + `)} +
    +
    + `:g} + +
    + +
    + + +
    +
    +
    + `}const Lo=200;function _p(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=qa(s.message),o=Xs(i.role),a=i.timestamp||Date.now();!n||n.role!==o?(n&&t.push(n),n={kind:"group",key:`group:${o}:${s.key}`,role:o,messages:[{message:s.message,key:s.key}],timestamp:a,isStreaming:!1}):n.messages.push({message:s.message,key:s.key})}return n&&t.push(n),t}function Tp(e){const t=[],n=Array.isArray(e.messages)?e.messages:[],s=Array.isArray(e.toolMessages)?e.toolMessages:[],i=Math.max(0,n.length-Lo);i>0&&t.push({kind:"message",key:"chat:history:notice",message:{role:"system",content:`Showing last ${Lo} messages (${i} hidden).`,timestamp:Date.now()}});for(let o=i;o0?t.push({kind:"stream",key:o,text:e.stream,startedAt:e.streamStartedAt??Date.now()}):t.push({kind:"reading-indicator",key:o})}return _p(t)}function Ro(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 o=typeof n.messageId=="string"?n.messageId:"";if(o)return`msg:${o}`;const a=typeof n.timestamp=="number"?n.timestamp:null,l=typeof n.role=="string"?n.role:"unknown";return a!=null?`msg:${l}:${a}:${t}`:`msg:${l}:${t}`}function de(e){if(e)return Array.isArray(e.type)?e.type.filter(n=>n!=="null")[0]??e.type[0]:e.type}function pr(e){if(!e)return"";if(e.default!==void 0)return e.default;switch(de(e)){case"object":return{};case"array":return[];case"boolean":return!1;case"number":case"integer":return 0;case"string":return"";default:return""}}function mn(e){return e.filter(t=>typeof t=="string").join(".")}function ee(e,t){const n=mn(e),s=t[n];if(s)return s;const i=n.split(".");for(const[o,a]of Object.entries(t)){if(!o.includes("*"))continue;const l=o.split(".");if(l.length!==i.length)continue;let r=!0;for(let p=0;pt.toUpperCase())}function Cp(e){const t=mn(e).toLowerCase();return t.includes("token")||t.includes("password")||t.includes("secret")||t.includes("apikey")||t.endsWith("key")}const Ep=new Set(["title","description","default","nullable"]);function Ip(e){return Object.keys(e??{}).filter(n=>!Ep.has(n)).length===0}function Lp(e){if(e===void 0)return"";try{return JSON.stringify(e,null,2)??""}catch{return""}}const St={chevronDown:c``,plus:c``,minus:c``,trash:c``,edit:c``};function be(e){const{schema:t,value:n,path:s,hints:i,unsupported:o,disabled:a,onPatch:l}=e,r=e.showLabel??!0,p=de(t),d=ee(s,i),u=d?.label??t.title??ye(String(s.at(-1))),h=d?.help??t.description,v=mn(s);if(o.has(v))return c`
    +
    ${u}
    +
    Unsupported schema node. Use Raw mode.
    +
    `;if(t.anyOf||t.oneOf){const $=(t.anyOf??t.oneOf??[]).filter(A=>!(A.type==="null"||Array.isArray(A.type)&&A.type.includes("null")));if($.length===1)return be({...e,schema:$[0]});const x=A=>{if(A.const!==void 0)return A.const;if(A.enum&&A.enum.length===1)return A.enum[0]},C=$.map(x),I=C.every(A=>A!==void 0);if(I&&C.length>0&&C.length<=5){const A=n??t.default;return c` +
    + ${r?c``:g} + ${h?c`
    ${h}
    `:g} +
    + ${C.map((B,ue)=>c` + + `)} +
    +
    + `}if(I&&C.length>5)return Po({...e,options:C,value:n??t.default});const R=new Set($.map(A=>de(A)).filter(Boolean)),E=new Set([...R].map(A=>A==="integer"?"number":A));if([...E].every(A=>["string","number","boolean"].includes(A))){const A=E.has("string"),B=E.has("number");if(E.has("boolean")&&E.size===1)return be({...e,schema:{...t,type:"boolean",anyOf:void 0,oneOf:void 0}});if(A||B)return Mo({...e,inputType:B&&!A?"number":"text"})}}if(t.enum){const w=t.enum;if(w.length<=5){const $=n??t.default;return c` +
    + ${r?c``:g} + ${h?c`
    ${h}
    `:g} +
    + ${w.map(x=>c` + + `)} +
    +
    + `}return Po({...e,options:w,value:n??t.default})}if(p==="object")return Mp(e);if(p==="array")return Pp(e);if(p==="boolean"){const w=typeof n=="boolean"?n:typeof t.default=="boolean"?t.default:!1;return c` + + `}return p==="number"||p==="integer"?Rp(e):p==="string"?Mo({...e,inputType:"text"}):c` +
    +
    ${u}
    +
    Unsupported type: ${p}. Use Raw mode.
    +
    + `}function Mo(e){const{schema:t,value:n,path:s,hints:i,disabled:o,onPatch:a,inputType:l}=e,r=e.showLabel??!0,p=ee(s,i),d=p?.label??t.title??ye(String(s.at(-1))),u=p?.help??t.description,h=p?.sensitive??Cp(s),v=p?.placeholder??(h?"••••":t.default!==void 0?`Default: ${t.default}`:""),w=n??"";return c` +
    + ${r?c``:g} + ${u?c`
    ${u}
    `:g} +
    + {const x=$.target.value;if(l==="number"){if(x.trim()===""){a(s,void 0);return}const C=Number(x);a(s,Number.isNaN(C)?x:C);return}a(s,x)}} + /> + ${t.default!==void 0?c` + + `:g} +
    +
    + `}function Rp(e){const{schema:t,value:n,path:s,hints:i,disabled:o,onPatch:a}=e,l=e.showLabel??!0,r=ee(s,i),p=r?.label??t.title??ye(String(s.at(-1))),d=r?.help??t.description,u=n??t.default??"",h=typeof u=="number"?u:0;return c` +
    + ${l?c``:g} + ${d?c`
    ${d}
    `:g} +
    + + {const w=v.target.value,$=w===""?void 0:Number(w);a(s,$)}} + /> + +
    +
    + `}function Po(e){const{schema:t,value:n,path:s,hints:i,disabled:o,options:a,onPatch:l}=e,r=e.showLabel??!0,p=ee(s,i),d=p?.label??t.title??ye(String(s.at(-1))),u=p?.help??t.description,h=n??t.default,v=a.findIndex($=>$===h||String($)===String(h)),w="__unset__";return c` +
    + ${r?c``:g} + ${u?c`
    ${u}
    `:g} + +
    + `}function Mp(e){const{schema:t,value:n,path:s,hints:i,unsupported:o,disabled:a,onPatch:l}=e;e.showLabel;const r=ee(s,i),p=r?.label??t.title??ye(String(s.at(-1))),d=r?.help??t.description,u=n??t.default,h=u&&typeof u=="object"&&!Array.isArray(u)?u:{},v=t.properties??{},$=Object.entries(v).sort((R,E)=>{const A=ee([...s,R[0]],i)?.order??0,B=ee([...s,E[0]],i)?.order??0;return A!==B?A-B:R[0].localeCompare(E[0])}),x=new Set(Object.keys(v)),C=t.additionalProperties,I=!!C&&typeof C=="object";return s.length===1?c` +
    + ${$.map(([R,E])=>be({schema:E,value:h[R],path:[...s,R],hints:i,unsupported:o,disabled:a,onPatch:l}))} + ${I?No({schema:C,value:h,path:s,hints:i,unsupported:o,disabled:a,reservedKeys:x,onPatch:l}):g} +
    + `:c` +
    + + ${p} + ${St.chevronDown} + + ${d?c`
    ${d}
    `:g} +
    + ${$.map(([R,E])=>be({schema:E,value:h[R],path:[...s,R],hints:i,unsupported:o,disabled:a,onPatch:l}))} + ${I?No({schema:C,value:h,path:s,hints:i,unsupported:o,disabled:a,reservedKeys:x,onPatch:l}):g} +
    +
    + `}function Pp(e){const{schema:t,value:n,path:s,hints:i,unsupported:o,disabled:a,onPatch:l}=e,r=e.showLabel??!0,p=ee(s,i),d=p?.label??t.title??ye(String(s.at(-1))),u=p?.help??t.description,h=Array.isArray(t.items)?t.items[0]:t.items;if(!h)return c` +
    +
    ${d}
    +
    Unsupported array schema. Use Raw mode.
    +
    + `;const v=Array.isArray(n)?n:Array.isArray(t.default)?t.default:[];return c` +
    +
    + ${r?c`${d}`:g} + ${v.length} item${v.length!==1?"s":""} + +
    + ${u?c`
    ${u}
    `:g} + + ${v.length===0?c` +
    + No items yet. Click "Add" to create one. +
    + `:c` +
    + ${v.map((w,$)=>c` +
    +
    + #${$+1} + +
    +
    + ${be({schema:h,value:w,path:[...s,$],hints:i,unsupported:o,disabled:a,showLabel:!1,onPatch:l})} +
    +
    + `)} +
    + `} +
    + `}function No(e){const{schema:t,value:n,path:s,hints:i,unsupported:o,disabled:a,reservedKeys:l,onPatch:r}=e,p=Ip(t),d=Object.entries(n??{}).filter(([u])=>!l.has(u));return c` +
    +
    + Custom entries + +
    + + ${d.length===0?c` +
    No custom entries.
    + `:c` +
    + ${d.map(([u,h])=>{const v=[...s,u],w=Lp(h);return c` +
    +
    + {const x=$.target.value.trim();if(!x||x===u)return;const C={...n??{}};x in C||(C[x]=C[u],delete C[u],r(s,C))}} + /> +
    +
    + ${p?c` + + `:be({schema:t,value:h,path:v,hints:i,unsupported:o,disabled:a,showLabel:!1,onPatch:r})} +
    + +
    + `})} +
    + `} +
    + `}const Oo={env:c``,update:c``,agents:c``,auth:c``,channels:c``,messages:c``,commands:c``,hooks:c``,skills:c``,tools:c``,gateway:c``,wizard:c``,meta:c``,logging:c``,browser:c``,ui:c``,models:c``,bindings:c``,broadcast:c``,audio:c``,session:c``,cron:c``,web:c``,discovery:c``,canvasHost:c``,talk:c``,plugins:c``,default:c``},di={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 Do(e){return Oo[e]??Oo.default}function Np(e,t,n){if(!n)return!0;const s=n.toLowerCase(),i=di[e];return e.toLowerCase().includes(s)||i&&(i.label.toLowerCase().includes(s)||i.description.toLowerCase().includes(s))?!0:vt(t,s)}function vt(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)||vt(i,t))return!0}if(e.items){const s=Array.isArray(e.items)?e.items:[e.items];for(const i of s)if(i&&vt(i,t))return!0}if(e.additionalProperties&&typeof e.additionalProperties=="object"&&vt(e.additionalProperties,t))return!0;const n=e.anyOf??e.oneOf??e.allOf;if(n){for(const s of n)if(s&&vt(s,t))return!0}return!1}function Op(e){if(!e.schema)return c`
    Schema unavailable.
    `;const t=e.schema,n=e.value??{};if(de(t)!=="object"||!t.properties)return c`
    Unsupported schema. Use Raw.
    `;const s=new Set(e.unsupportedPaths??[]),i=t.properties,o=e.searchQuery??"",a=e.activeSection,l=e.activeSubsection??null,p=Object.entries(i).sort((u,h)=>{const v=ee([u[0]],e.uiHints)?.order??50,w=ee([h[0]],e.uiHints)?.order??50;return v!==w?v-w:u[0].localeCompare(h[0])}).filter(([u,h])=>!(a&&u!==a||o&&!Np(u,h,o)));let d=null;if(a&&l&&p.length===1){const u=p[0]?.[1];u&&de(u)==="object"&&u.properties&&u.properties[l]&&(d={sectionKey:a,subsectionKey:l,schema:u.properties[l]})}return p.length===0?c` +
    +
    🔍
    +
    + ${o?`No settings match "${o}"`:"No settings in this section"} +
    +
    + `:c` +
    + ${d?(()=>{const{sectionKey:u,subsectionKey:h,schema:v}=d,w=ee([u,h],e.uiHints),$=w?.label??v.title??ye(h),x=w?.help??v.description??"",C=n[u],I=C&&typeof C=="object"?C[h]:void 0,R=`config-section-${u}-${h}`;return c` +
    +
    + ${Do(u)} +
    +

    ${$}

    + ${x?c`

    ${x}

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

    ${v.label}

    + ${v.description?c`

    ${v.description}

    `:g} +
    +
    +
    + ${be({schema:h,value:n[u],path:[u],hints:e.uiHints,unsupported:s,disabled:e.disabled??!1,showLabel:!1,onPatch:e.onPatch})} +
    +
    + `})} +
    + `}const Dp=new Set(["title","description","default","nullable"]);function Bp(e){return Object.keys(e??{}).filter(n=>!Dp.has(n)).length===0}function fr(e){const t=e.filter(i=>i!=null),n=t.length!==e.length,s=[];for(const i of t)s.some(o=>Object.is(o,i))||s.push(i);return{enumValues:s,nullable:n}}function hr(e){return!e||typeof e!="object"?{schema:null,unsupportedPaths:[""]}:yt(e,[])}function yt(e,t){const n=new Set,s={...e},i=mn(t)||"";if(e.anyOf||e.oneOf||e.allOf){const l=Fp(e,t);return l||{schema:e,unsupportedPaths:[i]}}const o=Array.isArray(e.type)&&e.type.includes("null"),a=de(e)??(e.properties||e.additionalProperties?"object":void 0);if(s.type=a??e.type,s.nullable=o||e.nullable,s.enum){const{enumValues:l,nullable:r}=fr(s.enum);s.enum=l,r&&(s.nullable=!0),l.length===0&&n.add(i)}if(a==="object"){const l=e.properties??{},r={};for(const[p,d]of Object.entries(l)){const u=yt(d,[...t,p]);u.schema&&(r[p]=u.schema);for(const h of u.unsupportedPaths)n.add(h)}if(s.properties=r,e.additionalProperties===!0)n.add(i);else if(e.additionalProperties===!1)s.additionalProperties=!1;else if(e.additionalProperties&&typeof e.additionalProperties=="object"&&!Bp(e.additionalProperties)){const p=yt(e.additionalProperties,[...t,"*"]);s.additionalProperties=p.schema??e.additionalProperties,p.unsupportedPaths.length>0&&n.add(i)}}else if(a==="array"){const l=Array.isArray(e.items)?e.items[0]:e.items;if(!l)n.add(i);else{const r=yt(l,[...t,"*"]);s.items=r.schema??l,r.unsupportedPaths.length>0&&n.add(i)}}else a!=="string"&&a!=="number"&&a!=="integer"&&a!=="boolean"&&!s.enum&&n.add(i);return{schema:s,unsupportedPaths:Array.from(n)}}function Fp(e,t){if(e.allOf)return null;const n=e.anyOf??e.oneOf;if(!n)return null;const s=[],i=[];let o=!1;for(const l of n){if(!l||typeof l!="object")return null;if(Array.isArray(l.enum)){const{enumValues:r,nullable:p}=fr(l.enum);s.push(...r),p&&(o=!0);continue}if("const"in l){if(l.const==null){o=!0;continue}s.push(l.const);continue}if(de(l)==="null"){o=!0;continue}i.push(l)}if(s.length>0&&i.length===0){const l=[];for(const r of s)l.some(p=>Object.is(p,r))||l.push(r);return{schema:{...e,enum:l,nullable:o,anyOf:void 0,oneOf:void 0,allOf:void 0},unsupportedPaths:[]}}if(i.length===1){const l=yt(i[0],t);return l.schema&&(l.schema.nullable=o||l.schema.nullable),l}const a=["string","number","integer","boolean"];return i.length>0&&s.length===0&&i.every(l=>l.type&&a.includes(String(l.type)))?{schema:{...e,nullable:o},unsupportedPaths:[]}:null}const Ss={all:c``,env:c``,update:c``,agents:c``,auth:c``,channels:c``,messages:c``,commands:c``,hooks:c``,skills:c``,tools:c``,gateway:c``,wizard:c``,meta:c``,logging:c``,browser:c``,ui:c``,models:c``,bindings:c``,broadcast:c``,audio:c``,session:c``,cron:c``,web:c``,discovery:c``,canvasHost:c``,talk:c``,plugins:c``,default:c``},Bo=[{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"}],Fo="__all__";function Uo(e){return Ss[e]??Ss.default}function Up(e,t){const n=di[e];return n||{label:t?.title??ye(e),description:t?.description??""}}function Kp(e){const{key:t,schema:n,uiHints:s}=e;if(!n||de(n)!=="object"||!n.properties)return[];const i=Object.entries(n.properties).map(([o,a])=>{const l=ee([t,o],s),r=l?.label??a.title??ye(o),p=l?.help??a.description??"",d=l?.order??50;return{key:o,label:r,description:p,order:d}});return i.sort((o,a)=>o.order!==a.order?o.order-a.order:o.key.localeCompare(a.key)),i}function Hp(e,t){if(!e||!t)return[];const n=[];function s(i,o,a){if(i===o)return;if(typeof i!=typeof o){n.push({path:a,from:i,to:o});return}if(typeof i!="object"||i===null||o===null){i!==o&&n.push({path:a,from:i,to:o});return}if(Array.isArray(i)&&Array.isArray(o)){JSON.stringify(i)!==JSON.stringify(o)&&n.push({path:a,from:i,to:o});return}const l=i,r=o,p=new Set([...Object.keys(l),...Object.keys(r)]);for(const d of p)s(l[d],r[d],a?`${a}.${d}`:d)}return s(e,t,""),n}function Ko(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 zp(e){const t=e.valid==null?"unknown":e.valid?"valid":"invalid",n=hr(e.schema),s=n.schema?n.unsupportedPaths.length>0:!1,i=!!e.formValue&&!e.loading&&!s,o=e.connected&&!e.saving&&(e.formMode==="raw"?!0:i),a=e.connected&&!e.applying&&!e.updating&&(e.formMode==="raw"?!0:i),l=e.connected&&!e.applying&&!e.updating,r=n.schema?.properties??{},p=Bo.filter(A=>A.key in r),d=new Set(Bo.map(A=>A.key)),u=Object.keys(r).filter(A=>!d.has(A)).map(A=>({key:A,label:A.charAt(0).toUpperCase()+A.slice(1)})),h=[...p,...u],v=e.activeSection&&n.schema&&de(n.schema)==="object"?n.schema.properties?.[e.activeSection]:void 0,w=e.activeSection?Up(e.activeSection,v):null,$=e.activeSection?Kp({key:e.activeSection,schema:v,uiHints:e.uiHints}):[],x=e.formMode==="form"&&!!e.activeSection&&$.length>0,C=e.activeSubsection===Fo,I=e.searchQuery||C?null:e.activeSubsection??$[0]?.key??null,R=e.formMode==="form"?Hp(e.originalValue,e.formValue):[],E=R.length>0;return c` +
    + + + + +
    + +
    +
    + ${E?c` + ${R.length} unsaved change${R.length!==1?"s":""} + `:c` + No changes + `} +
    +
    + + + + +
    +
    + + + ${E?c` +
    + + View ${R.length} pending change${R.length!==1?"s":""} + + + + +
    + ${R.map(A=>c` +
    +
    ${A.path}
    +
    + ${Ko(A.from)} + + ${Ko(A.to)} +
    +
    + `)} +
    +
    + `:g} + + ${w&&e.formMode==="form"?c` +
    +
    ${Uo(e.activeSection??"")}
    +
    +
    ${w.label}
    + ${w.description?c`
    ${w.description}
    `:g} +
    +
    + `:g} + + ${x?c` +
    + + ${$.map(A=>c` + + `)} +
    + `:g} + + +
    + ${e.formMode==="form"?c` + ${e.schemaLoading?c`
    +
    + Loading schema… +
    `:Op({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:I})} + ${s?c`
    + Form view can't safely edit some fields. + Use Raw to avoid losing config entries. +
    `:g} + `:c` + + `} +
    + + ${e.issues.length>0?c`
    +
    ${JSON.stringify(e.issues,null,2)}
    +
    `:g} +
    +
    + `}function jp(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 qp(e,t){const n=t.snapshot,s=n?.channels;if(!n||!s)return!1;const i=s[e],o=typeof i?.configured=="boolean"&&i.configured,a=typeof i?.running=="boolean"&&i.running,l=typeof i?.connected=="boolean"&&i.connected,p=(n.channelAccounts?.[e]??[]).some(d=>d.configured||d.running||d.connected);return o||a||l||p}function Wp(e,t){return t?.[e]?.length??0}function gr(e,t){const n=Wp(e,t);return n<2?g:c``}function Vp(e,t){let n=e;for(const s of t){if(!n)return null;const i=de(n);if(i==="object"){const o=n.properties??{};if(typeof s=="string"&&o[s]){n=o[s];continue}const a=n.additionalProperties;if(typeof s=="string"&&a&&typeof a=="object"){n=a;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 Gp(e,t){const s=(e.channels??{})[t],i=e[t];return(s&&typeof s=="object"?s:null)??(i&&typeof i=="object"?i:null)??{}}function Yp(e){const t=hr(e.schema),n=t.schema;if(!n)return c`
    Schema unavailable. Use Raw.
    `;const s=Vp(n,["channels",e.channelId]);if(!s)return c`
    Channel config schema unavailable.
    `;const i=e.configValue??{},o=Gp(i,e.channelId);return c` +
    + ${be({schema:s,value:o,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 c` +
    + ${n.configSchemaLoading?c`
    Loading config schema…
    `:Yp({channelId:t,configValue:n.configForm,schema:n.configSchema,uiHints:n.configUiHints,disabled:s,onPatch:n.onConfigPatch})} +
    + + +
    +
    + `}function Qp(e){const{props:t,discord:n,accountCountLabel:s}=e;return c` +
    +
    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?c`
    + ${n.lastError} +
    `:g} + + ${n?.probe?c`
    + Probe ${n.probe.ok?"ok":"failed"} · + ${n.probe.status??""} ${n.probe.error??""} +
    `:g} + + ${_e({channelId:"discord",props:t})} + +
    + +
    +
    + `}function Jp(e){const{props:t,imessage:n,accountCountLabel:s}=e;return c` +
    +
    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?c`
    + ${n.lastError} +
    `:g} + + ${n?.probe?c`
    + 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 Xp(e){const{state:t,callbacks:n,accountId:s}=e,i=Zp(t),o=(l,r,p={})=>{const{type:d="text",placeholder:u,maxLength:h,help:v}=p,w=t.values[l]??"",$=t.fieldErrors[l],x=`nostr-profile-${l}`;return d==="textarea"?c` +
    + + + ${v?c`
    ${v}
    `:g} + ${$?c`
    ${$}
    `:g} +
    + `:c` +
    + + {const I=C.target;n.onFieldChange(l,I.value)}} + ?disabled=${t.saving} + /> + ${v?c`
    ${v}
    `:g} + ${$?c`
    ${$}
    `:g} +
    + `},a=()=>{const l=t.values.picture;return l?c` +
    + Profile picture preview{const p=r.target;p.style.display="none"}} + @load=${r=>{const p=r.target;p.style.display="block"}} + /> +
    + `:g};return c` +
    +
    +
    Edit Profile
    +
    Account: ${s}
    +
    + + ${t.error?c`
    ${t.error}
    `:g} + + ${t.success?c`
    ${t.success}
    `:g} + + ${a()} + + ${o("name","Username",{placeholder:"satoshi",maxLength:256,help:"Short username (e.g., satoshi)"})} + + ${o("displayName","Display Name",{placeholder:"Satoshi Nakamoto",maxLength:256,help:"Your full display name"})} + + ${o("about","Bio",{type:"textarea",placeholder:"Tell people about yourself...",maxLength:2e3,help:"A brief bio or description"})} + + ${o("picture","Avatar URL",{type:"url",placeholder:"https://example.com/avatar.jpg",help:"HTTPS URL to your profile picture"})} + + ${t.showAdvanced?c` +
    +
    Advanced
    + + ${o("banner","Banner URL",{type:"url",placeholder:"https://example.com/banner.jpg",help:"HTTPS URL to a banner image"})} + + ${o("website","Website",{type:"url",placeholder:"https://example.com",help:"Your personal website"})} + + ${o("nip05","NIP-05 Identifier",{placeholder:"you@example.com",help:"Verifiable identifier (e.g., you@domain.com)"})} + + ${o("lud16","Lightning Address",{placeholder:"you@getalby.com",help:"Lightning address for tips (LUD-16)"})} +
    + `:g} + +
    + + + + + + + +
    + + ${i?c`
    + You have unsaved changes +
    `:g} +
    + `}function ef(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 Ho(e){return e?e.length<=20?e:`${e.slice(0,8)}...${e.slice(-8)}`:"n/a"}function tf(e){const{props:t,nostr:n,nostrAccounts:s,accountCountLabel:i,profileFormState:o,profileFormCallbacks:a,onEditProfile:l}=e,r=s[0],p=n?.configured??r?.configured??!1,d=n?.running??r?.running??!1,u=n?.publicKey??r?.publicKey,h=n?.lastStartAt??r?.lastStartAt??null,v=n?.lastError??r?.lastError??null,w=s.length>1,$=o!=null,x=I=>{const R=I.publicKey,E=I.profile,A=E?.displayName??E?.name??I.name??I.accountId;return c` + + `},C=()=>{if($&&a)return Xp({state:o,callbacks:a,accountId:s[0]?.accountId??"default"});const I=r?.profile??n?.profile,{name:R,displayName:E,about:A,picture:B,nip05:ue}=I??{},bn=R||E||A||B||ue;return c` +
    +
    +
    Profile
    + ${p?c` + + `:g} +
    + ${bn?c` +
    + ${B?c` +
    + Profile picture{yn.target.style.display="none"}} + /> +
    + `:g} + ${R?c`
    Name${R}
    `:g} + ${E?c`
    Display Name${E}
    `:g} + ${A?c`
    About${A}
    `:g} + ${ue?c`
    NIP-05${ue}
    `:g} +
    + `:c` +
    + No profile set. Click "Edit Profile" to add your name, bio, and avatar. +
    + `} +
    + `};return c` +
    +
    Nostr
    +
    Decentralized DMs via Nostr relays (NIP-04).
    + ${i} + + ${w?c` + + `:c` +
    +
    + Configured + ${p?"Yes":"No"} +
    +
    + Running + ${d?"Yes":"No"} +
    +
    + Public Key + ${Ho(u)} +
    +
    + Last start + ${h?O(h):"n/a"} +
    +
    + `} + + ${v?c`
    ${v}
    `:g} + + ${C()} + + ${_e({channelId:"nostr",props:t})} + +
    + +
    +
    + `}function nf(e){const{props:t,signal:n,accountCountLabel:s}=e;return c` +
    +
    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?c`
    + ${n.lastError} +
    `:g} + + ${n?.probe?c`
    + Probe ${n.probe.ok?"ok":"failed"} · + ${n.probe.status??""} ${n.probe.error??""} +
    `:g} + + ${_e({channelId:"signal",props:t})} + +
    + +
    +
    + `}function sf(e){const{props:t,slack:n,accountCountLabel:s}=e;return c` +
    +
    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?c`
    + ${n.lastError} +
    `:g} + + ${n?.probe?c`
    + Probe ${n.probe.ok?"ok":"failed"} · + ${n.probe.status??""} ${n.probe.error??""} +
    `:g} + + ${_e({channelId:"slack",props:t})} + +
    + +
    +
    + `}function of(e){const{props:t,telegram:n,telegramAccounts:s,accountCountLabel:i}=e,o=s.length>1,a=l=>{const p=l.probe?.bot?.username,d=l.name||l.accountId;return c` + + `};return c` +
    +
    Telegram
    +
    Bot status and channel configuration.
    + ${i} + + ${o?c` + + `:c` +
    +
    + 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?c`
    + ${n.lastError} +
    `:g} + + ${n?.probe?c`
    + 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 c` +
    +
    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?jp(n.authAgeMs):"n/a"} + +
    +
    + + ${n?.lastError?c`
    + ${n.lastError} +
    `:g} + + ${t.whatsappMessage?c`
    + ${t.whatsappMessage} +
    `:g} + + ${t.whatsappQrDataUrl?c`
    + WhatsApp QR +
    `:g} + +
    + + + + + +
    + + ${_e({channelId:"whatsapp",props:t})} +
    + `}function rf(e){const t=e.snapshot?.channels,n=t?.whatsapp??void 0,s=t?.telegram??void 0,i=t?.discord??null,o=t?.slack??null,a=t?.signal??null,l=t?.imessage??null,r=t?.nostr??null,d=lf(e.snapshot).map((u,h)=>({key:u,enabled:qp(u,e),order:h})).sort((u,h)=>u.enabled!==h.enabled?u.enabled?-1:1:u.order-h.order);return c` +
    + ${d.map(u=>cf(u.key,e,{whatsapp:n,telegram:s,discord:i,slack:o,signal:a,imessage:l,nostr:r,channelAccounts:e.snapshot?.channelAccounts??null}))} +
    + +
    +
    +
    +
    Channel health
    +
    Channel status snapshots from the gateway.
    +
    +
    ${e.lastSuccessAt?O(e.lastSuccessAt):"n/a"}
    +
    + ${e.lastError?c`
    + ${e.lastError} +
    `:g} +
    +${e.snapshot?JSON.stringify(e.snapshot,null,2):"No snapshot yet."}
    +      
    +
    + `}function lf(e){return e?.channelMeta?.length?e.channelMeta.map(t=>t.id):e?.channelOrder?.length?e.channelOrder:["whatsapp","telegram","discord","slack","signal","imessage","nostr"]}function cf(e,t,n){const s=gr(e,n.channelAccounts);switch(e){case"whatsapp":return af({props:t,whatsapp:n.whatsapp,accountCountLabel:s});case"telegram":return of({props:t,telegram:n.telegram,telegramAccounts:n.channelAccounts?.telegram??[],accountCountLabel:s});case"discord":return Qp({props:t,discord:n.discord,accountCountLabel:s});case"slack":return sf({props:t,slack:n.slack,accountCountLabel:s});case"signal":return nf({props:t,signal:n.signal,accountCountLabel:s});case"imessage":return Jp({props:t,imessage:n.imessage,accountCountLabel:s});case"nostr":{const i=n.channelAccounts?.nostr??[],o=i[0],a=o?.accountId??"default",l=o?.profile??null,r=t.nostrProfileAccountId===a?t.nostrProfileFormState:null,p=r?{onFieldChange:t.onNostrProfileFieldChange,onSave:t.onNostrProfileSave,onImport:t.onNostrProfileImport,onCancel:t.onNostrProfileCancel,onToggleAdvanced:t.onNostrProfileToggleAdvanced}:null;return tf({props:t,nostr:n.nostr,nostrAccounts:i,accountCountLabel:s,profileFormState:r,profileFormCallbacks:p,onEditProfile:()=>t.onNostrProfileEdit(a,l)})}default:return df(e,t,n.channelAccounts??{})}}function df(e,t,n){const s=pf(t.snapshot,e),i=t.snapshot?.channels?.[e],o=typeof i?.configured=="boolean"?i.configured:void 0,a=typeof i?.running=="boolean"?i.running:void 0,l=typeof i?.connected=="boolean"?i.connected:void 0,r=typeof i?.lastError=="string"?i.lastError:void 0,p=n[e]??[],d=gr(e,n);return c` +
    +
    ${s}
    +
    Channel status and configuration.
    + ${d} + + ${p.length>0?c` + + `:c` +
    +
    + Configured + ${o==null?"n/a":o?"Yes":"No"} +
    +
    + Running + ${a==null?"n/a":a?"Yes":"No"} +
    +
    + Connected + ${l==null?"n/a":l?"Yes":"No"} +
    +
    + `} + + ${r?c`
    + ${r} +
    `:g} + + ${_e({channelId:e,props:t})} +
    + `}function uf(e){return e?.channelMeta?.length?Object.fromEntries(e.channelMeta.map(t=>[t.id,t])):{}}function pf(e,t){return uf(e)[t]?.label??e?.channelLabels?.[t]??t}const ff=600*1e3;function vr(e){return e.lastInboundAt?Date.now()-e.lastInboundAt
    + `:c` +
    + Auth failed. Re-copy a tokenized URL with + clawdbot dashboard --no-open, or update the token, + then click Connect. + +
    + `})(),o=(()=>{if(e.connected||!e.lastError||(typeof window<"u"?window.isSecureContext:!0)!==!1)return null;const l=e.lastError.toLowerCase();return!l.includes("secure context")&&!l.includes("device identity required")?null:c` +
    + 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). +
    + +
    + `})();return c` +
    +
    +
    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?c`
    +
    ${e.lastError}
    + ${i??""} + ${o??""} +
    `:c`
    + 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 ${mr(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 lh=["","off","minimal","low","medium","high"],ch=["","off","on"],dh=[{value:"",label:"inherit"},{value:"off",label:"off (explicit)"},{value:"on",label:"on"}],uh=["","off","on","stream"];function ph(e){if(!e)return"";const t=e.trim().toLowerCase();return t==="z.ai"||t==="z-ai"?"zai":t}function br(e){return ph(e)==="zai"}function fh(e){return br(e)?ch:lh}function hh(e,t){return!t||!e||e==="off"?e:"on"}function gh(e,t){return e?t&&e==="on"?"low":e:null}function vh(e){const t=e.result?.sessions??[];return c` +
    +
    +
    +
    Sessions
    +
    Active session keys and per-session overrides.
    +
    + +
    + +
    + + + + +
    + + ${e.error?c`
    ${e.error}
    `:g} + +
    + ${e.result?`Store: ${e.result.path}`:""} +
    + +
    +
    +
    Key
    +
    Label
    +
    Kind
    +
    Updated
    +
    Tokens
    +
    Thinking
    +
    Verbose
    +
    Reasoning
    +
    Actions
    +
    + ${t.length===0?c`
    No sessions found.
    `:t.map(n=>mh(n,e.basePath,e.onPatch,e.onDelete,e.loading))} +
    +
    + `}function mh(e,t,n,s,i){const o=e.updatedAt?O(e.updatedAt):"n/a",a=e.thinkingLevel??"",l=br(e.modelProvider),r=hh(a,l),p=fh(e.modelProvider),d=e.verboseLevel??"",u=e.reasoningLevel??"",h=e.displayName??e.key,v=e.kind!=="global",w=v?`${Ps("chat",t)}?session=${encodeURIComponent(e.key)}`:null;return c` +
    +
    ${v?c`${h}`:h}
    +
    + {const x=$.target.value.trim();n(e.key,{label:x||null})}} + /> +
    +
    ${e.kind}
    +
    ${o}
    +
    ${yf(e)}
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + `}function bh(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 Le(e,t){return t?c`
    ${e}${t}
    `:g}function yh(e){const t=e.execApprovalQueue[0];if(!t)return g;const n=t.request,s=t.expiresAtMs-Date.now(),i=s>0?`expires in ${bh(s)}`:"expired",o=e.execApprovalQueue.length;return c` + + `}function wh(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 c` +
    +
    +
    +
    Skills
    +
    Bundled, managed, and workspace skills.
    +
    + +
    + +
    + +
    ${s.length} shown
    +
    + + ${e.error?c`
    ${e.error}
    `:g} + + ${s.length===0?c`
    No skills found.
    `:c` +
    + ${s.map(i=>$h(i,e))} +
    + `} +
    + `}function $h(e,t){const n=t.busyKey===e.skillKey,s=t.edits[e.skillKey]??"",i=t.messages[e.skillKey]??null,o=e.install.length>0&&e.missing.bins.length>0,a=[...e.missing.bins.map(r=>`bin:${r}`),...e.missing.env.map(r=>`env:${r}`),...e.missing.config.map(r=>`config:${r}`),...e.missing.os.map(r=>`os:${r}`)],l=[];return e.disabled&&l.push("disabled"),e.blockedByAllowlist&&l.push("blocked by allowlist"),c` +
    +
    +
    + ${e.emoji?`${e.emoji} `:""}${e.name} +
    +
    ${as(e.description,140)}
    +
    + ${e.source} + + ${e.eligible?"eligible":"blocked"} + + ${e.disabled?c`disabled`:g} +
    + ${a.length>0?c` +
    + Missing: ${a.join(", ")} +
    + `:g} + ${l.length>0?c` +
    + Reason: ${l.join(", ")} +
    + `:g} +
    +
    +
    + + ${o?c``:g} +
    + ${i?c`
    + ${i.message} +
    `:g} + ${e.primaryEnv?c` +
    + API key + t.onEdit(e.skillKey,r.target.value)} + /> +
    + + `:g} +
    +
    + `}function kh(e,t){const n=Ps(t,e.basePath);return c` + {s.defaultPrevented||s.button!==0||s.metaKey||s.ctrlKey||s.shiftKey||s.altKey||(s.preventDefault(),e.setTab(t))}} + title=${is(t)} + > + + ${is(t)} + + `}function xh(e){const t=Ah(e.sessionKey,e.sessionsResult),n=e.onboarding,s=e.onboarding,i=e.onboarding?!1:e.settings.chatShowThinking,o=e.onboarding?!0:e.settings.chatFocusMode,a=c``,l=c``;return c` +
    + + + | + + +
    + `}function Ah(e,t){const n=new Set,s=[],i=t?.sessions?.find(o=>o.key===e);if(n.add(e),s.push({key:e,displayName:i?.displayName}),t?.sessions)for(const o of t.sessions)n.has(o.key)||(n.add(o.key),s.push({key:o.key,displayName:o.displayName}));return s}const Sh=["system","light","dark"];function _h(e){const t=Math.max(0,Sh.indexOf(e.theme)),n=s=>i=>{const a={element:i.currentTarget};(i.clientX||i.clientY)&&(a.pointerClientX=i.clientX,a.pointerClientY=i.clientY),e.setTheme(s,a)};return c` +
    +
    + + + + +
    +
    + `}function Th(){return c` + + `}function Ch(){return c` + + `}function Eh(){return c` + + `}const Ih=/^data:/i,Lh=/^https?:\/\//i;function Rh(e){const t=e.agentsList?.agents??[],s=sa(e.sessionKey)?.agentId??e.agentsList?.defaultId??"main",o=t.find(l=>l.id===s)?.identity,a=o?.avatarUrl??o?.avatar;if(a)return Ih.test(a)||Lh.test(a)?a:o?.avatarUrl}function Mh(e){const t=e.presenceEntries.length,n=e.sessionsResult?.count??null,s=e.cronStatus?.nextWakeAtMs??null,i=e.connected?null:"Disconnected from gateway.",o=e.tab==="chat",a=o&&(e.settings.chatFocusMode||e.onboarding),l=e.onboarding?!1:e.settings.chatShowThinking,r=Rh(e),p=e.chatAvatarUrl??r??null;return c` +
    +
    +
    + +
    +
    CLAWDBOT
    +
    Gateway Dashboard
    +
    +
    +
    +
    + + Health + ${e.connected?"OK":"Offline"} +
    + ${_h(e)} +
    +
    + +
    +
    +
    +
    ${is(e.tab)}
    +
    ${yl(e.tab)}
    +
    +
    + ${e.lastError?c`
    ${e.lastError}
    `:g} + ${o?xh(e):g} +
    +
    + + ${e.tab==="overview"?rh({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"?rf({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)=>Ot(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:()=>qs(e)}):g} + + ${e.tab==="sessions"?vh({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:()=>nt(e),onPatch:(d,u)=>Il(e,d,u),onDelete:d=>Ll(e,d)}):g} + + ${e.tab==="cron"?_f({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:()=>Xl(e),onToggle:(d,u)=>ec(e,d,u),onRun:d=>tc(e,d),onRemove:d=>nc(e,d),onLoadRuns:d=>ha(e,d)}):g} + + ${e.tab==="skills"?wh({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:()=>Tt(e,{clearMessages:!0}),onToggle:(d,u)=>Qc(e,d,u),onEdit:(d,u)=>Yc(e,d,u),onSaveKey:d=>Jc(e,d),onInstall:(d,u,h)=>Zc(e,d,u,h)}):g} + + ${e.tab==="nodes"?Of({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:()=>un(e),onDevicesRefresh:()=>Se(e),onDeviceApprove:d=>Fc(e,d),onDeviceReject:d=>Uc(e,d),onDeviceRotate:(d,u,h)=>Kc(e,{deviceId:d,role:u,scopes:h}),onDeviceRevoke:(d,u)=>Hc(e,{deviceId:d,role:u}),onLoadConfig:()=>me(e),onLoadExecApprovals:()=>{const d=e.execApprovalsTarget==="node"&&e.execApprovalsTargetNodeId?{kind:"node",nodeId:e.execApprovalsTargetNodeId}:{kind:"gateway"};return js(e,d)},onBindDefault:d=>{d?Ot(e,["tools","exec","node"],d):Xi(e,["tools","exec","node"])},onBindAgent:(d,u)=>{const h=["agents","list",d,"tools","exec","node"];u?Ot(e,h,u):Xi(e,h)},onSaveBindings:()=>cs(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)=>Vc(e,d,u),onExecApprovalsRemove:d=>Gc(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"?Sp({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(),Ze(e),hs(e)},thinkingLevel:e.chatThinkingLevel,showThinking:l,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:a,onRefresh:()=>(e.resetToolStream(),Promise.all([Ze(e),hs(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"?zp({raw:e.configRaw,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)=>Ot(e,d,u),onSearchChange:d=>e.configSearchQuery=d,onSectionChange:d=>{e.configActiveSection=d,e.configActiveSubsection=null},onSubsectionChange:d=>e.configActiveSubsection=d,onReload:()=>me(e),onSave:()=>cs(e),onApply:()=>Yl(e),onUpdate:()=>Ql(e)}):g} + + ${e.tab==="debug"?If({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:()=>cn(e),onCall:()=>ac(e)}):g} + + ${e.tab==="logs"?Nf({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:()=>Ds(e,{reset:!0}),onExport:(d,u)=>e.exportLogs(d,u),onScroll:d=>e.handleLogsScroll(d)}):g} +
    + ${yh(e)} +
    + `}const Ph={trace:!0,debug:!0,info:!0,warn:!0,error:!0,fatal:!0},Nh={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 Oh(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 yr={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"},Wo=yr,_s={WEBCHAT:"webchat",CLI:"cli",UI:"ui",BACKEND:"backend",NODE:"node",PROBE:"probe",TEST:"test"};new Set(Object.values(yr));new Set(Object.values(_s));function Dh(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 Bh=4008;class Fh{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,o=!1,a=this.opts.token;if(t){i=await Ks();const d=Bc({deviceId:i.deviceId,role:s})?.token;a=d??this.opts.token,o=!!(d&&this.opts.token)}const l=a||this.opts.password?{token:a,password:this.opts.password}:void 0;let r;if(t&&i){const d=Date.now(),u=this.connectNonce??void 0,h=Dh({deviceId:i.deviceId,clientId:this.opts.clientName??Wo.CONTROL_UI,clientMode:this.opts.mode??_s.WEBCHAT,role:s,scopes:n,signedAtMs:d,token:a??null,nonce:u}),v=await Oc(i.privateKey,h);r={id:i.deviceId,publicKey:i.publicKey,signature:v,signedAt:d,nonce:u}}const p={minProtocol:3,maxProtocol:3,client:{id:this.opts.clientName??Wo.CONTROL_UI,version:this.opts.clientVersion??"dev",platform:this.opts.platform??navigator.platform??"web",mode:this.opts.mode??_s.WEBCHAT,instanceId:this.opts.instanceId},role:s,scopes:n,device:r,caps:[],auth:l,userAgent:navigator.userAgent,locale:navigator.language};this.request("connect",p).then(d=>{d?.auth?.deviceToken&&i&&La({deviceId:i.deviceId,role:d.auth.role??s,token:d.auth.deviceToken,scopes:d.auth.scopes??[]}),this.backoffMs=800,this.opts.onHello?.(d)}).catch(()=>{o&&i&&Ra({deviceId:i.deviceId,role:s}),this.ws?.close(Bh,"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 a=i.payload,l=a&&typeof a.nonce=="string"?a.nonce:null;l&&(this.connectNonce=l,this.sendConnect());return}const o=typeof i.seq=="number"?i.seq:null;o!==null&&(this.lastSeq!==null&&o>this.lastSeq+1&&this.opts.onGap?.({expected:this.lastSeq+1,received:o}),this.lastSeq=o);try{this.opts.onEvent?.(i)}catch(a){console.error("[gateway] event handler error:",a)}return}if(s.type==="res"){const i=n,o=this.pending.get(i.id);if(!o)return;this.pending.delete(i.id),i.ok?o.resolve(i.payload):o.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=Ns(),i={type:"req",id:s,method:t,params:n},o=new Promise((a,l)=>{this.pending.set(s,{resolve:r=>a(r),reject:l})});return this.ws.send(JSON.stringify(i)),o}queueConnect(){this.connectNonce=null,this.connectSent=!1,this.connectTimer!==null&&window.clearTimeout(this.connectTimer),this.connectTimer=window.setTimeout(()=>{this.sendConnect()},750)}}function Ts(e){return typeof e=="object"&&e!==null}function Uh(e){if(!Ts(e))return null;const t=typeof e.id=="string"?e.id.trim():"",n=e.request;if(!t||!Ts(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,o=typeof e.expiresAtMs=="number"?e.expiresAtMs:0;return!i||!o?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:o}}function Kh(e){if(!Ts(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 wr(e){const t=Date.now();return e.filter(n=>n.expiresAtMs>t)}function Hh(e,t){const n=wr(e).filter(s=>s.id!==t.id);return n.push(t),n}function Vo(e,t){return wr(e).filter(n=>n.id!==t)}async function $r(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 o=ss(i);e.assistantName=o.name,e.assistantAvatar=o.avatar,e.assistantAgentId=o.agentId??null}catch{}}function es(e,t){const n=(e??"").trim(),s=t.mainSessionKey?.trim();if(!s)return n;if(!n)return s;const i=t.mainKey?.trim()||"main",o=t.defaultAgentId?.trim();return n==="main"||n===i||o&&(n===`agent:${o}:main`||n===`agent:${o}:${i}`)?s:n}function zh(e,t){if(!t?.mainSessionKey)return;const n=es(e.sessionKey,t),s=es(e.settings.sessionKey,t),i=es(e.settings.lastActiveSessionKey,t),o=n||s||e.sessionKey,a={...e.settings,sessionKey:s||o,lastActiveSessionKey:i||o},l=a.sessionKey!==e.settings.sessionKey||a.lastActiveSessionKey!==e.settings.lastActiveSessionKey;o!==e.sessionKey&&(e.sessionKey=o),l&&$e(e,a)}function kr(e){e.lastError=null,e.hello=null,e.connected=!1,e.execApprovalQueue=[],e.execApprovalError=null,e.client?.stop(),e.client=new Fh({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.hello=t,Wh(e,t),$r(e),Oh(e),un(e,{quiet:!0}),Se(e,{quiet:!0}),Js(e)},onClose:({code:t,reason:n})=>{e.connected=!1,e.lastError=`disconnected (${t}): ${n||"no reason"}`},onEvent:t=>jh(e,t),onGap:({expected:t,received:n})=>{e.lastError=`event gap detected (expected seq ${t}, got ${n}); refresh recommended`}}),e.client.start()}function jh(e,t){try{qh(e,t)}catch(n){console.error("[gateway] handleGatewayEvent error:",t.event,n)}}function qh(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;Kl(e,t.payload);return}if(t.event==="chat"){const n=t.payload;n?.sessionKey&&Ma(e,n.sessionKey);const s=El(e,n);(s==="final"||s==="error"||s==="aborted")&&(Os(e),wd(e)),s==="final"&&Ze(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")&&Se(e,{quiet:!0}),t.event==="exec.approval.requested"){const n=Uh(t.payload);if(n){e.execApprovalQueue=Hh(e.execApprovalQueue,n),e.execApprovalError=null;const s=Math.max(0,n.expiresAtMs-Date.now()+500);window.setTimeout(()=>{e.execApprovalQueue=Vo(e.execApprovalQueue,n.id)},s)}return}if(t.event==="exec.approval.resolved"){const n=Kh(t.payload);n&&(e.execApprovalQueue=Vo(e.execApprovalQueue,n.id))}}function Wh(e,t){const n=t.snapshot;n?.presence&&Array.isArray(n.presence)&&(e.presenceEntries=n.presence),n?.health&&(e.debugHealth=n.health),n?.sessionDefaults&&zh(e,n.sessionDefaults)}function Vh(e){e.basePath=rd(),ud(e,!0),ld(e),cd(e),window.addEventListener("popstate",e.popStateHandler),id(e),kr(e),nd(e),e.tab==="logs"&&Vs(e),e.tab==="debug"&&Ys(e)}function Gh(e){Wl(e)}function Yh(e){window.removeEventListener("popstate",e.popStateHandler),sd(e),Gs(e),Qs(e),dd(e),e.topbarObserver?.disconnect(),e.topbarObserver=null}function Qh(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;rn(e,n||s||!e.chatHasAutoScrolled)}e.tab==="logs"&&(t.has("logsEntries")||t.has("logsAutoFollow")||t.has("tab"))&&e.logsAutoFollow&&e.logsAtBottom&&da(e,t.has("tab")||t.has("logsAutoFollow"))}async function Jh(e,t){await sc(e,t),await oe(e,!0)}async function Zh(e){await ic(e),await oe(e,!0)}async function Xh(e){await oc(e),await oe(e,!0)}async function eg(e){await cs(e),await me(e),await oe(e,!0)}async function tg(e){await me(e),await oe(e,!0)}function ng(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 o=s.trim(),a=i.join(":").trim();o&&a&&(t[o]=a)}return t}function xr(e){return(e.channelsSnapshot?.channelAccounts?.nostr??[])[0]?.accountId??e.nostrProfileAccountId??"default"}function Ar(e,t=""){return`/api/channels/nostr/${encodeURIComponent(e)}/profile${t}`}function sg(e,t,n){e.nostrProfileAccountId=t,e.nostrProfileFormState=ef(n??void 0)}function ig(e){e.nostrProfileFormState=null,e.nostrProfileAccountId=null}function og(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 rg(e){const t=e.nostrProfileFormState;if(!t||t.saving)return;const n=xr(e);e.nostrProfileFormState={...t,saving:!0,error:null,success:null,fieldErrors:{}};try{const s=await fetch(Ar(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 o=i?.error??`Profile update failed (${s.status})`;e.nostrProfileFormState={...t,saving:!1,error:o,success:null,fieldErrors:ng(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 lg(e){const t=e.nostrProfileFormState;if(!t||t.importing)return;const n=xr(e);e.nostrProfileFormState={...t,importing:!0,error:null,success:null};try{const s=await fetch(Ar(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 r=i?.error??`Profile import failed (${s.status})`;e.nostrProfileFormState={...t,importing:!1,error:r,success:null};return}const o=i.merged??i.imported??null,a=o?{...t.values,...o}:t.values,l=!!(a.banner||a.website||a.nip05||a.lud16);e.nostrProfileFormState={...t,importing:!1,values:a,error:null,success:i.saved?"Profile imported from relays. Review and publish.":"Profile imported. Review and publish.",showAdvanced:l},i.saved&&await oe(e,!0)}catch(s){e.nostrProfileFormState={...t,importing:!1,error:`Profile import failed: ${String(s)}`,success:null}}}var cg=Object.defineProperty,dg=Object.getOwnPropertyDescriptor,b=(e,t,n,s)=>{for(var i=s>1?void 0:s?dg(t,n):t,o=e.length-1,a;o>=0;o--)(a=e[o])&&(i=(s?a(t,n,i):a(i))||i);return s&&i&&cg(t,n,i),i};const ts=fl();function ug(){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 Qe{constructor(){super(...arguments),this.settings=hl(),this.password="",this.tab="chat",this.onboarding=ug(),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=ts.name,this.assistantAvatar=ts.avatar,this.assistantAgentId=ts.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.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={...Nh},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={...Ph},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=()=>pd(this),this.themeMedia=null,this.themeMediaHandler=null,this.topbarObserver=null}createRenderRoot(){return this}connectedCallback(){super.connectedCallback(),Vh(this)}firstUpdated(){Gh(this)}disconnectedCallback(){Yh(this),super.disconnectedCallback()}updated(e){Qh(this,e)}connect(){kr(this)}handleChatScroll(e){Hl(this,e)}handleLogsScroll(e){zl(this,e)}exportLogs(e,t){ql(e,t)}resetToolStream(){Os(this)}resetChatScroll(){jl(this)}async loadAssistantIdentity(){await $r(this)}applySettings(e){$e(this,e)}setTab(e){od(this,e)}setTheme(e,t){ad(this,e,t)}async loadOverview(){await Oa(this)}async loadCron(){await Zs(this)}async handleAbortChat(){await Ba(this)}removeQueuedMessage(e){md(this,e)}async handleSendChat(e,t){await bd(this,e,t)}async handleWhatsAppStart(e){await Jh(this,e)}async handleWhatsAppWait(){await Zh(this)}async handleWhatsAppLogout(){await Xh(this)}async handleChannelConfigSave(){await eg(this)}async handleChannelConfigReload(){await tg(this)}handleNostrProfileEdit(e,t){sg(this,e,t)}handleNostrProfileCancel(){ig(this)}handleNostrProfileFieldChange(e,t){og(this,e,t)}async handleNostrProfileSave(){await rg(this)}async handleNostrProfileImport(){await lg(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 Mh(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,"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([ta("clawdbot-app")],m); +//# sourceMappingURL=index-DsXRcnEw.js.map diff --git a/dist/control-ui/assets/index-DsXRcnEw.js.map b/dist/control-ui/assets/index-DsXRcnEw.js.map new file mode 100644 index 000000000..a46b0b5de --- /dev/null +++ b/dist/control-ui/assets/index-DsXRcnEw.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index-DsXRcnEw.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/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/icons.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.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","export 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): string {\n switch (tab) {\n case \"chat\":\n return \"💬\";\n case \"overview\":\n return \"📊\";\n case \"channels\":\n return \"🔗\";\n case \"instances\":\n return \"📡\";\n case \"sessions\":\n return \"📄\";\n case \"cron\":\n return \"⏰\";\n case \"skills\":\n return \"⚡️\";\n case \"nodes\":\n return \"🖥️\";\n case \"config\":\n return \"⚙️\";\n case \"debug\":\n return \"🐞\";\n case \"logs\":\n return \"🧾\";\n default:\n return \"📁\";\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","export 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\nconst THINKING_TAG_RE = /<\\s*\\/?\\s*think(?:ing)?\\s*>/gi;\nconst THINKING_OPEN_RE = /<\\s*think(?:ing)?\\s*>/i;\nconst THINKING_CLOSE_RE = /<\\s*\\/\\s*think(?:ing)?\\s*>/i;\n\nexport function stripThinkingTags(value: string): string {\n if (!value) return value;\n const hasOpen = THINKING_OPEN_RE.test(value);\n const hasClose = THINKING_CLOSE_RE.test(value);\n if (!hasOpen && !hasClose) return value;\n // If we don't have a balanced pair, avoid dropping trailing content.\n if (hasOpen !== hasClose) {\n if (!hasOpen) return value.replace(THINKING_CLOSE_RE, \"\").trimStart();\n return value.replace(THINKING_OPEN_RE, \"\").trimStart();\n }\n\n if (!THINKING_TAG_RE.test(value)) return value;\n THINKING_TAG_RE.lastIndex = 0;\n\n let result = \"\";\n let lastIndex = 0;\n let inThinking = false;\n for (const match of value.matchAll(THINKING_TAG_RE)) {\n const idx = match.index ?? 0;\n if (!inThinking) {\n result += value.slice(lastIndex, idx);\n }\n const tag = match[0].toLowerCase();\n inThinking = !tag.includes(\"/\");\n lastIndex = idx + match[0].length;\n }\n if (!inThinking) {\n result += value.slice(lastIndex);\n }\n return result.trimStart();\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 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 }\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\";\n\nexport function renderEmojiIcon(icon: string, className: string): TemplateResult {\n return html`${icon}`;\n}\n\nexport function setEmojiIcon(target: HTMLElement | null, icon: string): void {\n if (!target) return;\n target.textContent = icon;\n}\n","import { html, type TemplateResult } from \"lit\";\nimport { renderEmojiIcon, setEmojiIcon } 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\";\nconst COPY_ICON = \"📋\";\nconst COPIED_ICON = \"✓\";\nconst ERROR_ICON = \"!\";\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 icon = 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 setEmojiIcon(icon, ERROR_ICON);\n\n window.setTimeout(() => {\n if (!btn.isConnected) return;\n delete btn.dataset.error;\n setButtonLabel(btn, idleLabel);\n setEmojiIcon(icon, COPY_ICON);\n }, ERROR_FOR_MS);\n return;\n }\n\n btn.dataset.copied = \"1\";\n setButtonLabel(btn, COPIED_LABEL);\n setEmojiIcon(icon, COPIED_ICON);\n\n window.setTimeout(() => {\n if (!btn.isConnected) return;\n delete btn.dataset.copied;\n setButtonLabel(btn, idleLabel);\n setEmojiIcon(icon, COPY_ICON);\n }, COPIED_FOR_MS);\n }}\n >\n ${renderEmojiIcon(COPY_ICON, \"chat-copy-btn__icon\")}\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\";\n\ntype ToolDisplayActionSpec = {\n label?: string;\n detailKeys?: string[];\n};\n\ntype ToolDisplaySpec = {\n emoji?: 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 emoji: string;\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 ?? { emoji: \"🧩\" };\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 emoji = spec?.emoji ?? FALLBACK.emoji ?? \"🧩\";\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 emoji,\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\n ? `${display.emoji} ${display.label}: ${detail}`\n : `${display.emoji} ${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 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 ${display.emoji}\n ${display.label}\n
    \n ${canClick\n ? html`${hasText ? \"View ›\" : \"›\"}`\n : nothing}\n ${isEmpty && !canClick ? html`` : 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 { 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 {\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 🧹 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 🧹 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 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 ✕\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 ✕\n \n
    \n `,\n )}\n
    \n
    \n `\n : nothing}\n\n
    \n \n
    \n \n 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 {\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
    🔍
    \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 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 const canSaveForm =\n Boolean(props.formValue) && !props.loading && !formUnsafe;\n const canSave =\n props.connected &&\n !props.saving &&\n (props.formMode === \"raw\" ? true : canSaveForm);\n const canApply =\n props.connected &&\n !props.applying &&\n !props.updating &&\n (props.formMode === \"raw\" ? true : canSaveForm);\n const canUpdate = props.connected && !props.applying && !props.updating;\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\n const diff = props.formMode === \"form\" \n ? computeDiff(props.originalValue, props.formValue)\n : [];\n const hasChanges = diff.length > 0;\n\n return html`\n
    \n \n \n \n \n
    \n \n
    \n
    \n ${hasChanges ? html`\n ${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 ? 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 { 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 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 { 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 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 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 [\"whatsapp\", \"telegram\", \"discord\", \"slack\", \"signal\", \"imessage\", \"nostr\"];\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 \"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 { 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 ${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 🧠\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 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 \n \n
    \n
    CLAWDBOT
    \n
    Gateway Dashboard
    \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 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) => (state.configRaw = next),\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.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 host.lastError = `disconnected (${code}): ${reason || \"no reason\"}`;\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() 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","formatMs","ms","formatAgo","diff","sec","min","hr","formatDurationMs","formatList","values","clampText","max","truncateText","toNumber","fallback","THINKING_TAG_RE","THINKING_OPEN_RE","THINKING_CLOSE_RE","stripThinkingTags","hasOpen","hasClose","result","lastIndex","inThinking","match","idx","ENVELOPE_PREFIX","ENVELOPE_CHANNELS","textCache","thinkingCache","looksLikeEnvelopeHeader","header","label","stripEnvelope","text","extractText","message","role","content","item","joined","extractTextCached","obj","extractThinking","cleaned","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","options","updateSkillEdit","skillKey","updateSkillEnabled","saveSkillApiKey","apiKey","installSkill","installId","getSystemTheme","resolveTheme","mode","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","html","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","renderEmojiIcon","icon","className","setEmojiIcon","COPIED_FOR_MS","ERROR_FOR_MS","COPY_LABEL","COPIED_LABEL","ERROR_LABEL","COPY_ICON","COPIED_ICON","ERROR_ICON","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","emoji","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","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","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","icons","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","canSaveForm","canSave","canApply","canUpdate","schemaProps","availableSections","knownKeys","extraSections","allSections","activeSectionSchema","activeSectionMeta","subsections","allowSubnav","isAllSubsection","effectiveSubsection","hasChanges","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","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,24,37,38,39,41,42,43]} \ No newline at end of file diff --git a/dist/control-ui/index.html b/dist/control-ui/index.html new file mode 100644 index 000000000..af79791bc --- /dev/null +++ b/dist/control-ui/index.html @@ -0,0 +1,15 @@ + + + + + + Clawdbot Control + + + + + + + + + diff --git a/docker-compose.yml b/docker-compose.yml index 7970084fc..8b9859f05 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,7 @@ services: [ "node", "dist/index.js", - "gateway-daemon", + "gateway", "--bind", "${CLAWDBOT_GATEWAY_BIND:-lan}", "--port", diff --git a/docs/automation/cron-jobs.md b/docs/automation/cron-jobs.md index 1cfaba9e3..b75fd352e 100644 --- a/docs/automation/cron-jobs.md +++ b/docs/automation/cron-jobs.md @@ -3,9 +3,12 @@ summary: "Cron jobs + wakeups for the Gateway scheduler" read_when: - Scheduling background jobs or wakeups - Wiring automation that should run with or alongside heartbeats + - Deciding between heartbeat and cron for scheduled tasks --- # Cron jobs (Gateway scheduler) +> **Cron vs Heartbeat?** See [Cron vs Heartbeat](/automation/cron-vs-heartbeat) for guidance on when to use each. + Cron is the Gateway’s built-in scheduler. It persists jobs, wakes the agent at the right time, and can optionally deliver output back to a chat. @@ -121,14 +124,19 @@ Resolution priority: ### Delivery (channel + target) Isolated jobs can deliver output to a channel. The job payload can specify: -- `channel`: `whatsapp` / `telegram` / `discord` / `slack` / `signal` / `imessage` / `last` +- `channel`: `whatsapp` / `telegram` / `discord` / `slack` / `mattermost` (plugin) / `signal` / `imessage` / `last` - `to`: channel-specific recipient target If `channel` or `to` is omitted, cron can fall back to the main session’s “last route” (the last place the agent replied). +Delivery notes: +- If `to` is set, cron auto-delivers the agent’s final output even if `deliver` is omitted. +- Use `deliver: true` when you want last-route delivery without an explicit `to`. +- Use `deliver: false` to keep output internal even if a `to` is present. + Target format reminders: -- Slack/Discord targets should use explicit prefixes (e.g. `channel:`, `user:`) to avoid ambiguity. +- Slack/Discord/Mattermost (plugin) targets should use explicit prefixes (e.g. `channel:`, `user:`) to avoid ambiguity. - Telegram topics should use the `:topic:` form (see below). #### Telegram delivery targets (topics / forum threads) @@ -255,15 +263,15 @@ Run history: clawdbot cron runs --id --limit 50 ``` -Immediate wake without creating a job: +Immediate system event without creating a job: ```bash -clawdbot wake --mode now --text "Next heartbeat: check battery." +clawdbot system event --mode now --text "Next heartbeat: check battery." ``` ## Gateway API surface - `cron.list`, `cron.status`, `cron.add`, `cron.update`, `cron.remove` - `cron.run` (force or due), `cron.runs` -- `wake` (enqueue system event + optional heartbeat) +For immediate system events without a job, use [`clawdbot system event`](/cli/system). ## Troubleshooting diff --git a/docs/automation/cron-vs-heartbeat.md b/docs/automation/cron-vs-heartbeat.md new file mode 100644 index 000000000..333a45d0b --- /dev/null +++ b/docs/automation/cron-vs-heartbeat.md @@ -0,0 +1,274 @@ +--- +summary: "Guidance for choosing between heartbeat and cron jobs for automation" +read_when: + - Deciding how to schedule recurring tasks + - Setting up background monitoring or notifications + - Optimizing token usage for periodic checks +--- +# Cron vs Heartbeat: When to Use Each + +Both heartbeats and cron jobs let you run tasks on a schedule. This guide helps you choose the right mechanism for your use case. + +## Quick Decision Guide + +| Use Case | Recommended | Why | +|----------|-------------|-----| +| Check inbox every 30 min | Heartbeat | Batches with other checks, context-aware | +| Send daily report at 9am sharp | Cron (isolated) | Exact timing needed | +| Monitor calendar for upcoming events | Heartbeat | Natural fit for periodic awareness | +| Run weekly deep analysis | Cron (isolated) | Standalone task, can use different model | +| Remind me in 20 minutes | Cron (main, `--at`) | One-shot with precise timing | +| Background project health check | Heartbeat | Piggybacks on existing cycle | + +## Heartbeat: Periodic Awareness + +Heartbeats run in the **main session** at a regular interval (default: 30 min). They're designed for the agent to check on things and surface anything important. + +### When to use heartbeat + +- **Multiple periodic checks**: Instead of 5 separate cron jobs checking inbox, calendar, weather, notifications, and project status, a single heartbeat can batch all of these. +- **Context-aware decisions**: The agent has full main-session context, so it can make smart decisions about what's urgent vs. what can wait. +- **Conversational continuity**: Heartbeat runs share the same session, so the agent remembers recent conversations and can follow up naturally. +- **Low-overhead monitoring**: One heartbeat replaces many small polling tasks. + +### Heartbeat advantages + +- **Batches multiple checks**: One agent turn can review inbox, calendar, and notifications together. +- **Reduces API calls**: A single heartbeat is cheaper than 5 isolated cron jobs. +- **Context-aware**: The agent knows what you've been working on and can prioritize accordingly. +- **Smart suppression**: If nothing needs attention, the agent replies `HEARTBEAT_OK` and no message is delivered. +- **Natural timing**: Drifts slightly based on queue load, which is fine for most monitoring. + +### Heartbeat example: HEARTBEAT.md checklist + +```md +# Heartbeat checklist + +- Check email for urgent messages +- Review calendar for events in next 2 hours +- If a background task finished, summarize results +- If idle for 8+ hours, send a brief check-in +``` + +The agent reads this on each heartbeat and handles all items in one turn. + +### Configuring heartbeat + +```json5 +{ + agents: { + defaults: { + heartbeat: { + every: "30m", // interval + target: "last", // where to deliver alerts + activeHours: { start: "08:00", end: "22:00" } // optional + } + } + } +} +``` + +See [Heartbeat](/gateway/heartbeat) for full configuration. + +## Cron: Precise Scheduling + +Cron jobs run at **exact times** and can run in isolated sessions without affecting main context. + +### When to use cron + +- **Exact timing required**: "Send this at 9:00 AM every Monday" (not "sometime around 9"). +- **Standalone tasks**: Tasks that don't need conversational context. +- **Different model/thinking**: Heavy analysis that warrants a more powerful model. +- **One-shot reminders**: "Remind me in 20 minutes" with `--at`. +- **Noisy/frequent tasks**: Tasks that would clutter main session history. +- **External triggers**: Tasks that should run independently of whether the agent is otherwise active. + +### Cron advantages + +- **Exact timing**: 5-field cron expressions with timezone support. +- **Session isolation**: Runs in `cron:` without polluting main history. +- **Model overrides**: Use a cheaper or more powerful model per job. +- **Delivery control**: Can deliver directly to a channel; still posts a summary to main by default (configurable). +- **No agent context needed**: Runs even if main session is idle or compacted. +- **One-shot support**: `--at` for precise future timestamps. + +### Cron example: Daily morning briefing + +```bash +clawdbot cron add \ + --name "Morning briefing" \ + --cron "0 7 * * *" \ + --tz "America/New_York" \ + --session isolated \ + --message "Generate today's briefing: weather, calendar, top emails, news summary." \ + --model opus \ + --deliver \ + --channel whatsapp \ + --to "+15551234567" +``` + +This runs at exactly 7:00 AM New York time, uses Opus for quality, and delivers directly to WhatsApp. + +### Cron example: One-shot reminder + +```bash +clawdbot cron add \ + --name "Meeting reminder" \ + --at "20m" \ + --session main \ + --system-event "Reminder: standup meeting starts in 10 minutes." \ + --wake now \ + --delete-after-run +``` + +See [Cron jobs](/automation/cron-jobs) for full CLI reference. + +## Decision Flowchart + +``` +Does the task need to run at an EXACT time? + YES -> Use cron + NO -> Continue... + +Does the task need isolation from main session? + YES -> Use cron (isolated) + NO -> Continue... + +Can this task be batched with other periodic checks? + YES -> Use heartbeat (add to HEARTBEAT.md) + NO -> Use cron + +Is this a one-shot reminder? + YES -> Use cron with --at + NO -> Continue... + +Does it need a different model or thinking level? + YES -> Use cron (isolated) with --model/--thinking + NO -> Use heartbeat +``` + +## Combining Both + +The most efficient setup uses **both**: + +1. **Heartbeat** handles routine monitoring (inbox, calendar, notifications) in one batched turn every 30 minutes. +2. **Cron** handles precise schedules (daily reports, weekly reviews) and one-shot reminders. + +### Example: Efficient automation setup + +**HEARTBEAT.md** (checked every 30 min): +```md +# Heartbeat checklist +- Scan inbox for urgent emails +- Check calendar for events in next 2h +- Review any pending tasks +- Light check-in if quiet for 8+ hours +``` + +**Cron jobs** (precise timing): +```bash +# Daily morning briefing at 7am +clawdbot cron add --name "Morning brief" --cron "0 7 * * *" --session isolated --message "..." --deliver + +# Weekly project review on Mondays at 9am +clawdbot cron add --name "Weekly review" --cron "0 9 * * 1" --session isolated --message "..." --model opus + +# One-shot reminder +clawdbot cron add --name "Call back" --at "2h" --session main --system-event "Call back the client" --wake now +``` + + +## Lobster: Deterministic workflows with approvals + +Lobster is the workflow runtime for **multi-step tool pipelines** that need deterministic execution and explicit approvals. +Use it when the task is more than a single agent turn, and you want a resumable workflow with human checkpoints. + +### When Lobster fits + +- **Multi-step automation**: You need a fixed pipeline of tool calls, not a one-off prompt. +- **Approval gates**: Side effects should pause until you approve, then resume. +- **Resumable runs**: Continue a paused workflow without re-running earlier steps. + +### How it pairs with heartbeat and cron + +- **Heartbeat/cron** decide *when* a run happens. +- **Lobster** defines *what steps* happen once the run starts. + +For scheduled workflows, use cron or heartbeat to trigger an agent turn that calls Lobster. +For ad-hoc workflows, call Lobster directly. + +### Operational notes (from the code) + +- 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`. +- If you pass `lobsterPath`, it must be an **absolute path**. + +See [Lobster](/tools/lobster) for full usage and examples. + +## Main Session vs Isolated Session + +Both heartbeat and cron can interact with the main session, but differently: + +| | Heartbeat | Cron (main) | Cron (isolated) | +|---|---|---|---| +| Session | Main | Main (via system event) | `cron:` | +| History | Shared | Shared | Fresh each run | +| Context | Full | Full | None (starts clean) | +| Model | Main session model | Main session model | Can override | +| Output | Delivered if not `HEARTBEAT_OK` | Heartbeat prompt + event | Summary posted to main | + +### When to use main session cron + +Use `--session main` with `--system-event` when you want: +- The reminder/event to appear in main session context +- The agent to handle it during the next heartbeat with full context +- No separate isolated run + +```bash +clawdbot cron add \ + --name "Check project" \ + --every "4h" \ + --session main \ + --system-event "Time for a project health check" \ + --wake now +``` + +### When to use isolated cron + +Use `--session isolated` when you want: +- A clean slate without prior context +- Different model or thinking settings +- Output delivered directly to a channel (summary still posts to main by default) +- History that doesn't clutter main session + +```bash +clawdbot cron add \ + --name "Deep analysis" \ + --cron "0 6 * * 0" \ + --session isolated \ + --message "Weekly codebase analysis..." \ + --model opus \ + --thinking high \ + --deliver +``` + +## Cost Considerations + +| Mechanism | Cost Profile | +|-----------|--------------| +| Heartbeat | One turn every N minutes; scales with HEARTBEAT.md size | +| Cron (main) | Adds event to next heartbeat (no isolated turn) | +| Cron (isolated) | Full agent turn per job; can use cheaper model | + +**Tips**: +- Keep `HEARTBEAT.md` small to minimize token overhead. +- Batch similar checks into heartbeat instead of multiple cron jobs. +- Use `target: "none"` on heartbeat if you only want internal processing. +- Use isolated cron with a cheaper model for routine tasks. + +## Related + +- [Heartbeat](/gateway/heartbeat) - full heartbeat configuration +- [Cron jobs](/automation/cron-jobs) - full cron CLI and API reference +- [System](/cli/system) - system events + heartbeat controls diff --git a/docs/automation/webhook.md b/docs/automation/webhook.md index f2a62b4e3..0828483d2 100644 --- a/docs/automation/webhook.md +++ b/docs/automation/webhook.md @@ -71,8 +71,8 @@ Payload: - `sessionKey` optional (string): The key used to identify the agent's session. Defaults to a random `hook:`. Using a consistent key allows for a multi-turn conversation within the hook context. - `wakeMode` optional (`now` | `next-heartbeat`): Whether to trigger an immediate heartbeat (default `now`) or wait for the next periodic check. - `deliver` optional (boolean): If `true`, the agent's response will be sent to the messaging channel. Defaults to `true`. Responses that are only heartbeat acknowledgments are automatically skipped. -- `channel` optional (string): The messaging channel for delivery. One of: `last`, `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`, `msteams`. Defaults to `last`. -- `to` optional (string): The recipient identifier for the channel (e.g., phone number for WhatsApp/Signal, chat ID for Telegram, channel ID for Discord/Slack, conversation ID for MS Teams). Defaults to the last recipient in the main session. +- `channel` optional (string): The messaging channel for delivery. One of: `last`, `whatsapp`, `telegram`, `discord`, `slack`, `mattermost` (plugin), `signal`, `imessage`, `msteams`. Defaults to `last`. +- `to` optional (string): The recipient identifier for the channel (e.g., phone number for WhatsApp/Signal, chat ID for Telegram, channel ID for Discord/Slack/Mattermost (plugin), conversation ID for MS Teams). Defaults to the last recipient in the main session. - `model` optional (string): Model override (e.g., `anthropic/claude-3-5-sonnet` or an alias). Must be in the allowed model list if restricted. - `thinking` optional (string): Thinking level override (e.g., `low`, `medium`, `high`). - `timeoutSeconds` optional (number): Maximum duration for the agent run in seconds. diff --git a/docs/bedrock.md b/docs/bedrock.md index abdd3e1ab..9da196f96 100644 --- a/docs/bedrock.md +++ b/docs/bedrock.md @@ -17,6 +17,37 @@ not an API key. - Auth: AWS credentials (env vars, shared config, or instance role) - Region: `AWS_REGION` or `AWS_DEFAULT_REGION` (default: `us-east-1`) +## Automatic model discovery + +If AWS credentials are detected, Clawdbot can automatically discover Bedrock +models that support **streaming** and **text output**. Discovery uses +`bedrock:ListFoundationModels` and is cached (default: 1 hour). + +Config options live under `models.bedrockDiscovery`: + +```json5 +{ + models: { + bedrockDiscovery: { + enabled: true, + region: "us-east-1", + providerFilter: ["anthropic", "amazon"], + refreshInterval: 3600, + defaultContextWindow: 32000, + defaultMaxTokens: 4096 + } + } +} +``` + +Notes: +- `enabled` defaults to `true` when AWS credentials are present. +- `region` defaults to `AWS_REGION` or `AWS_DEFAULT_REGION`, then `us-east-1`. +- `providerFilter` matches Bedrock provider names (for example `anthropic`). +- `refreshInterval` is seconds; set to `0` to disable caching. +- `defaultContextWindow` (default: `32000`) and `defaultMaxTokens` (default: `4096`) + are used for discovered models (override if you know your model limits). + ## Setup (manual) 1) Ensure AWS credentials are available on the **gateway host**: @@ -28,9 +59,11 @@ export AWS_REGION="us-east-1" # Optional: export AWS_SESSION_TOKEN="..." export AWS_PROFILE="your-profile" +# Optional (Bedrock API key/bearer token): +export AWS_BEARER_TOKEN_BEDROCK="..." ``` -2) Add a Bedrock provider and model to your config: +2) Add a Bedrock provider and model to your config (no `apiKey` required): ```json5 { @@ -39,6 +72,7 @@ export AWS_PROFILE="your-profile" "amazon-bedrock": { baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", api: "bedrock-converse-stream", + auth: "aws-sdk", models: [ { id: "anthropic.claude-3-7-sonnet-20250219-v1:0", @@ -64,7 +98,11 @@ export AWS_PROFILE="your-profile" ## Notes - Bedrock requires **model access** enabled in your AWS account/region. +- Automatic discovery needs the `bedrock:ListFoundationModels` permission. - If you use profiles, set `AWS_PROFILE` on the gateway host. +- Clawdbot surfaces the credential source in this order: `AWS_BEARER_TOKEN_BEDROCK`, + then `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY`, then `AWS_PROFILE`, then the + default AWS SDK chain. - Reasoning support depends on the model; check the Bedrock model card for current capabilities. - If you prefer a managed key flow, you can also place an OpenAI‑compatible diff --git a/docs/channels/bluebubbles.md b/docs/channels/bluebubbles.md index a2b96dbe3..cf5faee1d 100644 --- a/docs/channels/bluebubbles.md +++ b/docs/channels/bluebubbles.md @@ -1,55 +1,217 @@ --- -summary: "iMessage via BlueBubbles macOS server (REST send/receive, typing, reactions, pairing)." +summary: "iMessage via BlueBubbles macOS server (REST send/receive, typing, reactions, pairing, advanced actions)." read_when: - Setting up BlueBubbles channel - Troubleshooting webhook pairing + - Configuring iMessage on macOS --- # BlueBubbles (macOS REST) -Status: bundled plugin (disabled by default) that talks to the BlueBubbles macOS server over HTTP. +Status: bundled plugin that talks to the BlueBubbles macOS server over HTTP. **Recommended for iMessage integration** due to its richer API and easier setup compared to the legacy imsg channel. ## Overview -- Runs on macOS via the BlueBubbles helper app (`https://bluebubbles.app`). +- Runs on macOS via the BlueBubbles helper app ([bluebubbles.app](https://bluebubbles.app)). +- Recommended/tested: macOS Sequoia (15). macOS Tahoe (26) works; edit is currently broken on Tahoe, and group icon updates may report success but not sync. - Clawdbot talks to it through its REST API (`GET /api/v1/ping`, `POST /message/text`, `POST /chat/:id/*`). - Incoming messages arrive via webhooks; outgoing replies, typing indicators, read receipts, and tapbacks are REST calls. - Attachments and stickers are ingested as inbound media (and surfaced to the agent when possible). - Pairing/allowlist works the same way as other channels (`/start/pairing` etc) with `channels.bluebubbles.allowFrom` + pairing codes. -- Reactions are surfaced as system events just like Slack/Telegram so agents can “mention” them before replying. +- Reactions are surfaced as system events just like Slack/Telegram so agents can "mention" them before replying. +- Advanced features: edit, unsend, reply threading, message effects, group management. ## Quick start -1. Install the BlueBubbles server on your Mac (follows the app store instructions at `https://bluebubbles.app/install`). -2. In the BlueBubbles config, enable the web API and set a password for `guid`/`password`. -3. Configure Clawdbot: +1. Install the BlueBubbles server on your Mac (follow the instructions at [bluebubbles.app/install](https://bluebubbles.app/install)). +2. In the BlueBubbles config, enable the web API and set a password. +3. Run `clawdbot onboard` and select BlueBubbles, or configure manually: ```json5 { channels: { bluebubbles: { enabled: true, - serverUrl: "http://bluebubbles-host:1234", + serverUrl: "http://192.168.1.100:1234", password: "example-password", - webhookPath: "/bluebubbles-webhook", - actions: { reactions: true } + webhookPath: "/bluebubbles-webhook" } } } ``` -4. Point BlueBubbles webhooks to your gateway (example: `http://your-gateway-host/bluebubbles-webhook?password=`). +4. Point BlueBubbles webhooks to your gateway (example: `https://your-gateway-host:3000/bluebubbles-webhook?password=`). 5. Start the gateway; it will register the webhook handler and start pairing. -## Configuration notes -- `channels.bluebubbles.serverUrl`: base URL of the BlueBubbles REST API. -- `channels.bluebubbles.password`: password that BlueBubbles expects on every request (`?password=...` or header). -- `channels.bluebubbles.webhookPath`: HTTP path the gateway exposes for BlueBubbles webhooks. -- `channels.bluebubbles.dmPolicy` / `groupPolicy` + `allowFrom`/`groupAllowFrom` behave like other channels; pairing/allowlist info is stored in `/pairing`. -- `channels.bluebubbles.actions.reactions` toggles whether the gateway enqueues system events for reactions/tapbacks. -- `channels.bluebubbles.textChunkLimit` overrides the default 4k limit. -- `channels.bluebubbles.mediaMaxMb` controls the max size of inbound attachments saved for analysis (default 8MB). +## Onboarding +BlueBubbles is available in the interactive setup wizard: +``` +clawdbot onboard +``` -## How it works -- Outbound replies: `sendMessageBlueBubbles` resolves a chat GUID via `/api/v1/chat/query` and posts to `/api/v1/message/text`. Typing (`/api/v1/chat//typing`) and read receipts (`/api/v1/chat//read`) are sent before/after responses. -- Webhooks: BlueBubbles POSTs JSON payloads with `type` and `data`. The plugin ignores non-message events (typing indicator, read status) and extracts `chatGuid` from `data.chats[0].guid`. -- Reactions/tapbacks generate `BlueBubbles reaction added/removed` system events so agents can mention them. Agents can also trigger tapbacks via the `react` action with `messageId`, `emoji`, and a `to`/`chatGuid`. -- Attachments are downloaded via the REST API and stored in the inbound media cache; text-less messages are converted into `` placeholders so the agent knows something was sent. +The wizard prompts for: +- **Server URL** (required): BlueBubbles server address (e.g., `http://192.168.1.100:1234`) +- **Password** (required): API password from BlueBubbles Server settings +- **Webhook path** (optional): Defaults to `/bluebubbles-webhook` +- **DM policy**: pairing, allowlist, open, or disabled +- **Allow list**: Phone numbers, emails, or chat targets + +You can also add BlueBubbles via CLI: +``` +clawdbot channels add bluebubbles --http-url http://192.168.1.100:1234 --password +``` + +## Access control (DMs + groups) +DMs: +- Default: `channels.bluebubbles.dmPolicy = "pairing"`. +- Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour). +- Approve via: + - `clawdbot pairing list bluebubbles` + - `clawdbot pairing approve bluebubbles ` +- Pairing is the default token exchange. Details: [Pairing](/start/pairing) + +Groups: +- `channels.bluebubbles.groupPolicy = open | allowlist | disabled` (default: `allowlist`). +- `channels.bluebubbles.groupAllowFrom` controls who can trigger in groups when `allowlist` is set. + +### Mention gating (groups) +BlueBubbles supports mention gating for group chats, matching iMessage/WhatsApp behavior: +- Uses `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) to detect mentions. +- When `requireMention` is enabled for a group, the agent only responds when mentioned. +- Control commands from authorized senders bypass mention gating. + +Per-group configuration: +```json5 +{ + channels: { + bluebubbles: { + groupPolicy: "allowlist", + groupAllowFrom: ["+15555550123"], + groups: { + "*": { requireMention: true }, // default for all groups + "iMessage;-;chat123": { requireMention: false } // override for specific group + } + } + } +} +``` + +### Command gating +- Control commands (e.g., `/config`, `/model`) require authorization. +- Uses `allowFrom` and `groupAllowFrom` to determine command authorization. +- Authorized senders can run control commands even without mentioning in groups. + +## Typing + read receipts +- **Typing indicators**: Sent automatically before and during response generation. +- **Read receipts**: Controlled by `channels.bluebubbles.sendReadReceipts` (default: `true`). +- **Typing indicators**: Clawdbot sends typing start events; BlueBubbles clears typing automatically on send or timeout (manual stop via DELETE is unreliable). + +```json5 +{ + channels: { + bluebubbles: { + sendReadReceipts: false // disable read receipts + } + } +} +``` + +## Advanced actions +BlueBubbles supports advanced message actions when enabled in config: + +```json5 +{ + channels: { + bluebubbles: { + actions: { + reactions: true, // tapbacks (default: true) + edit: true, // edit sent messages (macOS 13+, broken on macOS 26 Tahoe) + unsend: true, // unsend messages (macOS 13+) + reply: true, // reply threading by message GUID + sendWithEffect: true, // message effects (slam, loud, etc.) + renameGroup: true, // rename group chats + setGroupIcon: true, // set group chat icon/photo (flaky on macOS 26 Tahoe) + addParticipant: true, // add participants to groups + removeParticipant: true, // remove participants from groups + leaveGroup: true, // leave group chats + sendAttachment: true // send attachments/media + } + } + } +} +``` + +Available actions: +- **react**: Add/remove tapback reactions (`messageId`, `emoji`, `remove`) +- **edit**: Edit a sent message (`messageId`, `text`) +- **unsend**: Unsend a message (`messageId`) +- **reply**: Reply to a specific message (`messageId`, `text`, `to`) +- **sendWithEffect**: Send with iMessage effect (`text`, `to`, `effectId`) +- **renameGroup**: Rename a group chat (`chatGuid`, `displayName`) +- **setGroupIcon**: Set a group chat's icon/photo (`chatGuid`, `media`) — flaky on macOS 26 Tahoe (API may return success but the icon does not sync). +- **addParticipant**: Add someone to a group (`chatGuid`, `address`) +- **removeParticipant**: Remove someone from a group (`chatGuid`, `address`) +- **leaveGroup**: Leave a group chat (`chatGuid`) +- **sendAttachment**: Send media/files (`to`, `buffer`, `filename`, `asVoice`) + - Voice memos: set `asVoice: true` with **MP3** or **CAF** audio to send as an iMessage voice message. BlueBubbles converts MP3 → CAF when sending voice memos. + +### Message IDs (short vs full) +Clawdbot may surface *short* message IDs (e.g., `1`, `2`) to save tokens. +- `MessageSid` / `ReplyToId` can be short IDs. +- `MessageSidFull` / `ReplyToIdFull` contain the provider full IDs. +- Short IDs are in-memory; they can expire on restart or cache eviction. +- Actions accept short or full `messageId`, but short IDs will error if no longer available. + +Use full IDs for durable automations and storage: +- Templates: `{{MessageSidFull}}`, `{{ReplyToIdFull}}` +- Context: `MessageSidFull` / `ReplyToIdFull` in inbound payloads + +See [Configuration](/gateway/configuration) for template variables. + +## Block streaming +Control whether responses are sent as a single message or streamed in blocks: +```json5 +{ + channels: { + bluebubbles: { + blockStreaming: true // enable block streaming (default behavior) + } + } +} +``` + +## Media + limits +- Inbound attachments are downloaded and stored in the media cache. +- Media cap via `channels.bluebubbles.mediaMaxMb` (default: 8 MB). +- Outbound text is chunked to `channels.bluebubbles.textChunkLimit` (default: 4000 chars). + +## Configuration reference +Full configuration: [Configuration](/gateway/configuration) + +Provider options: +- `channels.bluebubbles.enabled`: Enable/disable the channel. +- `channels.bluebubbles.serverUrl`: BlueBubbles REST API base URL. +- `channels.bluebubbles.password`: API password. +- `channels.bluebubbles.webhookPath`: Webhook endpoint path (default: `/bluebubbles-webhook`). +- `channels.bluebubbles.dmPolicy`: `pairing | allowlist | open | disabled` (default: `pairing`). +- `channels.bluebubbles.allowFrom`: DM allowlist (handles, emails, E.164 numbers, `chat_id:*`, `chat_guid:*`). +- `channels.bluebubbles.groupPolicy`: `open | allowlist | disabled` (default: `allowlist`). +- `channels.bluebubbles.groupAllowFrom`: Group sender allowlist. +- `channels.bluebubbles.groups`: Per-group config (`requireMention`, etc.). +- `channels.bluebubbles.sendReadReceipts`: Send read receipts (default: `true`). +- `channels.bluebubbles.blockStreaming`: Enable block streaming (default: `true`). +- `channels.bluebubbles.textChunkLimit`: Outbound chunk size in chars (default: 4000). +- `channels.bluebubbles.mediaMaxMb`: Inbound media cap in MB (default: 8). +- `channels.bluebubbles.historyLimit`: Max group messages for context (0 disables). +- `channels.bluebubbles.dmHistoryLimit`: DM history limit. +- `channels.bluebubbles.actions`: Enable/disable specific actions. +- `channels.bluebubbles.accounts`: Multi-account configuration. + +Related global options: +- `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`). +- `messages.responsePrefix`. + +## Addressing / delivery targets +Prefer `chat_guid` for stable routing: +- `chat_guid:iMessage;-;+15555550123` (preferred for groups) +- `chat_id:123` +- `chat_identifier:...` +- Direct handles: `+15555550123`, `user@example.com` ## Security - Webhook requests are authenticated by comparing `guid`/`password` query params or headers against `channels.bluebubbles.password`. Requests from `localhost` are also accepted. @@ -57,8 +219,12 @@ Status: bundled plugin (disabled by default) that talks to the BlueBubbles macOS - Enable HTTPS + firewall rules on the BlueBubbles server if exposing it outside your LAN. ## Troubleshooting -- If Voice/typing events stop working, check the BlueBubbles webhook logs and verify the gateway path matches `channels.bluebubbles.webhookPath`. +- If typing/read events stop working, check the BlueBubbles webhook logs and verify the gateway path matches `channels.bluebubbles.webhookPath`. - Pairing codes expire after one hour; use `clawdbot pairing list bluebubbles` and `clawdbot pairing approve bluebubbles `. - Reactions require the BlueBubbles private API (`POST /api/v1/message/react`); ensure the server version exposes it. +- Edit/unsend require macOS 13+ and a compatible BlueBubbles server version. On macOS 26 (Tahoe), edit is currently broken due to private API changes. +- Group icon updates can be flaky on macOS 26 (Tahoe): the API may return success but the new icon does not sync. +- Clawdbot auto-hides known-broken actions based on the BlueBubbles server's macOS version. If edit still appears on macOS 26 (Tahoe), disable it manually with `channels.bluebubbles.actions.edit=false`. +- For status/health info: `clawdbot status --all` or `clawdbot status --deep`. -For general channel workflow reference, see [/channels/index] and the [[plugins|/plugin]] guide. +For general channel workflow reference, see [Channels](/channels) and the [Plugins](/plugins) guide. diff --git a/docs/channels/discord.md b/docs/channels/discord.md index a16b6ed04..ca6ff6c9c 100644 --- a/docs/channels/discord.md +++ b/docs/channels/discord.md @@ -175,6 +175,7 @@ Notes: - `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) also count as mentions for guild messages. - Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`. - If `channels` is present, any channel not listed is denied by default. +- Use a `"*"` channel entry to apply defaults across all channels; explicit channel entries override the wildcard. - Threads inherit parent channel config (allowlist, `requireMention`, skills, prompts, etc.) unless you add the thread channel id explicitly. - Bot-authored messages are ignored by default; set `channels.discord.allowBots=true` to allow them (own messages remain filtered). - Warning: If you allow replies to other bots (`channels.discord.allowBots=true`), prevent bot-to-bot reply loops with `requireMention`, `channels.discord.guilds.*.channels..users` allowlists, and/or clear guardrails in `AGENTS.md` and `SOUL.md`. @@ -287,7 +288,7 @@ ack reaction after the bot replies. - `dm.enabled`: set `false` to ignore all DMs (default `true`). - `dm.policy`: DM access control (`pairing` recommended). `"open"` requires `dm.allowFrom=["*"]`. -- `dm.allowFrom`: DM allowlist (user ids or names). Used by `dm.policy="allowlist"` and for `dm.policy="open"` validation. +- `dm.allowFrom`: DM allowlist (user ids or names). Used by `dm.policy="allowlist"` and for `dm.policy="open"` validation. The wizard accepts usernames and resolves them to ids when the bot can search members. - `dm.groupEnabled`: enable group DMs (default `false`). - `dm.groupChannels`: optional allowlist for group DM channel ids or slugs. - `groupPolicy`: controls guild channel handling (`open|disabled|allowlist`); `allowlist` requires channel allowlists. diff --git a/docs/channels/imessage.md b/docs/channels/imessage.md index 66cf51670..83b54cf5a 100644 --- a/docs/channels/imessage.md +++ b/docs/channels/imessage.md @@ -244,7 +244,7 @@ Provider options: - `channels.imessage.service`: `imessage | sms | auto`. - `channels.imessage.region`: SMS region. - `channels.imessage.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing). -- `channels.imessage.allowFrom`: DM allowlist (handles or `chat_id:*`). `open` requires `"*"`. +- `channels.imessage.allowFrom`: DM allowlist (handles, emails, E.164 numbers, or `chat_id:*`). `open` requires `"*"`. iMessage has no usernames; use handles or chat targets. - `channels.imessage.groupPolicy`: `open | allowlist | disabled` (default: allowlist). - `channels.imessage.groupAllowFrom`: group sender allowlist. - `channels.imessage.historyLimit` / `channels.imessage.accounts.*.historyLimit`: max group messages to include as context (0 disables). diff --git a/docs/channels/index.md b/docs/channels/index.md index d294cb03c..f8fd860c3 100644 --- a/docs/channels/index.md +++ b/docs/channels/index.md @@ -15,11 +15,15 @@ Text is supported everywhere; media and reactions vary by channel. - [Telegram](/channels/telegram) — Bot API via grammY; supports groups. - [Discord](/channels/discord) — Discord Bot API + Gateway; supports servers, channels, and DMs. - [Slack](/channels/slack) — Bolt SDK; workspace apps. +- [Mattermost](/channels/mattermost) — Bot API + WebSocket; channels, groups, DMs (plugin, installed separately). - [Signal](/channels/signal) — signal-cli; privacy-focused. -- [iMessage](/channels/imessage) — macOS only; native integration. -- [BlueBubbles](/channels/bluebubbles) — iMessage via BlueBubbles macOS server (bundled plugin, disabled by default). +- [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). +- [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). - [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/location.md b/docs/channels/location.md index f38031fb6..c3742c63a 100644 --- a/docs/channels/location.md +++ b/docs/channels/location.md @@ -14,6 +14,7 @@ Clawdbot normalizes shared locations from chat channels into: Currently supported: - **Telegram** (location pins + venues + live locations) - **WhatsApp** (locationMessage + liveLocationMessage) +- **Matrix** (`m.location` with `geo_uri`) ## Text formatting Locations are rendered as friendly lines without brackets: @@ -44,3 +45,4 @@ When a location is present, these fields are added to `ctx`: ## Channel notes - **Telegram**: venues map to `LocationName/LocationAddress`; live locations use `live_period`. - **WhatsApp**: `locationMessage.comment` and `liveLocationMessage.caption` are appended as the caption line. +- **Matrix**: `geo_uri` is parsed as a pin location; altitude is ignored and `LocationIsLive` is always false. diff --git a/docs/channels/matrix.md b/docs/channels/matrix.md index d0b632cb6..cafdacdf1 100644 --- a/docs/channels/matrix.md +++ b/docs/channels/matrix.md @@ -5,17 +5,26 @@ read_when: --- # Matrix (plugin) -Status: supported via plugin (matrix-js-sdk). Direct messages, rooms, threads, media, reactions, and polls. +Matrix is an open, decentralized messaging protocol. Clawdbot connects as a Matrix **user** +on any homeserver, so you need a Matrix account for the bot. Once it is logged in, you can DM +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, +polls (send + poll-start as text), location, and E2EE (with crypto support). ## Plugin required + Matrix ships as a plugin and is not bundled with the core install. Install via CLI (npm registry): + ```bash clawdbot plugins install @clawdbot/matrix ``` Local checkout (when running from a git repo): + ```bash clawdbot plugins install ./extensions/matrix ``` @@ -25,27 +34,54 @@ Clawdbot will offer the local install path automatically. Details: [Plugins](/plugin) -## Quick setup (beginner) +## Setup + 1) Install the Matrix plugin: - From npm: `clawdbot plugins install @clawdbot/matrix` - From a local checkout: `clawdbot plugins install ./extensions/matrix` -2) Configure credentials: - - Env: `MATRIX_HOMESERVER`, `MATRIX_USER_ID`, `MATRIX_ACCESS_TOKEN` (or `MATRIX_PASSWORD`) +2) Create a Matrix account on a homeserver: + - Browse hosting options at [https://matrix.org/ecosystem/hosting/](https://matrix.org/ecosystem/hosting/) + - Or host it yourself. +3) Get an access token for the bot account: + - Use the Matrix login API with `curl` at your home server: + + ```bash + curl --request POST \ + --url https://matrix.example.org/_matrix/client/v3/login \ + --header 'Content-Type: application/json' \ + --data '{ + "type": "m.login.password", + "identifier": { + "type": "m.id.user", + "user": "your-user-name" + }, + "password": "your-password" + }' + ``` + + - Replace `matrix.example.org` with your homeserver URL. + - Or set `channels.matrix.userId` + `channels.matrix.password`: Clawdbot calls the same + login endpoint, stores the access token in `~/.clawdbot/credentials/matrix/credentials.json`, + and reuses it on next start. +4) Configure credentials: + - Env: `MATRIX_HOMESERVER`, `MATRIX_ACCESS_TOKEN` (or `MATRIX_USER_ID` + `MATRIX_PASSWORD`) - Or config: `channels.matrix.*` - If both are set, config takes precedence. -3) Restart the gateway (or finish onboarding). -4) DM access defaults to pairing; approve the pairing code on first contact. + - With access token: user ID is fetched automatically via `/whoami`. + - When set, `channels.matrix.userId` should be the full Matrix ID (example: `@bot:example.org`). +5) Restart the gateway (or finish onboarding). +6) Start a DM with the bot or invite it to a room from any Matrix client + (Element, Beeper, etc.; see https://matrix.org/ecosystem/clients/). Beeper requires E2EE, + so set `channels.matrix.encryption: true` and verify the device. -Runtime note: Matrix requires Node.js (Bun is not supported). +Minimal config (access token, user ID auto-fetched): -Minimal config: ```json5 { channels: { matrix: { enabled: true, homeserver: "https://matrix.example.org", - userId: "@clawdbot:example.org", accessToken: "syt_***", dm: { policy: "pairing" } } @@ -53,78 +89,139 @@ Minimal config: } ``` -## Encryption (E2EE) -End-to-end encrypted rooms are **not** supported. -- Use unencrypted rooms or disable encryption when creating the room. -- If a room is E2EE, the bot will receive encrypted events and won’t reply. +E2EE config (end to end encryption enabled): -## What it is -Matrix is an open messaging protocol. Clawdbot connects as a Matrix user and listens to DMs and rooms. -- A Matrix user account owned by the Gateway. -- Deterministic routing: replies go back to Matrix. +```json5 +{ + channels: { + matrix: { + enabled: true, + homeserver: "https://matrix.example.org", + accessToken: "syt_***", + encryption: true, + dm: { policy: "pairing" } + } + } +} +``` + +## Encryption (E2EE) + +End-to-end encryption is **supported** via the Rust crypto SDK. + +Enable with `channels.matrix.encryption: true`: + +- If the crypto module loads, encrypted rooms are decrypted automatically. +- Outbound media is encrypted when sending to encrypted rooms. +- On first connection, Clawdbot requests device verification from your other sessions. +- Verify the device in another Matrix client (Element, etc.) to enable key sharing. +- If the crypto module cannot be loaded, E2EE is disabled and encrypted rooms will not decrypt; + Clawdbot logs a warning. +- If you see missing crypto module errors (for example, `@matrix-org/matrix-sdk-crypto-nodejs-*`), + allow build scripts for `@matrix-org/matrix-sdk-crypto-nodejs` and run + `pnpm rebuild @matrix-org/matrix-sdk-crypto-nodejs` or fetch the binary with + `node node_modules/@matrix-org/matrix-sdk-crypto-nodejs/download-lib.js`. + +Crypto state is stored per account + access token in +`~/.clawdbot/matrix/accounts//__//crypto/` +(SQLite database). Sync state lives alongside it in `bot-storage.json`. +If the access token (device) changes, a new store is created and the bot must be +re-verified for encrypted rooms. + +**Device verification:** +When E2EE is enabled, the bot will request verification from your other sessions on startup. +Open Element (or another client) and approve the verification request to establish trust. +Once verified, the bot can decrypt messages in encrypted rooms. + +## Routing model + +- Replies always go back to Matrix. - DMs share the agent's main session; rooms map to group sessions. ## Access control (DMs) + - Default: `channels.matrix.dm.policy = "pairing"`. Unknown senders get a pairing code. - Approve via: - `clawdbot pairing list matrix` - `clawdbot pairing approve matrix ` - Public DMs: `channels.matrix.dm.policy="open"` plus `channels.matrix.dm.allowFrom=["*"]`. -- `channels.matrix.dm.allowFrom` accepts user IDs or display names (resolved at startup when directory search is available). +- `channels.matrix.dm.allowFrom` accepts user IDs or display names. The wizard resolves display names to user IDs when directory search is available. ## Rooms (groups) + - Default: `channels.matrix.groupPolicy = "allowlist"` (mention-gated). Use `channels.defaults.groupPolicy` to override the default when unset. -- Allowlist rooms with `channels.matrix.rooms`: +- Allowlist rooms with `channels.matrix.groups` (room IDs, aliases, or names): + ```json5 { channels: { matrix: { - rooms: { - "!roomId:example.org": { requireMention: true } - } + groupPolicy: "allowlist", + groups: { + "!roomId:example.org": { allow: true }, + "#alias:example.org": { allow: true } + }, + groupAllowFrom: ["@owner:example.org"] } } } ``` + - `requireMention: false` enables auto-reply in that room. +- `groups."*"` can set defaults for mention gating across rooms. +- `groupAllowFrom` restricts which senders can trigger the bot in rooms (optional). +- Per-room `users` allowlists can further restrict senders inside a specific room. - The configure wizard prompts for room allowlists (room IDs, aliases, or names) and resolves names when possible. - On startup, Clawdbot resolves room/user names in allowlists to IDs and logs the mapping; unresolved entries are kept as typed. +- Invites are auto-joined by default; control with `channels.matrix.autoJoin` and `channels.matrix.autoJoinAllowlist`. - To allow **no rooms**, set `channels.matrix.groupPolicy: "disabled"` (or keep an empty allowlist). +- Legacy key: `channels.matrix.rooms` (same shape as `groups`). ## Threads + - Reply threading is supported. -- `channels.matrix.replyToMode` controls replies when tagged: +- `channels.matrix.threadReplies` controls whether replies stay in threads: + - `off`, `inbound` (default), `always` +- `channels.matrix.replyToMode` controls reply-to metadata when not replying in a thread: - `off` (default), `first`, `all` ## Capabilities + | Feature | Status | |---------|--------| | Direct messages | ✅ Supported | | Rooms | ✅ Supported | | Threads | ✅ Supported | | Media | ✅ Supported | -| Reactions | ✅ Supported | -| Polls | ✅ Supported | +| E2EE | ✅ Supported (crypto module required) | +| Reactions | ✅ Supported (send/read via tools) | +| Polls | ✅ Send supported; inbound poll starts are converted to text (responses/ends ignored) | +| Location | ✅ Supported (geo URI; altitude ignored) | | Native commands | ✅ Supported | ## Configuration reference (Matrix) + Full configuration: [Configuration](/gateway/configuration) Provider options: + - `channels.matrix.enabled`: enable/disable channel startup. - `channels.matrix.homeserver`: homeserver URL. -- `channels.matrix.userId`: Matrix user ID. +- `channels.matrix.userId`: Matrix user ID (optional with access token). - `channels.matrix.accessToken`: access token. - `channels.matrix.password`: password for login (token stored). - `channels.matrix.deviceName`: device display name. +- `channels.matrix.encryption`: enable E2EE (default: false). - `channels.matrix.initialSyncLimit`: initial sync limit. - `channels.matrix.threadReplies`: `off | inbound | always` (default: inbound). - `channels.matrix.textChunkLimit`: outbound text chunk size (chars). - `channels.matrix.dm.policy`: `pairing | allowlist | open | disabled` (default: pairing). -- `channels.matrix.dm.allowFrom`: DM allowlist. `open` requires `"*"`. +- `channels.matrix.dm.allowFrom`: DM allowlist (user IDs or display names). `open` requires `"*"`. The wizard resolves names to IDs when possible. - `channels.matrix.groupPolicy`: `allowlist | open | disabled` (default: allowlist). +- `channels.matrix.groupAllowFrom`: allowlisted senders for group messages. - `channels.matrix.allowlistOnly`: force allowlist rules for DMs + rooms. -- `channels.matrix.rooms`: per-room settings and allowlist. +- `channels.matrix.groups`: group allowlist + per-room settings map. +- `channels.matrix.rooms`: legacy group allowlist/config. - `channels.matrix.replyToMode`: reply-to mode for threads/tags. - `channels.matrix.mediaMaxMb`: inbound/outbound media cap (MB). - `channels.matrix.autoJoin`: invite handling (`always | allowlist | off`, default: always). diff --git a/docs/channels/mattermost.md b/docs/channels/mattermost.md new file mode 100644 index 000000000..de4771745 --- /dev/null +++ b/docs/channels/mattermost.md @@ -0,0 +1,123 @@ +--- +summary: "Mattermost bot setup and Clawdbot config" +read_when: + - Setting up Mattermost + - Debugging Mattermost routing +--- + +# Mattermost (plugin) + +Status: supported via plugin (bot token + WebSocket events). Channels, groups, and DMs are supported. +Mattermost is a self-hostable team messaging platform; see the official site at +[mattermost.com](https://mattermost.com) for product details and downloads. + +## Plugin required +Mattermost ships as a plugin and is not bundled with the core install. + +Install via CLI (npm registry): +```bash +clawdbot plugins install @clawdbot/mattermost +``` + +Local checkout (when running from a git repo): +```bash +clawdbot plugins install ./extensions/mattermost +``` + +If you choose Mattermost during configure/onboarding and a git checkout is detected, +Clawdbot will offer the local install path automatically. + +Details: [Plugins](/plugin) + +## Quick setup +1) Install the Mattermost plugin. +2) Create a Mattermost bot account and copy the **bot token**. +3) Copy the Mattermost **base URL** (e.g., `https://chat.example.com`). +4) Configure Clawdbot and start the gateway. + +Minimal config: +```json5 +{ + channels: { + mattermost: { + enabled: true, + botToken: "mm-token", + baseUrl: "https://chat.example.com", + dmPolicy: "pairing" + } + } +} +``` + +## Environment variables (default account) +Set these on the gateway host if you prefer env vars: + +- `MATTERMOST_BOT_TOKEN=...` +- `MATTERMOST_URL=https://chat.example.com` + +Env vars apply only to the **default** account (`default`). Other accounts must use config values. + +## Chat modes +Mattermost responds to DMs automatically. Channel behavior is controlled by `chatmode`: + +- `oncall` (default): respond only when @mentioned in channels. +- `onmessage`: respond to every channel message. +- `onchar`: respond when a message starts with a trigger prefix. + +Config example: +```json5 +{ + channels: { + mattermost: { + chatmode: "onchar", + oncharPrefixes: [">", "!"] + } + } +} +``` + +Notes: +- `onchar` still responds to explicit @mentions. +- `channels.mattermost.requireMention` is honored for legacy configs but `chatmode` is preferred. + +## Access control (DMs) +- Default: `channels.mattermost.dmPolicy = "pairing"` (unknown senders get a pairing code). +- Approve via: + - `clawdbot pairing list mattermost` + - `clawdbot pairing approve mattermost ` +- Public DMs: `channels.mattermost.dmPolicy="open"` plus `channels.mattermost.allowFrom=["*"]`. + +## Channels (groups) +- Default: `channels.mattermost.groupPolicy = "allowlist"` (mention-gated). +- Allowlist senders with `channels.mattermost.groupAllowFrom` (user IDs or `@username`). +- Open channels: `channels.mattermost.groupPolicy="open"` (mention-gated). + +## Targets for outbound delivery +Use these target formats with `clawdbot message send` or cron/webhooks: + +- `channel:` for a channel +- `user:` for a DM +- `@username` for a DM (resolved via the Mattermost API) + +Bare IDs are treated as channels. + +## Multi-account +Mattermost supports multiple accounts under `channels.mattermost.accounts`: + +```json5 +{ + channels: { + mattermost: { + accounts: { + default: { name: "Primary", botToken: "mm-token", baseUrl: "https://chat.example.com" }, + alerts: { name: "Alerts", botToken: "mm-token-2", baseUrl: "https://alerts.example.com" } + } + } + } +} +``` + +## Troubleshooting +- No replies in channels: ensure the bot is in the channel and mention it (oncall), use a trigger prefix (onchar), or set `chatmode: "onmessage"`. +- Auth errors: check the bot token, base URL, and whether the account is enabled. +- Multi-account issues: env vars only apply to the `default` account. diff --git a/docs/channels/msteams.md b/docs/channels/msteams.md index c8c668e84..3315153e6 100644 --- a/docs/channels/msteams.md +++ b/docs/channels/msteams.md @@ -8,9 +8,9 @@ read_when: > "Abandon all hope, ye who enter here." -Updated: 2026-01-16 +Updated: 2026-01-21 -Status: text + DM attachments are supported; channel/group attachments require Microsoft Graph permissions. Polls are sent via Adaptive Cards. +Status: text + DM attachments are supported; channel/group file sending requires `sharePointSiteId` + Graph permissions (see [Sending files in group chats](#sending-files-in-group-chats)). Polls are sent via Adaptive Cards. ## Plugin required Microsoft Teams ships as a plugin and is not bundled with the core install. @@ -76,7 +76,7 @@ Disable with: **DM access** - Default: `channels.msteams.dmPolicy = "pairing"`. Unknown senders are ignored until approved. -- `channels.msteams.allowFrom` accepts AAD object IDs, UPNs, or display names (resolved at startup when Graph allows). +- `channels.msteams.allowFrom` accepts AAD object IDs, UPNs, or display names. The wizard resolves names to IDs via Microsoft Graph when credentials allow. **Group access** - Default: `channels.msteams.groupPolicy = "allowlist"` (blocked unless you add `groupAllowFrom`). Use `channels.defaults.groupPolicy` to override the default when unset. @@ -403,7 +403,7 @@ Clawdbot handles this by returning quickly and sending replies proactively, but Teams markdown is more limited than Slack or Discord: - Basic formatting works: **bold**, *italic*, `code`, links - Complex markdown (tables, nested lists) may not render correctly -- Adaptive Cards are used for polls; other card types are not yet supported +- Adaptive Cards are supported for polls and arbitrary card sends (see below) ## Configuration Key settings (see `/gateway/configuration` for shared channel patterns): @@ -413,7 +413,7 @@ Key settings (see `/gateway/configuration` for shared channel patterns): - `channels.msteams.webhook.port` (default `3978`) - `channels.msteams.webhook.path` (default `/api/messages`) - `channels.msteams.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing) -- `channels.msteams.allowFrom`: allowlist for DMs (AAD object IDs or UPNs). +- `channels.msteams.allowFrom`: allowlist for DMs (AAD object IDs, UPNs, or display names). The wizard resolves names to IDs during setup when Graph access is available. - `channels.msteams.textChunkLimit`: outbound text chunk size. - `channels.msteams.mediaAllowHosts`: allowlist for inbound attachment hosts (defaults to Microsoft/Teams domains). - `channels.msteams.requireMention`: require @mention in channels/groups (default true). @@ -422,6 +422,7 @@ Key settings (see `/gateway/configuration` for shared channel patterns): - `channels.msteams.teams..requireMention`: per-team override. - `channels.msteams.teams..channels..replyStyle`: per-channel override. - `channels.msteams.teams..channels..requireMention`: per-channel override. +- `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 - Session keys follow the standard agent format (see [/concepts/session](/concepts/session)): @@ -471,6 +472,75 @@ Teams recently introduced two channel UI styles over the same underlying data mo Without Graph permissions, channel messages with images will be received as text-only (the image content is not accessible to the bot). By default, Clawdbot only downloads media from Microsoft/Teams hostnames. Override with `channels.msteams.mediaAllowHosts` (use `["*"]` to allow any host). +## Sending files in group chats + +Bots can send files in DMs using the FileConsentCard flow (built-in). However, **sending files in group chats/channels** requires additional setup: + +| Context | How files are sent | Setup needed | +|---------|-------------------|--------------| +| **DMs** | FileConsentCard → user accepts → bot uploads | Works out of the box | +| **Group chats/channels** | Upload to SharePoint → share link | Requires `sharePointSiteId` + Graph permissions | +| **Images (any context)** | Base64-encoded inline | Works out of the box | + +### Why group chats need SharePoint + +Bots don't have a personal OneDrive drive (the `/me/drive` Graph API endpoint doesn't work for application identities). To send files in group chats/channels, the bot uploads to a **SharePoint site** and creates a sharing link. + +### Setup + +1. **Add Graph API permissions** in Entra ID (Azure AD) → App Registration: + - `Sites.ReadWrite.All` (Application) - upload files to SharePoint + - `Chat.Read.All` (Application) - optional, enables per-user sharing links + +2. **Grant admin consent** for the tenant. + +3. **Get your SharePoint site ID:** + ```bash + # Via Graph Explorer or curl with a valid token: + curl -H "Authorization: Bearer $TOKEN" \ + "https://graph.microsoft.com/v1.0/sites/{hostname}:/{site-path}" + + # Example: for a site at "contoso.sharepoint.com/sites/BotFiles" + curl -H "Authorization: Bearer $TOKEN" \ + "https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com:/sites/BotFiles" + + # Response includes: "id": "contoso.sharepoint.com,guid1,guid2" + ``` + +4. **Configure Clawdbot:** + ```json5 + { + channels: { + msteams: { + // ... other config ... + sharePointSiteId: "contoso.sharepoint.com,guid1,guid2" + } + } + } + ``` + +### Sharing behavior + +| Permission | Sharing behavior | +|------------|------------------| +| `Sites.ReadWrite.All` only | Organization-wide sharing link (anyone in org can access) | +| `Sites.ReadWrite.All` + `Chat.Read.All` | Per-user sharing link (only chat members can access) | + +Per-user sharing is more secure as only the chat participants can access the file. If `Chat.Read.All` permission is missing, the bot falls back to organization-wide sharing. + +### Fallback behavior + +| Scenario | Result | +|----------|--------| +| Group chat + file + `sharePointSiteId` configured | Upload to SharePoint, send sharing link | +| Group chat + file + no `sharePointSiteId` | Attempt OneDrive upload (may fail), send text only | +| Personal chat + file | FileConsentCard flow (works without SharePoint) | +| Any context + image | Base64-encoded inline (works without SharePoint) | + +### Files stored location + +Uploaded files are stored in a `/ClawdbotShared/` folder in the configured SharePoint site's default document library. + ## Polls (Adaptive Cards) Clawdbot sends Teams polls as Adaptive Cards (there is no native Teams poll API). @@ -479,6 +549,82 @@ Clawdbot sends Teams polls as Adaptive Cards (there is no native Teams poll API) - The gateway must stay online to record votes. - Polls do not auto-post result summaries yet (inspect the store file if needed). +## Adaptive Cards (arbitrary) +Send any Adaptive Card JSON to Teams users or conversations using the `message` tool or CLI. + +The `card` parameter accepts an Adaptive Card JSON object. When `card` is provided, the message text is optional. + +**Agent tool:** +```json +{ + "action": "send", + "channel": "msteams", + "target": "user:", + "card": { + "type": "AdaptiveCard", + "version": "1.5", + "body": [{"type": "TextBlock", "text": "Hello!"}] + } +} +``` + +**CLI:** +```bash +clawdbot message send --channel msteams \ + --target "conversation:19:abc...@thread.tacv2" \ + --card '{"type":"AdaptiveCard","version":"1.5","body":[{"type":"TextBlock","text":"Hello!"}]}' +``` + +See [Adaptive Cards documentation](https://adaptivecards.io/) for card schema and examples. For target format details, see [Target formats](#target-formats) below. + +## Target formats + +MSTeams targets use prefixes to distinguish between users and conversations: + +| Target type | Format | Example | +|-------------|--------|---------| +| User (by ID) | `user:` | `user:40a1a0ed-4ff2-4164-a219-55518990c197` | +| User (by name) | `user:` | `user:John Smith` (requires Graph API) | +| Group/channel | `conversation:` | `conversation:19:abc123...@thread.tacv2` | +| Group/channel (raw) | `` | `19:abc123...@thread.tacv2` (if contains `@thread`) | + +**CLI examples:** +```bash +# Send to a user by ID +clawdbot message send --channel msteams --target "user:40a1a0ed-..." --message "Hello" + +# Send to a user by display name (triggers Graph API lookup) +clawdbot message send --channel msteams --target "user:John Smith" --message "Hello" + +# Send to a group chat or channel +clawdbot message send --channel msteams --target "conversation:19:abc...@thread.tacv2" --message "Hello" + +# Send an Adaptive Card to a conversation +clawdbot message send --channel msteams --target "conversation:19:abc...@thread.tacv2" \ + --card '{"type":"AdaptiveCard","version":"1.5","body":[{"type":"TextBlock","text":"Hello"}]}' +``` + +**Agent tool examples:** +```json +{ + "action": "send", + "channel": "msteams", + "target": "user:John Smith", + "message": "Hello!" +} +``` + +```json +{ + "action": "send", + "channel": "msteams", + "target": "conversation:19:abc...@thread.tacv2", + "card": {"type": "AdaptiveCard", "version": "1.5", "body": [{"type": "TextBlock", "text": "Hello"}]} +} +``` + +Note: Without the `user:` prefix, names default to group/team resolution. Always use `user:` when targeting people by display name. + ## Proactive messaging - Proactive messages are only possible **after** a user has interacted, because we store conversation references at that point. - See `/gateway/configuration` for `dmPolicy` and allowlist gating. diff --git a/docs/channels/nextcloud-talk.md b/docs/channels/nextcloud-talk.md new file mode 100644 index 000000000..756b2fe30 --- /dev/null +++ b/docs/channels/nextcloud-talk.md @@ -0,0 +1,119 @@ +--- +summary: "Nextcloud Talk support status, capabilities, and configuration" +read_when: + - Working on Nextcloud Talk channel features +--- +# Nextcloud Talk (plugin) + +Status: supported via plugin (webhook bot). Direct messages, rooms, reactions, and markdown messages are supported. + +## Plugin required +Nextcloud Talk ships as a plugin and is not bundled with the core install. + +Install via CLI (npm registry): +```bash +clawdbot plugins install @clawdbot/nextcloud-talk +``` + +Local checkout (when running from a git repo): +```bash +clawdbot plugins install ./extensions/nextcloud-talk +``` + +If you choose Nextcloud Talk during configure/onboarding and a git checkout is detected, +Clawdbot will offer the local install path automatically. + +Details: [Plugins](/plugin) + +## Quick setup (beginner) +1) Install the Nextcloud Talk plugin. +2) On your Nextcloud server, create a bot: + ```bash + ./occ talk:bot:install "Clawdbot" "" "" --feature reaction + ``` +3) Enable the bot in the target room settings. +4) Configure Clawdbot: + - Config: `channels.nextcloud-talk.baseUrl` + `channels.nextcloud-talk.botSecret` + - Or env: `NEXTCLOUD_TALK_BOT_SECRET` (default account only) +5) Restart the gateway (or finish onboarding). + +Minimal config: +```json5 +{ + channels: { + "nextcloud-talk": { + enabled: true, + baseUrl: "https://cloud.example.com", + botSecret: "shared-secret", + dmPolicy: "pairing" + } + } +} +``` + +## Notes +- Bots cannot initiate DMs. The user must message the bot first. +- Webhook URL must be reachable by the Gateway; set `webhookPublicUrl` if behind a proxy. +- Media uploads are not supported by the bot API; media is sent as URLs. +- The webhook payload does not distinguish DMs vs rooms; set `apiUser` + `apiPassword` to enable room-type lookups (otherwise DMs are treated as rooms). + +## Access control (DMs) +- Default: `channels.nextcloud-talk.dmPolicy = "pairing"`. Unknown senders get a pairing code. +- Approve via: + - `clawdbot pairing list nextcloud-talk` + - `clawdbot pairing approve nextcloud-talk ` +- Public DMs: `channels.nextcloud-talk.dmPolicy="open"` plus `channels.nextcloud-talk.allowFrom=["*"]`. + +## Rooms (groups) +- Default: `channels.nextcloud-talk.groupPolicy = "allowlist"` (mention-gated). +- Allowlist rooms with `channels.nextcloud-talk.rooms`: +```json5 +{ + channels: { + "nextcloud-talk": { + rooms: { + "room-token": { requireMention: true } + } + } + } +} +``` +- To allow no rooms, keep the allowlist empty or set `channels.nextcloud-talk.groupPolicy="disabled"`. + +## Capabilities +| Feature | Status | +|---------|--------| +| Direct messages | Supported | +| Rooms | Supported | +| Threads | Not supported | +| Media | URL-only | +| Reactions | Supported | +| Native commands | Not supported | + +## Configuration reference (Nextcloud Talk) +Full configuration: [Configuration](/gateway/configuration) + +Provider options: +- `channels.nextcloud-talk.enabled`: enable/disable channel startup. +- `channels.nextcloud-talk.baseUrl`: Nextcloud instance URL. +- `channels.nextcloud-talk.botSecret`: bot shared secret. +- `channels.nextcloud-talk.botSecretFile`: secret file path. +- `channels.nextcloud-talk.apiUser`: API user for room lookups (DM detection). +- `channels.nextcloud-talk.apiPassword`: API/app password for room lookups. +- `channels.nextcloud-talk.apiPasswordFile`: API password file path. +- `channels.nextcloud-talk.webhookPort`: webhook listener port (default: 8788). +- `channels.nextcloud-talk.webhookHost`: webhook host (default: 0.0.0.0). +- `channels.nextcloud-talk.webhookPath`: webhook path (default: /nextcloud-talk-webhook). +- `channels.nextcloud-talk.webhookPublicUrl`: externally reachable webhook URL. +- `channels.nextcloud-talk.dmPolicy`: `pairing | allowlist | open | disabled`. +- `channels.nextcloud-talk.allowFrom`: DM allowlist (user IDs). `open` requires `"*"`. +- `channels.nextcloud-talk.groupPolicy`: `allowlist | open | disabled`. +- `channels.nextcloud-talk.groupAllowFrom`: group allowlist (user IDs). +- `channels.nextcloud-talk.rooms`: per-room settings and allowlist. +- `channels.nextcloud-talk.historyLimit`: group history limit (0 disables). +- `channels.nextcloud-talk.dmHistoryLimit`: DM history limit (0 disables). +- `channels.nextcloud-talk.dms`: per-DM overrides (historyLimit). +- `channels.nextcloud-talk.textChunkLimit`: outbound text chunk size (chars). +- `channels.nextcloud-talk.blockStreaming`: disable block streaming for this channel. +- `channels.nextcloud-talk.blockStreamingCoalesce`: block streaming coalesce tuning. +- `channels.nextcloud-talk.mediaMaxMb`: inbound media cap (MB). diff --git a/docs/channels/nostr.md b/docs/channels/nostr.md new file mode 100644 index 000000000..d1dc284bb --- /dev/null +++ b/docs/channels/nostr.md @@ -0,0 +1,235 @@ +--- +summary: "Nostr DM channel via NIP-04 encrypted messages" +read_when: + - You want Clawdbot to receive DMs via Nostr + - You're setting up decentralized messaging +--- +# Nostr + +**Status:** Optional plugin (disabled by default). + +Nostr is a decentralized protocol for social networking. This channel enables Clawdbot to receive and respond to encrypted direct messages (DMs) via NIP-04. + +## Install (on demand) + +### Onboarding (recommended) + +- The onboarding wizard (`clawdbot onboard`) and `clawdbot channels add` list optional channel plugins. +- Selecting Nostr prompts you to install the plugin on demand. + +Install defaults: + +- **Dev channel + git checkout available:** uses the local plugin path. +- **Stable/Beta:** downloads from npm. + +You can always override the choice in the prompt. + +### Manual install + +```bash +clawdbot plugins install @clawdbot/nostr +``` + +Use a local checkout (dev workflows): + +```bash +clawdbot plugins install --link /extensions/nostr +``` + +Restart the Gateway after installing or enabling plugins. + +## Quick setup + +1) Generate a Nostr keypair (if needed): + +```bash +# Using nak +nak key generate +``` + +2) Add to config: + +```json +{ + "channels": { + "nostr": { + "privateKey": "${NOSTR_PRIVATE_KEY}" + } + } +} +``` + +3) Export the key: + +```bash +export NOSTR_PRIVATE_KEY="nsec1..." +``` + +4) Restart the Gateway. + +## Configuration reference + +| Key | Type | Default | Description | +| --- | --- | --- | --- | +| `privateKey` | string | required | Private key in `nsec` or hex format | +| `relays` | string[] | `['wss://relay.damus.io', 'wss://nos.lol']` | Relay URLs (WebSocket) | +| `dmPolicy` | string | `pairing` | DM access policy | +| `allowFrom` | string[] | `[]` | Allowed sender pubkeys | +| `enabled` | boolean | `true` | Enable/disable channel | +| `name` | string | - | Display name | +| `profile` | object | - | NIP-01 profile metadata | + +## Profile metadata + +Profile data is published as a NIP-01 `kind:0` event. You can manage it from the Control UI (Channels -> Nostr -> Profile) or set it directly in config. + +Example: + +```json +{ + "channels": { + "nostr": { + "privateKey": "${NOSTR_PRIVATE_KEY}", + "profile": { + "name": "clawdbot", + "displayName": "Clawdbot", + "about": "Personal assistant DM bot", + "picture": "https://example.com/avatar.png", + "banner": "https://example.com/banner.png", + "website": "https://example.com", + "nip05": "clawdbot@example.com", + "lud16": "clawdbot@example.com" + } + } + } +} +``` + +Notes: + +- Profile URLs must use `https://`. +- Importing from relays merges fields and preserves local overrides. + +## Access control + +### DM policies + +- **pairing** (default): unknown senders get a pairing code. +- **allowlist**: only pubkeys in `allowFrom` can DM. +- **open**: public inbound DMs (requires `allowFrom: ["*"]`). +- **disabled**: ignore inbound DMs. + +### Allowlist example + +```json +{ + "channels": { + "nostr": { + "privateKey": "${NOSTR_PRIVATE_KEY}", + "dmPolicy": "allowlist", + "allowFrom": ["npub1abc...", "npub1xyz..."] + } + } +} +``` + +## Key formats + +Accepted formats: + +- **Private key:** `nsec...` or 64-char hex +- **Pubkeys (`allowFrom`):** `npub...` or hex + +## Relays + +Defaults: `relay.damus.io` and `nos.lol`. + +```json +{ + "channels": { + "nostr": { + "privateKey": "${NOSTR_PRIVATE_KEY}", + "relays": [ + "wss://relay.damus.io", + "wss://relay.primal.net", + "wss://nostr.wine" + ] + } + } +} +``` + +Tips: + +- Use 2-3 relays for redundancy. +- Avoid too many relays (latency, duplication). +- Paid relays can improve reliability. +- Local relays are fine for testing (`ws://localhost:7777`). + +## Protocol support + +| NIP | Status | Description | +| --- | --- | --- | +| NIP-01 | Supported | Basic event format + profile metadata | +| NIP-04 | Supported | Encrypted DMs (`kind:4`) | +| NIP-17 | Planned | Gift-wrapped DMs | +| NIP-44 | Planned | Versioned encryption | + +## Testing + +### Local relay + +```bash +# Start strfry +docker run -p 7777:7777 ghcr.io/hoytech/strfry +``` + +```json +{ + "channels": { + "nostr": { + "privateKey": "${NOSTR_PRIVATE_KEY}", + "relays": ["ws://localhost:7777"] + } + } +} +``` + +### Manual test + +1) Note the bot pubkey (npub) from logs. +2) Open a Nostr client (Damus, Amethyst, etc.). +3) DM the bot pubkey. +4) Verify the response. + +## Troubleshooting + +### Not receiving messages + +- Verify the private key is valid. +- Ensure relay URLs are reachable and use `wss://` (or `ws://` for local). +- Confirm `enabled` is not `false`. +- Check Gateway logs for relay connection errors. + +### Not sending responses + +- Check relay accepts writes. +- Verify outbound connectivity. +- Watch for relay rate limits. + +### Duplicate responses + +- Expected when using multiple relays. +- Messages are deduplicated by event ID; only the first delivery triggers a response. + +## Security + +- Never commit private keys. +- Use environment variables for keys. +- Consider `allowlist` for production bots. + +## Limitations (MVP) + +- Direct messages only (no group chats). +- No media attachments. +- NIP-04 only (NIP-17 gift-wrap planned). diff --git a/docs/channels/signal.md b/docs/channels/signal.md index e19c6579b..b015d02bf 100644 --- a/docs/channels/signal.md +++ b/docs/channels/signal.md @@ -100,6 +100,11 @@ Groups: - Use `channels.signal.ignoreAttachments` to skip downloading media. - Group history context uses `channels.signal.historyLimit` (or `channels.signal.accounts.*.historyLimit`), falling back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50). +## Typing + read receipts +- **Typing indicators**: Clawdbot sends typing signals via `signal-cli sendTyping` and refreshes them while a reply is running. +- **Read receipts**: when `channels.signal.sendReadReceipts` is true, Clawdbot forwards read receipts for allowed DMs. +- Signal-cli does not expose read receipts for groups. + ## Delivery targets (CLI/cron) - DMs: `signal:+15551234567` (or plain E.164). - Groups: `signal:group:`. @@ -120,7 +125,7 @@ Provider options: - `channels.signal.ignoreStories`: ignore stories from the daemon. - `channels.signal.sendReadReceipts`: forward read receipts. - `channels.signal.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing). -- `channels.signal.allowFrom`: DM allowlist (E.164 or `uuid:`). `open` requires `"*"`. +- `channels.signal.allowFrom`: DM allowlist (E.164 or `uuid:`). `open` requires `"*"`. Signal has no usernames; use phone/UUID ids. - `channels.signal.groupPolicy`: `open | allowlist | disabled` (default: allowlist). - `channels.signal.groupAllowFrom`: group sender allowlist. - `channels.signal.historyLimit`: max group messages to include as context (0 disables). diff --git a/docs/channels/slack.md b/docs/channels/slack.md index 6607d54af..b112612e1 100644 --- a/docs/channels/slack.md +++ b/docs/channels/slack.md @@ -304,7 +304,8 @@ Slack uses Socket Mode only (no HTTP webhook server). Provide both tokens: "policy": "pairing", "allowFrom": ["U123", "U456", "*"], "groupEnabled": false, - "groupChannels": ["G123"] + "groupChannels": ["G123"], + "replyToMode": "all" }, "channels": { "C123": { "allow": true, "requireMention": true }, @@ -361,6 +362,73 @@ By default, Clawdbot replies in the main channel. Use `channels.slack.replyToMod The mode applies to both auto-replies and agent tool calls (`slack sendMessage`). +### Per-chat-type threading +You can configure different threading behavior per chat type by setting `channels.slack.replyToModeByChatType`: + +```json5 +{ + channels: { + slack: { + replyToMode: "off", // default for channels + replyToModeByChatType: { + direct: "all", // DMs always thread + group: "first" // group DMs/MPIM thread first reply + }, + } + } +} +``` + +Supported chat types: +- `direct`: 1:1 DMs (Slack `im`) +- `group`: group DMs / MPIMs (Slack `mpim`) +- `channel`: standard channels (public/private) + +Precedence: +1) `replyToModeByChatType.` +2) `replyToMode` +3) Provider default (`off`) + +Legacy `channels.slack.dm.replyToMode` is still accepted as a fallback for `direct` when no chat-type override is set. + +Examples: + +Thread DMs only: +```json5 +{ + channels: { + slack: { + replyToMode: "off", + replyToModeByChatType: { direct: "all" } + } + } +} +``` + +Thread group DMs but keep channels in the root: +```json5 +{ + channels: { + slack: { + replyToMode: "off", + replyToModeByChatType: { group: "first" } + } + } +} +``` + +Make channels thread, keep DMs in the root: +```json5 +{ + channels: { + slack: { + replyToMode: "first", + replyToModeByChatType: { direct: "off", group: "off" } + } + } +} +``` + ### Manual threading tags For fine-grained control, use these tags in agent responses: - `[[reply_to_current]]` — reply to the triggering message (start/continue thread). @@ -378,7 +446,7 @@ For fine-grained control, use these tags in agent responses: - Default: `channels.slack.dm.policy="pairing"` — unknown DM senders get a pairing code (expires after 1 hour). - Approve via: `clawdbot pairing approve slack `. - To allow anyone: set `channels.slack.dm.policy="open"` and `channels.slack.dm.allowFrom=["*"]`. -- `channels.slack.dm.allowFrom` accepts user IDs, @handles, or emails (resolved at startup when tokens allow). +- `channels.slack.dm.allowFrom` accepts user IDs, @handles, or emails (resolved at startup when tokens allow). The wizard accepts usernames and resolves them to ids during setup when tokens allow. ## Group policy - `channels.slack.groupPolicy` controls channel handling (`open|disabled|allowlist`). diff --git a/docs/channels/telegram.md b/docs/channels/telegram.md index 7f655b1c9..da29b3c90 100644 --- a/docs/channels/telegram.md +++ b/docs/channels/telegram.md @@ -305,7 +305,7 @@ Use the global setting when all Telegram bots/accounts should behave the same. U - `clawdbot pairing list telegram` - `clawdbot pairing approve telegram ` - Pairing is the default token exchange used for Telegram DMs. Details: [Pairing](/start/pairing) -- `channels.telegram.allowFrom` accepts numeric user IDs (recommended) or `@username` entries. It is **not** the bot username; use the human sender’s ID. +- `channels.telegram.allowFrom` accepts numeric user IDs (recommended) or `@username` entries. It is **not** the bot username; use the human sender’s ID. The wizard accepts `@username` and resolves it to the numeric ID when possible. #### Finding your Telegram user ID Safer (no third-party bot): @@ -484,6 +484,10 @@ The agent sees reactions as **system notifications** in the conversation history - Make sure your Telegram user ID is authorized (via pairing or `channels.telegram.allowFrom`) - Commands require authorization even in groups with `groupPolicy: "open"` +**Long-polling aborts immediately on Node 22+ (often with proxies/custom fetch):** +- Node 22+ is stricter about `AbortSignal` instances; foreign signals can abort `fetch` calls right away. +- Upgrade to a Clawdbot build that normalizes abort signals, or run the gateway on Node 20 until you can upgrade. + **Bot starts, then silently stops responding (or logs `HttpError: Network request ... failed`):** - Some hosts resolve `api.telegram.org` to IPv6 first. If your server does not have working IPv6 egress, grammY can get stuck on IPv6-only requests. - Fix by enabling IPv6 egress **or** forcing IPv4 resolution for `api.telegram.org` (for example, add an `/etc/hosts` entry using the IPv4 A record, or prefer IPv4 in your OS DNS stack), then restart the gateway. diff --git a/docs/channels/tlon.md b/docs/channels/tlon.md new file mode 100644 index 000000000..a2436d5e7 --- /dev/null +++ b/docs/channels/tlon.md @@ -0,0 +1,133 @@ +--- +summary: "Tlon/Urbit support status, capabilities, and configuration" +read_when: + - Working on Tlon/Urbit channel features +--- +# Tlon (plugin) + +Tlon is a decentralized messenger built on Urbit. Clawdbot connects to your Urbit ship and can +respond to DMs and group chat messages. Group replies require an @ mention by default and can +be further restricted via allowlists. + +Status: supported via plugin. DMs, group mentions, thread replies, and text-only media fallback +(URL appended to caption). Reactions, polls, and native media uploads are not supported. + +## Plugin required + +Tlon ships as a plugin and is not bundled with the core install. + +Install via CLI (npm registry): + +```bash +clawdbot plugins install @clawdbot/tlon +``` + +Local checkout (when running from a git repo): + +```bash +clawdbot plugins install ./extensions/tlon +``` + +Details: [Plugins](/plugin) + +## Setup + +1) Install the Tlon plugin. +2) Gather your ship URL and login code. +3) Configure `channels.tlon`. +4) Restart the gateway. +5) DM the bot or mention it in a group channel. + +Minimal config (single account): + +```json5 +{ + channels: { + tlon: { + enabled: true, + ship: "~sampel-palnet", + url: "https://your-ship-host", + code: "lidlut-tabwed-pillex-ridrup" + } + } +} +``` + +## Group channels + +Auto-discovery is enabled by default. You can also pin channels manually: + +```json5 +{ + channels: { + tlon: { + groupChannels: [ + "chat/~host-ship/general", + "chat/~host-ship/support" + ] + } + } +} +``` + +Disable auto-discovery: + +```json5 +{ + channels: { + tlon: { + autoDiscoverChannels: false + } + } +} +``` + +## Access control + +DM allowlist (empty = allow all): + +```json5 +{ + channels: { + tlon: { + dmAllowlist: ["~zod", "~nec"] + } + } +} +``` + +Group authorization (restricted by default): + +```json5 +{ + channels: { + tlon: { + defaultAuthorizedShips: ["~zod"], + authorization: { + channelRules: { + "chat/~host-ship/general": { + mode: "restricted", + allowedShips: ["~zod", "~nec"] + }, + "chat/~host-ship/announcements": { + mode: "open" + } + } + } + } + } +} +``` + +## Delivery targets (CLI/cron) + +Use these with `clawdbot message send` or cron delivery: + +- DM: `~sampel-palnet` or `dm/~sampel-palnet` +- Group: `chat/~host-ship/channel` or `group:~host-ship/channel` + +## Notes + +- Group replies require a mention (e.g. `~your-bot-ship`) to respond. +- Thread replies: if the inbound message is in a thread, Clawdbot replies in-thread. +- Media: `sendMedia` falls back to text + URL (no native upload). diff --git a/docs/channels/whatsapp.md b/docs/channels/whatsapp.md index 8bbabaa65..a496d1654 100644 --- a/docs/channels/whatsapp.md +++ b/docs/channels/whatsapp.md @@ -286,6 +286,11 @@ WhatsApp can automatically send emoji reactions to incoming messages immediately - CLI: `clawdbot message send --media --gif-playback` - Gateway: `send` params include `gifPlayback: true` +## Voice notes (PTT audio) +WhatsApp sends audio as **voice notes** (PTT bubble). +- Best results: OGG/Opus. Clawdbot rewrites `audio/ogg` to `audio/ogg; codecs=opus`. +- `[[audio_as_voice]]` is ignored for WhatsApp (audio already ships as voice note). + ## Media limits + optimization - Default outbound cap: 5 MB (per media item). - Override: `agents.defaults.mediaMaxMb`. @@ -308,7 +313,7 @@ WhatsApp can automatically send emoji reactions to incoming messages immediately ## Config quick map - `channels.whatsapp.dmPolicy` (DM policy: pairing/allowlist/open/disabled). - `channels.whatsapp.selfChatMode` (same-phone setup; bot uses your personal WhatsApp number). -- `channels.whatsapp.allowFrom` (DM allowlist). +- `channels.whatsapp.allowFrom` (DM allowlist). WhatsApp uses E.164 phone numbers (no usernames). - `channels.whatsapp.mediaMaxMb` (inbound media save cap). - `channels.whatsapp.ackReaction` (auto-reaction on message receipt: `{emoji, direct, group}`). - `channels.whatsapp.accounts..*` (per-account settings + optional `authDir`). @@ -329,6 +334,7 @@ WhatsApp can automatically send emoji reactions to incoming messages immediately - `agents.defaults.heartbeat.model` (optional override) - `agents.defaults.heartbeat.target` - `agents.defaults.heartbeat.to` +- `agents.defaults.heartbeat.session` - `agents.list[].heartbeat.*` (per-agent overrides) - `session.*` (scope, idle, store, mainKey) - `web.enabled` (disable channel startup when false) diff --git a/docs/channels/zalo.md b/docs/channels/zalo.md index 7e31545dc..55f2d5104 100644 --- a/docs/channels/zalo.md +++ b/docs/channels/zalo.md @@ -92,7 +92,7 @@ Multi-account support: use `channels.zalo.accounts` with per-account tokens and - `clawdbot pairing list zalo` - `clawdbot pairing approve zalo ` - Pairing is the default token exchange. Details: [Pairing](/start/pairing) -- `channels.zalo.allowFrom` accepts numeric user IDs. +- `channels.zalo.allowFrom` accepts numeric user IDs (no username lookup available). ## Long-polling vs webhook - Default: long-polling (no public URL required). @@ -147,7 +147,7 @@ Provider options: - `channels.zalo.botToken`: bot token from Zalo Bot Platform. - `channels.zalo.tokenFile`: read token from file path. - `channels.zalo.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing). -- `channels.zalo.allowFrom`: DM allowlist (user IDs). `open` requires `"*"`. +- `channels.zalo.allowFrom`: DM allowlist (user IDs). `open` requires `"*"`. The wizard will ask for numeric IDs. - `channels.zalo.mediaMaxMb`: inbound/outbound media cap (MB, default 5). - `channels.zalo.webhookUrl`: enable webhook mode (HTTPS required). - `channels.zalo.webhookSecret`: webhook secret (8-256 chars). diff --git a/docs/channels/zalouser.md b/docs/channels/zalouser.md index a667a3f82..004a9d223 100644 --- a/docs/channels/zalouser.md +++ b/docs/channels/zalouser.md @@ -66,7 +66,7 @@ clawdbot directory groups list --channel zalouser --query "work" ## Access control (DMs) `channels.zalouser.dmPolicy` supports: `pairing | allowlist | open | disabled` (default: `pairing`). -`channels.zalouser.allowFrom` accepts user IDs or names (resolved at startup when available). +`channels.zalouser.allowFrom` accepts user IDs or names. The wizard resolves names to IDs via `zca friend find` when available. Approve via: - `clawdbot pairing list zalouser` diff --git a/docs/cli/agent.md b/docs/cli/agent.md index 4e711d2e0..fc0137455 100644 --- a/docs/cli/agent.md +++ b/docs/cli/agent.md @@ -7,6 +7,7 @@ read_when: # `clawdbot agent` Run an agent turn via the Gateway (use `--local` for embedded). +Use `--agent ` to target a configured agent directly. Related: - Agent send tool: [Agent send](/tools/agent-send) @@ -15,6 +16,7 @@ Related: ```bash clawdbot agent --to +15555550123 --message "status update" --deliver +clawdbot agent --agent ops --message "Summarize logs" clawdbot agent --session-id 1234 --message "Summarize inbox" --thinking medium +clawdbot agent --agent ops --message "Generate report" --deliver --reply-channel slack --reply-to "#reports" ``` - diff --git a/docs/cli/agents.md b/docs/cli/agents.md index bf9668717..e7df32e52 100644 --- a/docs/cli/agents.md +++ b/docs/cli/agents.md @@ -1,5 +1,5 @@ --- -summary: "CLI reference for `clawdbot agents` (list/add/delete isolated agents)" +summary: "CLI reference for `clawdbot agents` (list/add/delete/set identity)" read_when: - You want multiple isolated agents (workspaces + routing + auth) --- @@ -17,6 +17,55 @@ Related: ```bash clawdbot agents list clawdbot agents add work --workspace ~/clawd-work +clawdbot agents set-identity --workspace ~/clawd --from-identity +clawdbot agents set-identity --agent main --avatar avatars/clawd.png clawdbot agents delete work ``` +## Identity files + +Each agent workspace can include an `IDENTITY.md` at the workspace root: +- Example path: `~/clawd/IDENTITY.md` +- `set-identity --from-identity` reads from the workspace root (or an explicit `--identity-file`) + +Avatar paths resolve relative to the workspace root. + +## Set identity + +`set-identity` writes fields into `agents.list[].identity`: +- `name` +- `theme` +- `emoji` +- `avatar` (workspace-relative path, http(s) URL, or data URI) + +Load from `IDENTITY.md`: + +```bash +clawdbot agents set-identity --workspace ~/clawd --from-identity +``` + +Override fields explicitly: + +```bash +clawdbot agents set-identity --agent main --name "Clawd" --emoji "🦞" --avatar avatars/clawd.png +``` + +Config sample: + +```json5 +{ + agents: { + list: [ + { + id: "main", + identity: { + name: "Clawd", + theme: "space lobster", + emoji: "🦞", + avatar: "avatars/clawd.png" + } + } + ] + } +} +``` diff --git a/docs/cli/approvals.md b/docs/cli/approvals.md index d91eb4bf9..eccc98f77 100644 --- a/docs/cli/approvals.md +++ b/docs/cli/approvals.md @@ -7,8 +7,8 @@ read_when: # `clawdbot approvals` -Manage exec approvals for the **gateway host** or a **node host**. -By default, commands target the gateway. Use `--node` to edit a node’s approvals. +Manage exec approvals for the **local host**, **gateway host**, or a **node host**. +By default, commands target the local approvals file on disk. Use `--gateway` to target the gateway, or `--node` to target a specific node. Related: - Exec approvals: [Exec approvals](/tools/exec-approvals) @@ -19,6 +19,7 @@ Related: ```bash clawdbot approvals get clawdbot approvals get --node +clawdbot approvals get --gateway ``` ## Replace approvals from a file @@ -26,6 +27,7 @@ clawdbot approvals get --node ```bash clawdbot approvals set --file ./exec-approvals.json clawdbot approvals set --node --file ./exec-approvals.json +clawdbot approvals set --gateway --file ./exec-approvals.json ``` ## Allowlist helpers @@ -33,6 +35,7 @@ clawdbot approvals set --node --file ./exec-approvals.json ```bash clawdbot approvals allowlist add "~/Projects/**/bin/rg" clawdbot approvals allowlist add --agent main --node "/usr/bin/uptime" +clawdbot approvals allowlist add --agent "*" "/usr/bin/uname" clawdbot approvals allowlist remove "~/Projects/**/bin/rg" ``` @@ -40,5 +43,6 @@ clawdbot approvals allowlist remove "~/Projects/**/bin/rg" ## Notes - `--node` uses the same resolver as `clawdbot nodes` (id, name, ip, or id prefix). +- `--agent` defaults to `"*"`, which applies to all agents. - The node host must advertise `system.execApprovals.get/set` (macOS app or headless node host). - Approvals files are stored per host at `~/.clawdbot/exec-approvals.json`. diff --git a/docs/cli/channels.md b/docs/cli/channels.md index 55214ae63..2fe9e90df 100644 --- a/docs/cli/channels.md +++ b/docs/cli/channels.md @@ -1,7 +1,7 @@ --- summary: "CLI reference for `clawdbot channels` (accounts, status, login/logout, logs)" read_when: - - You want to add/remove channel accounts (WhatsApp/Telegram/Discord/Slack/Signal/iMessage) + - You want to add/remove channel accounts (WhatsApp/Telegram/Discord/Slack/Mattermost (plugin)/Signal/iMessage) - You want to check channel status or tail channel logs --- @@ -44,6 +44,7 @@ clawdbot channels logout --channel whatsapp - Run `clawdbot status --deep` for a broad probe. - Use `clawdbot doctor` for guided fixes. +- `clawdbot channels list` prints `Claude: HTTP 403 ... user:profile` → usage snapshot needs the `user:profile` scope. Use `--no-usage`, or provide a claude.ai session key (`CLAUDE_WEB_SESSION_KEY` / `CLAUDE_WEB_COOKIE`), or re-auth via Claude Code CLI. ## Capabilities probe diff --git a/docs/cli/configure.md b/docs/cli/configure.md index 2ffa23de6..d7159b9b8 100644 --- a/docs/cli/configure.md +++ b/docs/cli/configure.md @@ -8,6 +8,9 @@ read_when: Interactive prompt to set up credentials, devices, and agent defaults. +Note: The **Model** section now includes a multi-select for the +`agents.defaults.models` allowlist (what shows up in `/model` and the model picker). + Tip: `clawdbot config` without a subcommand opens the same wizard. Use `clawdbot config get|set|unset` for non-interactive edits. diff --git a/docs/cli/cron.md b/docs/cli/cron.md index 36185a9cb..5980d16d5 100644 --- a/docs/cli/cron.md +++ b/docs/cli/cron.md @@ -14,3 +14,16 @@ Related: Tip: run `clawdbot cron --help` for the full command surface. +## Common edits + +Update delivery settings without changing the message: + +```bash +clawdbot cron edit --deliver --channel telegram --to "123456789" +``` + +Disable delivery for an isolated job: + +```bash +clawdbot cron edit --no-deliver +``` diff --git a/docs/cli/daemon.md b/docs/cli/daemon.md deleted file mode 100644 index 71c43d1f8..000000000 --- a/docs/cli/daemon.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -summary: "CLI reference for `clawdbot daemon` (install/uninstall/status for the Gateway service)" -read_when: - - You want to run the Gateway as a background service - - You’re debugging daemon install, status, or logs ---- - -# `clawdbot daemon` - -Manage the Gateway daemon (background service). - -Note: `clawdbot service gateway …` is the preferred surface; `daemon` remains -as a legacy alias for compatibility. - -Related: -- Gateway CLI: [Gateway](/cli/gateway) -- macOS platform notes: [macOS](/platforms/macos) - -Tip: run `clawdbot daemon --help` for platform-specific flags. - -Notes: -- `daemon status` supports `--json` for scripting. -- `daemon install|uninstall|start|stop|restart` support `--json` for scripting (default output stays human-friendly). diff --git a/docs/cli/devices.md b/docs/cli/devices.md new file mode 100644 index 000000000..910eec957 --- /dev/null +++ b/docs/cli/devices.md @@ -0,0 +1,66 @@ +--- +summary: "CLI reference for `clawdbot devices` (device pairing + token rotation/revocation)" +read_when: + - You are approving device pairing requests + - You need to rotate or revoke device tokens +--- + +# `clawdbot devices` + +Manage device pairing requests and device-scoped tokens. + +## Commands + +### `clawdbot devices list` + +List pending pairing requests and paired devices. + +``` +clawdbot devices list +clawdbot devices list --json +``` + +### `clawdbot devices approve ` + +Approve a pending device pairing request. + +``` +clawdbot devices approve +``` + +### `clawdbot devices reject ` + +Reject a pending device pairing request. + +``` +clawdbot devices reject +``` + +### `clawdbot devices rotate --device --role [--scope ]` + +Rotate a device token for a specific role (optionally updating scopes). + +``` +clawdbot devices rotate --device --role operator --scope operator.read --scope operator.write +``` + +### `clawdbot devices revoke --device --role ` + +Revoke a device token for a specific role. + +``` +clawdbot devices revoke --device --role node +``` + +## Common options + +- `--url `: Gateway WebSocket URL (defaults to `gateway.remote.url` when configured). +- `--token `: Gateway token (if required). +- `--password `: Gateway password (password auth). +- `--timeout `: RPC timeout. +- `--json`: JSON output (recommended for scripting). + +## Notes + +- Token rotation returns a new token (sensitive). Treat it like a secret. +- These commands require `operator.pairing` (or `operator.admin`) scope. diff --git a/docs/cli/doctor.md b/docs/cli/doctor.md index 0f91b60bf..994f6a80d 100644 --- a/docs/cli/doctor.md +++ b/docs/cli/doctor.md @@ -23,6 +23,7 @@ clawdbot doctor --deep Notes: - Interactive prompts (like keychain/OAuth fixes) only run when stdin is a TTY and `--non-interactive` is **not** set. Headless runs (cron, Telegram, no terminal) will skip prompts. +- `--fix` (alias for `--repair`) writes a backup to `~/.clawdbot/clawdbot.json.bak` and drops unknown config keys, listing each removal. ## macOS: `launchctl` env overrides diff --git a/docs/cli/gateway.md b/docs/cli/gateway.md index c5627c854..017718745 100644 --- a/docs/cli/gateway.md +++ b/docs/cli/gateway.md @@ -25,16 +25,22 @@ Run a local Gateway process: clawdbot gateway ``` +Foreground alias: + +```bash +clawdbot gateway run +``` + Notes: - By default, the Gateway refuses to start unless `gateway.mode=local` is set in `~/.clawdbot/clawdbot.json`. Use `--allow-unconfigured` for ad-hoc/dev runs. - Binding beyond loopback without auth is blocked (safety guardrail). -- `SIGUSR1` triggers an in-process restart (useful without a supervisor). +- `SIGUSR1` triggers an in-process restart when authorized (enable `commands.restart` or use the gateway tool/config apply/update). - `SIGINT`/`SIGTERM` handlers stop the gateway process, but they don’t restore any custom terminal state. If you wrap the CLI with a TUI or raw-mode input, restore the terminal before exit. ### Options - `--port `: WebSocket port (default comes from config/env; usually `18789`). -- `--bind `: listener bind mode. +- `--bind `: listener bind mode. - `--auth `: auth mode override. - `--token `: token override (also sets `CLAWDBOT_GATEWAY_TOKEN` for the process). - `--password `: password override (also sets `CLAWDBOT_GATEWAY_PASSWORD` for the process). @@ -75,15 +81,32 @@ clawdbot gateway health --url ws://127.0.0.1:18789 ### `gateway status` -`gateway status` is the “debug everything” command. It always probes: +`gateway status` shows the Gateway service (launchd/systemd/schtasks) plus an optional RPC probe. + +```bash +clawdbot gateway status +clawdbot gateway status --json +``` + +Options: +- `--url `: override the probe URL. +- `--token `: token auth for the probe. +- `--password `: password auth for the probe. +- `--timeout `: probe timeout (default `10000`). +- `--no-probe`: skip the RPC probe (service-only view). +- `--deep`: scan system-level services too. + +### `gateway probe` + +`gateway probe` is the “debug everything” command. It always probes: - your configured remote gateway (if set), and - localhost (loopback) **even if remote is configured**. If multiple gateways are reachable, it prints all of them. Multiple gateways are supported when you use isolated profiles/ports (e.g., a rescue bot), but most installs still run a single gateway. ```bash -clawdbot gateway status -clawdbot gateway status --json +clawdbot gateway probe +clawdbot gateway probe --json ``` #### Remote over SSH (Mac app parity) @@ -93,13 +116,13 @@ The macOS app “Remote over SSH” mode uses a local port-forward so the remote CLI equivalent: ```bash -clawdbot gateway status --ssh user@gateway-host +clawdbot gateway probe --ssh user@gateway-host ``` Options: - `--ssh `: `user@host` or `user@host:port` (port defaults to `22`). - `--ssh-identity `: identity file. -- `--ssh-auto`: pick the first discovered bridge host as SSH target (LAN/WAB only). +- `--ssh-auto`: pick the first discovered gateway host as SSH target (LAN/WAB only). Config (optional, used as defaults): - `gateway.remote.sshTarget` @@ -114,19 +137,36 @@ clawdbot gateway call status clawdbot gateway call logs.tail --params '{"sinceMs": 60000}' ``` +## Manage the Gateway service + +```bash +clawdbot gateway install +clawdbot gateway start +clawdbot gateway stop +clawdbot gateway restart +clawdbot gateway uninstall +``` + +Notes: +- `gateway install` supports `--port`, `--runtime`, `--token`, `--force`, `--json`. +- Lifecycle commands accept `--json` for scripting. + ## Discover gateways (Bonjour) -`gateway discover` scans for Gateway bridge beacons (`_clawdbot-bridge._tcp`). +`gateway discover` scans for Gateway beacons (`_clawdbot-gw._tcp`). - Multicast DNS-SD: `local.` - Unicast DNS-SD (Wide-Area Bonjour): `clawdbot.internal.` (requires split DNS + DNS server; see [/gateway/bonjour](/gateway/bonjour)) -Only gateways with the **bridge enabled** will advertise the discovery beacon. +Only gateways with Bonjour discovery enabled (default) advertise the beacon. Wide-Area discovery records include (TXT): +- `role` (gateway role hint) +- `transport` (transport hint, e.g. `gateway`) - `gatewayPort` (WebSocket port, usually `18789`) - `sshPort` (SSH port; defaults to `22` if not present) - `tailnetDns` (MagicDNS hostname, when available) +- `gatewayTls` / `gatewayTlsSha256` (TLS enabled + cert fingerprint) - `cliPath` (optional hint for remote installs) ### `gateway discover` diff --git a/docs/cli/index.md b/docs/cli/index.md index 47ae49c37..ce1c619d5 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -28,18 +28,17 @@ This page describes the current CLI behavior. If commands change, update this do - [`health`](/cli/health) - [`sessions`](/cli/sessions) - [`gateway`](/cli/gateway) -- [`daemon`](/cli/daemon) -- [`service`](/cli/service) - [`logs`](/cli/logs) +- [`system`](/cli/system) - [`models`](/cli/models) - [`memory`](/cli/memory) - [`nodes`](/cli/nodes) +- [`devices`](/cli/devices) - [`node`](/cli/node) - [`approvals`](/cli/approvals) - [`sandbox`](/cli/sandbox) - [`tui`](/cli/tui) - [`browser`](/cli/browser) -- [`wake`](/cli/wake) - [`cron`](/cli/cron) - [`dns`](/cli/dns) - [`docs`](/cli/docs) @@ -137,30 +136,19 @@ clawdbot [--dev] [--profile ] call health status + probe discover - daemon - status install uninstall start stop restart - service - gateway - status - install - uninstall - start - stop - restart - node - status - install - uninstall - start - stop - restart + run logs + system + event + heartbeat last|enable|disable + presence models list status @@ -176,7 +164,6 @@ clawdbot [--dev] [--profile ] list recreate explain - wake cron status list @@ -188,15 +175,15 @@ clawdbot [--dev] [--profile ] runs run nodes + devices node + run + status + install + uninstall start - daemon - status - install - uninstall - start - stop - restart + stop + restart approvals get set @@ -309,7 +296,7 @@ Options: - `--reset` (reset config + credentials + sessions + workspace before wizard) - `--non-interactive` - `--mode ` -- `--flow ` +- `--flow ` (manual is an alias for advanced) - `--auth-choice ` - `--token-provider ` (non-interactive; used with `--auth-choice token`) - `--token ` (non-interactive; used with `--auth-choice token`) @@ -326,7 +313,7 @@ Options: - `--minimax-api-key ` - `--opencode-zen-api-key ` - `--gateway-port ` -- `--gateway-bind ` +- `--gateway-bind ` - `--gateway-auth ` - `--gateway-token ` - `--gateway-password ` @@ -368,7 +355,7 @@ Options: ## Channel helpers ### `channels` -Manage chat channel accounts (WhatsApp/Telegram/Discord/Slack/Signal/iMessage/MS Teams). +Manage chat channel accounts (WhatsApp/Telegram/Discord/Slack/Mattermost (plugin)/Signal/iMessage/MS Teams). Subcommands: - `channels list`: show configured channels and auth profiles (Claude Code + Codex CLI OAuth sync included). @@ -381,7 +368,7 @@ Subcommands: - `channels logout`: log out of a channel session (if supported). Common options: -- `--channel `: `whatsapp|telegram|discord|slack|signal|imessage|msteams` +- `--channel `: `whatsapp|telegram|discord|slack|mattermost|signal|imessage|msteams` - `--account `: channel account id (default `default`) - `--name