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
apps/macos/.build/
apps/shared/ClawdisKit/.build/
apps/shared/ClawdbotKit/.build/
bin/
bin/clawdis-mac
bin/clawdbot-mac
bin/docs-list
apps/macos/.build-local/
apps/macos/.swiftpm/
apps/shared/ClawdisKit/.swiftpm/
apps/shared/ClawdbotKit/.swiftpm/
Core/
apps/ios/*.xcodeproj/
apps/ios/*.xcworkspace/

View File

@@ -7,7 +7,7 @@
## Build, Test, and Development Commands
- 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)
- Lint/format: `pnpm lint` (biome check), `pnpm format` (biome format)
- 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.
## Security & Configuration Tips
- Web provider stores creds at `~/.clawdis/credentials/`; rerun `clawdis login` if logged out.
- Pi sessions live under `~/.clawdis/sessions/` by default; the base directory is not configurable.
- Web provider stores creds at `~/.clawdbot/credentials/`; rerun `clawdbot login` if logged out.
- 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.
## 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.**
- 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`.
- 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.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.
- 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.
@@ -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** 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.
- 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.
- 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.
- 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 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.
- 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:
- Command template should stay `clawdis-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`.
- For manual `clawdis send` messages that include `!`, use the heredoc pattern noted below to avoid the Bash tools escaping.
- 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`/`clawdbot` binaries resolve when invoked via `clawdbot-mac`.
- For manual `clawdbot send` messages that include `!`, use the heredoc pattern noted below to avoid the Bash tools escaping.
## 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
# WRONG - will send "Hello\\!" with backslash
clawdis send --to "+1234" --message 'Hello!'
clawdbot send --to "+1234" --message 'Hello!'
# CORRECT - use heredoc to avoid escaping
clawdis send --to "+1234" --message "$(cat <<'EOF'
clawdbot send --to "+1234" --message "$(cat <<'EOF'
Hello!
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
### 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`.
- Agent tools: drop the `clawdis_` prefix (`browser`, `canvas`, `nodes`, `cron`, `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.
- 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.
- Sessions: primary session key is fixed to `main` (or `global` for global scope); `session.mainKey` is ignored.
### 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.
- 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.
@@ -20,6 +22,7 @@
- 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 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.
- 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.
- 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: 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.
- 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).
@@ -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.
- 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`.
- Config: remove `routing.allowFrom`; use `whatsapp.allowFrom` instead (run `clawdis 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.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 `clawdbot doctor` to migrate).
### Features
- 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.
- 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).
- 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.
- 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.
@@ -114,7 +117,7 @@
- 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.
- 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: 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).
@@ -126,7 +129,7 @@
- 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 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).
- 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.
@@ -151,7 +154,7 @@
- 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).
- 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).
- 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.
@@ -201,7 +204,7 @@
- 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: 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.
- 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.
@@ -265,7 +268,7 @@
- macOS menu: top status line now shows pending node pairing approvals (incl. repairs).
- 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: 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.
## 2.0.0-beta4 — 2025-12-27
@@ -281,12 +284,12 @@
## 2.0.0-beta3 — 2025-12-27
### Highlights
- First-class Clawdis tools (browser, canvas, nodes, cron) replace the old `clawdis-*` 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.
- 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 `~/.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.
- 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.
- 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.
- 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.
@@ -383,7 +386,7 @@
### Tests
- 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).
- 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
@@ -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.
### 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
- 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
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
- 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
- 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.
- 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>`.
- 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
- 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.
- Node pairing + management via `clawdis 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.
- Node pairing + management via `clawdbot nodes {pending,approve,reject,invoke}` (used by the iOS node and future remote nodes).
- 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
- **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.
- **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.
### 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.
- `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.
### 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.
- 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.
- 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
- New `clawdis agent` command plus a persistent Pi RPC worker (auto-started) enables direct agent chats; `clawdis 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).
- Added `--help`/`--version` flags; login/logout accept `--provider` (WhatsApp default). Console output is mirrored into pino logs under `/tmp/clawdis`.
- New `clawdbot agent` command plus a persistent Pi RPC worker (auto-started) enables direct agent chats; `clawdbot status` renders a colored session/recipient table.
- `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/clawdbot`.
- 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
@@ -468,32 +471,32 @@ First Clawdis release post rebrand. This is a semver-major because we dropped le
### 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.
- 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
### Breaking
- 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
- 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.
- 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.
- 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/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.
- 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.
- `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
### 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]`).
- 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
- **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.
- **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.
@@ -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.
- **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`.
- **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.
### 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).
### 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.
### 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`.
- Command failures return user-friendly messages.
- 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.
### 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.
- Watchdog restarts WhatsApp after long inactivity; heartbeat logging includes minutes since last message.
- 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
### 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

View File

@@ -1,9 +1,9 @@
# Contributing to Clawdis
# Contributing to Clawdbot
Welcome to the lobster tank! 🦞
## Quick Links
- **GitHub:** https://github.com/steipete/clawdis
- **GitHub:** https://github.com/clawdbot/clawdbot
- **Discord:** https://discord.gg/qkhbAGHRBT
- **X/Twitter:** [@steipete](https://x.com/steipete) / [@clawdbot](https://x.com/clawdbot)
@@ -20,11 +20,11 @@ Welcome to the lobster tank! 🦞
## How to Contribute
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
## Before You PR
- Test locally with your Clawdis instance
- Test locally with your Clawdbot instance
- Run linter: `npm run lint`
- Keep PRs focused (one thing per PR)
- Describe what & why

View File

@@ -19,9 +19,9 @@ RUN apt-get update \
xvfb \
&& rm -rf /var/lib/apt/lists/*
COPY scripts/sandbox-browser-entrypoint.sh /usr/local/bin/clawdis-sandbox-browser
RUN chmod +x /usr/local/bin/clawdis-sandbox-browser
COPY scripts/sandbox-browser-entrypoint.sh /usr/local/bin/clawdbot-sandbox-browser
RUN chmod +x /usr/local/bin/clawdbot-sandbox-browser
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">
<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 align="center">
@@ -9,20 +9,20 @@
</p>
<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/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/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/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="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge" alt="MIT License"></a>
</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.
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
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.
@@ -36,13 +36,13 @@ Your surfaces
└──────────────┬────────────────┘
├─ Pi agent (RPC)
├─ CLI (clawdis …)
├─ CLI (clawdbot …)
├─ WebChat (browser)
├─ macOS app (Clawdis.app)
├─ macOS app (Clawdbot.app)
└─ iOS node (Canvas + voice)
```
## What Clawdis does
## What Clawdbot does
- **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`).
@@ -51,7 +51,7 @@ Your surfaces
- **Automation-ready** — browser control, media handling, and tool streaming.
- **Local-first control plane** — the Gateway owns state, everything else connects.
- **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)
@@ -70,25 +70,25 @@ pnpm build
pnpm ui:build
# Recommended: run the onboarding wizard
pnpm clawdis onboard
pnpm clawdbot onboard
# Link WhatsApp (stores creds in ~/.clawdis/credentials)
pnpm clawdis login
# Link WhatsApp (stores creds in ~/.clawdbot/credentials)
pnpm clawdbot login
# Start the gateway
pnpm clawdis gateway --port 18789 --verbose
pnpm clawdbot gateway --port 18789 --verbose
# Dev loop (auto-reload on TS changes)
pnpm gateway:watch
# 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)
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
@@ -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.
- **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`).
- **Canvas**: `WKWebView` with bundled Canvas scaffold + A2UI, JS eval, snapshot capture, and `clawdis://` 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`.
- **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; `clawdbot://agent` links emit `agent.request`. Voice wake triggers sync via `voicewake.get` + `voicewake.changed`.
## 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.
### macOS (Clawdis.app)
### macOS (Clawdbot.app)
- Menu bar control for the Gateway and health.
- 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.
- Voice trigger forwarding + Canvas surface.
- Controlled via `clawdis nodes …`.
- Controlled via `clawdbot nodes …`.
Runbook: `docs/ios/connect.md`.
@@ -153,7 +153,7 @@ Runbook: `docs/ios/connect.md`.
## Configuration
Minimal `~/.clawdis/clawdis.json`:
Minimal `~/.clawdbot/clawdbot.json`:
```json5
{
@@ -165,7 +165,7 @@ Minimal `~/.clawdis/clawdis.json`:
### 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`.
### Telegram
@@ -223,13 +223,13 @@ Browser control (optional):
## Email hooks (Gmail)
```bash
clawdis hooks gmail setup --account you@gmail.com
clawdis hooks gmail run
clawdbot hooks gmail setup --account you@gmail.com
clawdbot hooks gmail run
```
- [`docs/security.md`](docs/security.md)
- [`docs/troubleshooting.md`](docs/troubleshooting.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
@@ -239,7 +239,7 @@ AI/vibe-coded PRs welcome! 🤖
## 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://soul.md

View File

@@ -1,15 +1,15 @@
<?xml version="1.0" standalone="yes"?>
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>Clawdis</title>
<title>Clawdbot</title>
<item>
<title>2.0.0-beta5</title>
<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:shortVersionString>2.0.0-beta5</sparkle:shortVersionString>
<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>
<ul>
<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>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>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.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.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>clawdbot doctor</code> to migrate).</li>
</ul>
<h3>Features</h3>
<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>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>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>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>
@@ -50,7 +50,7 @@
<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>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: 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>
@@ -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 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: 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>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>
@@ -87,7 +87,7 @@
<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>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>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>
@@ -137,7 +137,7 @@
<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: 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>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>
@@ -201,12 +201,12 @@
<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>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>
</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>
<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>
</channel>
</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:
- 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):
```bash
pnpm clawdis gateway --port 18789 --verbose
pnpm clawdbot gateway --port 18789 --verbose
```
2) In the Android app:
@@ -34,8 +34,8 @@ pnpm clawdis gateway --port 18789 --verbose
3) Approve pairing (on the gateway machine):
```bash
clawdis nodes pending
clawdis nodes approve <requestId>
clawdbot nodes pending
clawdbot nodes approve <requestId>
```
More details: `docs/android/connect.md`.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package com.clawdis.android
package com.clawdbot.android
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.NotificationChannel
@@ -29,7 +29,7 @@ class NodeForegroundService : Service() {
override fun onCreate() {
super.onCreate()
ensureChannel()
val initial = buildNotification(title = "Clawdis Node", text = "Starting…")
val initial = buildNotification(title = "Clawdbot Node", text = "Starting…")
startForegroundWithTypes(notification = initial, requiresMic = false)
val runtime = (application as NodeApp).runtime
@@ -44,7 +44,7 @@ class NodeForegroundService : Service() {
) { status, server, connected, voiceMode, voiceListening ->
Quint(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 =
if (voiceMode == VoiceWakeMode.Always) {
if (voiceListening) " · Voice Wake: Listening" else " · Voice Wake: Paused"
@@ -91,7 +91,7 @@ class NodeForegroundService : Service() {
"Connection",
NotificationManager.IMPORTANCE_LOW,
).apply {
description = "Clawdis node connection status"
description = "Clawdbot node connection status"
setShowBadge(false)
}
mgr.createNotificationChannel(channel)
@@ -146,7 +146,7 @@ class NodeForegroundService : Service() {
private const val CHANNEL_ID = "connection"
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) {
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.content.Context
@@ -7,31 +7,31 @@ import android.location.LocationManager
import android.os.Build
import android.os.SystemClock
import androidx.core.content.ContextCompat
import com.clawdis.android.chat.ChatController
import com.clawdis.android.chat.ChatMessage
import com.clawdis.android.chat.ChatPendingToolCall
import com.clawdis.android.chat.ChatSessionEntry
import com.clawdis.android.chat.OutgoingAttachment
import com.clawdis.android.bridge.BridgeDiscovery
import com.clawdis.android.bridge.BridgeEndpoint
import com.clawdis.android.bridge.BridgePairingClient
import com.clawdis.android.bridge.BridgeSession
import com.clawdis.android.node.CameraCaptureManager
import com.clawdis.android.node.LocationCaptureManager
import com.clawdis.android.BuildConfig
import com.clawdis.android.node.CanvasController
import com.clawdis.android.node.ScreenRecordManager
import com.clawdis.android.node.SmsManager
import com.clawdis.android.protocol.ClawdisCapability
import com.clawdis.android.protocol.ClawdisCameraCommand
import com.clawdis.android.protocol.ClawdisCanvasA2UIAction
import com.clawdis.android.protocol.ClawdisCanvasA2UICommand
import com.clawdis.android.protocol.ClawdisCanvasCommand
import com.clawdis.android.protocol.ClawdisScreenCommand
import com.clawdis.android.protocol.ClawdisLocationCommand
import com.clawdis.android.protocol.ClawdisSmsCommand
import com.clawdis.android.voice.TalkModeManager
import com.clawdis.android.voice.VoiceWakeManager
import com.clawdbot.android.chat.ChatController
import com.clawdbot.android.chat.ChatMessage
import com.clawdbot.android.chat.ChatPendingToolCall
import com.clawdbot.android.chat.ChatSessionEntry
import com.clawdbot.android.chat.OutgoingAttachment
import com.clawdbot.android.bridge.BridgeDiscovery
import com.clawdbot.android.bridge.BridgeEndpoint
import com.clawdbot.android.bridge.BridgePairingClient
import com.clawdbot.android.bridge.BridgeSession
import com.clawdbot.android.node.CameraCaptureManager
import com.clawdbot.android.node.LocationCaptureManager
import com.clawdbot.android.BuildConfig
import com.clawdbot.android.node.CanvasController
import com.clawdbot.android.node.ScreenRecordManager
import com.clawdbot.android.node.SmsManager
import com.clawdbot.android.protocol.ClawdbotCapability
import com.clawdbot.android.protocol.ClawdbotCameraCommand
import com.clawdbot.android.protocol.ClawdbotCanvasA2UIAction
import com.clawdbot.android.protocol.ClawdbotCanvasA2UICommand
import com.clawdbot.android.protocol.ClawdbotCanvasCommand
import com.clawdbot.android.protocol.ClawdbotScreenCommand
import com.clawdbot.android.protocol.ClawdbotLocationCommand
import com.clawdbot.android.protocol.ClawdbotSmsCommand
import com.clawdbot.android.voice.TalkModeManager
import com.clawdbot.android.voice.VoiceWakeManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -369,38 +369,38 @@ class NodeRuntime(context: Context) {
private fun buildInvokeCommands(): List<String> =
buildList {
add(ClawdisCanvasCommand.Present.rawValue)
add(ClawdisCanvasCommand.Hide.rawValue)
add(ClawdisCanvasCommand.Navigate.rawValue)
add(ClawdisCanvasCommand.Eval.rawValue)
add(ClawdisCanvasCommand.Snapshot.rawValue)
add(ClawdisCanvasA2UICommand.Push.rawValue)
add(ClawdisCanvasA2UICommand.PushJSONL.rawValue)
add(ClawdisCanvasA2UICommand.Reset.rawValue)
add(ClawdisScreenCommand.Record.rawValue)
add(ClawdbotCanvasCommand.Present.rawValue)
add(ClawdbotCanvasCommand.Hide.rawValue)
add(ClawdbotCanvasCommand.Navigate.rawValue)
add(ClawdbotCanvasCommand.Eval.rawValue)
add(ClawdbotCanvasCommand.Snapshot.rawValue)
add(ClawdbotCanvasA2UICommand.Push.rawValue)
add(ClawdbotCanvasA2UICommand.PushJSONL.rawValue)
add(ClawdbotCanvasA2UICommand.Reset.rawValue)
add(ClawdbotScreenCommand.Record.rawValue)
if (cameraEnabled.value) {
add(ClawdisCameraCommand.Snap.rawValue)
add(ClawdisCameraCommand.Clip.rawValue)
add(ClawdbotCameraCommand.Snap.rawValue)
add(ClawdbotCameraCommand.Clip.rawValue)
}
if (locationMode.value != LocationMode.Off) {
add(ClawdisLocationCommand.Get.rawValue)
add(ClawdbotLocationCommand.Get.rawValue)
}
if (sms.canSendSms()) {
add(ClawdisSmsCommand.Send.rawValue)
add(ClawdbotSmsCommand.Send.rawValue)
}
}
private fun buildCapabilities(): List<String> =
buildList {
add(ClawdisCapability.Canvas.rawValue)
add(ClawdisCapability.Screen.rawValue)
if (cameraEnabled.value) add(ClawdisCapability.Camera.rawValue)
if (sms.canSendSms()) add(ClawdisCapability.Sms.rawValue)
add(ClawdbotCapability.Canvas.rawValue)
add(ClawdbotCapability.Screen.rawValue)
if (cameraEnabled.value) add(ClawdbotCapability.Camera.rawValue)
if (sms.canSendSms()) add(ClawdbotCapability.Sms.rawValue)
if (voiceWakeMode.value != VoiceWakeMode.Off && hasRecordAudioPermission()) {
add(ClawdisCapability.VoiceWake.rawValue)
add(ClawdbotCapability.VoiceWake.rawValue)
}
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 {
java.util.UUID.randomUUID().toString()
}
val name = ClawdisCanvasA2UIAction.extractActionName(userActionObj) ?: return@launch
val name = ClawdbotCanvasA2UIAction.extractActionName(userActionObj) ?: return@launch
val surfaceId =
(userActionObj["surfaceId"] as? JsonPrimitive)?.content?.trim().orEmpty().ifEmpty { "main" }
@@ -562,7 +562,7 @@ class NodeRuntime(context: Context) {
val sessionKey = "main"
val message =
ClawdisCanvasA2UIAction.formatAgentMessage(
ClawdbotCanvasA2UIAction.formatAgentMessage(
actionName = name,
sessionKey = sessionKey,
surfaceId = surfaceId,
@@ -596,7 +596,7 @@ class NodeRuntime(context: Context) {
try {
canvas.eval(
ClawdisCanvasA2UIAction.jsDispatchA2UIActionStatus(
ClawdbotCanvasA2UIAction.jsDispatchA2UIActionStatus(
actionId = actionId,
ok = connected && error == null,
error = error,
@@ -713,10 +713,10 @@ class NodeRuntime(context: Context) {
private suspend fun handleInvoke(command: String, paramsJson: String?): BridgeSession.InvokeResult {
if (
command.startsWith(ClawdisCanvasCommand.NamespacePrefix) ||
command.startsWith(ClawdisCanvasA2UICommand.NamespacePrefix) ||
command.startsWith(ClawdisCameraCommand.NamespacePrefix) ||
command.startsWith(ClawdisScreenCommand.NamespacePrefix)
command.startsWith(ClawdbotCanvasCommand.NamespacePrefix) ||
command.startsWith(ClawdbotCanvasA2UICommand.NamespacePrefix) ||
command.startsWith(ClawdbotCameraCommand.NamespacePrefix) ||
command.startsWith(ClawdbotScreenCommand.NamespacePrefix)
) {
if (!isForeground.value) {
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(
code = "CAMERA_DISABLED",
message = "CAMERA_DISABLED: enable Camera in Settings",
)
}
if (command.startsWith(ClawdisLocationCommand.NamespacePrefix) &&
if (command.startsWith(ClawdbotLocationCommand.NamespacePrefix) &&
locationMode.value == LocationMode.Off
) {
return BridgeSession.InvokeResult.error(
@@ -741,18 +741,18 @@ class NodeRuntime(context: Context) {
}
return when (command) {
ClawdisCanvasCommand.Present.rawValue -> {
ClawdbotCanvasCommand.Present.rawValue -> {
val url = CanvasController.parseNavigateUrl(paramsJson)
canvas.navigate(url)
BridgeSession.InvokeResult.ok(null)
}
ClawdisCanvasCommand.Hide.rawValue -> BridgeSession.InvokeResult.ok(null)
ClawdisCanvasCommand.Navigate.rawValue -> {
ClawdbotCanvasCommand.Hide.rawValue -> BridgeSession.InvokeResult.ok(null)
ClawdbotCanvasCommand.Navigate.rawValue -> {
val url = CanvasController.parseNavigateUrl(paramsJson)
canvas.navigate(url)
BridgeSession.InvokeResult.ok(null)
}
ClawdisCanvasCommand.Eval.rawValue -> {
ClawdbotCanvasCommand.Eval.rawValue -> {
val js =
CanvasController.parseEvalJs(paramsJson)
?: return BridgeSession.InvokeResult.error(
@@ -770,7 +770,7 @@ class NodeRuntime(context: Context) {
}
BridgeSession.InvokeResult.ok("""{"result":${result.toJsonString()}}""")
}
ClawdisCanvasCommand.Snapshot.rawValue -> {
ClawdbotCanvasCommand.Snapshot.rawValue -> {
val snapshotParams = CanvasController.parseSnapshotParams(paramsJson)
val base64 =
try {
@@ -787,7 +787,7 @@ class NodeRuntime(context: Context) {
}
BridgeSession.InvokeResult.ok("""{"format":"${snapshotParams.format.rawValue}","base64":"$base64"}""")
}
ClawdisCanvasA2UICommand.Reset.rawValue -> {
ClawdbotCanvasA2UICommand.Reset.rawValue -> {
val a2uiUrl = resolveA2uiHostUrl()
?: return BridgeSession.InvokeResult.error(
code = "A2UI_HOST_NOT_CONFIGURED",
@@ -803,7 +803,7 @@ class NodeRuntime(context: Context) {
val res = canvas.eval(a2uiResetJS)
BridgeSession.InvokeResult.ok(res)
}
ClawdisCanvasA2UICommand.Push.rawValue, ClawdisCanvasA2UICommand.PushJSONL.rawValue -> {
ClawdbotCanvasA2UICommand.Push.rawValue, ClawdbotCanvasA2UICommand.PushJSONL.rawValue -> {
val messages =
try {
decodeA2uiMessages(command, paramsJson)
@@ -826,7 +826,7 @@ class NodeRuntime(context: Context) {
val res = canvas.eval(js)
BridgeSession.InvokeResult.ok(res)
}
ClawdisCameraCommand.Snap.rawValue -> {
ClawdbotCameraCommand.Snap.rawValue -> {
showCameraHud(message = "Taking photo…", kind = CameraHudKind.Photo)
triggerCameraFlash()
val res =
@@ -840,7 +840,7 @@ class NodeRuntime(context: Context) {
showCameraHud(message = "Photo captured", kind = CameraHudKind.Success, autoHideMs = 1600)
BridgeSession.InvokeResult.ok(res.payloadJson)
}
ClawdisCameraCommand.Clip.rawValue -> {
ClawdbotCameraCommand.Clip.rawValue -> {
val includeAudio = paramsJson?.contains("\"includeAudio\":true") != false
if (includeAudio) externalAudioCaptureActive.value = true
try {
@@ -859,7 +859,7 @@ class NodeRuntime(context: Context) {
if (includeAudio) externalAudioCaptureActive.value = false
}
}
ClawdisLocationCommand.Get.rawValue -> {
ClawdbotLocationCommand.Get.rawValue -> {
val mode = locationMode.value
if (!isForeground.value && mode != LocationMode.Always) {
return BridgeSession.InvokeResult.error(
@@ -912,7 +912,7 @@ class NodeRuntime(context: Context) {
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.
_screenRecordActive.value = true
try {
@@ -928,7 +928,7 @@ class NodeRuntime(context: Context) {
_screenRecordActive.value = false
}
}
ClawdisSmsCommand.Send.rawValue -> {
ClawdbotSmsCommand.Send.rawValue -> {
val res = sms.send(paramsJson)
if (res.ok) {
BridgeSession.InvokeResult.ok(res.payloadJson)
@@ -999,7 +999,7 @@ class NodeRuntime(context: Context) {
val raw = session.currentCanvasHostUrl()?.trim().orEmpty()
if (raw.isBlank()) return null
val base = raw.trimEnd('/')
return "${base}/__clawdis__/a2ui/?platform=android"
return "${base}/__clawdbot__/a2ui/?platform=android"
}
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 hasMessagesArray = obj["messages"] is JsonArray
if (command == ClawdisCanvasA2UICommand.PushJSONL.rawValue || (!hasMessagesArray && jsonlField.isNotBlank())) {
if (command == ClawdbotCanvasA2UICommand.PushJSONL.rawValue || (!hasMessagesArray && jsonlField.isNotBlank())) {
val jsonl = jsonlField
if (jsonl.isBlank()) throw IllegalArgumentException("INVALID_REQUEST: jsonl required")
val messages =
@@ -1091,7 +1091,7 @@ private const val a2uiReadyCheckJS: String =
"""
(() => {
try {
return !!globalThis.clawdisA2UI && typeof globalThis.clawdisA2UI.applyMessages === 'function';
return !!globalThis.clawdbotA2UI && typeof globalThis.clawdbotA2UI.applyMessages === 'function';
} catch (_) {
return false;
}
@@ -1102,8 +1102,8 @@ private const val a2uiResetJS: String =
"""
(() => {
try {
if (!globalThis.clawdisA2UI) return { ok: false, error: "missing clawdisA2UI" };
return globalThis.clawdisA2UI.reset();
if (!globalThis.clawdbotA2UI) return { ok: false, error: "missing clawdbotA2UI" };
return globalThis.clawdbotA2UI.reset();
} catch (e) {
return { ok: false, error: String(e?.message ?? e) };
}
@@ -1114,9 +1114,9 @@ private fun a2uiApplyMessagesJS(messagesJson: String): String {
return """
(() => {
try {
if (!globalThis.clawdisA2UI) return { ok: false, error: "missing clawdisA2UI" };
if (!globalThis.clawdbotA2UI) return { ok: false, error: "missing clawdbotA2UI" };
const messages = $messagesJson;
return globalThis.clawdisA2UI.applyMessages(messages);
return globalThis.clawdbotA2UI.applyMessages(messages);
} catch (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.Intent
@@ -115,7 +115,7 @@ class PermissionRequester(private val activity: ComponentActivity) {
private fun buildRationaleMessage(permissions: List<String>): String {
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 {

View File

@@ -1,4 +1,4 @@
package com.clawdis.android
package com.clawdbot.android
import android.app.Activity
import android.content.Context
@@ -55,7 +55,7 @@ class ScreenCaptureRequester(private val activity: ComponentActivity) {
suspendCancellableCoroutine { cont ->
AlertDialog.Builder(activity)
.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) }
.setNegativeButton("Not now") { _, _ -> cont.resume(false) }
.setOnCancelListener { cont.resume(false) }

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.bridge
package com.clawdbot.android.bridge
object BonjourEscapes {
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.net.ConnectivityManager
@@ -51,9 +51,9 @@ class BridgeDiscovery(
private val nsd = context.getSystemService(NsdManager::class.java)
private val connectivity = context.getSystemService(ConnectivityManager::class.java)
private val dns = DnsResolver.getInstance()
private val serviceType = "_clawdis-bridge._tcp."
private val wideAreaDomain = "clawdis.internal."
private val logTag = "Clawdis/BridgeDiscovery"
private val serviceType = "_clawdbot-bridge._tcp."
private val wideAreaDomain = "clawdbot.internal."
private val logTag = "Clawdbot/BridgeDiscovery"
private val localById = 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(
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.withContext

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.bridge
package com.clawdbot.android.bridge
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
@@ -11,7 +11,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import com.clawdis.android.BuildConfig
import com.clawdbot.android.BuildConfig
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
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".
runCatching {
android.util.Log.d(
"ClawdisBridge",
"ClawdbotBridge",
"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.concurrent.ConcurrentHashMap
import kotlinx.coroutines.CoroutineScope

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.node
package com.clawdbot.android.node
import android.Manifest
import android.content.Context
@@ -11,7 +11,7 @@ import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.encodeToString
import com.clawdis.android.PermissionRequester
import com.clawdbot.android.PermissionRequester
/**
* 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.JsonPrimitive
object ClawdisCanvasA2UIAction {
object ClawdbotCanvasA2UIAction {
fun extractActionName(userAction: JsonObject): String? {
val name =
(userAction["name"] as? JsonPrimitive)
@@ -61,6 +61,6 @@ object ClawdisCanvasA2UIAction {
val err = (error ?: "").replace("\\", "\\\\").replace("\"", "\\\"")
val okLiteral = if (ok) "true" else "false"
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"),
Camera("camera"),
Screen("screen"),
@@ -9,7 +9,7 @@ enum class ClawdisCapability(val rawValue: String) {
Location("location"),
}
enum class ClawdisCanvasCommand(val rawValue: String) {
enum class ClawdbotCanvasCommand(val rawValue: String) {
Present("canvas.present"),
Hide("canvas.hide"),
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"),
PushJSONL("canvas.a2ui.pushJSONL"),
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"),
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"),
;
@@ -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"),
;
@@ -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"),
;

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.tools
package com.clawdbot.android.tools
import android.content.Context
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.layout.Box

View File

@@ -1,8 +1,8 @@
package com.clawdis.android.ui
package com.clawdbot.android.ui
import androidx.compose.runtime.Composable
import com.clawdis.android.MainViewModel
import com.clawdis.android.ui.chat.ChatSheetContent
import com.clawdbot.android.MainViewModel
import com.clawdbot.android.ui.chat.ChatSheetContent
@Composable
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.material3.MaterialTheme
@@ -9,7 +9,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
@Composable
fun ClawdisTheme(content: @Composable () -> Unit) {
fun ClawdbotTheme(content: @Composable () -> Unit) {
val context = LocalContext.current
val isDark = isSystemInDarkTheme()
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.Manifest
@@ -65,8 +65,8 @@ import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import androidx.core.content.ContextCompat
import com.clawdis.android.CameraHudKind
import com.clawdis.android.MainViewModel
import com.clawdbot.android.CameraHudKind
import com.clawdbot.android.MainViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -334,7 +334,7 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier)
WebSettingsCompat.setAlgorithmicDarkeningAllowed(settings, false)
}
if (isDebuggable) {
Log.d("ClawdisWebView", "userAgent: ${settings.userAgentString}")
Log.d("ClawdbotWebView", "userAgent: ${settings.userAgentString}")
}
isScrollContainer = true
overScrollMode = View.OVER_SCROLL_IF_CONTENT_SCROLLS
@@ -349,7 +349,7 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier)
) {
if (!isDebuggable) 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(
@@ -360,14 +360,14 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier)
if (!isDebuggable) return
if (!request.isForMainFrame) return
Log.e(
"ClawdisWebView",
"ClawdbotWebView",
"onReceivedHttpError: ${errorResponse.statusCode} ${errorResponse.reasonPhrase} ${request.url}",
)
}
override fun onPageFinished(view: WebView, url: String?) {
if (isDebuggable) {
Log.d("ClawdisWebView", "onPageFinished: $url")
Log.d("ClawdbotWebView", "onPageFinished: $url")
}
viewModel.canvas.onPageFinished()
}
@@ -378,7 +378,7 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier)
): Boolean {
if (isDebuggable) {
Log.e(
"ClawdisWebView",
"ClawdbotWebView",
"onRenderProcessGone didCrash=${detail.didCrash()} priorityAtExit=${detail.rendererPriorityAtExit()}",
)
}
@@ -391,7 +391,7 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier)
if (!isDebuggable) return false
val msg = consoleMessage ?: return false
Log.d(
"ClawdisWebView",
"ClawdbotWebView",
"console ${msg.messageLevel()} @ ${msg.sourceId()}:${msg.lineNumber()} ${msg.message()}",
)
return false
@@ -423,7 +423,7 @@ private class CanvasA2UIActionBridge(private val onMessage: (String) -> Unit) {
}
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.content.Context
@@ -52,11 +52,11 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import com.clawdis.android.BuildConfig
import com.clawdis.android.LocationMode
import com.clawdis.android.MainViewModel
import com.clawdis.android.NodeForegroundService
import com.clawdis.android.VoiceWakeMode
import com.clawdbot.android.BuildConfig
import com.clawdbot.android.LocationMode
import com.clawdbot.android.MainViewModel
import com.clawdbot.android.NodeForegroundService
import com.clawdbot.android.VoiceWakeMode
@Composable
fun SettingsSheet(viewModel: MainViewModel) {
@@ -436,7 +436,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
Column(verticalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.fillMaxWidth()) {
ListItem(
headlineContent = { Text("Foreground Only") },
supportingContent = { Text("Listens only while Clawdis is open.") },
supportingContent = { Text("Listens only while Clawdbot is open.") },
trailingContent = {
RadioButton(
selected = voiceWakeMode == VoiceWakeMode.Foreground,
@@ -482,7 +482,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(
onClick = {
val parsed = com.clawdis.android.WakeWords.parseCommaSeparated(wakeWordsText)
val parsed = com.clawdbot.android.WakeWords.parseCommaSeparated(wakeWordsText)
viewModel.setWakeWords(parsed)
},
enabled = isConnected,
@@ -580,7 +580,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
)
ListItem(
headlineContent = { Text("While Using") },
supportingContent = { Text("Only while Clawdis is open.") },
supportingContent = { Text("Only while Clawdbot is open.") },
trailingContent = {
RadioButton(
selected = locationMode == LocationMode.WhileUsing,
@@ -627,7 +627,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
item {
ListItem(
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) },
)
}

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.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.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.Box
@@ -38,7 +38,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.clawdis.android.chat.ChatSessionEntry
import com.clawdbot.android.chat.ChatSessionEntry
@Composable
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.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.Box
@@ -20,8 +20,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.unit.dp
import com.clawdis.android.chat.ChatMessage
import com.clawdis.android.chat.ChatPendingToolCall
import com.clawdbot.android.chat.ChatMessage
import com.clawdbot.android.chat.ChatPendingToolCall
@Composable
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.util.Base64
@@ -31,10 +31,10 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.Image
import com.clawdis.android.chat.ChatMessage
import com.clawdis.android.chat.ChatMessageContent
import com.clawdis.android.chat.ChatPendingToolCall
import com.clawdis.android.tools.ToolDisplayRegistry
import com.clawdbot.android.chat.ChatMessage
import com.clawdbot.android.chat.ChatMessageContent
import com.clawdbot.android.chat.ChatPendingToolCall
import com.clawdbot.android.tools.ToolDisplayRegistry
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
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.Column
@@ -20,7 +20,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.clawdis.android.chat.ChatSessionEntry
import com.clawdbot.android.chat.ChatSessionEntry
@Composable
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.net.Uri
@@ -19,8 +19,8 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.clawdis.android.MainViewModel
import com.clawdis.android.chat.OutgoingAttachment
import com.clawdbot.android.MainViewModel
import com.clawdbot.android.chat.OutgoingAttachment
import java.io.ByteArrayOutputStream
import kotlinx.coroutines.Dispatchers
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 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 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.JsonElement

View File

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

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.voice
package com.clawdbot.android.voice
object VoiceWakeCommandExtractor {
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.Intent

View File

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

View File

@@ -1,5 +1,5 @@
<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:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">false</item>

View File

@@ -4,7 +4,7 @@
<base-config cleartextTrafficPermitted="true" tools:ignore="InsecureBaseConfiguration" />
<!-- Allow HTTP for tailnet/local dev endpoints (e.g. canvas/background web). -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">clawdis.internal</domain>
<domain includeSubdomains="true">clawdbot.internal</domain>
</domain-config>
<domain-config cleartextTrafficPermitted="true">
<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.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.Test
@@ -12,7 +12,7 @@ class BonjourEscapesTest {
@Test
fun decodeDecodesDecimalEscapes() {
assertEquals("Clawdis Gateway", BonjourEscapes.decode("Clawdis\\032Gateway"))
assertEquals("Clawdbot Gateway", BonjourEscapes.decode("Clawdbot\\032Gateway"))
assertEquals("A B", BonjourEscapes.decode("A\\032B"))
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.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.async

View File

@@ -1,4 +1,4 @@
package com.clawdis.android.bridge
package com.clawdbot.android.bridge
import kotlinx.coroutines.CompletableDeferred
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.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.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.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.jsonObject
import org.junit.Assert.assertEquals
import org.junit.Test
class ClawdisCanvasA2UIActionTest {
class ClawdbotCanvasA2UIActionTest {
@Test
fun extractActionNameAcceptsNameOrAction() {
val nameObj = Json.parseToJsonElement("{\"name\":\"Hello\"}").jsonObject
assertEquals("Hello", ClawdisCanvasA2UIAction.extractActionName(nameObj))
assertEquals("Hello", ClawdbotCanvasA2UIAction.extractActionName(nameObj))
val actionObj = Json.parseToJsonElement("{\"action\":\"Wave\"}").jsonObject
assertEquals("Wave", ClawdisCanvasA2UIAction.extractActionName(actionObj))
assertEquals("Wave", ClawdbotCanvasA2UIAction.extractActionName(actionObj))
val fallbackObj =
Json.parseToJsonElement("{\"name\":\" \",\"action\":\"Fallback\"}").jsonObject
assertEquals("Fallback", ClawdisCanvasA2UIAction.extractActionName(fallbackObj))
assertEquals("Fallback", ClawdbotCanvasA2UIAction.extractActionName(fallbackObj))
}
@Test
fun formatAgentMessageMatchesSharedSpec() {
val msg =
ClawdisCanvasA2UIAction.formatAgentMessage(
ClawdbotCanvasA2UIAction.formatAgentMessage(
actionName = "Get Weather",
sessionKey = "main",
surfaceId = "main",
@@ -40,9 +40,9 @@ class ClawdisCanvasA2UIActionTest {
@Test
fun jsDispatchA2uiStatusIsStable() {
val js = ClawdisCanvasA2UIAction.jsDispatchA2UIActionStatus(actionId = "a1", ok = true, error = null)
val js = ClawdbotCanvasA2UIAction.jsDispatchA2UIActionStatus(actionId = "a1", ok = true, error = null)
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,
)
}

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.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.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.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")

View File

@@ -1,4 +1,4 @@
# Clawdis (iOS)
# Clawdbot (iOS)
Internal-only SwiftUI app scaffold.
@@ -11,11 +11,11 @@ brew install swiftformat swiftlint
```bash
cd apps/ios
xcodegen generate
open Clawdis.xcodeproj
open Clawdbot.xcodeproj
```
## 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
```bash

View File

@@ -1,4 +1,4 @@
import ClawdisKit
import ClawdbotKit
import Foundation
import Network
@@ -14,7 +14,7 @@ actor BridgeClient {
{
self.lineBuffer = Data()
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() }
try await self.withTimeout(seconds: 8, purpose: "connect") {
try await self.startAndWaitForReady(connection, queue: queue)

View File

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

View File

@@ -1,4 +1,4 @@
import ClawdisKit
import ClawdbotKit
import Foundation
import Network
import Observation
@@ -51,11 +51,11 @@ final class BridgeDiscoveryModel {
if !self.browsers.isEmpty { return }
self.appendDebugLog("start()")
for domain in ClawdisBonjour.bridgeServiceDomains {
for domain in ClawdbotBonjour.bridgeServiceDomains {
let params = NWParameters.tcp
params.includePeerToPeer = true
let browser = NWBrowser(
for: .bonjour(type: ClawdisBonjour.bridgeServiceType, domain: domain),
for: .bonjour(type: ClawdbotBonjour.bridgeServiceType, domain: domain),
using: params)
browser.stateUpdateHandler = { [weak self] state in
@@ -102,7 +102,7 @@ final class BridgeDiscoveryModel {
}
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 {
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)
return stripped.trimmingCharacters(in: .whitespacesAndNewlines)
}

View File

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

View File

@@ -1,4 +1,4 @@
import ClawdisKit
import ClawdbotKit
import Foundation
import Network
@@ -78,7 +78,7 @@ actor BridgeSession {
let params = NWParameters.tcp
params.includePeerToPeer = true
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.queue = queue

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
import ClawdisChatUI
import ClawdisKit
import ClawdbotChatUI
import ClawdbotKit
import Foundation
struct IOSBridgeChatTransport: ClawdisChatTransport, Sendable {
struct IOSBridgeChatTransport: ClawdbotChatTransport, Sendable {
private let 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)
}
func listSessions(limit: Int?) async throws -> ClawdisChatSessionsListResponse {
func listSessions(limit: Int?) async throws -> ClawdbotChatSessionsListResponse {
struct Params: Codable {
var includeGlobal: 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 json = String(data: data, encoding: .utf8)
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 {
@@ -38,12 +38,12 @@ struct IOSBridgeChatTransport: ClawdisChatTransport, Sendable {
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 }
let data = try JSONEncoder().encode(Params(sessionKey: sessionKey))
let json = String(data: data, encoding: .utf8)
let res = try await self.bridge.request(method: "chat.history", paramsJSON: json, timeoutSeconds: 15)
return try JSONDecoder().decode(ClawdisChatHistoryPayload.self, from: res)
return try JSONDecoder().decode(ClawdbotChatHistoryPayload.self, from: res)
}
func sendMessage(
@@ -51,13 +51,13 @@ struct IOSBridgeChatTransport: ClawdisChatTransport, Sendable {
message: String,
thinking: String,
idempotencyKey: String,
attachments: [ClawdisChatAttachmentPayload]) async throws -> ClawdisChatSendResponse
attachments: [ClawdbotChatAttachmentPayload]) async throws -> ClawdbotChatSendResponse
{
struct Params: Codable {
var sessionKey: String
var message: String
var thinking: String
var attachments: [ClawdisChatAttachmentPayload]?
var attachments: [ClawdbotChatAttachmentPayload]?
var timeoutMs: Int
var idempotencyKey: String
}
@@ -72,16 +72,16 @@ struct IOSBridgeChatTransport: ClawdisChatTransport, Sendable {
let data = try JSONEncoder().encode(params)
let json = String(data: data, encoding: .utf8)
let res = try await self.bridge.request(method: "chat.send", paramsJSON: json, timeoutSeconds: 35)
return try JSONDecoder().decode(ClawdisChatSendResponse.self, from: res)
return try JSONDecoder().decode(ClawdbotChatSendResponse.self, from: res)
}
func requestHealth(timeoutMs: Int) async throws -> Bool {
let seconds = max(1, Int(ceil(Double(timeoutMs) / 1000.0)))
let res = try await self.bridge.request(method: "health", paramsJSON: nil, timeoutSeconds: seconds)
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
let task = Task {
let stream = await self.bridge.subscribeServerEvents()
@@ -94,16 +94,16 @@ struct IOSBridgeChatTransport: ClawdisChatTransport, Sendable {
continuation.yield(.seqGap)
case "health":
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))
case "chat":
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))
}
case "agent":
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))
}
default:

View File

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

View File

@@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Clawdis</string>
<string>Clawdbot</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconName</key>
@@ -29,20 +29,20 @@
</dict>
<key>NSBonjourServices</key>
<array>
<string>_clawdis-bridge._tcp</string>
<string>_clawdbot-bridge._tcp</string>
</array>
<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>
<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>
<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>
<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>
<string>Clawdis needs microphone access for voice wake.</string>
<string>Clawdbot needs microphone access for voice wake.</string>
<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>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>

View File

@@ -1,4 +1,4 @@
import ClawdisKit
import ClawdbotKit
import CoreLocation
import Foundation
@@ -30,7 +30,7 @@ final class LocationService: NSObject, CLLocationManagerDelegate {
return .fullAccuracy
}
func ensureAuthorization(mode: ClawdisLocationMode) async -> CLAuthorizationStatus {
func ensureAuthorization(mode: ClawdbotLocationMode) async -> CLAuthorizationStatus {
guard CLLocationManager.locationServicesEnabled() else { return .denied }
let status = self.manager.authorizationStatus
@@ -53,8 +53,8 @@ final class LocationService: NSObject, CLLocationManagerDelegate {
}
func currentLocation(
params: ClawdisLocationGetParams,
desiredAccuracy: ClawdisLocationAccuracy,
params: ClawdbotLocationGetParams,
desiredAccuracy: ClawdbotLocationAccuracy,
maxAgeMs: Int?,
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 {
case .coarse:
return kCLLocationAccuracyKilometer

View File

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

View File

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

View File

@@ -63,12 +63,12 @@ final class ScreenRecordService: @unchecked Sendable {
return URL(fileURLWithPath: outPath)
}
return FileManager.default.temporaryDirectory
.appendingPathComponent("clawdis-screen-record-\(UUID().uuidString).mp4")
.appendingPathComponent("clawdbot-screen-record-\(UUID().uuidString).mp4")
}()
try? FileManager.default.removeItem(at: outURL)
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
let handler: @Sendable (CMSampleBuffer, RPSampleBufferType, Error?) -> Void = { sample, type, error in

View File

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

View File

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

View File

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

View File

@@ -30,7 +30,7 @@ struct VoiceWakeWordsSettingsView: View {
Text("Wake Words")
} footer: {
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.")
}
}

View File

@@ -1,5 +1,5 @@
import AVFAudio
import ClawdisKit
import ClawdbotKit
import Foundation
import Observation
import OSLog
@@ -47,7 +47,7 @@ final class TalkModeManager: NSObject {
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) {
self.bridge = bridge

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
import ClawdisKit
import ClawdbotKit
import Foundation
import Network
import Testing
@testable import Clawdis
@testable import Clawdbot
@Suite struct BridgeClientTests {
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 var connection: NWConnection?
private var buffer = Data()

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