chore: rename project to clawdbot

This commit is contained in:
Peter Steinberger
2026-01-04 14:32:47 +00:00
parent d48dc71fa4
commit 246adaa119
841 changed files with 4590 additions and 4328 deletions

6
.gitignore vendored
View File

@@ -15,13 +15,13 @@ ui/test-results/
# Bun build artifacts # Bun build artifacts
*.bun-build *.bun-build
apps/macos/.build/ apps/macos/.build/
apps/shared/ClawdisKit/.build/ apps/shared/ClawdbotKit/.build/
bin/ bin/
bin/clawdis-mac bin/clawdbot-mac
bin/docs-list bin/docs-list
apps/macos/.build-local/ apps/macos/.build-local/
apps/macos/.swiftpm/ apps/macos/.swiftpm/
apps/shared/ClawdisKit/.swiftpm/ apps/shared/ClawdbotKit/.swiftpm/
Core/ Core/
apps/ios/*.xcodeproj/ apps/ios/*.xcodeproj/
apps/ios/*.xcworkspace/ apps/ios/*.xcworkspace/

View File

@@ -7,7 +7,7 @@
## Build, Test, and Development Commands ## Build, Test, and Development Commands
- Install deps: `pnpm install` - Install deps: `pnpm install`
- Run CLI in dev: `pnpm clawdis ...` (tsx entry) or `pnpm dev` for `src/index.ts`. - Run CLI in dev: `pnpm clawdbot ...` (tsx entry) or `pnpm dev` for `src/index.ts`.
- Type-check/build: `pnpm build` (tsc) - Type-check/build: `pnpm build` (tsc)
- Lint/format: `pnpm lint` (biome check), `pnpm format` (biome format) - Lint/format: `pnpm lint` (biome check), `pnpm format` (biome format)
- Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage` - Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage`
@@ -32,13 +32,13 @@
- PRs should summarize scope, note testing performed, and mention any user-facing changes or new flags. - PRs should summarize scope, note testing performed, and mention any user-facing changes or new flags.
## Security & Configuration Tips ## Security & Configuration Tips
- Web provider stores creds at `~/.clawdis/credentials/`; rerun `clawdis login` if logged out. - Web provider stores creds at `~/.clawdbot/credentials/`; rerun `clawdbot login` if logged out.
- Pi sessions live under `~/.clawdis/sessions/` by default; the base directory is not configurable. - Pi sessions live under `~/.clawdbot/sessions/` by default; the base directory is not configurable.
- Never commit or publish real phone numbers, videos, or live configuration values. Use obviously fake placeholders in docs, tests, and examples. - Never commit or publish real phone numbers, videos, or live configuration values. Use obviously fake placeholders in docs, tests, and examples.
## Agent-Specific Notes ## Agent-Specific Notes
- Gateway currently runs only as the menubar app (launchctl shows `application.com.steipete.clawdis.debug.*`), there is no separate LaunchAgent/helper label installed. Restart via the Clawdis Mac app or `scripts/restart-mac.sh`; to verify/kill use `launchctl print gui/$UID | grep clawdis` rather than expecting `com.steipete.clawdis`. **When debugging on macOS, start/stop the gateway via the app, not ad-hoc tmux sessions; kill any temporary tunnels before handoff.** - Gateway currently runs only as the menubar app (launchctl shows `application.com.steipete.clawdbot.debug.*`), 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 expecting `com.steipete.clawdbot`. **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 subsystem `com.steipete.clawdis`; it supports follow/tail/category filters and expects passwordless sudo for `/usr/bin/log`. - macOS logs: use `./scripts/clawlog.sh` (aka `vtlog`) to query unified logs for subsystem `com.steipete.clawdbot`; it supports follow/tail/category filters and expects passwordless sudo for `/usr/bin/log`.
- Also read the shared guardrails at `~/Projects/oracle/AGENTS.md` and `~/Projects/agent-scripts/AGENTS.MD` before making changes; align with any cross-repo rules noted there. - Also read the shared guardrails at `~/Projects/oracle/AGENTS.md` and `~/Projects/agent-scripts/AGENTS.MD` before making changes; align with any cross-repo rules noted there.
- SwiftUI state management (iOS/macOS): prefer the `Observation` framework (`@Observable`, `@Bindable`) over `ObservableObject`/`@StateObject`; dont introduce new `ObservableObject` unless required for compatibility, and migrate existing usages when touching related code. - SwiftUI state management (iOS/macOS): prefer the `Observation` framework (`@Observable`, `@Bindable`) over `ObservableObject`/`@StateObject`; dont 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. - 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.
@@ -52,27 +52,27 @@
- **Multi-agent safety:** do **not** create/remove/modify `git worktree` checkouts (or edit `.worktrees/*`) unless Peter explicitly asks. - **Multi-agent safety:** do **not** create/remove/modify `git worktree` checkouts (or edit `.worktrees/*`) unless Peter explicitly asks.
- **Multi-agent safety:** do **not** switch branches / check out a different branch unless Peter explicitly asks. - **Multi-agent safety:** do **not** switch branches / check out a different branch unless Peter explicitly asks.
- **Multi-agent safety:** running multiple agents is OK as long as each agent has its own session. - **Multi-agent safety:** running multiple agents is OK as long as each agent has its own session.
- When asked to open a “session” file, open the Pi session logs under `~/.clawdis/sessions/*.jsonl` (newest unless a specific ID is given), not the default `sessions.json`. If logs are needed from Mac Studio, SSH via Tailscale and read the same path there. - When asked to open a “session” file, open the Pi session logs under `~/.clawdbot/sessions/*.jsonl` (newest unless a specific ID is given), not the default `sessions.json`. If logs are needed from Mac Studio, SSH via Tailscale and read the same path there.
- Menubar dimming + restart flow mirrors Trimmy: use `scripts/restart-mac.sh` (kills all Clawdis variants, runs `swift build`, packages, relaunches). Icon dimming depends on MenuBarExtraAccess wiring in AppMain; keep `appearsDisabled` updates intact when touching the status item. - 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.
- Do not rebuild the macOS app over SSH; rebuilds must be run directly on the Mac. - 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. - 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: - Voice wake forwarding tips:
- Command template should stay `clawdis-mac agent --message "${text}" --thinking low`; `VoiceWakeForwarder` already shell-escapes `${text}`. Dont add extra quotes. - Command template should stay `clawdbot-mac agent --message "${text}" --thinking low`; `VoiceWakeForwarder` already shell-escapes `${text}`. Dont add extra quotes.
- launchd PATH is minimal; ensure the apps launch agent sets PATH to include `/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/steipete/Library/pnpm` so `pnpm`/`clawdis` binaries resolve when invoked via `clawdis-mac`. - launchd PATH is minimal; ensure the apps launch agent sets PATH to include `/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/steipete/Library/pnpm` so `pnpm`/`clawdbot` binaries resolve when invoked via `clawdbot-mac`.
- For manual `clawdis send` messages that include `!`, use the heredoc pattern noted below to avoid the Bash tools escaping. - For manual `clawdbot send` messages that include `!`, use the heredoc pattern noted below to avoid the Bash tools escaping.
## Exclamation Mark Escaping Workaround ## Exclamation Mark Escaping Workaround
The Claude Code Bash tool escapes `!` to `\\!` in command arguments. When using `clawdis send` with messages containing exclamation marks, use heredoc syntax: The Claude Code Bash tool escapes `!` to `\\!` in command arguments. When using `clawdbot send` with messages containing exclamation marks, use heredoc syntax:
```bash ```bash
# WRONG - will send "Hello\\!" with backslash # WRONG - will send "Hello\\!" with backslash
clawdis send --to "+1234" --message 'Hello!' clawdbot send --to "+1234" --message 'Hello!'
# CORRECT - use heredoc to avoid escaping # CORRECT - use heredoc to avoid escaping
clawdis send --to "+1234" --message "$(cat <<'EOF' clawdbot send --to "+1234" --message "$(cat <<'EOF'
Hello! Hello!
EOF EOF
)" )"
``` ```
This is a Claude Code quirk, not a clawdis bug. This is a Claude Code quirk, not a clawdbot bug.

View File

@@ -3,13 +3,15 @@
## Unreleased ## Unreleased
### Breaking ### Breaking
- Identifiers: rename bundle IDs and internal domains to `com.clawdis.*` (macOS: `com.clawdis.mac`, iOS: `com.clawdis.ios`, Android: `com.clawdis.android`) and update the gateway LaunchAgent label to `com.clawdis.gateway`. - Project rename: Clawdis → Clawdbot. All CLIs, package/binary names, bundle IDs, config/state paths (`~/.clawdbot`), env vars (`CLAWDBOT_*`), gateway URLs, and docs now use Clawdbot. Old `clawdis` names no longer work.
- Agent tools: drop the `clawdis_` prefix (`browser`, `canvas`, `nodes`, `cron`, `gateway`). - Identifiers: rename bundle IDs and internal domains to `com.clawdbot.*` (macOS: `com.clawdbot.mac`, iOS: `com.clawdbot.ios`, Android: `com.clawdbot.android`) and update the gateway LaunchAgent label to `com.clawdbot.gateway`.
- Agent tools: drop the `clawdbot_` prefix (`browser`, `canvas`, `nodes`, `cron`, `gateway`).
- Bash tool: remove `stdinMode: "pty"`/node-pty support; use the tmux skill for real TTYs. - Bash tool: remove `stdinMode: "pty"`/node-pty support; use the tmux skill for real TTYs.
- Sessions: primary session key is fixed to `main` (or `global` for global scope); `session.mainKey` is ignored. - Sessions: primary session key is fixed to `main` (or `global` for global scope); `session.mainKey` is ignored.
### Features ### Features
- Gateway: support `gateway.port` + `CLAWDIS_GATEWAY_PORT` across CLI, TUI, and macOS app. - Highlight: agent-to-agent ping-pong (reply-back loop) with `REPLY_SKIP` plus target announce step with `ANNOUNCE_SKIP` (max turns configurable, 05).
- Gateway: support `gateway.port` + `CLAWDBOT_GATEWAY_PORT` across CLI, TUI, and macOS app.
- Gateway: add config hot reload with hybrid restart strategy (`gateway.reload`) and per-section reload handling. - Gateway: add config hot reload with hybrid restart strategy (`gateway.reload`) and per-section reload handling.
- Canvas host: add `canvasHost.liveReload` to disable file watching + reload injection. - Canvas host: add `canvasHost.liveReload` to disable file watching + reload injection.
- UI: centralize tool display metadata and show action/detail summaries across Web Chat, SwiftUI, Android, and the TUI. - UI: centralize tool display metadata and show action/detail summaries across Web Chat, SwiftUI, Android, and the TUI.
@@ -20,6 +22,7 @@
- Discord: emit system events for reaction add/remove with per-guild reaction notifications (off|own|all|allowlist) (#140) — thanks @thewilloftheshadow. - Discord: emit system events for reaction add/remove with per-guild reaction notifications (off|own|all|allowlist) (#140) — thanks @thewilloftheshadow.
- Agent: add optional per-session Docker sandbox for tool execution (`agent.sandbox`) with allow/deny policy and auto-pruning. - Agent: add optional per-session Docker sandbox for tool execution (`agent.sandbox`) with allow/deny policy and auto-pruning.
- Agent: add sandboxed Chromium browser (CDP + optional noVNC observer) for sandboxed sessions. - Agent: add sandboxed Chromium browser (CDP + optional noVNC observer) for sandboxed sessions.
- Agent: add configurable Docker hardening options for sandboxed sessions (resource limits, seccomp/apparmor, DNS/hosts) and default network isolation.
- Nodes: add `location.get` with Always/Precise settings on macOS/iOS/Android plus CLI/tool support. - Nodes: add `location.get` with Always/Precise settings on macOS/iOS/Android plus CLI/tool support.
- Sessions: add agenttoagent post step with `ANNOUNCE_SKIP` to suppress channel announcements. - Sessions: add agenttoagent post step with `ANNOUNCE_SKIP` to suppress channel announcements.
@@ -57,7 +60,7 @@
- Logging: decouple file log levels from console verbosity; verbose-only details are captured when `logging.level` is debug/trace. - Logging: decouple file log levels from console verbosity; verbose-only details are captured when `logging.level` is debug/trace.
- Build: fix regex literal in tool-meta path detection (watch build error). - Build: fix regex literal in tool-meta path detection (watch build error).
- Build: require AVX2 Bun for x86_64 relay packaging (reject baseline builds). - Build: require AVX2 Bun for x86_64 relay packaging (reject baseline builds).
- Build: drop stale ClawdisCLI product from macOS build-and-run script. - Build: drop stale ClawdbotCLI product from macOS build-and-run script.
- Auto-reply: add run-level telemetry + typing TTL guardrails to diagnose stuck replies. - Auto-reply: add run-level telemetry + typing TTL guardrails to diagnose stuck replies.
- WhatsApp: honor per-group mention gating overrides when group ids are stored as session keys. - WhatsApp: honor per-group mention gating overrides when group ids are stored as session keys.
- Canvas host: reuse shared handler to avoid double file watchers and close watchers on error (EMFILE resilience). - Canvas host: reuse shared handler to avoid double file watchers and close watchers on error (EMFILE resilience).
@@ -95,8 +98,8 @@
- Sessions: group keys now use `surface:group:<id>` / `surface:channel:<id>`; legacy `group:*` keys migrate on next message; `groupdm` keys are no longer recognized. - Sessions: group keys now use `surface:group:<id>` / `surface:channel:<id>`; legacy `group:*` keys migrate on next message; `groupdm` keys are no longer recognized.
- Discord: remove legacy `discord.allowFrom`, `discord.guildAllowFrom`, and `discord.requireMention`; use `discord.dm` + `discord.guilds`. - Discord: remove legacy `discord.allowFrom`, `discord.guildAllowFrom`, and `discord.requireMention`; use `discord.dm` + `discord.guilds`.
- Providers: Discord/Telegram no longer auto-start from env tokens alone; add `discord: { enabled: true }` / `telegram: { enabled: true }` to your config when using `DISCORD_BOT_TOKEN` / `TELEGRAM_BOT_TOKEN`. - Providers: Discord/Telegram no longer auto-start from env tokens alone; add `discord: { enabled: true }` / `telegram: { enabled: true }` to your config when using `DISCORD_BOT_TOKEN` / `TELEGRAM_BOT_TOKEN`.
- Config: remove `routing.allowFrom`; use `whatsapp.allowFrom` instead (run `clawdis doctor` to migrate). - Config: remove `routing.allowFrom`; use `whatsapp.allowFrom` instead (run `clawdbot doctor` to migrate).
- Config: remove `routing.groupChat.requireMention` + `telegram.requireMention`; use `whatsapp.groups`, `imessage.groups`, and `telegram.groups` defaults instead (run `clawdis doctor` to migrate). - Config: remove `routing.groupChat.requireMention` + `telegram.requireMention`; use `whatsapp.groups`, `imessage.groups`, and `telegram.groups` defaults instead (run `clawdbot doctor` to migrate).
### Features ### Features
- Discord: expand `discord` tool actions (reactions, stickers, polls, threads, search, moderation gates) (#115) — thanks @thewilloftheshadow. - Discord: expand `discord` tool actions (reactions, stickers, polls, threads, search, moderation gates) (#115) — thanks @thewilloftheshadow.
@@ -104,7 +107,7 @@
- Talk mode: continuous speech conversations (macOS/iOS/Android) with ElevenLabs TTS, reply directives, and optional interrupt-on-speech. - Talk mode: continuous speech conversations (macOS/iOS/Android) with ElevenLabs TTS, reply directives, and optional interrupt-on-speech.
- Auto-reply: expand queue modes (steer/followup/collect/steer-backlog) with debounce/cap/drop options and followup backlog handling. - Auto-reply: expand queue modes (steer/followup/collect/steer-backlog) with debounce/cap/drop options and followup backlog handling.
- UI: add optional `ui.seamColor` accent to tint the Talk Mode side bubble (macOS/iOS/Android). - UI: add optional `ui.seamColor` accent to tint the Talk Mode side bubble (macOS/iOS/Android).
- Nix mode: opt-in declarative config + read-only settings UI when `CLAWDIS_NIX_MODE=1` (thanks @joshp123 for the persistence — earned my trust; I'll merge these going forward). - Nix mode: opt-in declarative config + read-only settings UI when `CLAWDBOT_NIX_MODE=1` (thanks @joshp123 for the persistence — earned my trust; I'll merge these going forward).
- CLI: add Google Antigravity OAuth auth option for Claude Opus 4.5/Gemini 3 (#88) — thanks @mukhtharcm. - CLI: add Google Antigravity OAuth auth option for Claude Opus 4.5/Gemini 3 (#88) — thanks @mukhtharcm.
- Agent runtime: accept legacy `Z_AI_API_KEY` for Z.AI provider auth (maps to `ZAI_API_KEY`). - Agent runtime: accept legacy `Z_AI_API_KEY` for Z.AI provider auth (maps to `ZAI_API_KEY`).
- Groups: add per-group mention gating defaults/overrides for Telegram/WhatsApp/iMessage via `*.groups` with `"*"` defaults; Discord now supports `discord.guilds."*"` as a default. - Groups: add per-group mention gating defaults/overrides for Telegram/WhatsApp/iMessage via `*.groups` with `"*"` defaults; Discord now supports `discord.guilds."*"` as a default.
@@ -114,7 +117,7 @@
- iMessage: add imsg JSON-RPC integration (stdio), chat_id routing, and group chat support. - iMessage: add imsg JSON-RPC integration (stdio), chat_id routing, and group chat support.
- Chat UI: add recent-session dropdown switcher (main first) in macOS/iOS/Android + Control UI. - Chat UI: add recent-session dropdown switcher (main first) in macOS/iOS/Android + Control UI.
- UI: add Discord/Signal/iMessage connection panels in macOS + Control UI (thanks @thewilloftheshadow). - UI: add Discord/Signal/iMessage connection panels in macOS + Control UI (thanks @thewilloftheshadow).
- Discord: allow agent-triggered reactions via `clawdis_discord` when enabled, and surface message ids in context. - Discord: allow agent-triggered reactions via `clawdbot_discord` when enabled, and surface message ids in context.
- Discord: revamp guild routing config with per-guild/channel rules and slugged display names; add optional group DM support (default off). - Discord: revamp guild routing config with per-guild/channel rules and slugged display names; add optional group DM support (default off).
- Discord: remove legacy guild/channel ignore lists in favor of per-guild allowlists (and proposed per-guild ignore lists). - Discord: remove legacy guild/channel ignore lists in favor of per-guild allowlists (and proposed per-guild ignore lists).
- Skills: add Trello skill for board/list/card management (thanks @clawd). - Skills: add Trello skill for board/list/card management (thanks @clawd).
@@ -126,7 +129,7 @@
- CLI: add `configure`, `doctor`, and `update` wizards for ongoing setup, health checks, and modernization. - CLI: add `configure`, `doctor`, and `update` wizards for ongoing setup, health checks, and modernization.
- CLI: add Signal CLI auto-install from GitHub releases in the wizard and persist wizard run metadata in config. - CLI: add Signal CLI auto-install from GitHub releases in the wizard and persist wizard run metadata in config.
- CLI: add remote gateway client config (gateway.remote.*) with Bonjour-assisted discovery. - CLI: add remote gateway client config (gateway.remote.*) with Bonjour-assisted discovery.
- CLI: enhance `clawdis tui` with model/session pickers, tool cards, and slash commands (local or remote). - CLI: enhance `clawdbot tui` with model/session pickers, tool cards, and slash commands (local or remote).
- Gateway: allow `sessions.patch` to set per-session model overrides (used by the TUI `/model` flow). - Gateway: allow `sessions.patch` to set per-session model overrides (used by the TUI `/model` flow).
- Skills: allow `bun` as a node manager for skill installs. - Skills: allow `bun` as a node manager for skill installs.
- Skills: add `things-mac` (Things 3 CLI) for read/search plus add/update via URL scheme. - Skills: add `things-mac` (Things 3 CLI) for read/search plus add/update via URL scheme.
@@ -151,7 +154,7 @@
- Chat UI: add extra top padding before the first message bubble in Web Chat (macOS/iOS/Android). - Chat UI: add extra top padding before the first message bubble in Web Chat (macOS/iOS/Android).
- Control UI: refine Web Chat session selector styling (chevron spacing + background). - Control UI: refine Web Chat session selector styling (chevron spacing + background).
- WebChat: stream live updates for sessions even when runs start outside the chat UI. - WebChat: stream live updates for sessions even when runs start outside the chat UI.
- Gateway CLI: read `CLAWDIS_GATEWAY_PASSWORD` from environment in `callGateway()` — allows `doctor`/`health` commands to auth without explicit `--password` flag. - Gateway CLI: read `CLAWDBOT_GATEWAY_PASSWORD` from environment in `callGateway()` — allows `doctor`/`health` commands to auth without explicit `--password` flag.
- Gateway: add password auth support for remote gateway connections (thanks @jeffersonwarrior). - Gateway: add password auth support for remote gateway connections (thanks @jeffersonwarrior).
- Auto-reply: strip stray leading/trailing `HEARTBEAT_OK` from normal replies; drop short (≤ 30 chars) heartbeat acks. - Auto-reply: strip stray leading/trailing `HEARTBEAT_OK` from normal replies; drop short (≤ 30 chars) heartbeat acks.
- WhatsApp auto-reply: default to self-only when no config is present. - WhatsApp auto-reply: default to self-only when no config is present.
@@ -201,7 +204,7 @@
- Docs: clarify self-chat mode and group mention gating config (#111) — thanks @rafaelreis-r. - Docs: clarify self-chat mode and group mention gating config (#111) — thanks @rafaelreis-r.
- Browser tools: `upload` supports auto-click refs, direct `inputRef`/`element` file inputs, and emits input/change after `setFiles` so JS-heavy sites pick up attachments. - Browser tools: `upload` supports auto-click refs, direct `inputRef`/`element` file inputs, and emits input/change after `setFiles` so JS-heavy sites pick up attachments.
- Browser tools: harden CDP readiness (HTTP + WS), retry CDP connects, and auto-restart the clawd browser when the socket handshake stalls. - Browser tools: harden CDP readiness (HTTP + WS), retry CDP connects, and auto-restart the clawd browser when the socket handshake stalls.
- Browser CLI: add `clawdis browser reset-profile` to move the clawd profile to Trash when it gets wedged. - Browser CLI: add `clawdbot browser reset-profile` to move the clawd profile to Trash when it gets wedged.
- Signal: fix daemon startup race (wait for `/api/v1/check`) and normalize JSON-RPC `version` probe parsing. - Signal: fix daemon startup race (wait for `/api/v1/check`) and normalize JSON-RPC `version` probe parsing.
- Docs/Signal: clarify bot-number vs personal-account setup (self-chat loop protection) and add a quickstart config snippet. - Docs/Signal: clarify bot-number vs personal-account setup (self-chat loop protection) and add a quickstart config snippet.
- Docs: refresh the CLI wizard guide and highlight onboarding in the README. - Docs: refresh the CLI wizard guide and highlight onboarding in the README.
@@ -265,7 +268,7 @@
- macOS menu: top status line now shows pending node pairing approvals (incl. repairs). - macOS menu: top status line now shows pending node pairing approvals (incl. repairs).
- CLI: avoid spurious gateway close errors after successful request/response cycles. - CLI: avoid spurious gateway close errors after successful request/response cycles.
- Agent runtime: clamp tool-result images to the 5MB Anthropic limit to avoid hard request rejections. - Agent runtime: clamp tool-result images to the 5MB Anthropic limit to avoid hard request rejections.
- Agent runtime: write v2 session headers so Pi session branching stays in the Clawdis sessions dir. - Agent runtime: write v2 session headers so Pi session branching stays in the Clawdbot sessions dir.
- Tests: add Swift Testing coverage for camera errors and Kotest coverage for Android bridge endpoints. - Tests: add Swift Testing coverage for camera errors and Kotest coverage for Android bridge endpoints.
## 2.0.0-beta4 — 2025-12-27 ## 2.0.0-beta4 — 2025-12-27
@@ -281,12 +284,12 @@
## 2.0.0-beta3 — 2025-12-27 ## 2.0.0-beta3 — 2025-12-27
### Highlights ### Highlights
- First-class Clawdis tools (browser, canvas, nodes, cron) replace the old `clawdis-*` skills; tool schemas are now injected directly into the agent runtime. - First-class Clawdbot tools (browser, canvas, nodes, cron) replace the old `clawdbot-*` skills; tool schemas are now injected directly into the agent runtime.
- Per-session model selection + custom model providers: `models.providers` merges into `~/.clawdis/agent/models.json` (merge/replace modes) for LiteLLM, local OpenAI-compatible servers, Anthropic proxies, etc. - Per-session model selection + custom model providers: `models.providers` merges into `~/.clawdbot/agent/models.json` (merge/replace modes) for LiteLLM, local OpenAI-compatible servers, Anthropic proxies, etc.
- Group chat activation modes: per-group `/activation mention|always` command with status visibility. - Group chat activation modes: per-group `/activation mention|always` command with status visibility.
- Discord bot transport for DMs and guild text channels, with allowlists + mention gating. - Discord bot transport for DMs and guild text channels, with allowlists + mention gating.
- Gateway webhooks: external `wake` and isolated `agent` hooks with dedicated token auth. - Gateway webhooks: external `wake` and isolated `agent` hooks with dedicated token auth.
- Hook mappings + Gmail Pub/Sub helper (`clawdis hooks gmail setup/run`) with auto-renew + Tailscale Funnel support. - Hook mappings + Gmail Pub/Sub helper (`clawdbot hooks gmail setup/run`) with auto-renew + Tailscale Funnel support.
- Command queue modes + per-session overrides (`/queue ...`) and new `agent.maxConcurrent` cap for safe parallelism across sessions. - Command queue modes + per-session overrides (`/queue ...`) and new `agent.maxConcurrent` cap for safe parallelism across sessions.
- Background bash tasks: `bash` auto-yields after 20s (or on demand) with a `process` tool to list/poll/log/write/kill sessions. - Background bash tasks: `bash` auto-yields after 20s (or on demand) with a `process` tool to list/poll/log/write/kill sessions.
- Gateway in-process restart: `gateway` tool action triggers a SIGUSR1 restart without needing a supervisor. - Gateway in-process restart: `gateway` tool action triggers a SIGUSR1 restart without needing a supervisor.
@@ -383,7 +386,7 @@
### Tests ### Tests
- Coverage added for models config merging, WhatsApp reply context, QR login flows, auto-reply behavior, and gateway SIGTERM timeouts. - Coverage added for models config merging, WhatsApp reply context, QR login flows, auto-reply behavior, and gateway SIGTERM timeouts.
- Added gateway webhook coverage (auth, validation, and summary posting). - Added gateway webhook coverage (auth, validation, and summary posting).
- Vitest now isolates HOME/XDG config roots so tests never touch a real `~/.clawdis` install. - Vitest now isolates HOME/XDG config roots so tests never touch a real `~/.clawdbot` install.
## 2.0.0-beta2 — 2025-12-21 ## 2.0.0-beta2 — 2025-12-21
@@ -411,41 +414,41 @@ Second beta focused on bundled gateway packaging, skills management, onboarding
- Remote/local gateway: auto-enable local gateway, clearer labels, re-ensure remote tunnel, hide local bridge discovery in remote mode. - Remote/local gateway: auto-enable local gateway, clearer labels, re-ensure remote tunnel, hide local bridge discovery in remote mode.
### Build, CI, deps ### Build, CI, deps
- Bundled playwright-core + chromium-bidi/long; bun gateway bytecode builds; swiftformat/biome CI fixes; iOS lint script updates; Android icon/compiler updates; ignored new ClawdisKit `.swiftpm` path. - Bundled playwright-core + chromium-bidi/long; bun gateway bytecode builds; swiftformat/biome CI fixes; iOS lint script updates; Android icon/compiler updates; ignored new ClawdbotKit `.swiftpm` path.
### Docs ### Docs
- README architecture refresh + npm header image fix; onboarding/bootstrap steps; skills install guidance + new skills; browser/canvas control docs; bundled gateway + DMG packaging notes. - README architecture refresh + npm header image fix; onboarding/bootstrap steps; skills install guidance + new skills; browser/canvas control docs; bundled gateway + DMG packaging notes.
## 2.0.0-beta1 — 2025-12-19 ## 2.0.0-beta1 — 2025-12-19
First Clawdis release post rebrand. This is a semver-major because we dropped legacy providers/agents and moved defaults to new paths while adding a full macOS companion app, a WebSocket Gateway, and an iOS node. First Clawdbot release post rebrand. This is a semver-major because we dropped legacy providers/agents and moved defaults to new paths while adding a full macOS companion app, a WebSocket Gateway, and an iOS node.
### Bug Fixes ### Bug Fixes
- macOS: Voice Wake / push-to-talk no longer initialize `AVAudioEngine` at app launch, preventing Bluetooth headphones from switching into headset profile when voice features are unused. (Thanks @Nachx639) - macOS: Voice Wake / push-to-talk no longer initialize `AVAudioEngine` at app launch, preventing Bluetooth headphones from switching into headset profile when voice features are unused. (Thanks @Nachx639)
### Breaking ### Breaking
- Renamed to **Clawdis**: defaults now live under `~/.clawdis` (sessions in `~/.clawdis/sessions/`, IPC at `~/.clawdis/clawdis.sock`, logs in `/tmp/clawdis`). Launchd labels and config filenames follow the new name; legacy stores are copied forward on first run. - Renamed to **Clawdbot**: defaults now live under `~/.clawdbot` (sessions in `~/.clawdbot/sessions/`, IPC at `~/.clawdbot/clawdbot.sock`, logs in `/tmp/clawdbot`). Launchd labels and config filenames follow the new name; legacy stores are copied forward on first run.
- Pi only: only the embedded Pi runtime remains, and the agent CLI/CLI flags for Claude/Codex/Gemini were removed. The Pi CLI runs in RPC mode with a persistent worker. - Pi only: only the embedded Pi runtime remains, and the agent CLI/CLI flags for Claude/Codex/Gemini were removed. The Pi CLI runs in RPC mode with a persistent worker.
- WhatsApp Web is the only transport; Twilio support and related CLI flags/tests were removed. - WhatsApp Web is the only transport; Twilio support and related CLI flags/tests were removed.
- Direct chats now collapse into a single `main` session by default (no config needed); groups stay isolated as `group:<jid>`. - Direct chats now collapse into a single `main` session by default (no config needed); groups stay isolated as `group:<jid>`.
- Gateway is now a loopback-only WebSocket daemon (`ws://127.0.0.1:18789`) that owns all providers/state; clients (CLI, WebChat, macOS app, nodes) connect to it. Start it explicitly (`clawdis gateway …`) or via Clawdis.app; helper subcommands no longer auto-spawn a gateway. - Gateway is now a loopback-only WebSocket daemon (`ws://127.0.0.1:18789`) that owns all providers/state; clients (CLI, WebChat, macOS app, nodes) connect to it. Start it explicitly (`clawdbot gateway …`) or via Clawdbot.app; helper subcommands no longer auto-spawn a gateway.
### Gateway, nodes, and automation ### Gateway, nodes, and automation
- New typed Gateway WS protocol (JSON schema validated) with `clawdis gateway {health,status,send,agent,call}` helpers and structured presence/instance updates for all clients. - New typed Gateway WS protocol (JSON schema validated) with `clawdbot gateway {health,status,send,agent,call}` helpers and structured presence/instance updates for all clients.
- Optional LAN-facing bridge (`tcp://0.0.0.0:18790`) keeps the Gateway loopback-only while enabling direct Bonjour-discovered connections for paired nodes. - Optional LAN-facing bridge (`tcp://0.0.0.0:18790`) keeps the Gateway loopback-only while enabling direct Bonjour-discovered connections for paired nodes.
- Node pairing + management via `clawdis nodes {pending,approve,reject,invoke}` (used by the iOS node and future remote nodes). - Node pairing + management via `clawdbot nodes {pending,approve,reject,invoke}` (used by the iOS node and future remote nodes).
- Cron jobs are Gateway-owned (`clawdis cron …`) with run history stored as JSONL and support for “isolated summary” posting into the main session. - Cron jobs are Gateway-owned (`clawdbot cron …`) with run history stored as JSONL and support for “isolated summary” posting into the main session.
### macOS companion app ### macOS companion app
- **Clawdis.app menu bar companion**: packaged, signed bundle with gateway start/stop, launchd toggle, project-root and pnpm/node auto-resolution, live log shortcut, restart button, and status/recipient table plus badges/dimming for attention and paused states. - **Clawdbot.app menu bar companion**: packaged, signed bundle with gateway start/stop, launchd toggle, project-root and pnpm/node auto-resolution, live log shortcut, restart button, and status/recipient table plus badges/dimming for attention and paused states.
- **On-device Voice Wake**: Apple speech recognizer with wake-word table, language picker, live mic meter, “hold until silence,” animated ears/legs, and main-session routing that replies on the **last used surface** (WhatsApp/Telegram/WebChat). Delivery failures are logged, and the run remains visible via WebChat/session logs. - **On-device Voice Wake**: Apple speech recognizer with wake-word table, language picker, live mic meter, “hold until silence,” animated ears/legs, and main-session routing that replies on the **last used surface** (WhatsApp/Telegram/WebChat). Delivery failures are logged, and the run remains visible via WebChat/session logs.
- **WebChat & Debugging**: bundled WebChat UI, Debug tab with heartbeat sliders, session-store picker, log opener (`clawlog`), gateway restart, health probes, and scrollable settings panes. - **WebChat & Debugging**: bundled WebChat UI, Debug tab with heartbeat sliders, session-store picker, log opener (`clawlog`), gateway restart, health probes, and scrollable settings panes.
- **Browser control**: manage clawds dedicated Chrome/Chromium with tab listing/open/focus/close, screenshots, DOM query/dump, and “AI snapshots” (aria/domSnapshot/ai) via `clawdis browser …` and UI controls. - **Browser control**: manage clawds dedicated Chrome/Chromium with tab listing/open/focus/close, screenshots, DOM query/dump, and “AI snapshots” (aria/domSnapshot/ai) via `clawdbot browser …` and UI controls.
- **Remote gateway control**: Bonjour discovery for local masters plus SSH-tunnel fallback for remote control when multicast is unavailable. - **Remote gateway control**: Bonjour discovery for local masters plus SSH-tunnel fallback for remote control when multicast is unavailable.
### iOS node ### iOS node
- New iOS companion app that pairs to the Gateway bridge, reports presence as a node, and exposes a WKWebView “Canvas” for agent-driven UI. - New iOS companion app that pairs to the Gateway bridge, reports presence as a node, and exposes a WKWebView “Canvas” for agent-driven UI.
- `clawdis nodes invoke` supports `canvas.eval` and `canvas.snapshot` to drive and verify the iOS Canvas (fails fast when the iOS node is backgrounded). - `clawdbot nodes invoke` supports `canvas.eval` and `canvas.snapshot` to drive and verify the iOS Canvas (fails fast when the iOS node is backgrounded).
- Voice wake words are configurable in-app; the iOS node reconnects to the last bridge when credentials are still present in Keychain. - Voice wake words are configurable in-app; the iOS node reconnects to the last bridge when credentials are still present in Keychain.
### WhatsApp & agent experience ### WhatsApp & agent experience
@@ -453,12 +456,12 @@ First Clawdis release post rebrand. This is a semver-major because we dropped le
- Thinking/verbosity directives: `/think` and `/verbose` acknowledge and persist per session while allowing inline overrides; verbose mode streams tool metadata with emoji/args/previews and coalesces bursts to reduce WhatsApp noise. - Thinking/verbosity directives: `/think` and `/verbose` acknowledge and persist per session while allowing inline overrides; verbose mode streams tool metadata with emoji/args/previews and coalesces bursts to reduce WhatsApp noise.
- Heartbeats: configurable cadence with CLI/GUI toggles; directive acks suppressed during heartbeats; array/multi-payload replies normalized for Baileys. - Heartbeats: configurable cadence with CLI/GUI toggles; directive acks suppressed during heartbeats; array/multi-payload replies normalized for Baileys.
- Reply quality: smarter chunking on words/newlines, fallback warnings when media fails to send, self-number mention detection, and primed group sessions send the roster on first turn. - Reply quality: smarter chunking on words/newlines, fallback warnings when media fails to send, self-number mention detection, and primed group sessions send the roster on first turn.
- In-chat `/status`: prints agent readiness, session context usage %, current thinking/verbose options, and when the WhatsApp web creds were refreshed (helps decide when to re-scan QR); still available via `clawdis status` CLI for web session health. - In-chat `/status`: prints agent readiness, session context usage %, current thinking/verbose options, and when the WhatsApp web creds were refreshed (helps decide when to re-scan QR); still available via `clawdbot status` CLI for web session health.
### CLI, RPC, and health ### CLI, RPC, and health
- New `clawdis agent` command plus a persistent Pi RPC worker (auto-started) enables direct agent chats; `clawdis status` renders a colored session/recipient table. - New `clawdbot agent` command plus a persistent Pi RPC worker (auto-started) enables direct agent chats; `clawdbot status` renders a colored session/recipient table.
- `clawdis health` probes WhatsApp link status, connect latency, heartbeat interval, session-store recency, and IPC socket presence (JSON mode for monitors). - `clawdbot health` probes WhatsApp link status, connect latency, heartbeat interval, session-store recency, and IPC socket presence (JSON mode for monitors).
- Added `--help`/`--version` flags; login/logout accept `--provider` (WhatsApp default). Console output is mirrored into pino logs under `/tmp/clawdis`. - Added `--help`/`--version` flags; login/logout accept `--provider` (WhatsApp default). Console output is mirrored into pino logs under `/tmp/clawdbot`.
- RPC stability: stdin/stdout loop for Pi, auto-restart worker, raw error surfacing, and deliver-via-RPC when JSON agent output is returned. - RPC stability: stdin/stdout loop for Pi, auto-restart worker, raw error surfacing, and deliver-via-RPC when JSON agent output is returned.
### Security & hardening ### Security & hardening
@@ -468,32 +471,32 @@ First Clawdis release post rebrand. This is a semver-major because we dropped le
### Docs ### Docs
- Added `docs/telegram.md` outlining the Telegram Bot API provider (grammY) and how it shares the `main` session. Default grammY throttler keeps Bot API calls under rate limits. - Added `docs/telegram.md` outlining the Telegram Bot API provider (grammY) and how it shares the `main` session. Default grammY throttler keeps Bot API calls under rate limits.
- Gateway can run WhatsApp + Telegram together when configured; `clawdis send --provider telegram …` sends via the Telegram bot (webhook/proxy options documented). - Gateway can run WhatsApp + Telegram together when configured; `clawdbot send --provider telegram …` sends via the Telegram bot (webhook/proxy options documented).
## 1.5.0 — 2025-12-05 ## 1.5.0 — 2025-12-05
### Breaking ### Breaking
- Dropped all non-Pi agents (Claude, Codex, Gemini, Opencode); only the embedded Pi runtime remains and related CLI helpers have been removed. - Dropped all non-Pi agents (Claude, Codex, Gemini, Opencode); only the embedded Pi runtime remains and related CLI helpers have been removed.
- Removed Twilio support and all related commands/options (webhook/up/provider flags/wait-poll); CLAWDIS is Baileys Web-only. - Removed Twilio support and all related commands/options (webhook/up/provider flags/wait-poll); CLAWDBOT is Baileys Web-only.
### Changes ### Changes
- Default agent handling now favors Pi RPC while falling back to plain command execution for non-Pi invocations, keeping heartbeat/session plumbing intact. - Default agent handling now favors Pi RPC while falling back to plain command execution for non-Pi invocations, keeping heartbeat/session plumbing intact.
- Documentation updated to reflect Pi-only support and to mark legacy Claude paths as historical. - Documentation updated to reflect Pi-only support and to mark legacy Claude paths as historical.
- Status command reports web session health + session recipients; config paths are locked to `~/.clawdis` with session metadata stored under `~/.clawdis/sessions/`. - Status command reports web session health + session recipients; config paths are locked to `~/.clawdbot` with session metadata stored under `~/.clawdbot/sessions/`.
- Simplified send/agent/gateway/heartbeat to web-only delivery; removed Twilio mocks/tests and dead code. - Simplified send/agent/gateway/heartbeat to web-only delivery; removed Twilio mocks/tests and dead code.
- Pi RPC timeout is now inactivity-based (5m without events) and error messages show seconds only. - Pi RPC timeout is now inactivity-based (5m without events) and error messages show seconds only.
- Pi sessions now write to `~/.clawdis/sessions/` by default (legacy session logs from older installs are copied over when present). - Pi sessions now write to `~/.clawdbot/sessions/` by default (legacy session logs from older installs are copied over when present).
- Directive triggers (`/think`, `/verbose`, `/stop` et al.) now reply immediately using normalized bodies (timestamps/group prefixes stripped) without waiting for the agent. - Directive triggers (`/think`, `/verbose`, `/stop` et al.) now reply immediately using normalized bodies (timestamps/group prefixes stripped) without waiting for the agent.
- Directive/system acks carry a `⚙️` prefix and verbose parsing rejects typoed `/ver*` strings so unrelated text doesnt flip verbosity. - Directive/system acks carry a `⚙️` prefix and verbose parsing rejects typoed `/ver*` strings so unrelated text doesnt flip verbosity.
- Batched history blocks no longer trip directive parsing; `/think` in prior messages won't emit stray acknowledgements. - Batched history blocks no longer trip directive parsing; `/think` in prior messages won't emit stray acknowledgements.
- RPC fallbacks no longer echo the user's prompt (e.g., pasting a link) when the agent returns no assistant text. - RPC fallbacks no longer echo the user's prompt (e.g., pasting a link) when the agent returns no assistant text.
- Heartbeat prompts with `/think` no longer send directive acks; heartbeat replies stay silent on settings. - Heartbeat prompts with `/think` no longer send directive acks; heartbeat replies stay silent on settings.
- `clawdis sessions` now renders a colored table (a la oracle) with context usage shown in k tokens and percent of the context window. - `clawdbot sessions` now renders a colored table (a la oracle) with context usage shown in k tokens and percent of the context window.
## 1.4.1 — 2025-12-04 ## 1.4.1 — 2025-12-04
### Changes ### Changes
- Added `clawdis agent` CLI command to talk directly to the configured agent using existing session handling (no WhatsApp send), with JSON output and delivery option. - Added `clawdbot agent` CLI command to talk directly to the configured agent using existing session handling (no WhatsApp send), with JSON output and delivery option.
- `/new` reset trigger now works even when inbound messages have timestamp prefixes (e.g., `[Dec 4 17:35]`). - `/new` reset trigger now works even when inbound messages have timestamp prefixes (e.g., `[Dec 4 17:35]`).
- WhatsApp mention parsing accepts nullable arrays and flattens safely to avoid missed mentions. - WhatsApp mention parsing accepts nullable arrays and flattens safely to avoid missed mentions.
@@ -501,7 +504,7 @@ First Clawdis release post rebrand. This is a semver-major because we dropped le
### Highlights ### Highlights
- **Thinking directives & state:** `/t|/think|/thinking <level>` (aliases off|minimal|low|medium|high|max/highest). Inline applies to that message; directive-only message pins the level for the session; `/think:off` clears. Resolution: inline > session override > `agent.thinkingDefault` > off. Pi gets `--thinking <level>` (except off); other agents append cue words (`think``think hard``think harder``ultrathink`). Heartbeat probe uses `HEARTBEAT /think:high`. - **Thinking directives & state:** `/t|/think|/thinking <level>` (aliases off|minimal|low|medium|high|max/highest). Inline applies to that message; directive-only message pins the level for the session; `/think:off` clears. Resolution: inline > session override > `agent.thinkingDefault` > off. Pi gets `--thinking <level>` (except off); other agents append cue words (`think``think hard``think harder``ultrathink`). Heartbeat probe uses `HEARTBEAT /think:high`.
- **Group chats (web provider):** Clawdis now fully supports WhatsApp groups: mention-gated triggers (including image-only @ mentions), recent group history injection, per-group sessions, sender attribution, and a first-turn primer with group subject/member roster; heartbeats are skipped for groups. - **Group chats (web provider):** Clawdbot now fully supports WhatsApp groups: mention-gated triggers (including image-only @ mentions), recent group history injection, per-group sessions, sender attribution, and a first-turn primer with group subject/member roster; heartbeats are skipped for groups.
- **Group session primer:** The first turn of a group session now tells the agent it is in a WhatsApp group and lists known members/subject so it can address the right speaker. - **Group session primer:** The first turn of a group session now tells the agent it is in a WhatsApp group and lists known members/subject so it can address the right speaker.
- **Media failures are surfaced:** When a web auto-reply media fetch/send fails (e.g., HTTP 404), we now append a warning to the fallback text so you know the attachment was skipped. - **Media failures are surfaced:** When a web auto-reply media fetch/send fails (e.g., HTTP 404), we now append a warning to the fallback text so you know the attachment was skipped.
- **Verbose directives + session hints:** `/v|/verbose on|full|off` mirrors thinking: inline > session > config default. Directive-only replies with an acknowledgement; invalid levels return a hint. When enabled, tool results from JSON-emitting agents (Pi, etc.) are forwarded as metadata-only `[🛠️ <tool-name> <arg>]` messages (now streamed as they happen), and new sessions surface a `🧭 New session: <id>` hint. - **Verbose directives + session hints:** `/v|/verbose on|full|off` mirrors thinking: inline > session > config default. Directive-only replies with an acknowledgement; invalid levels return a hint. When enabled, tool results from JSON-emitting agents (Pi, etc.) are forwarded as metadata-only `[🛠️ <tool-name> <arg>]` messages (now streamed as they happen), and new sessions surface a `🧭 New session: <id>` hint.
@@ -510,7 +513,7 @@ First Clawdis release post rebrand. This is a semver-major because we dropped le
- **Pi stability:** RPC replies buffered until the assistant turn finishes; parsers return consistent `texts[]`; web auto-replies keep a warm Pi RPC process to avoid cold starts. - **Pi stability:** RPC replies buffered until the assistant turn finishes; parsers return consistent `texts[]`; web auto-replies keep a warm Pi RPC process to avoid cold starts.
- **Claude prompt flow:** One-time `sessionIntro` with per-message `/think:high` bodyPrefix; system prompt always sent on first turn even with `sendSystemOnce`. - **Claude prompt flow:** One-time `sessionIntro` with per-message `/think:high` bodyPrefix; system prompt always sent on first turn even with `sendSystemOnce`.
- **Heartbeat UX:** Backpressure skips reply heartbeats while other commands run; skips dont refresh session `updatedAt`; web heartbeats normalize array payloads and optional `heartbeatCommand`. - **Heartbeat UX:** Backpressure skips reply heartbeats while other commands run; skips dont refresh session `updatedAt`; web heartbeats normalize array payloads and optional `heartbeatCommand`.
- **Control via WhatsApp:** Send `/restart` to restart the launchd service (`com.steipete.clawdis`) from your allowed numbers. - **Control via WhatsApp:** Send `/restart` to restart the launchd service (`com.steipete.clawdbot`) from your allowed numbers.
- **Pi completion signal:** RPC now resolves on Pis `agent_end` (or process exit) so late assistant messages arent truncated; 5-minute hard cap only as a failsafe. - **Pi completion signal:** RPC now resolves on Pis `agent_end` (or process exit) so late assistant messages arent truncated; 5-minute hard cap only as a failsafe.
### Reliability & UX ### Reliability & UX
@@ -523,7 +526,7 @@ First Clawdis release post rebrand. This is a semver-major because we dropped le
- Verbose tool messages now include emoji + args + a short result preview for bash/read/edit/write/attach (derived from RPC tool start/end events). - Verbose tool messages now include emoji + args + a short result preview for bash/read/edit/write/attach (derived from RPC tool start/end events).
### Security / Hardening ### Security / Hardening
- IPC socket hardened (0700 dir / 0600 socket, no symlinks/foreign owners); `clawdis logout` also prunes session store. - IPC socket hardened (0700 dir / 0600 socket, no symlinks/foreign owners); `clawdbot logout` also prunes session store.
- Media server blocks symlinks and enforces path containment; logging rotates daily and prunes >24h. - Media server blocks symlinks and enforces path containment; logging rotates daily and prunes >24h.
### Bug Fixes ### Bug Fixes
@@ -551,11 +554,11 @@ First Clawdis release post rebrand. This is a semver-major because we dropped le
- Heartbeat alerts now honor `responsePrefix`. - Heartbeat alerts now honor `responsePrefix`.
- Command failures return user-friendly messages. - Command failures return user-friendly messages.
- Test session isolation to avoid touching real `sessions.json`. - Test session isolation to avoid touching real `sessions.json`.
- (Removed in 2.0.0) IPC reuse for `clawdis send/heartbeat` prevents Signal/WhatsApp session corruption. - (Removed in 2.0.0) IPC reuse for `clawdbot send/heartbeat` prevents Signal/WhatsApp session corruption.
- Web send respects media kind (image/audio/video/document) with correct limits. - Web send respects media kind (image/audio/video/document) with correct limits.
### Changes ### Changes
- (Removed in 2.0.0) IPC gateway socket at `~/.clawdis/ipc/gateway.sock` with automatic CLI fallback. - (Removed in 2.0.0) IPC gateway socket at `~/.clawdbot/ipc/gateway.sock` with automatic CLI fallback.
- Batched inbound messages with timestamps; typing indicator after sends. - Batched inbound messages with timestamps; typing indicator after sends.
- Watchdog restarts WhatsApp after long inactivity; heartbeat logging includes minutes since last message. - Watchdog restarts WhatsApp after long inactivity; heartbeat logging includes minutes since last message.
- Early `allowFrom` filtering before decryption. - Early `allowFrom` filtering before decryption.
@@ -564,7 +567,7 @@ First Clawdis release post rebrand. This is a semver-major because we dropped le
## 1.2.2 — 2025-11-28 ## 1.2.2 — 2025-11-28
### Changes ### Changes
- Manual heartbeat sends: `clawdis heartbeat --message/--body` (web provider only); `--dry-run` previews payloads. - Manual heartbeat sends: `clawdbot heartbeat --message/--body` (web provider only); `--dry-run` previews payloads.
## 1.2.1 — 2025-11-28 ## 1.2.1 — 2025-11-28

View File

@@ -1,9 +1,9 @@
# Contributing to Clawdis # Contributing to Clawdbot
Welcome to the lobster tank! 🦞 Welcome to the lobster tank! 🦞
## Quick Links ## Quick Links
- **GitHub:** https://github.com/steipete/clawdis - **GitHub:** https://github.com/clawdbot/clawdbot
- **Discord:** https://discord.gg/qkhbAGHRBT - **Discord:** https://discord.gg/qkhbAGHRBT
- **X/Twitter:** [@steipete](https://x.com/steipete) / [@clawdbot](https://x.com/clawdbot) - **X/Twitter:** [@steipete](https://x.com/steipete) / [@clawdbot](https://x.com/clawdbot)
@@ -20,11 +20,11 @@ Welcome to the lobster tank! 🦞
## How to Contribute ## How to Contribute
1. **Bugs & small fixes** → Open a PR! 1. **Bugs & small fixes** → Open a PR!
2. **New features / architecture** → Start a [GitHub Discussion](https://github.com/steipete/clawdis/discussions) or ask in Discord first 2. **New features / architecture** → Start a [GitHub Discussion](https://github.com/clawdbot/clawdbot/discussions) or ask in Discord first
3. **Questions** → Discord #setup-help 3. **Questions** → Discord #setup-help
## Before You PR ## Before You PR
- Test locally with your Clawdis instance - Test locally with your Clawdbot instance
- Run linter: `npm run lint` - Run linter: `npm run lint`
- Keep PRs focused (one thing per PR) - Keep PRs focused (one thing per PR)
- Describe what & why - Describe what & why

View File

@@ -19,9 +19,9 @@ RUN apt-get update \
xvfb \ xvfb \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
COPY scripts/sandbox-browser-entrypoint.sh /usr/local/bin/clawdis-sandbox-browser COPY scripts/sandbox-browser-entrypoint.sh /usr/local/bin/clawdbot-sandbox-browser
RUN chmod +x /usr/local/bin/clawdis-sandbox-browser RUN chmod +x /usr/local/bin/clawdbot-sandbox-browser
EXPOSE 9222 5900 6080 EXPOSE 9222 5900 6080
CMD ["clawdis-sandbox-browser"] CMD ["clawdbot-sandbox-browser"]

View File

@@ -1,7 +1,7 @@
# 🦞 CLAWDIS — Personal AI Assistant # 🦞 CLAWDBOT — Personal AI Assistant
<p align="center"> <p align="center">
<img src="https://raw.githubusercontent.com/steipete/clawdis/main/docs/whatsapp-clawd.jpg" alt="CLAWDIS" width="400"> <img src="https://raw.githubusercontent.com/clawdbot/clawdbot/main/docs/whatsapp-clawd.jpg" alt="CLAWDBOT" width="400">
</p> </p>
<p align="center"> <p align="center">
@@ -9,20 +9,20 @@
</p> </p>
<p align="center"> <p align="center">
<a href="https://github.com/steipete/clawdis/actions/workflows/ci.yml?branch=main"><img src="https://img.shields.io/github/actions/workflow/status/steipete/clawdis/ci.yml?branch=main&style=for-the-badge" alt="CI status"></a> <a href="https://github.com/clawdbot/clawdbot/actions/workflows/ci.yml?branch=main"><img src="https://img.shields.io/github/actions/workflow/status/clawdbot/clawdbot/ci.yml?branch=main&style=for-the-badge" alt="CI status"></a>
<a href="https://github.com/steipete/clawdis/releases"><img src="https://img.shields.io/github/v/release/steipete/clawdis?include_prereleases&style=for-the-badge" alt="GitHub release"></a> <a href="https://github.com/clawdbot/clawdbot/releases"><img src="https://img.shields.io/github/v/release/clawdbot/clawdbot?include_prereleases&style=for-the-badge" alt="GitHub release"></a>
<a href="https://discord.gg/clawd"><img src="https://img.shields.io/discord/1456350064065904867?label=Discord&logo=discord&logoColor=white&color=5865F2&style=for-the-badge" alt="Discord"></a> <a href="https://discord.gg/clawd"><img src="https://img.shields.io/discord/1456350064065904867?label=Discord&logo=discord&logoColor=white&color=5865F2&style=for-the-badge" alt="Discord"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge" alt="MIT License"></a> <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge" alt="MIT License"></a>
</p> </p>
**Clawdis** is a *personal AI assistant* you run on your own devices. **Clawdbot** is a *personal AI assistant* you run on your own devices.
It answers you on the surfaces you already use (WhatsApp, Telegram, Discord, iMessage, WebChat), can speak and listen on macOS/iOS, 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 surfaces you already use (WhatsApp, Telegram, Discord, iMessage, WebChat), can speak and listen on macOS/iOS, 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. If you want a personal, single-user assistant that feels local, fast, and always-on, this is it.
Website: https://clawd.me · Docs: [`docs/index.md`](docs/index.md) · FAQ: [`docs/faq.md`](docs/faq.md) · Wizard: [`docs/wizard.md`](docs/wizard.md) · Docker (optional): [`docs/docker.md`](docs/docker.md) · Discord: https://discord.gg/clawd Website: https://clawd.me · Docs: [`docs/index.md`](docs/index.md) · FAQ: [`docs/faq.md`](docs/faq.md) · Wizard: [`docs/wizard.md`](docs/wizard.md) · Docker (optional): [`docs/docker.md`](docs/docker.md) · Discord: https://discord.gg/clawd
Preferred setup: run the onboarding wizard (`clawdis onboard`). It walks through gateway, workspace, providers, and skills. The CLI wizard is the recommended path and works on **macOS, Windows, and Linux**. Preferred setup: run the onboarding wizard (`clawdbot onboard`). It walks through gateway, workspace, providers, and skills. The CLI wizard is the recommended path and works on **macOS, Windows, and Linux**.
Using Claude Pro/Max subscription? See `docs/onboarding.md` for the Anthropic OAuth setup. Using Claude Pro/Max subscription? See `docs/onboarding.md` for the Anthropic OAuth setup.
@@ -36,13 +36,13 @@ Your surfaces
└──────────────┬────────────────┘ └──────────────┬────────────────┘
├─ Pi agent (RPC) ├─ Pi agent (RPC)
├─ CLI (clawdis …) ├─ CLI (clawdbot …)
├─ WebChat (browser) ├─ WebChat (browser)
├─ macOS app (Clawdis.app) ├─ macOS app (Clawdbot.app)
└─ iOS node (Canvas + voice) └─ iOS node (Canvas + voice)
``` ```
## What Clawdis does ## What Clawdbot does
- **Personal assistant** — one user, one identity, one memory surface. - **Personal assistant** — one user, one identity, one memory surface.
- **Multi-surface inbox** — WhatsApp, Telegram, Discord, iMessage, WebChat, macOS, iOS. Signal support via `signal-cli` (see `docs/signal.md`). iMessage uses `imsg` (see `docs/imessage.md`). - **Multi-surface inbox** — WhatsApp, Telegram, Discord, iMessage, WebChat, macOS, iOS. Signal support via `signal-cli` (see `docs/signal.md`). iMessage uses `imsg` (see `docs/imessage.md`).
@@ -51,7 +51,7 @@ Your surfaces
- **Automation-ready** — browser control, media handling, and tool streaming. - **Automation-ready** — browser control, media handling, and tool streaming.
- **Local-first control plane** — the Gateway owns state, everything else connects. - **Local-first control plane** — the Gateway owns state, everything else connects.
- **Group chats** — mention-based by default, `/activation always|mention` per group (owner-only). - **Group chats** — mention-based by default, `/activation always|mention` per group (owner-only).
- **Nix mode** — opt-in declarative config + read-only UI when `CLAWDIS_NIX_MODE=1`. - **Nix mode** — opt-in declarative config + read-only UI when `CLAWDBOT_NIX_MODE=1`.
## How it works (short) ## How it works (short)
@@ -70,25 +70,25 @@ pnpm build
pnpm ui:build pnpm ui:build
# Recommended: run the onboarding wizard # Recommended: run the onboarding wizard
pnpm clawdis onboard pnpm clawdbot onboard
# Link WhatsApp (stores creds in ~/.clawdis/credentials) # Link WhatsApp (stores creds in ~/.clawdbot/credentials)
pnpm clawdis login pnpm clawdbot login
# Start the gateway # Start the gateway
pnpm clawdis gateway --port 18789 --verbose pnpm clawdbot gateway --port 18789 --verbose
# Dev loop (auto-reload on TS changes) # Dev loop (auto-reload on TS changes)
pnpm gateway:watch pnpm gateway:watch
# Send a message # Send a message
pnpm clawdis send --to +1234567890 --message "Hello from Clawdis" pnpm clawdbot send --to +1234567890 --message "Hello from Clawdbot"
# Talk to the assistant (optionally deliver back to WhatsApp/Telegram/Discord) # Talk to the assistant (optionally deliver back to WhatsApp/Telegram/Discord)
pnpm clawdis agent --message "Ship checklist" --thinking high pnpm clawdbot agent --message "Ship checklist" --thinking high
``` ```
If you run from source, prefer `pnpm clawdis …` (not global `clawdis`). If you run from source, prefer `pnpm clawdbot …` (not global `clawdbot`).
## Chat commands ## Chat commands
@@ -115,14 +115,14 @@ Send these in WhatsApp/Telegram/WebChat (group commands are owner-only):
- **Discovery + pairing**: Bonjour discovery via `BridgeDiscoveryModel` (NWBrowser). `BridgeConnectionController` autoconnects using Keychain token or allows manual host/port. - **Discovery + pairing**: Bonjour discovery via `BridgeDiscoveryModel` (NWBrowser). `BridgeConnectionController` autoconnects using Keychain token or allows manual host/port.
- **Node runtime**: `BridgeSession` (actor) maintains the `NWConnection`, hello handshake, ping/pong, RPC requests, and `invoke` callbacks. - **Node runtime**: `BridgeSession` (actor) maintains the `NWConnection`, hello handshake, ping/pong, RPC requests, and `invoke` callbacks.
- **Capabilities + commands**: advertises `canvas`, `screen`, `camera`, `voiceWake` (settingsdriven) and executes `canvas.*`, `canvas.a2ui.*`, `camera.*`, `screen.record` (`NodeAppModel.handleInvoke`). - **Capabilities + commands**: advertises `canvas`, `screen`, `camera`, `voiceWake` (settingsdriven) and executes `canvas.*`, `canvas.a2ui.*`, `camera.*`, `screen.record` (`NodeAppModel.handleInvoke`).
- **Canvas**: `WKWebView` with bundled Canvas scaffold + A2UI, JS eval, snapshot capture, and `clawdis://` deeplink interception (`ScreenController`). - **Canvas**: `WKWebView` with bundled Canvas scaffold + A2UI, JS eval, snapshot capture, and `clawdbot://` deeplink interception (`ScreenController`).
- **Voice + deep links**: voice wake sends `voice.transcript` events; `clawdis://agent` links emit `agent.request`. Voice wake triggers sync via `voicewake.get` + `voicewake.changed`. - **Voice + deep links**: voice wake sends `voice.transcript` events; `clawdbot://agent` links emit `agent.request`. Voice wake triggers sync via `voicewake.get` + `voicewake.changed`.
## Companion apps ## Companion apps
The **macOS app is critical**: it runs the menubar control plane, owns local permissions (TCC), hosts Voice Wake, exposes WebChat/debug tools, and coordinates local/remote gateway mode. Most “assistant” UX lives here. The **macOS app is critical**: it runs the menubar control plane, owns local permissions (TCC), hosts Voice Wake, exposes WebChat/debug tools, and coordinates local/remote gateway mode. Most “assistant” UX lives here.
### macOS (Clawdis.app) ### macOS (Clawdbot.app)
- Menu bar control for the Gateway and health. - Menu bar control for the Gateway and health.
- Voice Wake + push-to-talk overlay. - Voice Wake + push-to-talk overlay.
@@ -135,7 +135,7 @@ Build/run: `./scripts/restart-mac.sh` (packages + launches).
- Pairs as a node via the Bridge. - Pairs as a node via the Bridge.
- Voice trigger forwarding + Canvas surface. - Voice trigger forwarding + Canvas surface.
- Controlled via `clawdis nodes …`. - Controlled via `clawdbot nodes …`.
Runbook: `docs/ios/connect.md`. Runbook: `docs/ios/connect.md`.
@@ -153,7 +153,7 @@ Runbook: `docs/ios/connect.md`.
## Configuration ## Configuration
Minimal `~/.clawdis/clawdis.json`: Minimal `~/.clawdbot/clawdbot.json`:
```json5 ```json5
{ {
@@ -165,7 +165,7 @@ Minimal `~/.clawdis/clawdis.json`:
### WhatsApp ### WhatsApp
- Link the device: `pnpm clawdis login` (stores creds in `~/.clawdis/credentials`). - Link the device: `pnpm clawdbot login` (stores creds in `~/.clawdbot/credentials`).
- Allowlist who can talk to the assistant via `whatsapp.allowFrom`. - Allowlist who can talk to the assistant via `whatsapp.allowFrom`.
### Telegram ### Telegram
@@ -223,13 +223,13 @@ Browser control (optional):
## Email hooks (Gmail) ## Email hooks (Gmail)
```bash ```bash
clawdis hooks gmail setup --account you@gmail.com clawdbot hooks gmail setup --account you@gmail.com
clawdis hooks gmail run clawdbot hooks gmail run
``` ```
- [`docs/security.md`](docs/security.md) - [`docs/security.md`](docs/security.md)
- [`docs/troubleshooting.md`](docs/troubleshooting.md) - [`docs/troubleshooting.md`](docs/troubleshooting.md)
- [`docs/ios/connect.md`](docs/ios/connect.md) - [`docs/ios/connect.md`](docs/ios/connect.md)
- [`docs/clawdis-mac.md`](docs/clawdis-mac.md) - [`docs/clawdbot-mac.md`](docs/clawdbot-mac.md)
## Contributing ## Contributing
@@ -239,7 +239,7 @@ AI/vibe-coded PRs welcome! 🤖
## Clawd ## Clawd
Clawdis was built for **Clawd**, a space lobster AI assistant. Clawdbot was built for **Clawd**, a space lobster AI assistant.
- https://clawd.me - https://clawd.me
- https://soul.md - https://soul.md

View File

@@ -1,15 +1,15 @@
<?xml version="1.0" standalone="yes"?> <?xml version="1.0" standalone="yes"?>
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0"> <rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel> <channel>
<title>Clawdis</title> <title>Clawdbot</title>
<item> <item>
<title>2.0.0-beta5</title> <title>2.0.0-beta5</title>
<pubDate>Sat, 03 Jan 2026 07:15:16 +0100</pubDate> <pubDate>Sat, 03 Jan 2026 07:15:16 +0100</pubDate>
<link>https://raw.githubusercontent.com/steipete/clawdis/main/appcast.xml</link> <link>https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml</link>
<sparkle:version>2765</sparkle:version> <sparkle:version>2765</sparkle:version>
<sparkle:shortVersionString>2.0.0-beta5</sparkle:shortVersionString> <sparkle:shortVersionString>2.0.0-beta5</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion> <sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>Clawdis 2.0.0-beta5</h2> <description><![CDATA[<h2>Clawdbot 2.0.0-beta5</h2>
<h3>Fixed</h3> <h3>Fixed</h3>
<ul> <ul>
<li>Media: preserve GIF animation when uploading to Discord/other providers (skip JPEG optimization for image/gif).</li> <li>Media: preserve GIF animation when uploading to Discord/other providers (skip JPEG optimization for image/gif).</li>
@@ -30,8 +30,8 @@
<li>Sessions: group keys now use <code>surface:group:<id></code> / <code>surface:channel:<id></code>; legacy <code>group:*</code> keys migrate on next message; <code>groupdm</code> keys are no longer recognized.</li> <li>Sessions: group keys now use <code>surface:group:<id></code> / <code>surface:channel:<id></code>; legacy <code>group:*</code> keys migrate on next message; <code>groupdm</code> keys are no longer recognized.</li>
<li>Discord: remove legacy <code>discord.allowFrom</code>, <code>discord.guildAllowFrom</code>, and <code>discord.requireMention</code>; use <code>discord.dm</code> + <code>discord.guilds</code>.</li> <li>Discord: remove legacy <code>discord.allowFrom</code>, <code>discord.guildAllowFrom</code>, and <code>discord.requireMention</code>; use <code>discord.dm</code> + <code>discord.guilds</code>.</li>
<li>Providers: Discord/Telegram no longer auto-start from env tokens alone; add <code>discord: { enabled: true }</code> / <code>telegram: { enabled: true }</code> to your config when using <code>DISCORD_BOT_TOKEN</code> / <code>TELEGRAM_BOT_TOKEN</code>.</li> <li>Providers: Discord/Telegram no longer auto-start from env tokens alone; add <code>discord: { enabled: true }</code> / <code>telegram: { enabled: true }</code> to your config when using <code>DISCORD_BOT_TOKEN</code> / <code>TELEGRAM_BOT_TOKEN</code>.</li>
<li>Config: remove <code>routing.allowFrom</code>; use <code>whatsapp.allowFrom</code> instead (run <code>clawdis doctor</code> to migrate).</li> <li>Config: remove <code>routing.allowFrom</code>; use <code>whatsapp.allowFrom</code> instead (run <code>clawdbot doctor</code> to migrate).</li>
<li>Config: remove <code>routing.groupChat.requireMention</code> + <code>telegram.requireMention</code>; use <code>whatsapp.groups</code>, <code>imessage.groups</code>, and <code>telegram.groups</code> defaults instead (run <code>clawdis doctor</code> to migrate).</li> <li>Config: remove <code>routing.groupChat.requireMention</code> + <code>telegram.requireMention</code>; use <code>whatsapp.groups</code>, <code>imessage.groups</code>, and <code>telegram.groups</code> defaults instead (run <code>clawdbot doctor</code> to migrate).</li>
</ul> </ul>
<h3>Features</h3> <h3>Features</h3>
<ul> <ul>
@@ -40,7 +40,7 @@
<li>Talk mode: continuous speech conversations (macOS/iOS/Android) with ElevenLabs TTS, reply directives, and optional interrupt-on-speech.</li> <li>Talk mode: continuous speech conversations (macOS/iOS/Android) with ElevenLabs TTS, reply directives, and optional interrupt-on-speech.</li>
<li>Auto-reply: expand queue modes (steer/followup/collect/steer-backlog) with debounce/cap/drop options and followup backlog handling.</li> <li>Auto-reply: expand queue modes (steer/followup/collect/steer-backlog) with debounce/cap/drop options and followup backlog handling.</li>
<li>UI: add optional <code>ui.seamColor</code> accent to tint the Talk Mode side bubble (macOS/iOS/Android).</li> <li>UI: add optional <code>ui.seamColor</code> accent to tint the Talk Mode side bubble (macOS/iOS/Android).</li>
<li>Nix mode: opt-in declarative config + read-only settings UI when <code>CLAWDIS_NIX_MODE=1</code> (thanks @joshp123 for the persistence — earned my trust; I'll merge these going forward).</li> <li>Nix mode: opt-in declarative config + read-only settings UI when <code>CLAWDBOT_NIX_MODE=1</code> (thanks @joshp123 for the persistence — earned my trust; I'll merge these going forward).</li>
<li>CLI: add Google Antigravity OAuth auth option for Claude Opus 4.5/Gemini 3 (#88) — thanks @mukhtharcm.</li> <li>CLI: add Google Antigravity OAuth auth option for Claude Opus 4.5/Gemini 3 (#88) — thanks @mukhtharcm.</li>
<li>Agent runtime: accept legacy <code>Z_AI_API_KEY</code> for Z.AI provider auth (maps to <code>ZAI_API_KEY</code>).</li> <li>Agent runtime: accept legacy <code>Z_AI_API_KEY</code> for Z.AI provider auth (maps to <code>ZAI_API_KEY</code>).</li>
<li>Groups: add per-group mention gating defaults/overrides for Telegram/WhatsApp/iMessage via <code>*.groups</code> with <code>"*"</code> defaults; Discord now supports <code>discord.guilds."*"</code> as a default.</li> <li>Groups: add per-group mention gating defaults/overrides for Telegram/WhatsApp/iMessage via <code>*.groups</code> with <code>"*"</code> defaults; Discord now supports <code>discord.guilds."*"</code> as a default.</li>
@@ -50,7 +50,7 @@
<li>iMessage: add imsg JSON-RPC integration (stdio), chat_id routing, and group chat support.</li> <li>iMessage: add imsg JSON-RPC integration (stdio), chat_id routing, and group chat support.</li>
<li>Chat UI: add recent-session dropdown switcher (main first) in macOS/iOS/Android + Control UI.</li> <li>Chat UI: add recent-session dropdown switcher (main first) in macOS/iOS/Android + Control UI.</li>
<li>UI: add Discord/Signal/iMessage connection panels in macOS + Control UI (thanks @thewilloftheshadow).</li> <li>UI: add Discord/Signal/iMessage connection panels in macOS + Control UI (thanks @thewilloftheshadow).</li>
<li>Discord: allow agent-triggered reactions via <code>clawdis_discord</code> when enabled, and surface message ids in context.</li> <li>Discord: allow agent-triggered reactions via <code>clawdbot_discord</code> when enabled, and surface message ids in context.</li>
<li>Discord: revamp guild routing config with per-guild/channel rules and slugged display names; add optional group DM support (default off).</li> <li>Discord: revamp guild routing config with per-guild/channel rules and slugged display names; add optional group DM support (default off).</li>
<li>Discord: remove legacy guild/channel ignore lists in favor of per-guild allowlists (and proposed per-guild ignore lists).</li> <li>Discord: remove legacy guild/channel ignore lists in favor of per-guild allowlists (and proposed per-guild ignore lists).</li>
<li>Skills: add Trello skill for board/list/card management (thanks @clawd).</li> <li>Skills: add Trello skill for board/list/card management (thanks @clawd).</li>
@@ -62,7 +62,7 @@
<li>CLI: add <code>configure</code>, <code>doctor</code>, and <code>update</code> wizards for ongoing setup, health checks, and modernization.</li> <li>CLI: add <code>configure</code>, <code>doctor</code>, and <code>update</code> wizards for ongoing setup, health checks, and modernization.</li>
<li>CLI: add Signal CLI auto-install from GitHub releases in the wizard and persist wizard run metadata in config.</li> <li>CLI: add Signal CLI auto-install from GitHub releases in the wizard and persist wizard run metadata in config.</li>
<li>CLI: add remote gateway client config (gateway.remote.*) with Bonjour-assisted discovery.</li> <li>CLI: add remote gateway client config (gateway.remote.*) with Bonjour-assisted discovery.</li>
<li>CLI: enhance <code>clawdis tui</code> with model/session pickers, tool cards, and slash commands (local or remote).</li> <li>CLI: enhance <code>clawdbot tui</code> with model/session pickers, tool cards, and slash commands (local or remote).</li>
<li>Gateway: allow <code>sessions.patch</code> to set per-session model overrides (used by the TUI <code>/model</code> flow).</li> <li>Gateway: allow <code>sessions.patch</code> to set per-session model overrides (used by the TUI <code>/model</code> flow).</li>
<li>Skills: allow <code>bun</code> as a node manager for skill installs.</li> <li>Skills: allow <code>bun</code> as a node manager for skill installs.</li>
<li>Skills: add <code>things-mac</code> (Things 3 CLI) for read/search plus add/update via URL scheme.</li> <li>Skills: add <code>things-mac</code> (Things 3 CLI) for read/search plus add/update via URL scheme.</li>
@@ -87,7 +87,7 @@
<li>Chat UI: add extra top padding before the first message bubble in Web Chat (macOS/iOS/Android).</li> <li>Chat UI: add extra top padding before the first message bubble in Web Chat (macOS/iOS/Android).</li>
<li>Control UI: refine Web Chat session selector styling (chevron spacing + background).</li> <li>Control UI: refine Web Chat session selector styling (chevron spacing + background).</li>
<li>WebChat: stream live updates for sessions even when runs start outside the chat UI.</li> <li>WebChat: stream live updates for sessions even when runs start outside the chat UI.</li>
<li>Gateway CLI: read <code>CLAWDIS_GATEWAY_PASSWORD</code> from environment in <code>callGateway()</code> — allows <code>doctor</code>/<code>health</code> commands to auth without explicit <code>--password</code> flag.</li> <li>Gateway CLI: read <code>CLAWDBOT_GATEWAY_PASSWORD</code> from environment in <code>callGateway()</code> — allows <code>doctor</code>/<code>health</code> commands to auth without explicit <code>--password</code> flag.</li>
<li>Gateway: add password auth support for remote gateway connections (thanks @jeffersonwarrior).</li> <li>Gateway: add password auth support for remote gateway connections (thanks @jeffersonwarrior).</li>
<li>Auto-reply: strip stray leading/trailing <code>HEARTBEAT_OK</code> from normal replies; drop short (≤ 30 chars) heartbeat acks.</li> <li>Auto-reply: strip stray leading/trailing <code>HEARTBEAT_OK</code> from normal replies; drop short (≤ 30 chars) heartbeat acks.</li>
<li>WhatsApp auto-reply: default to self-only when no config is present.</li> <li>WhatsApp auto-reply: default to self-only when no config is present.</li>
@@ -137,7 +137,7 @@
<li>Docs: clarify self-chat mode and group mention gating config (#111) — thanks @rafaelreis-r.</li> <li>Docs: clarify self-chat mode and group mention gating config (#111) — thanks @rafaelreis-r.</li>
<li>Browser tools: <code>upload</code> supports auto-click refs, direct <code>inputRef</code>/<code>element</code> file inputs, and emits input/change after <code>setFiles</code> so JS-heavy sites pick up attachments.</li> <li>Browser tools: <code>upload</code> supports auto-click refs, direct <code>inputRef</code>/<code>element</code> file inputs, and emits input/change after <code>setFiles</code> so JS-heavy sites pick up attachments.</li>
<li>Browser tools: harden CDP readiness (HTTP + WS), retry CDP connects, and auto-restart the clawd browser when the socket handshake stalls.</li> <li>Browser tools: harden CDP readiness (HTTP + WS), retry CDP connects, and auto-restart the clawd browser when the socket handshake stalls.</li>
<li>Browser CLI: add <code>clawdis browser reset-profile</code> to move the clawd profile to Trash when it gets wedged.</li> <li>Browser CLI: add <code>clawdbot browser reset-profile</code> to move the clawd profile to Trash when it gets wedged.</li>
<li>Signal: fix daemon startup race (wait for <code>/api/v1/check</code>) and normalize JSON-RPC <code>version</code> probe parsing.</li> <li>Signal: fix daemon startup race (wait for <code>/api/v1/check</code>) and normalize JSON-RPC <code>version</code> probe parsing.</li>
<li>Docs/Signal: clarify bot-number vs personal-account setup (self-chat loop protection) and add a quickstart config snippet.</li> <li>Docs/Signal: clarify bot-number vs personal-account setup (self-chat loop protection) and add a quickstart config snippet.</li>
<li>Docs: refresh the CLI wizard guide and highlight onboarding in the README.</li> <li>Docs: refresh the CLI wizard guide and highlight onboarding in the README.</li>
@@ -201,12 +201,12 @@
<li>macOS menu: top status line now shows pending node pairing approvals (incl. repairs).</li> <li>macOS menu: top status line now shows pending node pairing approvals (incl. repairs).</li>
<li>CLI: avoid spurious gateway close errors after successful request/response cycles.</li> <li>CLI: avoid spurious gateway close errors after successful request/response cycles.</li>
<li>Agent runtime: clamp tool-result images to the 5MB Anthropic limit to avoid hard request rejections.</li> <li>Agent runtime: clamp tool-result images to the 5MB Anthropic limit to avoid hard request rejections.</li>
<li>Agent runtime: write v2 session headers so Pi session branching stays in the Clawdis sessions dir.</li> <li>Agent runtime: write v2 session headers so Pi session branching stays in the Clawdbot sessions dir.</li>
<li>Tests: add Swift Testing coverage for camera errors and Kotest coverage for Android bridge endpoints.</li> <li>Tests: add Swift Testing coverage for camera errors and Kotest coverage for Android bridge endpoints.</li>
</ul> </ul>
<p><a href="https://github.com/steipete/clawdis/blob/main/CHANGELOG.md">View full changelog</a></p> <p><a href="https://github.com/clawdbot/clawdbot/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description> ]]></description>
<enclosure url="https://github.com/steipete/clawdis/releases/download/v2.0.0-beta5/Clawdis-2.0.0-beta5.zip" length="145432870" type="application/octet-stream" sparkle:edSignature="qKPcmSx2pAaIYz9NqFp0TY63KrcDlpctUHnNpRs6Q60qQqBWtQycLIhhvhxmGnHupaiEXJfspb/Ad9RgODIzAw=="/> <enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2.0.0-beta5/Clawdbot-2.0.0-beta5.zip" length="145432870" type="application/octet-stream" sparkle:edSignature="qKPcmSx2pAaIYz9NqFp0TY63KrcDlpctUHnNpRs6Q60qQqBWtQycLIhhvhxmGnHupaiEXJfspb/Ad9RgODIzAw=="/>
</item> </item>
</channel> </channel>
</rss> </rss>

View File

@@ -1,6 +1,6 @@
## Clawdis Node (Android) (internal) ## Clawdbot Node (Android) (internal)
Modern Android node app: connects to the **Gateway-owned bridge** (`_clawdis-bridge._tcp`) over TCP and exposes **Canvas + Chat + Camera**. Modern Android node app: connects to the **Gateway-owned bridge** (`_clawdbot-bridge._tcp`) over TCP and exposes **Canvas + Chat + Camera**.
Notes: Notes:
- The node keeps the connection alive via a **foreground service** (persistent notification with a Disconnect action). - The node keeps the connection alive via a **foreground service** (persistent notification with a Disconnect action).
@@ -25,7 +25,7 @@ cd apps/android
1) Start the gateway (on your “master” machine): 1) Start the gateway (on your “master” machine):
```bash ```bash
pnpm clawdis gateway --port 18789 --verbose pnpm clawdbot gateway --port 18789 --verbose
``` ```
2) In the Android app: 2) In the Android app:
@@ -34,8 +34,8 @@ pnpm clawdis gateway --port 18789 --verbose
3) Approve pairing (on the gateway machine): 3) Approve pairing (on the gateway machine):
```bash ```bash
clawdis nodes pending clawdbot nodes pending
clawdis nodes approve <requestId> clawdbot nodes approve <requestId>
``` ```
More details: `docs/android/connect.md`. More details: `docs/android/connect.md`.

View File

@@ -6,17 +6,17 @@ plugins {
} }
android { android {
namespace = "com.clawdis.android" namespace = "com.clawdbot.android"
compileSdk = 36 compileSdk = 36
sourceSets { sourceSets {
getByName("main") { getByName("main") {
assets.srcDir(file("../../shared/ClawdisKit/Sources/ClawdisKit/Resources")) assets.srcDir(file("../../shared/ClawdbotKit/Sources/ClawdbotKit/Resources"))
} }
} }
defaultConfig { defaultConfig {
applicationId = "com.clawdis.android" applicationId = "com.clawdbot.android"
minSdk = 31 minSdk = 31
targetSdk = 36 targetSdk = 36
versionCode = 1 versionCode = 1

View File

@@ -32,7 +32,7 @@
android:label="@string/app_name" android:label="@string/app_name"
android:supportsRtl="true" android:supportsRtl="true"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:theme="@style/Theme.ClawdisNode"> android:theme="@style/Theme.ClawdbotNode">
<service <service
android:name=".NodeForegroundService" android:name=".NodeForegroundService"
android:exported="false" android:exported="false"

View File

@@ -1,4 +1,4 @@
package com.clawdis.android package com.clawdbot.android
enum class CameraHudKind { enum class CameraHudKind {
Photo, Photo,

View File

@@ -1,4 +1,4 @@
package com.clawdis.android package com.clawdbot.android
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build

View File

@@ -1,4 +1,4 @@
package com.clawdis.android package com.clawdbot.android
enum class LocationMode(val rawValue: String) { enum class LocationMode(val rawValue: String) {
Off("off"), Off("off"),

View File

@@ -1,4 +1,4 @@
package com.clawdis.android package com.clawdbot.android
import android.Manifest import android.Manifest
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
@@ -18,8 +18,8 @@ import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
import com.clawdis.android.ui.RootScreen import com.clawdbot.android.ui.RootScreen
import com.clawdis.android.ui.ClawdisTheme import com.clawdbot.android.ui.ClawdbotTheme
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@@ -56,7 +56,7 @@ class MainActivity : ComponentActivity() {
} }
setContent { setContent {
ClawdisTheme { ClawdbotTheme {
Surface(modifier = Modifier) { Surface(modifier = Modifier) {
RootScreen(viewModel = viewModel) RootScreen(viewModel = viewModel)
} }

View File

@@ -1,13 +1,13 @@
package com.clawdis.android package com.clawdbot.android
import android.app.Application import android.app.Application
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import com.clawdis.android.bridge.BridgeEndpoint import com.clawdbot.android.bridge.BridgeEndpoint
import com.clawdis.android.chat.OutgoingAttachment import com.clawdbot.android.chat.OutgoingAttachment
import com.clawdis.android.node.CameraCaptureManager import com.clawdbot.android.node.CameraCaptureManager
import com.clawdis.android.node.CanvasController import com.clawdbot.android.node.CanvasController
import com.clawdis.android.node.ScreenRecordManager import com.clawdbot.android.node.ScreenRecordManager
import com.clawdis.android.node.SmsManager import com.clawdbot.android.node.SmsManager
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
class MainViewModel(app: Application) : AndroidViewModel(app) { class MainViewModel(app: Application) : AndroidViewModel(app) {

View File

@@ -1,4 +1,4 @@
package com.clawdis.android package com.clawdbot.android
import android.app.Application import android.app.Application

View File

@@ -1,4 +1,4 @@
package com.clawdis.android package com.clawdbot.android
import android.app.Notification import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
@@ -29,7 +29,7 @@ class NodeForegroundService : Service() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
ensureChannel() ensureChannel()
val initial = buildNotification(title = "Clawdis Node", text = "Starting…") val initial = buildNotification(title = "Clawdbot Node", text = "Starting…")
startForegroundWithTypes(notification = initial, requiresMic = false) startForegroundWithTypes(notification = initial, requiresMic = false)
val runtime = (application as NodeApp).runtime val runtime = (application as NodeApp).runtime
@@ -44,7 +44,7 @@ class NodeForegroundService : Service() {
) { status, server, connected, voiceMode, voiceListening -> ) { status, server, connected, voiceMode, voiceListening ->
Quint(status, server, connected, voiceMode, voiceListening) Quint(status, server, connected, voiceMode, voiceListening)
}.collect { (status, server, connected, voiceMode, voiceListening) -> }.collect { (status, server, connected, voiceMode, voiceListening) ->
val title = if (connected) "Clawdis Node · Connected" else "Clawdis Node" val title = if (connected) "Clawdbot Node · Connected" else "Clawdbot Node"
val voiceSuffix = val voiceSuffix =
if (voiceMode == VoiceWakeMode.Always) { if (voiceMode == VoiceWakeMode.Always) {
if (voiceListening) " · Voice Wake: Listening" else " · Voice Wake: Paused" if (voiceListening) " · Voice Wake: Listening" else " · Voice Wake: Paused"
@@ -91,7 +91,7 @@ class NodeForegroundService : Service() {
"Connection", "Connection",
NotificationManager.IMPORTANCE_LOW, NotificationManager.IMPORTANCE_LOW,
).apply { ).apply {
description = "Clawdis node connection status" description = "Clawdbot node connection status"
setShowBadge(false) setShowBadge(false)
} }
mgr.createNotificationChannel(channel) mgr.createNotificationChannel(channel)
@@ -146,7 +146,7 @@ class NodeForegroundService : Service() {
private const val CHANNEL_ID = "connection" private const val CHANNEL_ID = "connection"
private const val NOTIFICATION_ID = 1 private const val NOTIFICATION_ID = 1
private const val ACTION_STOP = "com.clawdis.android.action.STOP" private const val ACTION_STOP = "com.clawdbot.android.action.STOP"
fun start(context: Context) { fun start(context: Context) {
val intent = Intent(context, NodeForegroundService::class.java) val intent = Intent(context, NodeForegroundService::class.java)

View File

@@ -1,4 +1,4 @@
package com.clawdis.android package com.clawdbot.android
import android.Manifest import android.Manifest
import android.content.Context import android.content.Context
@@ -7,31 +7,31 @@ import android.location.LocationManager
import android.os.Build import android.os.Build
import android.os.SystemClock import android.os.SystemClock
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.clawdis.android.chat.ChatController import com.clawdbot.android.chat.ChatController
import com.clawdis.android.chat.ChatMessage import com.clawdbot.android.chat.ChatMessage
import com.clawdis.android.chat.ChatPendingToolCall import com.clawdbot.android.chat.ChatPendingToolCall
import com.clawdis.android.chat.ChatSessionEntry import com.clawdbot.android.chat.ChatSessionEntry
import com.clawdis.android.chat.OutgoingAttachment import com.clawdbot.android.chat.OutgoingAttachment
import com.clawdis.android.bridge.BridgeDiscovery import com.clawdbot.android.bridge.BridgeDiscovery
import com.clawdis.android.bridge.BridgeEndpoint import com.clawdbot.android.bridge.BridgeEndpoint
import com.clawdis.android.bridge.BridgePairingClient import com.clawdbot.android.bridge.BridgePairingClient
import com.clawdis.android.bridge.BridgeSession import com.clawdbot.android.bridge.BridgeSession
import com.clawdis.android.node.CameraCaptureManager import com.clawdbot.android.node.CameraCaptureManager
import com.clawdis.android.node.LocationCaptureManager import com.clawdbot.android.node.LocationCaptureManager
import com.clawdis.android.BuildConfig import com.clawdbot.android.BuildConfig
import com.clawdis.android.node.CanvasController import com.clawdbot.android.node.CanvasController
import com.clawdis.android.node.ScreenRecordManager import com.clawdbot.android.node.ScreenRecordManager
import com.clawdis.android.node.SmsManager import com.clawdbot.android.node.SmsManager
import com.clawdis.android.protocol.ClawdisCapability import com.clawdbot.android.protocol.ClawdbotCapability
import com.clawdis.android.protocol.ClawdisCameraCommand import com.clawdbot.android.protocol.ClawdbotCameraCommand
import com.clawdis.android.protocol.ClawdisCanvasA2UIAction import com.clawdbot.android.protocol.ClawdbotCanvasA2UIAction
import com.clawdis.android.protocol.ClawdisCanvasA2UICommand import com.clawdbot.android.protocol.ClawdbotCanvasA2UICommand
import com.clawdis.android.protocol.ClawdisCanvasCommand import com.clawdbot.android.protocol.ClawdbotCanvasCommand
import com.clawdis.android.protocol.ClawdisScreenCommand import com.clawdbot.android.protocol.ClawdbotScreenCommand
import com.clawdis.android.protocol.ClawdisLocationCommand import com.clawdbot.android.protocol.ClawdbotLocationCommand
import com.clawdis.android.protocol.ClawdisSmsCommand import com.clawdbot.android.protocol.ClawdbotSmsCommand
import com.clawdis.android.voice.TalkModeManager import com.clawdbot.android.voice.TalkModeManager
import com.clawdis.android.voice.VoiceWakeManager import com.clawdbot.android.voice.VoiceWakeManager
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -369,38 +369,38 @@ class NodeRuntime(context: Context) {
private fun buildInvokeCommands(): List<String> = private fun buildInvokeCommands(): List<String> =
buildList { buildList {
add(ClawdisCanvasCommand.Present.rawValue) add(ClawdbotCanvasCommand.Present.rawValue)
add(ClawdisCanvasCommand.Hide.rawValue) add(ClawdbotCanvasCommand.Hide.rawValue)
add(ClawdisCanvasCommand.Navigate.rawValue) add(ClawdbotCanvasCommand.Navigate.rawValue)
add(ClawdisCanvasCommand.Eval.rawValue) add(ClawdbotCanvasCommand.Eval.rawValue)
add(ClawdisCanvasCommand.Snapshot.rawValue) add(ClawdbotCanvasCommand.Snapshot.rawValue)
add(ClawdisCanvasA2UICommand.Push.rawValue) add(ClawdbotCanvasA2UICommand.Push.rawValue)
add(ClawdisCanvasA2UICommand.PushJSONL.rawValue) add(ClawdbotCanvasA2UICommand.PushJSONL.rawValue)
add(ClawdisCanvasA2UICommand.Reset.rawValue) add(ClawdbotCanvasA2UICommand.Reset.rawValue)
add(ClawdisScreenCommand.Record.rawValue) add(ClawdbotScreenCommand.Record.rawValue)
if (cameraEnabled.value) { if (cameraEnabled.value) {
add(ClawdisCameraCommand.Snap.rawValue) add(ClawdbotCameraCommand.Snap.rawValue)
add(ClawdisCameraCommand.Clip.rawValue) add(ClawdbotCameraCommand.Clip.rawValue)
} }
if (locationMode.value != LocationMode.Off) { if (locationMode.value != LocationMode.Off) {
add(ClawdisLocationCommand.Get.rawValue) add(ClawdbotLocationCommand.Get.rawValue)
} }
if (sms.canSendSms()) { if (sms.canSendSms()) {
add(ClawdisSmsCommand.Send.rawValue) add(ClawdbotSmsCommand.Send.rawValue)
} }
} }
private fun buildCapabilities(): List<String> = private fun buildCapabilities(): List<String> =
buildList { buildList {
add(ClawdisCapability.Canvas.rawValue) add(ClawdbotCapability.Canvas.rawValue)
add(ClawdisCapability.Screen.rawValue) add(ClawdbotCapability.Screen.rawValue)
if (cameraEnabled.value) add(ClawdisCapability.Camera.rawValue) if (cameraEnabled.value) add(ClawdbotCapability.Camera.rawValue)
if (sms.canSendSms()) add(ClawdisCapability.Sms.rawValue) if (sms.canSendSms()) add(ClawdbotCapability.Sms.rawValue)
if (voiceWakeMode.value != VoiceWakeMode.Off && hasRecordAudioPermission()) { if (voiceWakeMode.value != VoiceWakeMode.Off && hasRecordAudioPermission()) {
add(ClawdisCapability.VoiceWake.rawValue) add(ClawdbotCapability.VoiceWake.rawValue)
} }
if (locationMode.value != LocationMode.Off) { if (locationMode.value != LocationMode.Off) {
add(ClawdisCapability.Location.rawValue) add(ClawdbotCapability.Location.rawValue)
} }
} }
@@ -552,7 +552,7 @@ class NodeRuntime(context: Context) {
val actionId = (userActionObj["id"] as? JsonPrimitive)?.content?.trim().orEmpty().ifEmpty { val actionId = (userActionObj["id"] as? JsonPrimitive)?.content?.trim().orEmpty().ifEmpty {
java.util.UUID.randomUUID().toString() java.util.UUID.randomUUID().toString()
} }
val name = ClawdisCanvasA2UIAction.extractActionName(userActionObj) ?: return@launch val name = ClawdbotCanvasA2UIAction.extractActionName(userActionObj) ?: return@launch
val surfaceId = val surfaceId =
(userActionObj["surfaceId"] as? JsonPrimitive)?.content?.trim().orEmpty().ifEmpty { "main" } (userActionObj["surfaceId"] as? JsonPrimitive)?.content?.trim().orEmpty().ifEmpty { "main" }
@@ -562,7 +562,7 @@ class NodeRuntime(context: Context) {
val sessionKey = "main" val sessionKey = "main"
val message = val message =
ClawdisCanvasA2UIAction.formatAgentMessage( ClawdbotCanvasA2UIAction.formatAgentMessage(
actionName = name, actionName = name,
sessionKey = sessionKey, sessionKey = sessionKey,
surfaceId = surfaceId, surfaceId = surfaceId,
@@ -596,7 +596,7 @@ class NodeRuntime(context: Context) {
try { try {
canvas.eval( canvas.eval(
ClawdisCanvasA2UIAction.jsDispatchA2UIActionStatus( ClawdbotCanvasA2UIAction.jsDispatchA2UIActionStatus(
actionId = actionId, actionId = actionId,
ok = connected && error == null, ok = connected && error == null,
error = error, error = error,
@@ -713,10 +713,10 @@ class NodeRuntime(context: Context) {
private suspend fun handleInvoke(command: String, paramsJson: String?): BridgeSession.InvokeResult { private suspend fun handleInvoke(command: String, paramsJson: String?): BridgeSession.InvokeResult {
if ( if (
command.startsWith(ClawdisCanvasCommand.NamespacePrefix) || command.startsWith(ClawdbotCanvasCommand.NamespacePrefix) ||
command.startsWith(ClawdisCanvasA2UICommand.NamespacePrefix) || command.startsWith(ClawdbotCanvasA2UICommand.NamespacePrefix) ||
command.startsWith(ClawdisCameraCommand.NamespacePrefix) || command.startsWith(ClawdbotCameraCommand.NamespacePrefix) ||
command.startsWith(ClawdisScreenCommand.NamespacePrefix) command.startsWith(ClawdbotScreenCommand.NamespacePrefix)
) { ) {
if (!isForeground.value) { if (!isForeground.value) {
return BridgeSession.InvokeResult.error( return BridgeSession.InvokeResult.error(
@@ -725,13 +725,13 @@ class NodeRuntime(context: Context) {
) )
} }
} }
if (command.startsWith(ClawdisCameraCommand.NamespacePrefix) && !cameraEnabled.value) { if (command.startsWith(ClawdbotCameraCommand.NamespacePrefix) && !cameraEnabled.value) {
return BridgeSession.InvokeResult.error( return BridgeSession.InvokeResult.error(
code = "CAMERA_DISABLED", code = "CAMERA_DISABLED",
message = "CAMERA_DISABLED: enable Camera in Settings", message = "CAMERA_DISABLED: enable Camera in Settings",
) )
} }
if (command.startsWith(ClawdisLocationCommand.NamespacePrefix) && if (command.startsWith(ClawdbotLocationCommand.NamespacePrefix) &&
locationMode.value == LocationMode.Off locationMode.value == LocationMode.Off
) { ) {
return BridgeSession.InvokeResult.error( return BridgeSession.InvokeResult.error(
@@ -741,18 +741,18 @@ class NodeRuntime(context: Context) {
} }
return when (command) { return when (command) {
ClawdisCanvasCommand.Present.rawValue -> { ClawdbotCanvasCommand.Present.rawValue -> {
val url = CanvasController.parseNavigateUrl(paramsJson) val url = CanvasController.parseNavigateUrl(paramsJson)
canvas.navigate(url) canvas.navigate(url)
BridgeSession.InvokeResult.ok(null) BridgeSession.InvokeResult.ok(null)
} }
ClawdisCanvasCommand.Hide.rawValue -> BridgeSession.InvokeResult.ok(null) ClawdbotCanvasCommand.Hide.rawValue -> BridgeSession.InvokeResult.ok(null)
ClawdisCanvasCommand.Navigate.rawValue -> { ClawdbotCanvasCommand.Navigate.rawValue -> {
val url = CanvasController.parseNavigateUrl(paramsJson) val url = CanvasController.parseNavigateUrl(paramsJson)
canvas.navigate(url) canvas.navigate(url)
BridgeSession.InvokeResult.ok(null) BridgeSession.InvokeResult.ok(null)
} }
ClawdisCanvasCommand.Eval.rawValue -> { ClawdbotCanvasCommand.Eval.rawValue -> {
val js = val js =
CanvasController.parseEvalJs(paramsJson) CanvasController.parseEvalJs(paramsJson)
?: return BridgeSession.InvokeResult.error( ?: return BridgeSession.InvokeResult.error(
@@ -770,7 +770,7 @@ class NodeRuntime(context: Context) {
} }
BridgeSession.InvokeResult.ok("""{"result":${result.toJsonString()}}""") BridgeSession.InvokeResult.ok("""{"result":${result.toJsonString()}}""")
} }
ClawdisCanvasCommand.Snapshot.rawValue -> { ClawdbotCanvasCommand.Snapshot.rawValue -> {
val snapshotParams = CanvasController.parseSnapshotParams(paramsJson) val snapshotParams = CanvasController.parseSnapshotParams(paramsJson)
val base64 = val base64 =
try { try {
@@ -787,7 +787,7 @@ class NodeRuntime(context: Context) {
} }
BridgeSession.InvokeResult.ok("""{"format":"${snapshotParams.format.rawValue}","base64":"$base64"}""") BridgeSession.InvokeResult.ok("""{"format":"${snapshotParams.format.rawValue}","base64":"$base64"}""")
} }
ClawdisCanvasA2UICommand.Reset.rawValue -> { ClawdbotCanvasA2UICommand.Reset.rawValue -> {
val a2uiUrl = resolveA2uiHostUrl() val a2uiUrl = resolveA2uiHostUrl()
?: return BridgeSession.InvokeResult.error( ?: return BridgeSession.InvokeResult.error(
code = "A2UI_HOST_NOT_CONFIGURED", code = "A2UI_HOST_NOT_CONFIGURED",
@@ -803,7 +803,7 @@ class NodeRuntime(context: Context) {
val res = canvas.eval(a2uiResetJS) val res = canvas.eval(a2uiResetJS)
BridgeSession.InvokeResult.ok(res) BridgeSession.InvokeResult.ok(res)
} }
ClawdisCanvasA2UICommand.Push.rawValue, ClawdisCanvasA2UICommand.PushJSONL.rawValue -> { ClawdbotCanvasA2UICommand.Push.rawValue, ClawdbotCanvasA2UICommand.PushJSONL.rawValue -> {
val messages = val messages =
try { try {
decodeA2uiMessages(command, paramsJson) decodeA2uiMessages(command, paramsJson)
@@ -826,7 +826,7 @@ class NodeRuntime(context: Context) {
val res = canvas.eval(js) val res = canvas.eval(js)
BridgeSession.InvokeResult.ok(res) BridgeSession.InvokeResult.ok(res)
} }
ClawdisCameraCommand.Snap.rawValue -> { ClawdbotCameraCommand.Snap.rawValue -> {
showCameraHud(message = "Taking photo…", kind = CameraHudKind.Photo) showCameraHud(message = "Taking photo…", kind = CameraHudKind.Photo)
triggerCameraFlash() triggerCameraFlash()
val res = val res =
@@ -840,7 +840,7 @@ class NodeRuntime(context: Context) {
showCameraHud(message = "Photo captured", kind = CameraHudKind.Success, autoHideMs = 1600) showCameraHud(message = "Photo captured", kind = CameraHudKind.Success, autoHideMs = 1600)
BridgeSession.InvokeResult.ok(res.payloadJson) BridgeSession.InvokeResult.ok(res.payloadJson)
} }
ClawdisCameraCommand.Clip.rawValue -> { ClawdbotCameraCommand.Clip.rawValue -> {
val includeAudio = paramsJson?.contains("\"includeAudio\":true") != false val includeAudio = paramsJson?.contains("\"includeAudio\":true") != false
if (includeAudio) externalAudioCaptureActive.value = true if (includeAudio) externalAudioCaptureActive.value = true
try { try {
@@ -859,7 +859,7 @@ class NodeRuntime(context: Context) {
if (includeAudio) externalAudioCaptureActive.value = false if (includeAudio) externalAudioCaptureActive.value = false
} }
} }
ClawdisLocationCommand.Get.rawValue -> { ClawdbotLocationCommand.Get.rawValue -> {
val mode = locationMode.value val mode = locationMode.value
if (!isForeground.value && mode != LocationMode.Always) { if (!isForeground.value && mode != LocationMode.Always) {
return BridgeSession.InvokeResult.error( return BridgeSession.InvokeResult.error(
@@ -912,7 +912,7 @@ class NodeRuntime(context: Context) {
BridgeSession.InvokeResult.error(code = "LOCATION_UNAVAILABLE", message = message) BridgeSession.InvokeResult.error(code = "LOCATION_UNAVAILABLE", message = message)
} }
} }
ClawdisScreenCommand.Record.rawValue -> { ClawdbotScreenCommand.Record.rawValue -> {
// Status pill mirrors screen recording state so it stays visible without overlay stacking. // Status pill mirrors screen recording state so it stays visible without overlay stacking.
_screenRecordActive.value = true _screenRecordActive.value = true
try { try {
@@ -928,7 +928,7 @@ class NodeRuntime(context: Context) {
_screenRecordActive.value = false _screenRecordActive.value = false
} }
} }
ClawdisSmsCommand.Send.rawValue -> { ClawdbotSmsCommand.Send.rawValue -> {
val res = sms.send(paramsJson) val res = sms.send(paramsJson)
if (res.ok) { if (res.ok) {
BridgeSession.InvokeResult.ok(res.payloadJson) BridgeSession.InvokeResult.ok(res.payloadJson)
@@ -999,7 +999,7 @@ class NodeRuntime(context: Context) {
val raw = session.currentCanvasHostUrl()?.trim().orEmpty() val raw = session.currentCanvasHostUrl()?.trim().orEmpty()
if (raw.isBlank()) return null if (raw.isBlank()) return null
val base = raw.trimEnd('/') val base = raw.trimEnd('/')
return "${base}/__clawdis__/a2ui/?platform=android" return "${base}/__clawdbot__/a2ui/?platform=android"
} }
private suspend fun ensureA2uiReady(a2uiUrl: String): Boolean { private suspend fun ensureA2uiReady(a2uiUrl: String): Boolean {
@@ -1034,7 +1034,7 @@ class NodeRuntime(context: Context) {
val jsonlField = (obj["jsonl"] as? JsonPrimitive)?.content?.trim().orEmpty() val jsonlField = (obj["jsonl"] as? JsonPrimitive)?.content?.trim().orEmpty()
val hasMessagesArray = obj["messages"] is JsonArray val hasMessagesArray = obj["messages"] is JsonArray
if (command == ClawdisCanvasA2UICommand.PushJSONL.rawValue || (!hasMessagesArray && jsonlField.isNotBlank())) { if (command == ClawdbotCanvasA2UICommand.PushJSONL.rawValue || (!hasMessagesArray && jsonlField.isNotBlank())) {
val jsonl = jsonlField val jsonl = jsonlField
if (jsonl.isBlank()) throw IllegalArgumentException("INVALID_REQUEST: jsonl required") if (jsonl.isBlank()) throw IllegalArgumentException("INVALID_REQUEST: jsonl required")
val messages = val messages =
@@ -1091,7 +1091,7 @@ private const val a2uiReadyCheckJS: String =
""" """
(() => { (() => {
try { try {
return !!globalThis.clawdisA2UI && typeof globalThis.clawdisA2UI.applyMessages === 'function'; return !!globalThis.clawdbotA2UI && typeof globalThis.clawdbotA2UI.applyMessages === 'function';
} catch (_) { } catch (_) {
return false; return false;
} }
@@ -1102,8 +1102,8 @@ private const val a2uiResetJS: String =
""" """
(() => { (() => {
try { try {
if (!globalThis.clawdisA2UI) return { ok: false, error: "missing clawdisA2UI" }; if (!globalThis.clawdbotA2UI) return { ok: false, error: "missing clawdbotA2UI" };
return globalThis.clawdisA2UI.reset(); return globalThis.clawdbotA2UI.reset();
} catch (e) { } catch (e) {
return { ok: false, error: String(e?.message ?? e) }; return { ok: false, error: String(e?.message ?? e) };
} }
@@ -1114,9 +1114,9 @@ private fun a2uiApplyMessagesJS(messagesJson: String): String {
return """ return """
(() => { (() => {
try { try {
if (!globalThis.clawdisA2UI) return { ok: false, error: "missing clawdisA2UI" }; if (!globalThis.clawdbotA2UI) return { ok: false, error: "missing clawdbotA2UI" };
const messages = $messagesJson; const messages = $messagesJson;
return globalThis.clawdisA2UI.applyMessages(messages); return globalThis.clawdbotA2UI.applyMessages(messages);
} catch (e) { } catch (e) {
return { ok: false, error: String(e?.message ?? e) }; return { ok: false, error: String(e?.message ?? e) };
} }

View File

@@ -1,4 +1,4 @@
package com.clawdis.android package com.clawdbot.android
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.Intent import android.content.Intent
@@ -115,7 +115,7 @@ class PermissionRequester(private val activity: ComponentActivity) {
private fun buildRationaleMessage(permissions: List<String>): String { private fun buildRationaleMessage(permissions: List<String>): String {
val labels = permissions.map { permissionLabel(it) } val labels = permissions.map { permissionLabel(it) }
return "Clawdis needs ${labels.joinToString(", ")} permissions to continue." return "Clawdbot needs ${labels.joinToString(", ")} permissions to continue."
} }
private fun buildSettingsMessage(permissions: List<String>): String { private fun buildSettingsMessage(permissions: List<String>): String {

View File

@@ -1,4 +1,4 @@
package com.clawdis.android package com.clawdbot.android
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
@@ -55,7 +55,7 @@ class ScreenCaptureRequester(private val activity: ComponentActivity) {
suspendCancellableCoroutine { cont -> suspendCancellableCoroutine { cont ->
AlertDialog.Builder(activity) AlertDialog.Builder(activity)
.setTitle("Screen recording required") .setTitle("Screen recording required")
.setMessage("Clawdis needs to record the screen for this command.") .setMessage("Clawdbot needs to record the screen for this command.")
.setPositiveButton("Continue") { _, _ -> cont.resume(true) } .setPositiveButton("Continue") { _, _ -> cont.resume(true) }
.setNegativeButton("Not now") { _, _ -> cont.resume(false) } .setNegativeButton("Not now") { _, _ -> cont.resume(false) }
.setOnCancelListener { cont.resume(false) } .setOnCancelListener { cont.resume(false) }

View File

@@ -1,6 +1,6 @@
@file:Suppress("DEPRECATION") @file:Suppress("DEPRECATION")
package com.clawdis.android package com.clawdbot.android
import android.content.Context import android.content.Context
import androidx.core.content.edit import androidx.core.content.edit
@@ -31,7 +31,7 @@ class SecurePrefs(context: Context) {
private val prefs = private val prefs =
EncryptedSharedPreferences.create( EncryptedSharedPreferences.create(
context, context,
"clawdis.node.secure", "clawdbot.node.secure",
masterKey, masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,

View File

@@ -1,4 +1,4 @@
package com.clawdis.android package com.clawdbot.android
enum class VoiceWakeMode(val rawValue: String) { enum class VoiceWakeMode(val rawValue: String) {
Off("off"), Off("off"),

View File

@@ -1,4 +1,4 @@
package com.clawdis.android package com.clawdbot.android
object WakeWords { object WakeWords {
const val maxWords: Int = 32 const val maxWords: Int = 32

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.bridge package com.clawdbot.android.bridge
object BonjourEscapes { object BonjourEscapes {
fun decode(input: String): String { fun decode(input: String): String {

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.bridge package com.clawdbot.android.bridge
import android.content.Context import android.content.Context
import android.net.ConnectivityManager import android.net.ConnectivityManager
@@ -51,9 +51,9 @@ class BridgeDiscovery(
private val nsd = context.getSystemService(NsdManager::class.java) private val nsd = context.getSystemService(NsdManager::class.java)
private val connectivity = context.getSystemService(ConnectivityManager::class.java) private val connectivity = context.getSystemService(ConnectivityManager::class.java)
private val dns = DnsResolver.getInstance() private val dns = DnsResolver.getInstance()
private val serviceType = "_clawdis-bridge._tcp." private val serviceType = "_clawdbot-bridge._tcp."
private val wideAreaDomain = "clawdis.internal." private val wideAreaDomain = "clawdbot.internal."
private val logTag = "Clawdis/BridgeDiscovery" private val logTag = "Clawdbot/BridgeDiscovery"
private val localById = ConcurrentHashMap<String, BridgeEndpoint>() private val localById = ConcurrentHashMap<String, BridgeEndpoint>()
private val unicastById = ConcurrentHashMap<String, BridgeEndpoint>() private val unicastById = ConcurrentHashMap<String, BridgeEndpoint>()

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.bridge package com.clawdbot.android.bridge
data class BridgeEndpoint( data class BridgeEndpoint(
val stableId: String, val stableId: String,

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.bridge package com.clawdbot.android.bridge
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.bridge package com.clawdbot.android.bridge
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -11,7 +11,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import com.clawdis.android.BuildConfig import com.clawdbot.android.BuildConfig
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
@@ -217,7 +217,7 @@ class BridgeSession(
// Local JVM unit tests use android.jar stubs; Log.d can throw "not mocked". // Local JVM unit tests use android.jar stubs; Log.d can throw "not mocked".
runCatching { runCatching {
android.util.Log.d( android.util.Log.d(
"ClawdisBridge", "ClawdbotBridge",
"canvasHostUrl resolved=${canvasHostUrl ?: "none"} (raw=${rawCanvasUrl ?: "none"})", "canvasHostUrl resolved=${canvasHostUrl ?: "none"} (raw=${rawCanvasUrl ?: "none"})",
) )
} }

View File

@@ -1,6 +1,6 @@
package com.clawdis.android.chat package com.clawdbot.android.chat
import com.clawdis.android.bridge.BridgeSession import com.clawdbot.android.bridge.BridgeSession
import java.util.UUID import java.util.UUID
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.chat package com.clawdbot.android.chat
data class ChatMessage( data class ChatMessage(
val id: String, val id: String,

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.node package com.clawdbot.android.node
import android.Manifest import android.Manifest
import android.content.Context import android.content.Context
@@ -20,7 +20,7 @@ import androidx.camera.video.VideoRecordEvent
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.checkSelfPermission import androidx.core.content.ContextCompat.checkSelfPermission
import androidx.core.graphics.scale import androidx.core.graphics.scale
import com.clawdis.android.PermissionRequester import com.clawdbot.android.PermissionRequester
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
@@ -152,7 +152,7 @@ class CameraCaptureManager(private val context: Context) {
provider.unbindAll() provider.unbindAll()
provider.bindToLifecycle(owner, selector, videoCapture) provider.bindToLifecycle(owner, selector, videoCapture)
val file = File.createTempFile("clawdis-clip-", ".mp4") val file = File.createTempFile("clawdbot-clip-", ".mp4")
val outputOptions = FileOutputOptions.Builder(file).build() val outputOptions = FileOutputOptions.Builder(file).build()
val finalized = kotlinx.coroutines.CompletableDeferred<VideoRecordEvent.Finalize>() val finalized = kotlinx.coroutines.CompletableDeferred<VideoRecordEvent.Finalize>()
@@ -256,7 +256,7 @@ private suspend fun Context.cameraProvider(): ProcessCameraProvider =
private suspend fun ImageCapture.takeJpegBytes(executor: Executor): ByteArray = private suspend fun ImageCapture.takeJpegBytes(executor: Executor): ByteArray =
suspendCancellableCoroutine { cont -> suspendCancellableCoroutine { cont ->
val file = File.createTempFile("clawdis-snap-", ".jpg") val file = File.createTempFile("clawdbot-snap-", ".jpg")
val options = ImageCapture.OutputFileOptions.Builder(file).build() val options = ImageCapture.OutputFileOptions.Builder(file).build()
takePicture( takePicture(
options, options,

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.node package com.clawdbot.android.node
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
@@ -17,7 +17,7 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonPrimitive
import com.clawdis.android.BuildConfig import com.clawdbot.android.BuildConfig
import kotlin.coroutines.resume import kotlin.coroutines.resume
class CanvasController { class CanvasController {
@@ -84,12 +84,12 @@ class CanvasController {
withWebViewOnMain { wv -> withWebViewOnMain { wv ->
if (currentUrl == null) { if (currentUrl == null) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Log.d("ClawdisCanvas", "load scaffold: $scaffoldAssetUrl") Log.d("ClawdbotCanvas", "load scaffold: $scaffoldAssetUrl")
} }
wv.loadUrl(scaffoldAssetUrl) wv.loadUrl(scaffoldAssetUrl)
} else { } else {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Log.d("ClawdisCanvas", "load url: $currentUrl") Log.d("ClawdbotCanvas", "load url: $currentUrl")
} }
wv.loadUrl(currentUrl) wv.loadUrl(currentUrl)
} }
@@ -106,7 +106,7 @@ class CanvasController {
val js = """ val js = """
(() => { (() => {
try { try {
const api = globalThis.__clawdis; const api = globalThis.__clawdbot;
if (!api) return; if (!api) return;
if (typeof api.setDebugStatusEnabled === 'function') { if (typeof api.setDebugStatusEnabled === 'function') {
api.setDebugStatusEnabled(${if (enabled) "true" else "false"}); api.setDebugStatusEnabled(${if (enabled) "true" else "false"});

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.node package com.clawdbot.android.node
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.node package com.clawdbot.android.node
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context

View File

@@ -1,11 +1,11 @@
package com.clawdis.android.node package com.clawdbot.android.node
import android.content.Context import android.content.Context
import android.hardware.display.DisplayManager import android.hardware.display.DisplayManager
import android.media.MediaRecorder import android.media.MediaRecorder
import android.media.projection.MediaProjectionManager import android.media.projection.MediaProjectionManager
import android.util.Base64 import android.util.Base64
import com.clawdis.android.ScreenCaptureRequester import com.clawdbot.android.ScreenCaptureRequester
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -16,13 +16,13 @@ class ScreenRecordManager(private val context: Context) {
data class Payload(val payloadJson: String) data class Payload(val payloadJson: String)
@Volatile private var screenCaptureRequester: ScreenCaptureRequester? = null @Volatile private var screenCaptureRequester: ScreenCaptureRequester? = null
@Volatile private var permissionRequester: com.clawdis.android.PermissionRequester? = null @Volatile private var permissionRequester: com.clawdbot.android.PermissionRequester? = null
fun attachScreenCaptureRequester(requester: ScreenCaptureRequester) { fun attachScreenCaptureRequester(requester: ScreenCaptureRequester) {
screenCaptureRequester = requester screenCaptureRequester = requester
} }
fun attachPermissionRequester(requester: com.clawdis.android.PermissionRequester) { fun attachPermissionRequester(requester: com.clawdbot.android.PermissionRequester) {
permissionRequester = requester permissionRequester = requester
} }
@@ -62,7 +62,7 @@ class ScreenRecordManager(private val context: Context) {
val height = metrics.heightPixels val height = metrics.heightPixels
val densityDpi = metrics.densityDpi val densityDpi = metrics.densityDpi
val file = File.createTempFile("clawdis-screen-", ".mp4") val file = File.createTempFile("clawdbot-screen-", ".mp4")
if (includeAudio) ensureMicPermission() if (includeAudio) ensureMicPermission()
val recorder = MediaRecorder() val recorder = MediaRecorder()
@@ -89,7 +89,7 @@ class ScreenRecordManager(private val context: Context) {
val surface = recorder.surface val surface = recorder.surface
virtualDisplay = virtualDisplay =
projection.createVirtualDisplay( projection.createVirtualDisplay(
"clawdis-screen", "clawdbot-screen",
width, width,
height, height,
densityDpi, densityDpi,

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.node package com.clawdbot.android.node
import android.Manifest import android.Manifest
import android.content.Context import android.content.Context
@@ -11,7 +11,7 @@ import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import com.clawdis.android.PermissionRequester import com.clawdbot.android.PermissionRequester
/** /**
* Sends SMS messages via the Android SMS API. * Sends SMS messages via the Android SMS API.

View File

@@ -1,9 +1,9 @@
package com.clawdis.android.protocol package com.clawdbot.android.protocol
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonPrimitive
object ClawdisCanvasA2UIAction { object ClawdbotCanvasA2UIAction {
fun extractActionName(userAction: JsonObject): String? { fun extractActionName(userAction: JsonObject): String? {
val name = val name =
(userAction["name"] as? JsonPrimitive) (userAction["name"] as? JsonPrimitive)
@@ -61,6 +61,6 @@ object ClawdisCanvasA2UIAction {
val err = (error ?: "").replace("\\", "\\\\").replace("\"", "\\\"") val err = (error ?: "").replace("\\", "\\\\").replace("\"", "\\\"")
val okLiteral = if (ok) "true" else "false" val okLiteral = if (ok) "true" else "false"
val idEscaped = actionId.replace("\\", "\\\\").replace("\"", "\\\"") val idEscaped = actionId.replace("\\", "\\\\").replace("\"", "\\\"")
return "window.dispatchEvent(new CustomEvent('clawdis:a2ui-action-status', { detail: { id: \"${idEscaped}\", ok: ${okLiteral}, error: \"${err}\" } }));" return "window.dispatchEvent(new CustomEvent('clawdbot:a2ui-action-status', { detail: { id: \"${idEscaped}\", ok: ${okLiteral}, error: \"${err}\" } }));"
} }
} }

View File

@@ -1,6 +1,6 @@
package com.clawdis.android.protocol package com.clawdbot.android.protocol
enum class ClawdisCapability(val rawValue: String) { enum class ClawdbotCapability(val rawValue: String) {
Canvas("canvas"), Canvas("canvas"),
Camera("camera"), Camera("camera"),
Screen("screen"), Screen("screen"),
@@ -9,7 +9,7 @@ enum class ClawdisCapability(val rawValue: String) {
Location("location"), Location("location"),
} }
enum class ClawdisCanvasCommand(val rawValue: String) { enum class ClawdbotCanvasCommand(val rawValue: String) {
Present("canvas.present"), Present("canvas.present"),
Hide("canvas.hide"), Hide("canvas.hide"),
Navigate("canvas.navigate"), Navigate("canvas.navigate"),
@@ -22,7 +22,7 @@ enum class ClawdisCanvasCommand(val rawValue: String) {
} }
} }
enum class ClawdisCanvasA2UICommand(val rawValue: String) { enum class ClawdbotCanvasA2UICommand(val rawValue: String) {
Push("canvas.a2ui.push"), Push("canvas.a2ui.push"),
PushJSONL("canvas.a2ui.pushJSONL"), PushJSONL("canvas.a2ui.pushJSONL"),
Reset("canvas.a2ui.reset"), Reset("canvas.a2ui.reset"),
@@ -33,7 +33,7 @@ enum class ClawdisCanvasA2UICommand(val rawValue: String) {
} }
} }
enum class ClawdisCameraCommand(val rawValue: String) { enum class ClawdbotCameraCommand(val rawValue: String) {
Snap("camera.snap"), Snap("camera.snap"),
Clip("camera.clip"), Clip("camera.clip"),
; ;
@@ -43,7 +43,7 @@ enum class ClawdisCameraCommand(val rawValue: String) {
} }
} }
enum class ClawdisScreenCommand(val rawValue: String) { enum class ClawdbotScreenCommand(val rawValue: String) {
Record("screen.record"), Record("screen.record"),
; ;
@@ -52,7 +52,7 @@ enum class ClawdisScreenCommand(val rawValue: String) {
} }
} }
enum class ClawdisSmsCommand(val rawValue: String) { enum class ClawdbotSmsCommand(val rawValue: String) {
Send("sms.send"), Send("sms.send"),
; ;
@@ -61,7 +61,7 @@ enum class ClawdisSmsCommand(val rawValue: String) {
} }
} }
enum class ClawdisLocationCommand(val rawValue: String) { enum class ClawdbotLocationCommand(val rawValue: String) {
Get("location.get"), Get("location.get"),
; ;

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.tools package com.clawdbot.android.tools
import android.content.Context import android.content.Context
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.ui package com.clawdbot.android.ui
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box

View File

@@ -1,8 +1,8 @@
package com.clawdis.android.ui package com.clawdbot.android.ui
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import com.clawdis.android.MainViewModel import com.clawdbot.android.MainViewModel
import com.clawdis.android.ui.chat.ChatSheetContent import com.clawdbot.android.ui.chat.ChatSheetContent
@Composable @Composable
fun ChatSheet(viewModel: MainViewModel) { fun ChatSheet(viewModel: MainViewModel) {

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.ui package com.clawdbot.android.ui
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -9,7 +9,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@Composable @Composable
fun ClawdisTheme(content: @Composable () -> Unit) { fun ClawdbotTheme(content: @Composable () -> Unit) {
val context = LocalContext.current val context = LocalContext.current
val isDark = isSystemInDarkTheme() val isDark = isSystemInDarkTheme()
val colorScheme = if (isDark) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) val colorScheme = if (isDark) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.ui package com.clawdbot.android.ui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.Manifest import android.Manifest
@@ -65,8 +65,8 @@ import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Popup import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties import androidx.compose.ui.window.PopupProperties
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.clawdis.android.CameraHudKind import com.clawdbot.android.CameraHudKind
import com.clawdis.android.MainViewModel import com.clawdbot.android.MainViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -334,7 +334,7 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier)
WebSettingsCompat.setAlgorithmicDarkeningAllowed(settings, false) WebSettingsCompat.setAlgorithmicDarkeningAllowed(settings, false)
} }
if (isDebuggable) { if (isDebuggable) {
Log.d("ClawdisWebView", "userAgent: ${settings.userAgentString}") Log.d("ClawdbotWebView", "userAgent: ${settings.userAgentString}")
} }
isScrollContainer = true isScrollContainer = true
overScrollMode = View.OVER_SCROLL_IF_CONTENT_SCROLLS overScrollMode = View.OVER_SCROLL_IF_CONTENT_SCROLLS
@@ -349,7 +349,7 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier)
) { ) {
if (!isDebuggable) return if (!isDebuggable) return
if (!request.isForMainFrame) return if (!request.isForMainFrame) return
Log.e("ClawdisWebView", "onReceivedError: ${error.errorCode} ${error.description} ${request.url}") Log.e("ClawdbotWebView", "onReceivedError: ${error.errorCode} ${error.description} ${request.url}")
} }
override fun onReceivedHttpError( override fun onReceivedHttpError(
@@ -360,14 +360,14 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier)
if (!isDebuggable) return if (!isDebuggable) return
if (!request.isForMainFrame) return if (!request.isForMainFrame) return
Log.e( Log.e(
"ClawdisWebView", "ClawdbotWebView",
"onReceivedHttpError: ${errorResponse.statusCode} ${errorResponse.reasonPhrase} ${request.url}", "onReceivedHttpError: ${errorResponse.statusCode} ${errorResponse.reasonPhrase} ${request.url}",
) )
} }
override fun onPageFinished(view: WebView, url: String?) { override fun onPageFinished(view: WebView, url: String?) {
if (isDebuggable) { if (isDebuggable) {
Log.d("ClawdisWebView", "onPageFinished: $url") Log.d("ClawdbotWebView", "onPageFinished: $url")
} }
viewModel.canvas.onPageFinished() viewModel.canvas.onPageFinished()
} }
@@ -378,7 +378,7 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier)
): Boolean { ): Boolean {
if (isDebuggable) { if (isDebuggable) {
Log.e( Log.e(
"ClawdisWebView", "ClawdbotWebView",
"onRenderProcessGone didCrash=${detail.didCrash()} priorityAtExit=${detail.rendererPriorityAtExit()}", "onRenderProcessGone didCrash=${detail.didCrash()} priorityAtExit=${detail.rendererPriorityAtExit()}",
) )
} }
@@ -391,7 +391,7 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier)
if (!isDebuggable) return false if (!isDebuggable) return false
val msg = consoleMessage ?: return false val msg = consoleMessage ?: return false
Log.d( Log.d(
"ClawdisWebView", "ClawdbotWebView",
"console ${msg.messageLevel()} @ ${msg.sourceId()}:${msg.lineNumber()} ${msg.message()}", "console ${msg.messageLevel()} @ ${msg.sourceId()}:${msg.lineNumber()} ${msg.message()}",
) )
return false return false
@@ -423,7 +423,7 @@ private class CanvasA2UIActionBridge(private val onMessage: (String) -> Unit) {
} }
companion object { companion object {
const val interfaceName: String = "clawdisCanvasA2UIAction" const val interfaceName: String = "clawdbotCanvasA2UIAction"
} }
} }

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.ui package com.clawdbot.android.ui
import android.Manifest import android.Manifest
import android.content.Context import android.content.Context
@@ -52,11 +52,11 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.clawdis.android.BuildConfig import com.clawdbot.android.BuildConfig
import com.clawdis.android.LocationMode import com.clawdbot.android.LocationMode
import com.clawdis.android.MainViewModel import com.clawdbot.android.MainViewModel
import com.clawdis.android.NodeForegroundService import com.clawdbot.android.NodeForegroundService
import com.clawdis.android.VoiceWakeMode import com.clawdbot.android.VoiceWakeMode
@Composable @Composable
fun SettingsSheet(viewModel: MainViewModel) { fun SettingsSheet(viewModel: MainViewModel) {
@@ -436,7 +436,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
Column(verticalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.fillMaxWidth()) { Column(verticalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.fillMaxWidth()) {
ListItem( ListItem(
headlineContent = { Text("Foreground Only") }, headlineContent = { Text("Foreground Only") },
supportingContent = { Text("Listens only while Clawdis is open.") }, supportingContent = { Text("Listens only while Clawdbot is open.") },
trailingContent = { trailingContent = {
RadioButton( RadioButton(
selected = voiceWakeMode == VoiceWakeMode.Foreground, selected = voiceWakeMode == VoiceWakeMode.Foreground,
@@ -482,7 +482,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button( Button(
onClick = { onClick = {
val parsed = com.clawdis.android.WakeWords.parseCommaSeparated(wakeWordsText) val parsed = com.clawdbot.android.WakeWords.parseCommaSeparated(wakeWordsText)
viewModel.setWakeWords(parsed) viewModel.setWakeWords(parsed)
}, },
enabled = isConnected, enabled = isConnected,
@@ -580,7 +580,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
) )
ListItem( ListItem(
headlineContent = { Text("While Using") }, headlineContent = { Text("While Using") },
supportingContent = { Text("Only while Clawdis is open.") }, supportingContent = { Text("Only while Clawdbot is open.") },
trailingContent = { trailingContent = {
RadioButton( RadioButton(
selected = locationMode == LocationMode.WhileUsing, selected = locationMode == LocationMode.WhileUsing,
@@ -627,7 +627,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
item { item {
ListItem( ListItem(
headlineContent = { Text("Prevent Sleep") }, headlineContent = { Text("Prevent Sleep") },
supportingContent = { Text("Keeps the screen awake while Clawdis is open.") }, supportingContent = { Text("Keeps the screen awake while Clawdbot is open.") },
trailingContent = { Switch(checked = preventSleep, onCheckedChange = viewModel::setPreventSleep) }, trailingContent = { Switch(checked = preventSleep, onCheckedChange = viewModel::setPreventSleep) },
) )
} }

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.ui package com.clawdbot.android.ui
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.ui package com.clawdbot.android.ui
import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.RepeatMode

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.ui.chat package com.clawdbot.android.ui.chat
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -38,7 +38,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.clawdis.android.chat.ChatSessionEntry import com.clawdbot.android.chat.ChatSessionEntry
@Composable @Composable
fun ChatComposer( fun ChatComposer(

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.ui.chat package com.clawdbot.android.ui.chat
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.util.Base64 import android.util.Base64

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.ui.chat package com.clawdbot.android.ui.chat
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -20,8 +20,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.clawdis.android.chat.ChatMessage import com.clawdbot.android.chat.ChatMessage
import com.clawdis.android.chat.ChatPendingToolCall import com.clawdbot.android.chat.ChatPendingToolCall
@Composable @Composable
fun ChatMessageListCard( fun ChatMessageListCard(

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.ui.chat package com.clawdbot.android.ui.chat
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.util.Base64 import android.util.Base64
@@ -31,10 +31,10 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import com.clawdis.android.chat.ChatMessage import com.clawdbot.android.chat.ChatMessage
import com.clawdis.android.chat.ChatMessageContent import com.clawdbot.android.chat.ChatMessageContent
import com.clawdis.android.chat.ChatPendingToolCall import com.clawdbot.android.chat.ChatPendingToolCall
import com.clawdis.android.tools.ToolDisplayRegistry import com.clawdbot.android.tools.ToolDisplayRegistry
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.ui.chat package com.clawdbot.android.ui.chat
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -20,7 +20,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.clawdis.android.chat.ChatSessionEntry import com.clawdbot.android.chat.ChatSessionEntry
@Composable @Composable
fun ChatSessionsDialog( fun ChatSessionsDialog(

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.ui.chat package com.clawdbot.android.ui.chat
import android.content.ContentResolver import android.content.ContentResolver
import android.net.Uri import android.net.Uri
@@ -19,8 +19,8 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.clawdis.android.MainViewModel import com.clawdbot.android.MainViewModel
import com.clawdis.android.chat.OutgoingAttachment import com.clawdbot.android.chat.OutgoingAttachment
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch

View File

@@ -1,6 +1,6 @@
package com.clawdis.android.ui.chat package com.clawdbot.android.ui.chat
import com.clawdis.android.chat.ChatSessionEntry import com.clawdbot.android.chat.ChatSessionEntry
private const val MAIN_SESSION_KEY = "main" private const val MAIN_SESSION_KEY = "main"
private const val RECENT_WINDOW_MS = 24 * 60 * 60 * 1000L private const val RECENT_WINDOW_MS = 24 * 60 * 60 * 1000L

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.voice package com.clawdbot.android.voice
import android.media.MediaDataSource import android.media.MediaDataSource
import kotlin.math.min import kotlin.math.min

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.voice package com.clawdbot.android.voice
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.voice package com.clawdbot.android.voice
import android.Manifest import android.Manifest
import android.content.Context import android.content.Context
@@ -20,7 +20,7 @@ import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener import android.speech.tts.UtteranceProgressListener
import android.util.Log import android.util.Log
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.clawdis.android.bridge.BridgeSession import com.clawdbot.android.bridge.BridgeSession
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.util.UUID import java.util.UUID

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.voice package com.clawdbot.android.voice
object VoiceWakeCommandExtractor { object VoiceWakeCommandExtractor {
fun extractCommand(text: String, triggerWords: List<String>): String? { fun extractCommand(text: String, triggerWords: List<String>): String? {

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.voice package com.clawdbot.android.voice
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent

View File

@@ -1,4 +1,4 @@
<resources> <resources>
<string name="app_name">Clawdis Node</string> <string name="app_name">Clawdbot Node</string>
</resources> </resources>

View File

@@ -1,5 +1,5 @@
<resources> <resources>
<style name="Theme.ClawdisNode" parent="Theme.Material3.DayNight.NoActionBar"> <style name="Theme.ClawdbotNode" parent="Theme.Material3.DayNight.NoActionBar">
<item name="android:statusBarColor">@android:color/transparent</item> <item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item> <item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">false</item> <item name="android:windowLightStatusBar">false</item>

View File

@@ -4,7 +4,7 @@
<base-config cleartextTrafficPermitted="true" tools:ignore="InsecureBaseConfiguration" /> <base-config cleartextTrafficPermitted="true" tools:ignore="InsecureBaseConfiguration" />
<!-- Allow HTTP for tailnet/local dev endpoints (e.g. canvas/background web). --> <!-- Allow HTTP for tailnet/local dev endpoints (e.g. canvas/background web). -->
<domain-config cleartextTrafficPermitted="true"> <domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">clawdis.internal</domain> <domain includeSubdomains="true">clawdbot.internal</domain>
</domain-config> </domain-config>
<domain-config cleartextTrafficPermitted="true"> <domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">ts.net</domain> <domain includeSubdomains="true">ts.net</domain>

View File

@@ -1,4 +1,4 @@
package com.clawdis.android package com.clawdbot.android
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.bridge package com.clawdbot.android.bridge
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
@@ -12,7 +12,7 @@ class BonjourEscapesTest {
@Test @Test
fun decodeDecodesDecimalEscapes() { fun decodeDecodesDecimalEscapes() {
assertEquals("Clawdis Gateway", BonjourEscapes.decode("Clawdis\\032Gateway")) assertEquals("Clawdbot Gateway", BonjourEscapes.decode("Clawdbot\\032Gateway"))
assertEquals("A B", BonjourEscapes.decode("A\\032B")) assertEquals("A B", BonjourEscapes.decode("A\\032B"))
assertEquals("Peter\u2019s Mac", BonjourEscapes.decode("Peter\\226\\128\\153s Mac")) assertEquals("Peter\u2019s Mac", BonjourEscapes.decode("Peter\\226\\128\\153s Mac"))
} }

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.bridge package com.clawdbot.android.bridge
import io.kotest.core.spec.style.StringSpec import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.bridge package com.clawdbot.android.bridge
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.bridge package com.clawdbot.android.bridge
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.node package com.clawdbot.android.node
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.node package com.clawdbot.android.node
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.node package com.clawdbot.android.node
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive

View File

@@ -1,28 +1,28 @@
package com.clawdis.android.protocol package com.clawdbot.android.protocol
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
class ClawdisCanvasA2UIActionTest { class ClawdbotCanvasA2UIActionTest {
@Test @Test
fun extractActionNameAcceptsNameOrAction() { fun extractActionNameAcceptsNameOrAction() {
val nameObj = Json.parseToJsonElement("{\"name\":\"Hello\"}").jsonObject val nameObj = Json.parseToJsonElement("{\"name\":\"Hello\"}").jsonObject
assertEquals("Hello", ClawdisCanvasA2UIAction.extractActionName(nameObj)) assertEquals("Hello", ClawdbotCanvasA2UIAction.extractActionName(nameObj))
val actionObj = Json.parseToJsonElement("{\"action\":\"Wave\"}").jsonObject val actionObj = Json.parseToJsonElement("{\"action\":\"Wave\"}").jsonObject
assertEquals("Wave", ClawdisCanvasA2UIAction.extractActionName(actionObj)) assertEquals("Wave", ClawdbotCanvasA2UIAction.extractActionName(actionObj))
val fallbackObj = val fallbackObj =
Json.parseToJsonElement("{\"name\":\" \",\"action\":\"Fallback\"}").jsonObject Json.parseToJsonElement("{\"name\":\" \",\"action\":\"Fallback\"}").jsonObject
assertEquals("Fallback", ClawdisCanvasA2UIAction.extractActionName(fallbackObj)) assertEquals("Fallback", ClawdbotCanvasA2UIAction.extractActionName(fallbackObj))
} }
@Test @Test
fun formatAgentMessageMatchesSharedSpec() { fun formatAgentMessageMatchesSharedSpec() {
val msg = val msg =
ClawdisCanvasA2UIAction.formatAgentMessage( ClawdbotCanvasA2UIAction.formatAgentMessage(
actionName = "Get Weather", actionName = "Get Weather",
sessionKey = "main", sessionKey = "main",
surfaceId = "main", surfaceId = "main",
@@ -40,9 +40,9 @@ class ClawdisCanvasA2UIActionTest {
@Test @Test
fun jsDispatchA2uiStatusIsStable() { fun jsDispatchA2uiStatusIsStable() {
val js = ClawdisCanvasA2UIAction.jsDispatchA2UIActionStatus(actionId = "a1", ok = true, error = null) val js = ClawdbotCanvasA2UIAction.jsDispatchA2UIActionStatus(actionId = "a1", ok = true, error = null)
assertEquals( assertEquals(
"window.dispatchEvent(new CustomEvent('clawdis:a2ui-action-status', { detail: { id: \"a1\", ok: true, error: \"\" } }));", "window.dispatchEvent(new CustomEvent('clawdbot:a2ui-action-status', { detail: { id: \"a1\", ok: true, error: \"\" } }));",
js, js,
) )
} }

View File

@@ -0,0 +1,35 @@
package com.clawdbot.android.protocol
import org.junit.Assert.assertEquals
import org.junit.Test
class ClawdbotProtocolConstantsTest {
@Test
fun canvasCommandsUseStableStrings() {
assertEquals("canvas.present", ClawdbotCanvasCommand.Present.rawValue)
assertEquals("canvas.hide", ClawdbotCanvasCommand.Hide.rawValue)
assertEquals("canvas.navigate", ClawdbotCanvasCommand.Navigate.rawValue)
assertEquals("canvas.eval", ClawdbotCanvasCommand.Eval.rawValue)
assertEquals("canvas.snapshot", ClawdbotCanvasCommand.Snapshot.rawValue)
}
@Test
fun a2uiCommandsUseStableStrings() {
assertEquals("canvas.a2ui.push", ClawdbotCanvasA2UICommand.Push.rawValue)
assertEquals("canvas.a2ui.pushJSONL", ClawdbotCanvasA2UICommand.PushJSONL.rawValue)
assertEquals("canvas.a2ui.reset", ClawdbotCanvasA2UICommand.Reset.rawValue)
}
@Test
fun capabilitiesUseStableStrings() {
assertEquals("canvas", ClawdbotCapability.Canvas.rawValue)
assertEquals("camera", ClawdbotCapability.Camera.rawValue)
assertEquals("screen", ClawdbotCapability.Screen.rawValue)
assertEquals("voiceWake", ClawdbotCapability.VoiceWake.rawValue)
}
@Test
fun screenCommandsUseStableStrings() {
assertEquals("screen.record", ClawdbotScreenCommand.Record.rawValue)
}
}

View File

@@ -1,6 +1,6 @@
package com.clawdis.android.ui.chat package com.clawdbot.android.ui.chat
import com.clawdis.android.chat.ChatSessionEntry import com.clawdbot.android.chat.ChatSessionEntry
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.voice package com.clawdbot.android.voice
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.voice package com.clawdbot.android.voice
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull

View File

@@ -1,35 +0,0 @@
package com.clawdis.android.protocol
import org.junit.Assert.assertEquals
import org.junit.Test
class ClawdisProtocolConstantsTest {
@Test
fun canvasCommandsUseStableStrings() {
assertEquals("canvas.present", ClawdisCanvasCommand.Present.rawValue)
assertEquals("canvas.hide", ClawdisCanvasCommand.Hide.rawValue)
assertEquals("canvas.navigate", ClawdisCanvasCommand.Navigate.rawValue)
assertEquals("canvas.eval", ClawdisCanvasCommand.Eval.rawValue)
assertEquals("canvas.snapshot", ClawdisCanvasCommand.Snapshot.rawValue)
}
@Test
fun a2uiCommandsUseStableStrings() {
assertEquals("canvas.a2ui.push", ClawdisCanvasA2UICommand.Push.rawValue)
assertEquals("canvas.a2ui.pushJSONL", ClawdisCanvasA2UICommand.PushJSONL.rawValue)
assertEquals("canvas.a2ui.reset", ClawdisCanvasA2UICommand.Reset.rawValue)
}
@Test
fun capabilitiesUseStableStrings() {
assertEquals("canvas", ClawdisCapability.Canvas.rawValue)
assertEquals("camera", ClawdisCapability.Camera.rawValue)
assertEquals("screen", ClawdisCapability.Screen.rawValue)
assertEquals("voiceWake", ClawdisCapability.VoiceWake.rawValue)
}
@Test
fun screenCommandsUseStableStrings() {
assertEquals("screen.record", ClawdisScreenCommand.Record.rawValue)
}
}

View File

@@ -14,6 +14,6 @@ dependencyResolutionManagement {
} }
} }
rootProject.name = "ClawdisNodeAndroid" rootProject.name = "ClawdbotNodeAndroid"
include(":app") include(":app")

View File

@@ -1,4 +1,4 @@
# Clawdis (iOS) # Clawdbot (iOS)
Internal-only SwiftUI app scaffold. Internal-only SwiftUI app scaffold.
@@ -11,11 +11,11 @@ brew install swiftformat swiftlint
```bash ```bash
cd apps/ios cd apps/ios
xcodegen generate xcodegen generate
open Clawdis.xcodeproj open Clawdbot.xcodeproj
``` ```
## Shared packages ## Shared packages
- `../shared/ClawdisKit` — shared types/constants used by iOS (and later macOS bridge + gateway routing). - `../shared/ClawdbotKit` — shared types/constants used by iOS (and later macOS bridge + gateway routing).
## fastlane ## fastlane
```bash ```bash

View File

@@ -1,4 +1,4 @@
import ClawdisKit import ClawdbotKit
import Foundation import Foundation
import Network import Network
@@ -14,7 +14,7 @@ actor BridgeClient {
{ {
self.lineBuffer = Data() self.lineBuffer = Data()
let connection = NWConnection(to: endpoint, using: .tcp) let connection = NWConnection(to: endpoint, using: .tcp)
let queue = DispatchQueue(label: "com.clawdis.ios.bridge-client") let queue = DispatchQueue(label: "com.clawdbot.ios.bridge-client")
defer { connection.cancel() } defer { connection.cancel() }
try await self.withTimeout(seconds: 8, purpose: "connect") { try await self.withTimeout(seconds: 8, purpose: "connect") {
try await self.startAndWaitForReady(connection, queue: queue) try await self.startAndWaitForReady(connection, queue: queue)

View File

@@ -1,4 +1,4 @@
import ClawdisKit import ClawdbotKit
import Darwin import Darwin
import Foundation import Foundation
import Network import Network
@@ -99,7 +99,7 @@ final class BridgeConnectionController {
guard !instanceId.isEmpty else { return } guard !instanceId.isEmpty else { return }
let token = KeychainStore.loadString( let token = KeychainStore.loadString(
service: "com.clawdis.bridge", service: "com.clawdbot.bridge",
account: self.keychainAccount(instanceId: instanceId))? account: self.keychainAccount(instanceId: instanceId))?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" .trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
guard !token.isEmpty else { return } guard !token.isEmpty else { return }
@@ -189,7 +189,7 @@ final class BridgeConnectionController {
if !refreshed.isEmpty, refreshed != token { if !refreshed.isEmpty, refreshed != token {
_ = KeychainStore.saveString( _ = KeychainStore.saveString(
refreshed, refreshed,
service: "com.clawdis.bridge", service: "com.clawdbot.bridge",
account: self.keychainAccount(instanceId: instanceId)) account: self.keychainAccount(instanceId: instanceId))
} }
appModel.connectToBridge(endpoint: endpoint, hello: self.makeHello(token: resolvedToken)) appModel.connectToBridge(endpoint: endpoint, hello: self.makeHello(token: resolvedToken))
@@ -217,46 +217,46 @@ final class BridgeConnectionController {
} }
private func currentCaps() -> [String] { private func currentCaps() -> [String] {
var caps = [ClawdisCapability.canvas.rawValue, ClawdisCapability.screen.rawValue] var caps = [ClawdbotCapability.canvas.rawValue, ClawdbotCapability.screen.rawValue]
// Default-on: if the key doesn't exist yet, treat it as enabled. // Default-on: if the key doesn't exist yet, treat it as enabled.
let cameraEnabled = let cameraEnabled =
UserDefaults.standard.object(forKey: "camera.enabled") == nil UserDefaults.standard.object(forKey: "camera.enabled") == nil
? true ? true
: UserDefaults.standard.bool(forKey: "camera.enabled") : UserDefaults.standard.bool(forKey: "camera.enabled")
if cameraEnabled { caps.append(ClawdisCapability.camera.rawValue) } if cameraEnabled { caps.append(ClawdbotCapability.camera.rawValue) }
let voiceWakeEnabled = UserDefaults.standard.bool(forKey: VoiceWakePreferences.enabledKey) let voiceWakeEnabled = UserDefaults.standard.bool(forKey: VoiceWakePreferences.enabledKey)
if voiceWakeEnabled { caps.append(ClawdisCapability.voiceWake.rawValue) } if voiceWakeEnabled { caps.append(ClawdbotCapability.voiceWake.rawValue) }
let locationModeRaw = UserDefaults.standard.string(forKey: "location.enabledMode") ?? "off" let locationModeRaw = UserDefaults.standard.string(forKey: "location.enabledMode") ?? "off"
let locationMode = ClawdisLocationMode(rawValue: locationModeRaw) ?? .off let locationMode = ClawdbotLocationMode(rawValue: locationModeRaw) ?? .off
if locationMode != .off { caps.append(ClawdisCapability.location.rawValue) } if locationMode != .off { caps.append(ClawdbotCapability.location.rawValue) }
return caps return caps
} }
private func currentCommands() -> [String] { private func currentCommands() -> [String] {
var commands: [String] = [ var commands: [String] = [
ClawdisCanvasCommand.present.rawValue, ClawdbotCanvasCommand.present.rawValue,
ClawdisCanvasCommand.hide.rawValue, ClawdbotCanvasCommand.hide.rawValue,
ClawdisCanvasCommand.navigate.rawValue, ClawdbotCanvasCommand.navigate.rawValue,
ClawdisCanvasCommand.evalJS.rawValue, ClawdbotCanvasCommand.evalJS.rawValue,
ClawdisCanvasCommand.snapshot.rawValue, ClawdbotCanvasCommand.snapshot.rawValue,
ClawdisCanvasA2UICommand.push.rawValue, ClawdbotCanvasA2UICommand.push.rawValue,
ClawdisCanvasA2UICommand.pushJSONL.rawValue, ClawdbotCanvasA2UICommand.pushJSONL.rawValue,
ClawdisCanvasA2UICommand.reset.rawValue, ClawdbotCanvasA2UICommand.reset.rawValue,
ClawdisScreenCommand.record.rawValue, ClawdbotScreenCommand.record.rawValue,
] ]
let caps = Set(self.currentCaps()) let caps = Set(self.currentCaps())
if caps.contains(ClawdisCapability.camera.rawValue) { if caps.contains(ClawdbotCapability.camera.rawValue) {
commands.append(ClawdisCameraCommand.list.rawValue) commands.append(ClawdbotCameraCommand.list.rawValue)
commands.append(ClawdisCameraCommand.snap.rawValue) commands.append(ClawdbotCameraCommand.snap.rawValue)
commands.append(ClawdisCameraCommand.clip.rawValue) commands.append(ClawdbotCameraCommand.clip.rawValue)
} }
if caps.contains(ClawdisCapability.location.rawValue) { if caps.contains(ClawdbotCapability.location.rawValue) {
commands.append(ClawdisLocationCommand.get.rawValue) commands.append(ClawdbotLocationCommand.get.rawValue)
} }
return commands return commands

View File

@@ -1,4 +1,4 @@
import ClawdisKit import ClawdbotKit
import Foundation import Foundation
import Network import Network
import Observation import Observation
@@ -51,11 +51,11 @@ final class BridgeDiscoveryModel {
if !self.browsers.isEmpty { return } if !self.browsers.isEmpty { return }
self.appendDebugLog("start()") self.appendDebugLog("start()")
for domain in ClawdisBonjour.bridgeServiceDomains { for domain in ClawdbotBonjour.bridgeServiceDomains {
let params = NWParameters.tcp let params = NWParameters.tcp
params.includePeerToPeer = true params.includePeerToPeer = true
let browser = NWBrowser( let browser = NWBrowser(
for: .bonjour(type: ClawdisBonjour.bridgeServiceType, domain: domain), for: .bonjour(type: ClawdbotBonjour.bridgeServiceType, domain: domain),
using: params) using: params)
browser.stateUpdateHandler = { [weak self] state in browser.stateUpdateHandler = { [weak self] state in
@@ -102,7 +102,7 @@ final class BridgeDiscoveryModel {
} }
self.browsers[domain] = browser self.browsers[domain] = browser
browser.start(queue: DispatchQueue(label: "com.clawdis.ios.bridge-discovery.\(domain)")) browser.start(queue: DispatchQueue(label: "com.clawdbot.ios.bridge-discovery.\(domain)"))
} }
} }
@@ -200,7 +200,7 @@ final class BridgeDiscoveryModel {
private static func prettifyInstanceName(_ decodedName: String) -> String { private static func prettifyInstanceName(_ decodedName: String) -> String {
let normalized = decodedName.split(whereSeparator: \.isWhitespace).joined(separator: " ") let normalized = decodedName.split(whereSeparator: \.isWhitespace).joined(separator: " ")
let stripped = normalized.replacingOccurrences(of: " (Clawdis)", with: "") let stripped = normalized.replacingOccurrences(of: " (Clawdbot)", with: "")
.replacingOccurrences(of: #"\s+\(\d+\)$"#, with: "", options: .regularExpression) .replacingOccurrences(of: #"\s+\(\d+\)$"#, with: "", options: .regularExpression)
return stripped.trimmingCharacters(in: .whitespacesAndNewlines) return stripped.trimmingCharacters(in: .whitespacesAndNewlines)
} }

View File

@@ -1,4 +1,4 @@
import ClawdisKit import ClawdbotKit
import Foundation import Foundation
import Network import Network

View File

@@ -1,4 +1,4 @@
import ClawdisKit import ClawdbotKit
import Foundation import Foundation
import Network import Network
@@ -78,7 +78,7 @@ actor BridgeSession {
let params = NWParameters.tcp let params = NWParameters.tcp
params.includePeerToPeer = true params.includePeerToPeer = true
let connection = NWConnection(to: endpoint, using: params) let connection = NWConnection(to: endpoint, using: params)
let queue = DispatchQueue(label: "com.clawdis.ios.bridge-session") let queue = DispatchQueue(label: "com.clawdbot.ios.bridge-session")
self.connection = connection self.connection = connection
self.queue = queue self.queue = queue

View File

@@ -1,8 +1,8 @@
import Foundation import Foundation
enum BridgeSettingsStore { enum BridgeSettingsStore {
private static let bridgeService = "com.clawdis.bridge" private static let bridgeService = "com.clawdbot.bridge"
private static let nodeService = "com.clawdis.node" private static let nodeService = "com.clawdbot.node"
private static let instanceIdDefaultsKey = "node.instanceId" private static let instanceIdDefaultsKey = "node.instanceId"
private static let preferredBridgeStableIDDefaultsKey = "bridge.preferredStableID" private static let preferredBridgeStableIDDefaultsKey = "bridge.preferredStableID"

View File

@@ -1,5 +1,5 @@
import AVFoundation import AVFoundation
import ClawdisKit import ClawdbotKit
import Foundation import Foundation
actor CameraController { actor CameraController {
@@ -36,7 +36,7 @@ actor CameraController {
} }
} }
func snap(params: ClawdisCameraSnapParams) async throws -> ( func snap(params: ClawdbotCameraSnapParams) async throws -> (
format: String, format: String,
base64: String, base64: String,
width: Int, width: Int,
@@ -109,7 +109,7 @@ actor CameraController {
height: res.heightPx) height: res.heightPx)
} }
func clip(params: ClawdisCameraClipParams) async throws -> ( func clip(params: ClawdbotCameraClipParams) async throws -> (
format: String, format: String,
base64: String, base64: String,
durationMs: Int, durationMs: Int,
@@ -161,9 +161,9 @@ actor CameraController {
await Self.warmUpCaptureSession() await Self.warmUpCaptureSession()
let movURL = FileManager.default.temporaryDirectory let movURL = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdis-camera-\(UUID().uuidString).mov") .appendingPathComponent("clawdbot-camera-\(UUID().uuidString).mov")
let mp4URL = FileManager.default.temporaryDirectory let mp4URL = FileManager.default.temporaryDirectory
.appendingPathComponent("clawdis-camera-\(UUID().uuidString).mp4") .appendingPathComponent("clawdbot-camera-\(UUID().uuidString).mp4")
defer { defer {
try? FileManager.default.removeItem(at: movURL) try? FileManager.default.removeItem(at: movURL)
@@ -228,7 +228,7 @@ actor CameraController {
} }
private nonisolated static func pickCamera( private nonisolated static func pickCamera(
facing: ClawdisCameraFacing, facing: ClawdbotCameraFacing,
deviceId: String?) -> AVCaptureDevice? deviceId: String?) -> AVCaptureDevice?
{ {
if let deviceId, !deviceId.isEmpty { if let deviceId, !deviceId.isEmpty {

View File

@@ -1,15 +1,15 @@
import ClawdisChatUI import ClawdbotChatUI
import SwiftUI import SwiftUI
struct ChatSheet: View { struct ChatSheet: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@State private var viewModel: ClawdisChatViewModel @State private var viewModel: ClawdbotChatViewModel
private let userAccent: Color? private let userAccent: Color?
init(bridge: BridgeSession, sessionKey: String = "main", userAccent: Color? = nil) { init(bridge: BridgeSession, sessionKey: String = "main", userAccent: Color? = nil) {
let transport = IOSBridgeChatTransport(bridge: bridge) let transport = IOSBridgeChatTransport(bridge: bridge)
self._viewModel = State( self._viewModel = State(
initialValue: ClawdisChatViewModel( initialValue: ClawdbotChatViewModel(
sessionKey: sessionKey, sessionKey: sessionKey,
transport: transport)) transport: transport))
self.userAccent = userAccent self.userAccent = userAccent
@@ -17,7 +17,7 @@ struct ChatSheet: View {
var body: some View { var body: some View {
NavigationStack { NavigationStack {
ClawdisChatView( ClawdbotChatView(
viewModel: self.viewModel, viewModel: self.viewModel,
showsSessionSwitcher: true, showsSessionSwitcher: true,
userAccent: self.userAccent) userAccent: self.userAccent)

View File

@@ -1,8 +1,8 @@
import ClawdisChatUI import ClawdbotChatUI
import ClawdisKit import ClawdbotKit
import Foundation import Foundation
struct IOSBridgeChatTransport: ClawdisChatTransport, Sendable { struct IOSBridgeChatTransport: ClawdbotChatTransport, Sendable {
private let bridge: BridgeSession private let bridge: BridgeSession
init(bridge: BridgeSession) { init(bridge: BridgeSession) {
@@ -19,7 +19,7 @@ struct IOSBridgeChatTransport: ClawdisChatTransport, Sendable {
_ = try await self.bridge.request(method: "chat.abort", paramsJSON: json, timeoutSeconds: 10) _ = try await self.bridge.request(method: "chat.abort", paramsJSON: json, timeoutSeconds: 10)
} }
func listSessions(limit: Int?) async throws -> ClawdisChatSessionsListResponse { func listSessions(limit: Int?) async throws -> ClawdbotChatSessionsListResponse {
struct Params: Codable { struct Params: Codable {
var includeGlobal: Bool var includeGlobal: Bool
var includeUnknown: Bool var includeUnknown: Bool
@@ -28,7 +28,7 @@ struct IOSBridgeChatTransport: ClawdisChatTransport, Sendable {
let data = try JSONEncoder().encode(Params(includeGlobal: true, includeUnknown: false, limit: limit)) let data = try JSONEncoder().encode(Params(includeGlobal: true, includeUnknown: false, limit: limit))
let json = String(data: data, encoding: .utf8) 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.bridge.request(method: "sessions.list", paramsJSON: json, timeoutSeconds: 15)
return try JSONDecoder().decode(ClawdisChatSessionsListResponse.self, from: res) return try JSONDecoder().decode(ClawdbotChatSessionsListResponse.self, from: res)
} }
func setActiveSessionKey(_ sessionKey: String) async throws { func setActiveSessionKey(_ sessionKey: String) async throws {
@@ -38,12 +38,12 @@ struct IOSBridgeChatTransport: ClawdisChatTransport, Sendable {
try await self.bridge.sendEvent(event: "chat.subscribe", payloadJSON: json) try await self.bridge.sendEvent(event: "chat.subscribe", payloadJSON: json)
} }
func requestHistory(sessionKey: String) async throws -> ClawdisChatHistoryPayload { func requestHistory(sessionKey: String) async throws -> ClawdbotChatHistoryPayload {
struct Params: Codable { var sessionKey: String } struct Params: Codable { var sessionKey: String }
let data = try JSONEncoder().encode(Params(sessionKey: sessionKey)) let data = try JSONEncoder().encode(Params(sessionKey: sessionKey))
let json = String(data: data, encoding: .utf8) 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.bridge.request(method: "chat.history", paramsJSON: json, timeoutSeconds: 15)
return try JSONDecoder().decode(ClawdisChatHistoryPayload.self, from: res) return try JSONDecoder().decode(ClawdbotChatHistoryPayload.self, from: res)
} }
func sendMessage( func sendMessage(
@@ -51,13 +51,13 @@ struct IOSBridgeChatTransport: ClawdisChatTransport, Sendable {
message: String, message: String,
thinking: String, thinking: String,
idempotencyKey: String, idempotencyKey: String,
attachments: [ClawdisChatAttachmentPayload]) async throws -> ClawdisChatSendResponse attachments: [ClawdbotChatAttachmentPayload]) async throws -> ClawdbotChatSendResponse
{ {
struct Params: Codable { struct Params: Codable {
var sessionKey: String var sessionKey: String
var message: String var message: String
var thinking: String var thinking: String
var attachments: [ClawdisChatAttachmentPayload]? var attachments: [ClawdbotChatAttachmentPayload]?
var timeoutMs: Int var timeoutMs: Int
var idempotencyKey: String var idempotencyKey: String
} }
@@ -72,16 +72,16 @@ struct IOSBridgeChatTransport: ClawdisChatTransport, Sendable {
let data = try JSONEncoder().encode(params) let data = try JSONEncoder().encode(params)
let json = String(data: data, encoding: .utf8) 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.bridge.request(method: "chat.send", paramsJSON: json, timeoutSeconds: 35)
return try JSONDecoder().decode(ClawdisChatSendResponse.self, from: res) return try JSONDecoder().decode(ClawdbotChatSendResponse.self, from: res)
} }
func requestHealth(timeoutMs: Int) async throws -> Bool { func requestHealth(timeoutMs: Int) async throws -> Bool {
let seconds = max(1, Int(ceil(Double(timeoutMs) / 1000.0))) 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.bridge.request(method: "health", paramsJSON: nil, timeoutSeconds: seconds)
return (try? JSONDecoder().decode(ClawdisGatewayHealthOK.self, from: res))?.ok ?? true return (try? JSONDecoder().decode(ClawdbotGatewayHealthOK.self, from: res))?.ok ?? true
} }
func events() -> AsyncStream<ClawdisChatTransportEvent> { func events() -> AsyncStream<ClawdbotChatTransportEvent> {
AsyncStream { continuation in AsyncStream { continuation in
let task = Task { let task = Task {
let stream = await self.bridge.subscribeServerEvents() let stream = await self.bridge.subscribeServerEvents()
@@ -94,16 +94,16 @@ struct IOSBridgeChatTransport: ClawdisChatTransport, Sendable {
continuation.yield(.seqGap) continuation.yield(.seqGap)
case "health": case "health":
guard let json = evt.payloadJSON, let data = json.data(using: .utf8) else { break } guard let json = evt.payloadJSON, let data = json.data(using: .utf8) else { break }
let ok = (try? JSONDecoder().decode(ClawdisGatewayHealthOK.self, from: data))?.ok ?? true let ok = (try? JSONDecoder().decode(ClawdbotGatewayHealthOK.self, from: data))?.ok ?? true
continuation.yield(.health(ok: ok)) continuation.yield(.health(ok: ok))
case "chat": case "chat":
guard let json = evt.payloadJSON, let data = json.data(using: .utf8) else { break } guard let json = evt.payloadJSON, let data = json.data(using: .utf8) else { break }
if let payload = try? JSONDecoder().decode(ClawdisChatEventPayload.self, from: data) { if let payload = try? JSONDecoder().decode(ClawdbotChatEventPayload.self, from: data) {
continuation.yield(.chat(payload)) continuation.yield(.chat(payload))
} }
case "agent": case "agent":
guard let json = evt.payloadJSON, let data = json.data(using: .utf8) else { break } guard let json = evt.payloadJSON, let data = json.data(using: .utf8) else { break }
if let payload = try? JSONDecoder().decode(ClawdisAgentEventPayload.self, from: data) { if let payload = try? JSONDecoder().decode(ClawdbotAgentEventPayload.self, from: data) {
continuation.yield(.agent(payload)) continuation.yield(.agent(payload))
} }
default: default:

View File

@@ -1,7 +1,7 @@
import SwiftUI import SwiftUI
@main @main
struct ClawdisApp: App { struct ClawdbotApp: App {
@State private var appModel: NodeAppModel @State private var appModel: NodeAppModel
@State private var bridgeController: BridgeConnectionController @State private var bridgeController: BridgeConnectionController
@Environment(\.scenePhase) private var scenePhase @Environment(\.scenePhase) private var scenePhase

View File

@@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Clawdis</string> <string>Clawdbot</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconName</key> <key>CFBundleIconName</key>
@@ -29,20 +29,20 @@
</dict> </dict>
<key>NSBonjourServices</key> <key>NSBonjourServices</key>
<array> <array>
<string>_clawdis-bridge._tcp</string> <string>_clawdbot-bridge._tcp</string>
</array> </array>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>Clawdis can capture photos or short video clips when requested via the bridge.</string> <string>Clawdbot can capture photos or short video clips when requested via the bridge.</string>
<key>NSLocalNetworkUsageDescription</key> <key>NSLocalNetworkUsageDescription</key>
<string>Clawdis discovers and connects to your Clawdis bridge on the local network.</string> <string>Clawdbot discovers and connects to your Clawdbot bridge on the local network.</string>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>
<string>Clawdis uses your location when you allow location sharing.</string> <string>Clawdbot uses your location when you allow location sharing.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key> <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Clawdis can share your location in the background when you enable Always.</string> <string>Clawdbot can share your location in the background when you enable Always.</string>
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>Clawdis needs microphone access for voice wake.</string> <string>Clawdbot needs microphone access for voice wake.</string>
<key>NSSpeechRecognitionUsageDescription</key> <key>NSSpeechRecognitionUsageDescription</key>
<string>Clawdis uses on-device speech recognition for voice wake.</string> <string>Clawdbot uses on-device speech recognition for voice wake.</string>
<key>UIApplicationSceneManifest</key> <key>UIApplicationSceneManifest</key>
<dict> <dict>
<key>UIApplicationSupportsMultipleScenes</key> <key>UIApplicationSupportsMultipleScenes</key>

View File

@@ -1,4 +1,4 @@
import ClawdisKit import ClawdbotKit
import CoreLocation import CoreLocation
import Foundation import Foundation
@@ -30,7 +30,7 @@ final class LocationService: NSObject, CLLocationManagerDelegate {
return .fullAccuracy return .fullAccuracy
} }
func ensureAuthorization(mode: ClawdisLocationMode) async -> CLAuthorizationStatus { func ensureAuthorization(mode: ClawdbotLocationMode) async -> CLAuthorizationStatus {
guard CLLocationManager.locationServicesEnabled() else { return .denied } guard CLLocationManager.locationServicesEnabled() else { return .denied }
let status = self.manager.authorizationStatus let status = self.manager.authorizationStatus
@@ -53,8 +53,8 @@ final class LocationService: NSObject, CLLocationManagerDelegate {
} }
func currentLocation( func currentLocation(
params: ClawdisLocationGetParams, params: ClawdbotLocationGetParams,
desiredAccuracy: ClawdisLocationAccuracy, desiredAccuracy: ClawdbotLocationAccuracy,
maxAgeMs: Int?, maxAgeMs: Int?,
timeoutMs: Int?) async throws -> CLLocation timeoutMs: Int?) async throws -> CLLocation
{ {
@@ -106,7 +106,7 @@ final class LocationService: NSObject, CLLocationManagerDelegate {
} }
} }
private static func accuracyValue(_ accuracy: ClawdisLocationAccuracy) -> CLLocationAccuracy { private static func accuracyValue(_ accuracy: ClawdbotLocationAccuracy) -> CLLocationAccuracy {
switch accuracy { switch accuracy {
case .coarse: case .coarse:
return kCLLocationAccuracyKilometer return kCLLocationAccuracyKilometer

View File

@@ -1,4 +1,4 @@
import ClawdisKit import ClawdbotKit
import Network import Network
import Observation import Observation
import SwiftUI import SwiftUI
@@ -89,7 +89,7 @@ final class NodeAppModel {
}() }()
guard !userAction.isEmpty else { return } guard !userAction.isEmpty else { return }
guard let name = ClawdisCanvasA2UIAction.extractActionName(userAction) else { return } guard let name = ClawdbotCanvasA2UIAction.extractActionName(userAction) else { return }
let actionId: String = { let actionId: String = {
let id = (userAction["id"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" let id = (userAction["id"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return id.isEmpty ? UUID().uuidString : id return id.isEmpty ? UUID().uuidString : id
@@ -108,15 +108,15 @@ final class NodeAppModel {
let host = UserDefaults.standard.string(forKey: "node.displayName") ?? UIDevice.current.name let host = UserDefaults.standard.string(forKey: "node.displayName") ?? UIDevice.current.name
let instanceId = (UserDefaults.standard.string(forKey: "node.instanceId") ?? "ios-node").lowercased() let instanceId = (UserDefaults.standard.string(forKey: "node.instanceId") ?? "ios-node").lowercased()
let contextJSON = ClawdisCanvasA2UIAction.compactJSON(userAction["context"]) let contextJSON = ClawdbotCanvasA2UIAction.compactJSON(userAction["context"])
let sessionKey = "main" let sessionKey = "main"
let messageContext = ClawdisCanvasA2UIAction.AgentMessageContext( let messageContext = ClawdbotCanvasA2UIAction.AgentMessageContext(
actionName: name, actionName: name,
session: .init(key: sessionKey, surfaceId: surfaceId), session: .init(key: sessionKey, surfaceId: surfaceId),
component: .init(id: sourceComponentId, host: host, instanceId: instanceId), component: .init(id: sourceComponentId, host: host, instanceId: instanceId),
contextJSON: contextJSON) contextJSON: contextJSON)
let message = ClawdisCanvasA2UIAction.formatAgentMessage(messageContext) let message = ClawdbotCanvasA2UIAction.formatAgentMessage(messageContext)
let ok: Bool let ok: Bool
var errorText: String? var errorText: String?
@@ -141,7 +141,7 @@ final class NodeAppModel {
} }
} }
let js = ClawdisCanvasA2UIAction.jsDispatchA2UIActionStatus(actionId: actionId, ok: ok, error: errorText) let js = ClawdbotCanvasA2UIAction.jsDispatchA2UIActionStatus(actionId: actionId, ok: ok, error: errorText)
do { do {
_ = try await self.screen.eval(javaScript: js) _ = try await self.screen.eval(javaScript: js)
} catch { } catch {
@@ -153,7 +153,7 @@ final class NodeAppModel {
guard let raw = await self.bridge.currentCanvasHostUrl() else { return nil } guard let raw = await self.bridge.currentCanvasHostUrl() else { return nil }
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, let base = URL(string: trimmed) else { return nil } guard !trimmed.isEmpty, let base = URL(string: trimmed) else { return nil }
return base.appendingPathComponent("__clawdis__/a2ui/").absoluteString + "?platform=ios" return base.appendingPathComponent("__clawdbot__/a2ui/").absoluteString + "?platform=ios"
} }
private func showA2UIOnConnectIfNeeded() async { private func showA2UIOnConnectIfNeeded() async {
@@ -189,7 +189,7 @@ final class NodeAppModel {
self.talkMode.setEnabled(enabled) self.talkMode.setEnabled(enabled)
} }
func requestLocationPermissions(mode: ClawdisLocationMode) async -> Bool { func requestLocationPermissions(mode: ClawdbotLocationMode) async -> Bool {
guard mode != .off else { return true } guard mode != .off else { return true }
let status = await self.locationService.ensureAuthorization(mode: mode) let status = await self.locationService.ensureAuthorization(mode: mode)
switch status { switch status {
@@ -250,7 +250,7 @@ final class NodeAppModel {
return BridgeInvokeResponse( return BridgeInvokeResponse(
id: req.id, id: req.id,
ok: false, ok: false,
error: ClawdisNodeError(code: .unavailable, message: "UNAVAILABLE: node not ready")) error: ClawdbotNodeError(code: .unavailable, message: "UNAVAILABLE: node not ready"))
} }
return await self.handleInvoke(req) return await self.handleInvoke(req)
}) })
@@ -439,7 +439,7 @@ final class NodeAppModel {
} }
// iOS bridge forwards to the gateway; no local auth prompts here. // iOS bridge forwards to the gateway; no local auth prompts here.
// (Key-based unattended auth is handled on macOS for clawdis:// links.) // (Key-based unattended auth is handled on macOS for clawdbot:// links.)
let data = try JSONEncoder().encode(link) let data = try JSONEncoder().encode(link)
guard let json = String(bytes: data, encoding: .utf8) else { guard let json = String(bytes: data, encoding: .utf8) else {
throw NSError(domain: "NodeAppModel", code: 2, userInfo: [ throw NSError(domain: "NodeAppModel", code: 2, userInfo: [
@@ -464,7 +464,7 @@ final class NodeAppModel {
return BridgeInvokeResponse( return BridgeInvokeResponse(
id: req.id, id: req.id,
ok: false, ok: false,
error: ClawdisNodeError( error: ClawdbotNodeError(
code: .backgroundUnavailable, code: .backgroundUnavailable,
message: "NODE_BACKGROUND_UNAVAILABLE: canvas/camera/screen commands require foreground")) message: "NODE_BACKGROUND_UNAVAILABLE: canvas/camera/screen commands require foreground"))
} }
@@ -473,20 +473,20 @@ final class NodeAppModel {
return BridgeInvokeResponse( return BridgeInvokeResponse(
id: req.id, id: req.id,
ok: false, ok: false,
error: ClawdisNodeError( error: ClawdbotNodeError(
code: .unavailable, code: .unavailable,
message: "CAMERA_DISABLED: enable Camera in iOS Settings → Camera → Allow Camera")) message: "CAMERA_DISABLED: enable Camera in iOS Settings → Camera → Allow Camera"))
} }
do { do {
switch command { switch command {
case ClawdisLocationCommand.get.rawValue: case ClawdbotLocationCommand.get.rawValue:
let mode = self.locationMode() let mode = self.locationMode()
guard mode != .off else { guard mode != .off else {
return BridgeInvokeResponse( return BridgeInvokeResponse(
id: req.id, id: req.id,
ok: false, ok: false,
error: ClawdisNodeError( error: ClawdbotNodeError(
code: .unavailable, code: .unavailable,
message: "LOCATION_DISABLED: enable Location in Settings")) message: "LOCATION_DISABLED: enable Location in Settings"))
} }
@@ -494,12 +494,12 @@ final class NodeAppModel {
return BridgeInvokeResponse( return BridgeInvokeResponse(
id: req.id, id: req.id,
ok: false, ok: false,
error: ClawdisNodeError( error: ClawdbotNodeError(
code: .backgroundUnavailable, code: .backgroundUnavailable,
message: "LOCATION_BACKGROUND_UNAVAILABLE: background location requires Always")) message: "LOCATION_BACKGROUND_UNAVAILABLE: background location requires Always"))
} }
let params = (try? Self.decodeParams(ClawdisLocationGetParams.self, from: req.paramsJSON)) ?? let params = (try? Self.decodeParams(ClawdbotLocationGetParams.self, from: req.paramsJSON)) ??
ClawdisLocationGetParams() ClawdbotLocationGetParams()
let desired = params.desiredAccuracy ?? let desired = params.desiredAccuracy ??
(self.isLocationPreciseEnabled() ? .precise : .balanced) (self.isLocationPreciseEnabled() ? .precise : .balanced)
let status = self.locationService.authorizationStatus() let status = self.locationService.authorizationStatus()
@@ -507,7 +507,7 @@ final class NodeAppModel {
return BridgeInvokeResponse( return BridgeInvokeResponse(
id: req.id, id: req.id,
ok: false, ok: false,
error: ClawdisNodeError( error: ClawdbotNodeError(
code: .unavailable, code: .unavailable,
message: "LOCATION_PERMISSION_REQUIRED: grant Location permission")) message: "LOCATION_PERMISSION_REQUIRED: grant Location permission"))
} }
@@ -515,7 +515,7 @@ final class NodeAppModel {
return BridgeInvokeResponse( return BridgeInvokeResponse(
id: req.id, id: req.id,
ok: false, ok: false,
error: ClawdisNodeError( error: ClawdbotNodeError(
code: .unavailable, code: .unavailable,
message: "LOCATION_PERMISSION_REQUIRED: enable Always for background access")) message: "LOCATION_PERMISSION_REQUIRED: enable Always for background access"))
} }
@@ -525,7 +525,7 @@ final class NodeAppModel {
maxAgeMs: params.maxAgeMs, maxAgeMs: params.maxAgeMs,
timeoutMs: params.timeoutMs) timeoutMs: params.timeoutMs)
let isPrecise = self.locationService.accuracyAuthorization() == .fullAccuracy let isPrecise = self.locationService.accuracyAuthorization() == .fullAccuracy
let payload = ClawdisLocationPayload( let payload = ClawdbotLocationPayload(
lat: location.coordinate.latitude, lat: location.coordinate.latitude,
lon: location.coordinate.longitude, lon: location.coordinate.longitude,
accuracyMeters: location.horizontalAccuracy, accuracyMeters: location.horizontalAccuracy,
@@ -538,9 +538,9 @@ final class NodeAppModel {
let json = try Self.encodePayload(payload) let json = try Self.encodePayload(payload)
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json) return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json)
case ClawdisCanvasCommand.present.rawValue: case ClawdbotCanvasCommand.present.rawValue:
let params = (try? Self.decodeParams(ClawdisCanvasPresentParams.self, from: req.paramsJSON)) ?? let params = (try? Self.decodeParams(ClawdbotCanvasPresentParams.self, from: req.paramsJSON)) ??
ClawdisCanvasPresentParams() ClawdbotCanvasPresentParams()
let url = params.url?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" let url = params.url?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if url.isEmpty { if url.isEmpty {
self.screen.showDefaultCanvas() self.screen.showDefaultCanvas()
@@ -549,22 +549,22 @@ final class NodeAppModel {
} }
return BridgeInvokeResponse(id: req.id, ok: true) return BridgeInvokeResponse(id: req.id, ok: true)
case ClawdisCanvasCommand.hide.rawValue: case ClawdbotCanvasCommand.hide.rawValue:
return BridgeInvokeResponse(id: req.id, ok: true) return BridgeInvokeResponse(id: req.id, ok: true)
case ClawdisCanvasCommand.navigate.rawValue: case ClawdbotCanvasCommand.navigate.rawValue:
let params = try Self.decodeParams(ClawdisCanvasNavigateParams.self, from: req.paramsJSON) let params = try Self.decodeParams(ClawdbotCanvasNavigateParams.self, from: req.paramsJSON)
self.screen.navigate(to: params.url) self.screen.navigate(to: params.url)
return BridgeInvokeResponse(id: req.id, ok: true) return BridgeInvokeResponse(id: req.id, ok: true)
case ClawdisCanvasCommand.evalJS.rawValue: case ClawdbotCanvasCommand.evalJS.rawValue:
let params = try Self.decodeParams(ClawdisCanvasEvalParams.self, from: req.paramsJSON) let params = try Self.decodeParams(ClawdbotCanvasEvalParams.self, from: req.paramsJSON)
let result = try await self.screen.eval(javaScript: params.javaScript) let result = try await self.screen.eval(javaScript: params.javaScript)
let payload = try Self.encodePayload(["result": result]) let payload = try Self.encodePayload(["result": result])
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload) return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
case ClawdisCanvasCommand.snapshot.rawValue: case ClawdbotCanvasCommand.snapshot.rawValue:
let params = try? Self.decodeParams(ClawdisCanvasSnapshotParams.self, from: req.paramsJSON) let params = try? Self.decodeParams(ClawdbotCanvasSnapshotParams.self, from: req.paramsJSON)
let format = params?.format ?? .jpeg let format = params?.format ?? .jpeg
let maxWidth: CGFloat? = { let maxWidth: CGFloat? = {
if let raw = params?.maxWidth, raw > 0 { return CGFloat(raw) } if let raw = params?.maxWidth, raw > 0 { return CGFloat(raw) }
@@ -585,12 +585,12 @@ final class NodeAppModel {
]) ])
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload) return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
case ClawdisCanvasA2UICommand.reset.rawValue: case ClawdbotCanvasA2UICommand.reset.rawValue:
guard let a2uiUrl = await self.resolveA2UIHostURL() else { guard let a2uiUrl = await self.resolveA2UIHostURL() else {
return BridgeInvokeResponse( return BridgeInvokeResponse(
id: req.id, id: req.id,
ok: false, ok: false,
error: ClawdisNodeError( error: ClawdbotNodeError(
code: .unavailable, code: .unavailable,
message: "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host")) message: "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host"))
} }
@@ -599,32 +599,32 @@ final class NodeAppModel {
return BridgeInvokeResponse( return BridgeInvokeResponse(
id: req.id, id: req.id,
ok: false, ok: false,
error: ClawdisNodeError( error: ClawdbotNodeError(
code: .unavailable, code: .unavailable,
message: "A2UI_HOST_UNAVAILABLE: A2UI host not reachable")) message: "A2UI_HOST_UNAVAILABLE: A2UI host not reachable"))
} }
let json = try await self.screen.eval(javaScript: """ let json = try await self.screen.eval(javaScript: """
(() => { (() => {
if (!globalThis.clawdisA2UI) return JSON.stringify({ ok: false, error: "missing clawdisA2UI" }); if (!globalThis.clawdbotA2UI) return JSON.stringify({ ok: false, error: "missing clawdbotA2UI" });
return JSON.stringify(globalThis.clawdisA2UI.reset()); return JSON.stringify(globalThis.clawdbotA2UI.reset());
})() })()
""") """)
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json) return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json)
case ClawdisCanvasA2UICommand.push.rawValue, ClawdisCanvasA2UICommand.pushJSONL.rawValue: case ClawdbotCanvasA2UICommand.push.rawValue, ClawdbotCanvasA2UICommand.pushJSONL.rawValue:
let messages: [AnyCodable] let messages: [AnyCodable]
if command == ClawdisCanvasA2UICommand.pushJSONL.rawValue { if command == ClawdbotCanvasA2UICommand.pushJSONL.rawValue {
let params = try Self.decodeParams(ClawdisCanvasA2UIPushJSONLParams.self, from: req.paramsJSON) let params = try Self.decodeParams(ClawdbotCanvasA2UIPushJSONLParams.self, from: req.paramsJSON)
messages = try ClawdisCanvasA2UIJSONL.decodeMessagesFromJSONL(params.jsonl) messages = try ClawdbotCanvasA2UIJSONL.decodeMessagesFromJSONL(params.jsonl)
} else { } else {
do { do {
let params = try Self.decodeParams(ClawdisCanvasA2UIPushParams.self, from: req.paramsJSON) let params = try Self.decodeParams(ClawdbotCanvasA2UIPushParams.self, from: req.paramsJSON)
messages = params.messages messages = params.messages
} catch { } catch {
// Be forgiving: some clients still send JSONL payloads to `canvas.a2ui.push`. // Be forgiving: some clients still send JSONL payloads to `canvas.a2ui.push`.
let params = try Self.decodeParams(ClawdisCanvasA2UIPushJSONLParams.self, from: req.paramsJSON) let params = try Self.decodeParams(ClawdbotCanvasA2UIPushJSONLParams.self, from: req.paramsJSON)
messages = try ClawdisCanvasA2UIJSONL.decodeMessagesFromJSONL(params.jsonl) messages = try ClawdbotCanvasA2UIJSONL.decodeMessagesFromJSONL(params.jsonl)
} }
} }
@@ -632,7 +632,7 @@ final class NodeAppModel {
return BridgeInvokeResponse( return BridgeInvokeResponse(
id: req.id, id: req.id,
ok: false, ok: false,
error: ClawdisNodeError( error: ClawdbotNodeError(
code: .unavailable, code: .unavailable,
message: "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host")) message: "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host"))
} }
@@ -641,18 +641,18 @@ final class NodeAppModel {
return BridgeInvokeResponse( return BridgeInvokeResponse(
id: req.id, id: req.id,
ok: false, ok: false,
error: ClawdisNodeError( error: ClawdbotNodeError(
code: .unavailable, code: .unavailable,
message: "A2UI_HOST_UNAVAILABLE: A2UI host not reachable")) message: "A2UI_HOST_UNAVAILABLE: A2UI host not reachable"))
} }
let messagesJSON = try ClawdisCanvasA2UIJSONL.encodeMessagesJSONArray(messages) let messagesJSON = try ClawdbotCanvasA2UIJSONL.encodeMessagesJSONArray(messages)
let js = """ let js = """
(() => { (() => {
try { try {
if (!globalThis.clawdisA2UI) return JSON.stringify({ ok: false, error: "missing clawdisA2UI" }); if (!globalThis.clawdbotA2UI) return JSON.stringify({ ok: false, error: "missing clawdbotA2UI" });
const messages = \(messagesJSON); const messages = \(messagesJSON);
return JSON.stringify(globalThis.clawdisA2UI.applyMessages(messages)); return JSON.stringify(globalThis.clawdbotA2UI.applyMessages(messages));
} catch (e) { } catch (e) {
return JSON.stringify({ ok: false, error: String(e?.message ?? e) }); return JSON.stringify({ ok: false, error: String(e?.message ?? e) });
} }
@@ -661,7 +661,7 @@ final class NodeAppModel {
let resultJSON = try await self.screen.eval(javaScript: js) let resultJSON = try await self.screen.eval(javaScript: js)
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: resultJSON) return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: resultJSON)
case ClawdisCameraCommand.list.rawValue: case ClawdbotCameraCommand.list.rawValue:
let devices = await self.camera.listDevices() let devices = await self.camera.listDevices()
struct Payload: Codable { struct Payload: Codable {
var devices: [CameraController.CameraDeviceInfo] var devices: [CameraController.CameraDeviceInfo]
@@ -669,11 +669,11 @@ final class NodeAppModel {
let payload = try Self.encodePayload(Payload(devices: devices)) let payload = try Self.encodePayload(Payload(devices: devices))
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload) return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
case ClawdisCameraCommand.snap.rawValue: case ClawdbotCameraCommand.snap.rawValue:
self.showCameraHUD(text: "Taking photo…", kind: .photo) self.showCameraHUD(text: "Taking photo…", kind: .photo)
self.triggerCameraFlash() self.triggerCameraFlash()
let params = (try? Self.decodeParams(ClawdisCameraSnapParams.self, from: req.paramsJSON)) ?? let params = (try? Self.decodeParams(ClawdbotCameraSnapParams.self, from: req.paramsJSON)) ??
ClawdisCameraSnapParams() ClawdbotCameraSnapParams()
let res = try await self.camera.snap(params: params) let res = try await self.camera.snap(params: params)
struct Payload: Codable { struct Payload: Codable {
@@ -690,9 +690,9 @@ final class NodeAppModel {
self.showCameraHUD(text: "Photo captured", kind: .success, autoHideSeconds: 1.6) self.showCameraHUD(text: "Photo captured", kind: .success, autoHideSeconds: 1.6)
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload) return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
case ClawdisCameraCommand.clip.rawValue: case ClawdbotCameraCommand.clip.rawValue:
let params = (try? Self.decodeParams(ClawdisCameraClipParams.self, from: req.paramsJSON)) ?? let params = (try? Self.decodeParams(ClawdbotCameraClipParams.self, from: req.paramsJSON)) ??
ClawdisCameraClipParams() ClawdbotCameraClipParams()
let suspended = (params.includeAudio ?? true) ? self.voiceWake.suspendForExternalAudioCapture() : false let suspended = (params.includeAudio ?? true) ? self.voiceWake.suspendForExternalAudioCapture() : false
defer { self.voiceWake.resumeAfterExternalAudioCapture(wasSuspended: suspended) } defer { self.voiceWake.resumeAfterExternalAudioCapture(wasSuspended: suspended) }
@@ -714,9 +714,9 @@ final class NodeAppModel {
self.showCameraHUD(text: "Clip captured", kind: .success, autoHideSeconds: 1.8) self.showCameraHUD(text: "Clip captured", kind: .success, autoHideSeconds: 1.8)
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload) return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
case ClawdisScreenCommand.record.rawValue: case ClawdbotScreenCommand.record.rawValue:
let params = (try? Self.decodeParams(ClawdisScreenRecordParams.self, from: req.paramsJSON)) ?? let params = (try? Self.decodeParams(ClawdbotScreenRecordParams.self, from: req.paramsJSON)) ??
ClawdisScreenRecordParams() ClawdbotScreenRecordParams()
if let format = params.format, format.lowercased() != "mp4" { if let format = params.format, format.lowercased() != "mp4" {
throw NSError(domain: "Screen", code: 30, userInfo: [ throw NSError(domain: "Screen", code: 30, userInfo: [
NSLocalizedDescriptionKey: "INVALID_REQUEST: screen format must be mp4", NSLocalizedDescriptionKey: "INVALID_REQUEST: screen format must be mp4",
@@ -754,7 +754,7 @@ final class NodeAppModel {
return BridgeInvokeResponse( return BridgeInvokeResponse(
id: req.id, id: req.id,
ok: false, ok: false,
error: ClawdisNodeError(code: .invalidRequest, message: "INVALID_REQUEST: unknown command")) error: ClawdbotNodeError(code: .invalidRequest, message: "INVALID_REQUEST: unknown command"))
} }
} catch { } catch {
if command.hasPrefix("camera.") { if command.hasPrefix("camera.") {
@@ -764,13 +764,13 @@ final class NodeAppModel {
return BridgeInvokeResponse( return BridgeInvokeResponse(
id: req.id, id: req.id,
ok: false, ok: false,
error: ClawdisNodeError(code: .unavailable, message: error.localizedDescription)) error: ClawdbotNodeError(code: .unavailable, message: error.localizedDescription))
} }
} }
private func locationMode() -> ClawdisLocationMode { private func locationMode() -> ClawdbotLocationMode {
let raw = UserDefaults.standard.string(forKey: "location.enabledMode") ?? "off" let raw = UserDefaults.standard.string(forKey: "location.enabledMode") ?? "off"
return ClawdisLocationMode(rawValue: raw) ?? .off return ClawdbotLocationMode(rawValue: raw) ?? .off
} }
private func isLocationPreciseEnabled() -> Bool { private func isLocationPreciseEnabled() -> Bool {

View File

@@ -1,4 +1,4 @@
import ClawdisKit import ClawdbotKit
import Observation import Observation
import SwiftUI import SwiftUI
import WebKit import WebKit
@@ -13,7 +13,7 @@ final class ScreenController {
var urlString: String = "" var urlString: String = ""
var errorText: String? var errorText: String?
/// Callback invoked when a clawdis:// deep link is tapped in the canvas /// Callback invoked when a clawdbot:// deep link is tapped in the canvas
var onDeepLink: ((URL) -> Void)? var onDeepLink: ((URL) -> Void)?
/// Callback invoked when the user clicks an A2UI action (e.g. button) inside the canvas web UI. /// Callback invoked when the user clicks an A2UI action (e.g. button) inside the canvas web UI.
@@ -101,7 +101,7 @@ final class ScreenController {
let js = """ let js = """
(() => { (() => {
try { try {
const api = globalThis.__clawdis; const api = globalThis.__clawdbot;
if (!api) return; if (!api) return;
if (typeof api.setDebugStatusEnabled === 'function') { if (typeof api.setDebugStatusEnabled === 'function') {
api.setDebugStatusEnabled(\(enabled ? "true" : "false")); api.setDebugStatusEnabled(\(enabled ? "true" : "false"));
@@ -124,7 +124,7 @@ final class ScreenController {
let res = try await self.eval(javaScript: """ let res = try await self.eval(javaScript: """
(() => { (() => {
try { try {
return !!globalThis.clawdisA2UI && typeof globalThis.clawdisA2UI.applyMessages === 'function'; return !!globalThis.clawdbotA2UI && typeof globalThis.clawdbotA2UI.applyMessages === 'function';
} catch (_) { return false; } } catch (_) { return false; }
})() })()
""") """)
@@ -184,7 +184,7 @@ final class ScreenController {
func snapshotBase64( func snapshotBase64(
maxWidth: CGFloat? = nil, maxWidth: CGFloat? = nil,
format: ClawdisCanvasSnapshotFormat, format: ClawdbotCanvasSnapshotFormat,
quality: Double? = nil) async throws -> String quality: Double? = nil) async throws -> String
{ {
let config = WKSnapshotConfiguration() let config = WKSnapshotConfiguration()
@@ -229,7 +229,7 @@ final class ScreenController {
subdirectory: String) subdirectory: String)
-> URL? -> URL?
{ {
let bundle = ClawdisKitResources.bundle let bundle = ClawdbotKitResources.bundle
return bundle.url(forResource: name, withExtension: ext, subdirectory: subdirectory) return bundle.url(forResource: name, withExtension: ext, subdirectory: subdirectory)
?? bundle.url(forResource: name, withExtension: ext) ?? bundle.url(forResource: name, withExtension: ext)
} }
@@ -342,7 +342,7 @@ extension Double {
// MARK: - Navigation Delegate // MARK: - Navigation Delegate
/// Handles navigation policy to intercept clawdis:// deep links from canvas /// Handles navigation policy to intercept clawdbot:// deep links from canvas
@MainActor @MainActor
private final class ScreenNavigationDelegate: NSObject, WKNavigationDelegate { private final class ScreenNavigationDelegate: NSObject, WKNavigationDelegate {
weak var controller: ScreenController? weak var controller: ScreenController?
@@ -357,8 +357,8 @@ private final class ScreenNavigationDelegate: NSObject, WKNavigationDelegate {
return return
} }
// Intercept clawdis:// deep links // Intercept clawdbot:// deep links
if url.scheme == "clawdis" { if url.scheme == "clawdbot" {
decisionHandler(.cancel) decisionHandler(.cancel)
self.controller?.onDeepLink?(url) self.controller?.onDeepLink?(url)
return return
@@ -386,7 +386,7 @@ private final class ScreenNavigationDelegate: NSObject, WKNavigationDelegate {
} }
private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler { private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler {
static let messageName = "clawdisCanvasA2UIAction" static let messageName = "clawdbotCanvasA2UIAction"
static let legacyMessageNames = ["canvas", "a2ui", "userAction", "action"] static let legacyMessageNames = ["canvas", "a2ui", "userAction", "action"]
static let handlerNames = [messageName] + legacyMessageNames static let handlerNames = [messageName] + legacyMessageNames

View File

@@ -63,12 +63,12 @@ final class ScreenRecordService: @unchecked Sendable {
return URL(fileURLWithPath: outPath) return URL(fileURLWithPath: outPath)
} }
return FileManager.default.temporaryDirectory return FileManager.default.temporaryDirectory
.appendingPathComponent("clawdis-screen-record-\(UUID().uuidString).mp4") .appendingPathComponent("clawdbot-screen-record-\(UUID().uuidString).mp4")
}() }()
try? FileManager.default.removeItem(at: outURL) try? FileManager.default.removeItem(at: outURL)
let state = CaptureState() let state = CaptureState()
let recordQueue = DispatchQueue(label: "com.clawdis.screenrecord") let recordQueue = DispatchQueue(label: "com.clawdbot.screenrecord")
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
let handler: @Sendable (CMSampleBuffer, RPSampleBufferType, Error?) -> Void = { sample, type, error in let handler: @Sendable (CMSampleBuffer, RPSampleBufferType, Error?) -> Void = { sample, type, error in

View File

@@ -1,4 +1,4 @@
import ClawdisKit import ClawdbotKit
import SwiftUI import SwiftUI
struct ScreenTab: View { struct ScreenTab: View {

View File

@@ -1,4 +1,4 @@
import ClawdisKit import ClawdbotKit
import SwiftUI import SwiftUI
import WebKit import WebKit

View File

@@ -1,4 +1,4 @@
import ClawdisKit import ClawdbotKit
import Network import Network
import Observation import Observation
import SwiftUI import SwiftUI
@@ -23,7 +23,7 @@ struct SettingsTab: View {
@AppStorage("talk.enabled") private var talkEnabled: Bool = false @AppStorage("talk.enabled") private var talkEnabled: Bool = false
@AppStorage("talk.button.enabled") private var talkButtonEnabled: Bool = true @AppStorage("talk.button.enabled") private var talkButtonEnabled: Bool = true
@AppStorage("camera.enabled") private var cameraEnabled: Bool = true @AppStorage("camera.enabled") private var cameraEnabled: Bool = true
@AppStorage("location.enabledMode") private var locationEnabledModeRaw: String = ClawdisLocationMode.off.rawValue @AppStorage("location.enabledMode") private var locationEnabledModeRaw: String = ClawdbotLocationMode.off.rawValue
@AppStorage("location.preciseEnabled") private var locationPreciseEnabled: Bool = true @AppStorage("location.preciseEnabled") private var locationPreciseEnabled: Bool = true
@AppStorage("screen.preventSleep") private var preventSleep: Bool = true @AppStorage("screen.preventSleep") private var preventSleep: Bool = true
@AppStorage("bridge.preferredStableID") private var preferredBridgeStableID: String = "" @AppStorage("bridge.preferredStableID") private var preferredBridgeStableID: String = ""
@@ -36,7 +36,7 @@ struct SettingsTab: View {
@State private var connectStatus = ConnectStatusStore() @State private var connectStatus = ConnectStatusStore()
@State private var connectingBridgeID: String? @State private var connectingBridgeID: String?
@State private var localIPAddress: String? @State private var localIPAddress: String?
@State private var lastLocationModeRaw: String = ClawdisLocationMode.off.rawValue @State private var lastLocationModeRaw: String = ClawdbotLocationMode.off.rawValue
var body: some View { var body: some View {
NavigationStack { NavigationStack {
@@ -186,9 +186,9 @@ struct SettingsTab: View {
Section("Location") { Section("Location") {
Picker("Location Access", selection: self.$locationEnabledModeRaw) { Picker("Location Access", selection: self.$locationEnabledModeRaw) {
Text("Off").tag(ClawdisLocationMode.off.rawValue) Text("Off").tag(ClawdbotLocationMode.off.rawValue)
Text("While Using").tag(ClawdisLocationMode.whileUsing.rawValue) Text("While Using").tag(ClawdbotLocationMode.whileUsing.rawValue)
Text("Always").tag(ClawdisLocationMode.always.rawValue) Text("Always").tag(ClawdbotLocationMode.always.rawValue)
} }
.pickerStyle(.segmented) .pickerStyle(.segmented)
@@ -202,7 +202,7 @@ struct SettingsTab: View {
Section("Screen") { Section("Screen") {
Toggle("Prevent Sleep", isOn: self.$preventSleep) Toggle("Prevent Sleep", isOn: self.$preventSleep)
Text("Keeps the screen awake while Clawdis is open.") Text("Keeps the screen awake while Clawdbot is open.")
.font(.footnote) .font(.footnote)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
@@ -233,7 +233,7 @@ struct SettingsTab: View {
.onChange(of: self.locationEnabledModeRaw) { _, newValue in .onChange(of: self.locationEnabledModeRaw) { _, newValue in
let previous = self.lastLocationModeRaw let previous = self.lastLocationModeRaw
self.lastLocationModeRaw = newValue self.lastLocationModeRaw = newValue
guard let mode = ClawdisLocationMode(rawValue: newValue) else { return } guard let mode = ClawdbotLocationMode(rawValue: newValue) else { return }
Task { Task {
let granted = await self.appModel.requestLocationPermissions(mode: mode) let granted = await self.appModel.requestLocationPermissions(mode: mode)
if !granted { if !granted {
@@ -312,8 +312,8 @@ struct SettingsTab: View {
return "iOS \(v.majorVersion).\(v.minorVersion).\(v.patchVersion)" return "iOS \(v.majorVersion).\(v.minorVersion).\(v.patchVersion)"
} }
private var locationMode: ClawdisLocationMode { private var locationMode: ClawdbotLocationMode {
ClawdisLocationMode(rawValue: self.locationEnabledModeRaw) ?? .off ClawdbotLocationMode(rawValue: self.locationEnabledModeRaw) ?? .off
} }
private func appVersion() -> String { private func appVersion() -> String {
@@ -342,38 +342,38 @@ struct SettingsTab: View {
} }
private func currentCaps() -> [String] { private func currentCaps() -> [String] {
var caps = [ClawdisCapability.canvas.rawValue, ClawdisCapability.screen.rawValue] var caps = [ClawdbotCapability.canvas.rawValue, ClawdbotCapability.screen.rawValue]
let cameraEnabled = let cameraEnabled =
UserDefaults.standard.object(forKey: "camera.enabled") == nil UserDefaults.standard.object(forKey: "camera.enabled") == nil
? true ? true
: UserDefaults.standard.bool(forKey: "camera.enabled") : UserDefaults.standard.bool(forKey: "camera.enabled")
if cameraEnabled { caps.append(ClawdisCapability.camera.rawValue) } if cameraEnabled { caps.append(ClawdbotCapability.camera.rawValue) }
let voiceWakeEnabled = UserDefaults.standard.bool(forKey: VoiceWakePreferences.enabledKey) let voiceWakeEnabled = UserDefaults.standard.bool(forKey: VoiceWakePreferences.enabledKey)
if voiceWakeEnabled { caps.append(ClawdisCapability.voiceWake.rawValue) } if voiceWakeEnabled { caps.append(ClawdbotCapability.voiceWake.rawValue) }
return caps return caps
} }
private func currentCommands() -> [String] { private func currentCommands() -> [String] {
var commands: [String] = [ var commands: [String] = [
ClawdisCanvasCommand.present.rawValue, ClawdbotCanvasCommand.present.rawValue,
ClawdisCanvasCommand.hide.rawValue, ClawdbotCanvasCommand.hide.rawValue,
ClawdisCanvasCommand.navigate.rawValue, ClawdbotCanvasCommand.navigate.rawValue,
ClawdisCanvasCommand.evalJS.rawValue, ClawdbotCanvasCommand.evalJS.rawValue,
ClawdisCanvasCommand.snapshot.rawValue, ClawdbotCanvasCommand.snapshot.rawValue,
ClawdisCanvasA2UICommand.push.rawValue, ClawdbotCanvasA2UICommand.push.rawValue,
ClawdisCanvasA2UICommand.pushJSONL.rawValue, ClawdbotCanvasA2UICommand.pushJSONL.rawValue,
ClawdisCanvasA2UICommand.reset.rawValue, ClawdbotCanvasA2UICommand.reset.rawValue,
ClawdisScreenCommand.record.rawValue, ClawdbotScreenCommand.record.rawValue,
] ]
let caps = Set(self.currentCaps()) let caps = Set(self.currentCaps())
if caps.contains(ClawdisCapability.camera.rawValue) { if caps.contains(ClawdbotCapability.camera.rawValue) {
commands.append(ClawdisCameraCommand.list.rawValue) commands.append(ClawdbotCameraCommand.list.rawValue)
commands.append(ClawdisCameraCommand.snap.rawValue) commands.append(ClawdbotCameraCommand.snap.rawValue)
commands.append(ClawdisCameraCommand.clip.rawValue) commands.append(ClawdbotCameraCommand.clip.rawValue)
} }
return commands return commands
@@ -391,7 +391,7 @@ struct SettingsTab: View {
do { do {
let statusStore = self.connectStatus let statusStore = self.connectStatus
let existing = KeychainStore.loadString( let existing = KeychainStore.loadString(
service: "com.clawdis.bridge", service: "com.clawdbot.bridge",
account: self.keychainAccount()) account: self.keychainAccount())
let existingToken = (existing?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false) ? let existingToken = (existing?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false) ?
existing : existing :
@@ -419,7 +419,7 @@ struct SettingsTab: View {
if !token.isEmpty, token != existingToken { if !token.isEmpty, token != existingToken {
_ = KeychainStore.saveString( _ = KeychainStore.saveString(
token, token,
service: "com.clawdis.bridge", service: "com.clawdbot.bridge",
account: self.keychainAccount()) account: self.keychainAccount())
} }
@@ -465,7 +465,7 @@ struct SettingsTab: View {
do { do {
let statusStore = self.connectStatus let statusStore = self.connectStatus
let existing = KeychainStore.loadString( let existing = KeychainStore.loadString(
service: "com.clawdis.bridge", service: "com.clawdbot.bridge",
account: self.keychainAccount()) account: self.keychainAccount())
let existingToken = (existing?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false) ? let existingToken = (existing?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false) ?
existing : existing :
@@ -493,7 +493,7 @@ struct SettingsTab: View {
if !token.isEmpty, token != existingToken { if !token.isEmpty, token != existingToken {
_ = KeychainStore.saveString( _ = KeychainStore.saveString(
token, token,
service: "com.clawdis.bridge", service: "com.clawdbot.bridge",
account: self.keychainAccount()) account: self.keychainAccount())
} }

View File

@@ -30,7 +30,7 @@ struct VoiceWakeWordsSettingsView: View {
Text("Wake Words") Text("Wake Words")
} footer: { } footer: {
Text( Text(
"Clawdis reacts when any trigger appears in a transcription. " "Clawdbot reacts when any trigger appears in a transcription. "
+ "Keep them short to avoid false positives.") + "Keep them short to avoid false positives.")
} }
} }

View File

@@ -1,5 +1,5 @@
import AVFAudio import AVFAudio
import ClawdisKit import ClawdbotKit
import Foundation import Foundation
import Observation import Observation
import OSLog import OSLog
@@ -47,7 +47,7 @@ final class TalkModeManager: NSObject {
private var chatSubscribedSessionKeys = Set<String>() private var chatSubscribedSessionKeys = Set<String>()
private let logger = Logger(subsystem: "com.clawdis", category: "TalkMode") private let logger = Logger(subsystem: "com.clawdbot", category: "TalkMode")
func attachBridge(_ bridge: BridgeSession) { func attachBridge(_ bridge: BridgeSession) {
self.bridge = bridge self.bridge = bridge

View File

@@ -9,7 +9,7 @@ Sources/Bridge/KeychainStore.swift
Sources/Camera/CameraController.swift Sources/Camera/CameraController.swift
Sources/Chat/ChatSheet.swift Sources/Chat/ChatSheet.swift
Sources/Chat/IOSBridgeChatTransport.swift Sources/Chat/IOSBridgeChatTransport.swift
Sources/ClawdisApp.swift Sources/ClawdbotApp.swift
Sources/Model/NodeAppModel.swift Sources/Model/NodeAppModel.swift
Sources/RootCanvas.swift Sources/RootCanvas.swift
Sources/RootTabs.swift Sources/RootTabs.swift
@@ -25,36 +25,36 @@ Sources/Status/VoiceWakeToast.swift
Sources/Voice/VoiceTab.swift Sources/Voice/VoiceTab.swift
Sources/Voice/VoiceWakeManager.swift Sources/Voice/VoiceWakeManager.swift
Sources/Voice/VoiceWakePreferences.swift Sources/Voice/VoiceWakePreferences.swift
../shared/ClawdisKit/Sources/ClawdisChatUI/ChatComposer.swift ../shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatComposer.swift
../shared/ClawdisKit/Sources/ClawdisChatUI/ChatMarkdownSplitter.swift ../shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatMarkdownSplitter.swift
../shared/ClawdisKit/Sources/ClawdisChatUI/ChatMessageViews.swift ../shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatMessageViews.swift
../shared/ClawdisKit/Sources/ClawdisChatUI/ChatModels.swift ../shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatModels.swift
../shared/ClawdisKit/Sources/ClawdisChatUI/ChatPayloadDecoding.swift ../shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatPayloadDecoding.swift
../shared/ClawdisKit/Sources/ClawdisChatUI/ChatSessions.swift ../shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatSessions.swift
../shared/ClawdisKit/Sources/ClawdisChatUI/ChatSheets.swift ../shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatSheets.swift
../shared/ClawdisKit/Sources/ClawdisChatUI/ChatTheme.swift ../shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatTheme.swift
../shared/ClawdisKit/Sources/ClawdisChatUI/ChatTransport.swift ../shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatTransport.swift
../shared/ClawdisKit/Sources/ClawdisChatUI/ChatView.swift ../shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatView.swift
../shared/ClawdisKit/Sources/ClawdisChatUI/ChatViewModel.swift ../shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatViewModel.swift
../shared/ClawdisKit/Sources/ClawdisKit/AnyCodable.swift ../shared/ClawdbotKit/Sources/ClawdbotKit/AnyCodable.swift
../shared/ClawdisKit/Sources/ClawdisKit/BonjourEscapes.swift ../shared/ClawdbotKit/Sources/ClawdbotKit/BonjourEscapes.swift
../shared/ClawdisKit/Sources/ClawdisKit/BonjourTypes.swift ../shared/ClawdbotKit/Sources/ClawdbotKit/BonjourTypes.swift
../shared/ClawdisKit/Sources/ClawdisKit/BridgeFrames.swift ../shared/ClawdbotKit/Sources/ClawdbotKit/BridgeFrames.swift
../shared/ClawdisKit/Sources/ClawdisKit/CameraCommands.swift ../shared/ClawdbotKit/Sources/ClawdbotKit/CameraCommands.swift
../shared/ClawdisKit/Sources/ClawdisKit/CanvasA2UIAction.swift ../shared/ClawdbotKit/Sources/ClawdbotKit/CanvasA2UIAction.swift
../shared/ClawdisKit/Sources/ClawdisKit/CanvasA2UICommands.swift ../shared/ClawdbotKit/Sources/ClawdbotKit/CanvasA2UICommands.swift
../shared/ClawdisKit/Sources/ClawdisKit/CanvasA2UIJSONL.swift ../shared/ClawdbotKit/Sources/ClawdbotKit/CanvasA2UIJSONL.swift
../shared/ClawdisKit/Sources/ClawdisKit/CanvasCommandParams.swift ../shared/ClawdbotKit/Sources/ClawdbotKit/CanvasCommandParams.swift
../shared/ClawdisKit/Sources/ClawdisKit/CanvasCommands.swift ../shared/ClawdbotKit/Sources/ClawdbotKit/CanvasCommands.swift
../shared/ClawdisKit/Sources/ClawdisKit/Capabilities.swift ../shared/ClawdbotKit/Sources/ClawdbotKit/Capabilities.swift
../shared/ClawdisKit/Sources/ClawdisKit/ClawdisKitResources.swift ../shared/ClawdbotKit/Sources/ClawdbotKit/ClawdbotKitResources.swift
../shared/ClawdisKit/Sources/ClawdisKit/DeepLinks.swift ../shared/ClawdbotKit/Sources/ClawdbotKit/DeepLinks.swift
../shared/ClawdisKit/Sources/ClawdisKit/JPEGTranscoder.swift ../shared/ClawdbotKit/Sources/ClawdbotKit/JPEGTranscoder.swift
../shared/ClawdisKit/Sources/ClawdisKit/NodeError.swift ../shared/ClawdbotKit/Sources/ClawdbotKit/NodeError.swift
../shared/ClawdisKit/Sources/ClawdisKit/ScreenCommands.swift ../shared/ClawdbotKit/Sources/ClawdbotKit/ScreenCommands.swift
../shared/ClawdisKit/Sources/ClawdisKit/StoragePaths.swift ../shared/ClawdbotKit/Sources/ClawdbotKit/StoragePaths.swift
../shared/ClawdisKit/Sources/ClawdisKit/SystemCommands.swift ../shared/ClawdbotKit/Sources/ClawdbotKit/SystemCommands.swift
../shared/ClawdisKit/Sources/ClawdisKit/TalkDirective.swift ../shared/ClawdbotKit/Sources/ClawdbotKit/TalkDirective.swift
../../Swabble/Sources/SwabbleKit/WakeWordGate.swift ../../Swabble/Sources/SwabbleKit/WakeWordGate.swift
Sources/Voice/TalkModeManager.swift Sources/Voice/TalkModeManager.swift
Sources/Voice/TalkOrbOverlay.swift Sources/Voice/TalkOrbOverlay.swift

View File

@@ -1,6 +1,6 @@
import SwiftUI import SwiftUI
import Testing import Testing
@testable import Clawdis @testable import Clawdbot
@Suite struct AppCoverageTests { @Suite struct AppCoverageTests {
@Test @MainActor func nodeAppModelUpdatesBackgroundedState() { @Test @MainActor func nodeAppModelUpdatesBackgroundedState() {

View File

@@ -1,12 +1,12 @@
import ClawdisKit import ClawdbotKit
import Foundation import Foundation
import Network import Network
import Testing import Testing
@testable import Clawdis @testable import Clawdbot
@Suite struct BridgeClientTests { @Suite struct BridgeClientTests {
private final class LineServer: @unchecked Sendable { private final class LineServer: @unchecked Sendable {
private let queue = DispatchQueue(label: "com.clawdis.tests.bridge-client-server") private let queue = DispatchQueue(label: "com.clawdbot.tests.bridge-client-server")
private let listener: NWListener private let listener: NWListener
private var connection: NWConnection? private var connection: NWConnection?
private var buffer = Data() private var buffer = Data()

Some files were not shown because too many files have changed in this diff Show More