Compare commits

..
378 changed files with 12091 additions and 29274 deletions

No files matched your search

-1
View File
@@ -30,7 +30,6 @@ vendor/
# Vendor build artifacts
vendor/a2ui/renderers/lit/dist/
.bundle.hash
# fastlane (iOS)
apps/ios/fastlane/README.md
+1 -1
View File
@@ -1 +1 @@
allow-build-scripts=@whiskeysockets/baileys,sharp,esbuild,protobufjs,fs-ext,node-pty
allow-build-scripts=@whiskeysockets/baileys,sharp,esbuild,protobufjs,fs-ext
+2 -61
View File
@@ -1,63 +1,12 @@
# Changelog
## 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`).
### Features
- Gateway: support `gateway.port` + `CLAWDIS_GATEWAY_PORT` across CLI, TUI, and macOS app.
- Gateway: add config hot reload with hybrid restart strategy (`gateway.reload`) and per-section reload handling.
- UI: centralize tool display metadata and show action/detail summaries across Web Chat, SwiftUI, Android, and the TUI.
- Control UI: support configurable base paths (`gateway.controlUi.basePath`, default unchanged) for hosting under URL prefixes.
- Onboarding: shared wizard engine powering CLI + macOS via gateway wizard RPC.
- Config: expose schema + UI hints for generic config forms (Web UI + future clients).
- Skills: add blogwatcher skill for RSS/Atom monitoring — thanks @Hyaxia.
- Discord: emit system events for reaction add/remove with per-guild reaction notifications (off|own|all|allowlist) (#140) — thanks @thewilloftheshadow.
### Fixes
- Auto-reply: drop final payloads when block streaming to avoid duplicate Discord sends.
- Telegram: chunk block-stream replies to avoid “message is too long” errors (#124) — thanks @mukhtharcm.
- Block streaming: default to text_end and suppress duplicate block sends while in-flight.
- Block streaming: avoid duplicate block chunks when providers repeat full content on text_end.
- Block streaming: drop final payloads after soft chunking to keep Discord order intact.
- Gmail hooks: resolve gcloud Python to a real executable when PATH uses mise shims — thanks @joargp.
- Control UI: generate UUIDs when `crypto.randomUUID()` is unavailable over HTTP — thanks @ratulsarna.
- Agent: add soft block-stream chunking (800–1200 chars default) with paragraph/newline preference.
- Agent tools: scope the Discord tool to Discord surface runs.
- Agent tools: format verbose tool summaries without brackets, with unique emojis and `tool: detail` style.
- Gateway: split server helpers/tests into hooks/session-utils/ws-log/net modules for better isolation; add unit coverage for hooks/session utils/ws log.
- Gateway: extract WS method handling + HTTP/provider/constant helpers to shrink server wiring and improve testability.
- Onboarding: fix Control UI basePath usage when showing/opening gateway URLs.
- macOS Connections: move to sidebar + detail layout with structured sections and header actions.
- macOS onboarding: increase window height so the permissions page fits without scrolling.
- Thinking: default to low for reasoning-capable models when no /think or config default is set.
- 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).
- 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.
- Dependencies: bump pi-mono packages to 0.32.3.
### Docs
- Skills: add Sheets/Docs examples to gog skill (#128) — thanks @mbelinky.
- Skills: clarify bear-notes token + callback usage (#120) — thanks @tylerwince.
- Skills: document Discord `sendMessage` media attachments and `to` format clarification.
- Skills: add tmux skill + interactive coding guidance in coding-agent.
- Gateway: document port configuration + multi-instance isolation.
- Gateway: document config hot reload + reload matrix.
- Onboarding/Config: add protocol notes for wizard + schema RPC.
- Queue: clarify steer-backlog behavior with inline commands and update examples for streaming surfaces.
## 2.0.0-beta5 — 2026-01-03
## 2.0.0-beta5 — Unreleased
### Fixed
- Media: preserve GIF animation when uploading to Discord/other providers (skip JPEG optimization for image/gif).
- Agent runtime: update pi-mono dependencies to 0.31.1 (agent-core split).
- Dependencies: bump to latest compatible versions (TypeBox, grammY, Zod, Rolldown, oxlint-tsgolint).
- Tests: cover read tool image metadata + text output.
- Tests: add queue mode coverage (collect/followup + directive parsing).
### Breaking
- Skills config schema moved under `skills.*`:
@@ -72,13 +21,10 @@
- Config: remove `routing.groupChat.requireMention` + `telegram.requireMention`; use `whatsapp.groups`, `imessage.groups`, and `telegram.groups` defaults instead (run `clawdis doctor` to migrate).
### Features
- Discord: expand `discord` tool actions (reactions, stickers, polls, threads, search, moderation gates) (#115) — thanks @thewilloftheshadow.
- Discord/Telegram: add reply tags (`[[reply_to_current]]`, `[[reply_to:<id>]]`) with per-provider `replyToMode` (off|first|all) for native threaded replies.
- 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).
- 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.
- Discord: add user-installed slash command handling with per-user sessions and auto-registration (#94) — thanks @thewilloftheshadow.
@@ -99,8 +45,6 @@
- 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).
- 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.
- Skills: add Apple Notes + Reminders skills via memo CLI (thanks @tylerwince).
@@ -110,11 +54,9 @@
- Skills: add tmux-first coding-agent skill + `requires.anyBins` gate for multi-CLI setup (thanks @sreekaransrinath).
### Fixes
- macOS codesign: make ad-hoc signing opt-in with loud warnings and document TCC permission fragility — thanks @mcinteerj.
- Gog calendar: format date ranges as RFC 3339 with timezone to satisfy Google Calendar API (thanks @jayhickey).
- macOS onboarding: add scrollable page gutter for overflowing content (#105) — thanks @thewilloftheshadow.
- Chat UI: keep the chat scrolled to the latest message after switching sessions.
- Chat UI: show rich session display names in Web Chat + SwiftUI + Android.
- Auto-reply: stream completed reply blocks as soon as they finish (configurable default + break); skip empty tool-only blocks unless verbose.
- Discord: avoid duplicate sends when block streaming is enabled (race with typing hook).
- Providers: make outbound text chunk limits configurable via `*.textChunkLimit` (defaults remain 4000/Discord 2000).
@@ -153,7 +95,6 @@
- macOS: log health refresh failures and recovery to make gateway issues easier to diagnose.
- macOS codesign: skip hardened runtime for ad-hoc signing and avoid empty options args (#70) — thanks @petter-b
- macOS codesign: include camera entitlement so permission prompts work in the menu bar app.
- Agent tools: bash tool supports real TTY via `stdinMode: "pty"` with node-pty, warning + fallback on load/start failure.
- Agent tools: map `camera.snap` JPEG payloads to `image/jpeg` to avoid MIME mismatch errors.
- Tests: cover `camera.snap` MIME mapping to prevent image/png vs image/jpeg mismatches.
- macOS camera: wait for exposure/white balance to settle before capturing a snap to avoid dark images.
@@ -262,7 +203,7 @@
- Hook mappings + Gmail Pub/Sub helper (`clawdis 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.
- Gateway in-process restart: `clawdis_gateway` tool action triggers a SIGUSR1 restart without needing a supervisor.
### Breaking
- Config refactor: `inbound.*` removed; use top-level `routing` (allowlists + group rules + transcription), `messages` (prefixes/timestamps), and `session` (scoping/store/mainKey). No legacy keys read.
+1 -1
View File
@@ -109,7 +109,7 @@ Send these in WhatsApp/Telegram/WebChat (group commands are owner-only):
- **Events + snapshot**: handshake returns a snapshot (presence/health) and declares event types; runtime events include `agent`, `chat`, `presence`, `tick`, `health`, `heartbeat`, `cron`, `node.pair.*`, `voicewake.changed`, `shutdown`.
- **Idempotency & safety**: `send`/`agent`/`chat.send` require idempotency keys with a TTL cache (5 min, cap 1000) to avoid double‑sends on reconnects; payload sizes are capped per connection.
- **Bridge for nodes**: optional TCP bridge (`src/infra/bridge/server.ts`) is newline‑delimited JSON frames (`hello`, pairing, RPC, `invoke`); node connect/disconnect is surfaced into presence.
- **Control UI + Canvas Host**: HTTP serves Control UI assets (default `/`, optional base path) and can host a live‑reload Canvas host for nodes (`src/canvas-host/server.ts`), injecting the A2UI postMessage bridge.
- **Control UI + Canvas Host**: HTTP serves `/ui` assets (if built) and can host a live‑reload Canvas host for nodes (`src/canvas-host/server.ts`), injecting the A2UI postMessage bridge.
### iOS app (apps/ios)
- **Discovery + pairing**: Bonjour discovery via `BridgeDiscoveryModel` (NWBrowser). `BridgeConnectionController` auto‑connects using Keychain token or allows manual host/port.
+1 -10
View File
@@ -1,15 +1,6 @@
{
"originHash" : "5d29ee82825e0764775562242cfa1ff4dc79584797dd638f76c9876545454748",
"originHash" : "2012d083159d375d07febbc184c592c569d7ab48247045e35a762e3269d4cadc",
"pins" : [
{
"identity" : "elevenlabskit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/steipete/ElevenLabsKit",
"state" : {
"revision" : "c8679fbd37416a8780fe43be88a497ff16209e2d",
"version" : "0.1.0"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
+17 -198
View File
@@ -3,210 +3,29 @@
<channel>
<title>Clawdis</title>
<item>
<title>2.0.0-beta5</title>
<pubDate>Sat, 03 Jan 2026 07:15:16 +0100</pubDate>
<title>2.0.0-beta3</title>
<pubDate>Sat, 27 Dec 2025 19:02:02 +0100</pubDate>
<link>https://raw.githubusercontent.com/steipete/clawdis/main/appcast.xml</link>
<sparkle:version>2765</sparkle:version>
<sparkle:shortVersionString>2.0.0-beta5</sparkle:shortVersionString>
<sparkle:version>2.0.0-beta3</sparkle:version>
<sparkle:shortVersionString>2.0.0-beta3</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>Clawdis 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>
<li>Agent runtime: update pi-mono dependencies to 0.31.1 (agent-core split).</li>
<li>Dependencies: bump to latest compatible versions (TypeBox, grammY, Zod, Rolldown, oxlint-tsgolint).</li>
<li>Tests: cover read tool image metadata + text output.</li>
<li>Tests: add queue mode coverage (collect/followup + directive parsing).</li>
</ul>
<h3>Breaking</h3>
<ul>
<li>Skills config schema moved under <code>skills.*</code>:</li>
</ul>
- <code>skillsLoad.extraDirs</code> → <code>skills.load.extraDirs</code>
- <code>skillsInstall.*</code> → <code>skills.install.*</code>
- per-skill config map moved to <code>skills.entries</code> (e.g. <code>skills.peekaboo.enabled</code> → <code>skills.entries.peekaboo.enabled</code>)
- new optional bundled allowlist: <code>skills.allowBundled</code> (only affects bundled skills)
<ul>
<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>
</ul>
<h3>Features</h3>
<ul>
<li>Discord: expand <code>discord</code> tool actions (reactions, stickers, polls, threads, search, moderation gates) (#115) — thanks @thewilloftheshadow.</li>
<li>Discord/Telegram: add reply tags (<code>[[reply_to_current]]</code>, <code>[[reply_to:<id>]]</code>) with per-provider <code>replyToMode</code> (off|first|all) for native threaded replies.</li>
<li>Talk mode: continuous speech conversations (macOS/iOS/Android) with ElevenLabs TTS, reply directives, and optional interrupt-on-speech.</li>
<li>Auto-reply: expand queue modes (steer/followup/collect/steer-backlog) with debounce/cap/drop options and followup backlog handling.</li>
<li>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>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>
<li>Discord: add user-installed slash command handling with per-user sessions and auto-registration (#94) — thanks @thewilloftheshadow.</li>
<li>Discord: add DM enable/allowlist plus guild channel/user/guild allowlists with id/name matching.</li>
<li>Signal: add <code>signal-cli</code> JSON-RPC support for send/receive via the Signal provider.</li>
<li>iMessage: add imsg JSON-RPC integration (stdio), chat_id routing, and group chat support.</li>
<li>Chat UI: add recent-session dropdown switcher (main first) in macOS/iOS/Android + Control UI.</li>
<li>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: 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>
<li>Docker: add containerized gateway/CLI setup via Dockerfile, compose, and setup script (thanks @dan-dr).</li>
<li>Tests: add a Z.AI live test gate for smoke validation when keys are present.</li>
<li>macOS Debug: add app log verbosity and rolling file log toggle for swift-log-backed app logs.</li>
<li>CLI: add onboarding wizard (gateway + workspace + skills) with daemon installers and Anthropic/Minimax setup paths.</li>
<li>CLI: add ASCII banner header to wizard entry points.</li>
<li>CLI: add <code>configure</code>, <code>doctor</code>, and <code>update</code> wizards for ongoing setup, health checks, and modernization.</li>
<li>CLI: add Signal CLI auto-install from GitHub releases in the wizard and persist wizard run metadata in config.</li>
<li>CLI: add 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>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>
<li>Skills: add Apple Notes + Reminders skills via memo CLI (thanks @tylerwince).</li>
<li>Tests: add a Docker-based onboarding E2E harness.</li>
<li>Tests: harden wizard E2E flows for reset, providers, skills, and remote non-interactive runs.</li>
<li>Browser tools: add remote CDP URL support, Linux launcher options (<code>executablePath</code>, <code>noSandbox</code>), and surface <code>cdpUrl</code> in status.</li>
<li>Skills: add tmux-first coding-agent skill + <code>requires.anyBins</code> gate for multi-CLI setup (thanks @sreekaransrinath).</li>
</ul>
<enclosure url="https://github.com/steipete/clawdis/releases/download/v2.0.0-beta3/Clawdis-2.0.0-beta3.zip" length="70407960" type="application/octet-stream" sparkle:edSignature="A8ySMmbLRrpIkqkrmc9QrC+6om8Iyqray/6x/YNiJxDoJeXjp2T5t8XT0CKJeNBUlDkzIj/fwiK53v0qQ59cDQ=="/>
</item>
<item>
<title>2.0.0-beta4</title>
<pubDate>Sat, 27 Dec 2025 19:43:22 +0100</pubDate>
<link>https://raw.githubusercontent.com/steipete/clawdis/main/appcast.xml</link>
<sparkle:version>2.0.0-beta4</sparkle:version>
<sparkle:shortVersionString>2.0.0-beta4</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>Clawdis 2.0.0-beta4</h2>
<h3>Fixes</h3>
<ul>
<li>Gog calendar: format date ranges as RFC 3339 with timezone to satisfy Google Calendar API (thanks @jayhickey).</li>
<li>macOS onboarding: add scrollable page gutter for overflowing content (#105) — thanks @thewilloftheshadow.</li>
<li>Chat UI: keep the chat scrolled to the latest message after switching sessions.</li>
<li>Chat UI: show rich session display names in Web Chat + SwiftUI + Android.</li>
<li>Auto-reply: stream completed reply blocks as soon as they finish (configurable default + break); skip empty tool-only blocks unless verbose.</li>
<li>Discord: avoid duplicate sends when block streaming is enabled (race with typing hook).</li>
<li>Providers: make outbound text chunk limits configurable via <code>*.textChunkLimit</code> (defaults remain 4000/Discord 2000).</li>
<li>CLI onboarding: persist gateway token in config so local CLI auth works; recommend auth Off unless you need multi-machine access.</li>
<li>Control UI: accept a <code>?token=</code> URL param to auto-fill Gateway auth; onboarding now opens the dashboard with token auth when configured.</li>
<li>Agent prompt: remove hardcoded user name in system prompt example.</li>
<li>Chat UI: add extra top padding before the first message bubble in Web Chat (macOS/iOS/Android).</li>
<li>Control UI: refine Web Chat session selector styling (chevron spacing + background).</li>
<li>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: 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>
<li>Logging: trim provider prefix duplication in Discord/Signal/Telegram runtime log lines.</li>
<li>Logging/Signal: treat signal-cli "Failed …" lines as errors in gateway logs.</li>
<li>Discord: include recent guild context when replying to mentions and add <code>discord.historyLimit</code> to tune how many messages are captured.</li>
<li>Discord: include author tag + id in group context <code>[from:]</code> lines for ping-ready replies (thanks @thewilloftheshadow).</li>
<li>Discord: include replied-to message context when a Discord message references another message (thanks @thewilloftheshadow).</li>
<li>Discord: preserve newlines when stripping reply tags from agent output.</li>
<li>Gateway: fix TypeScript build by aligning hook mapping <code>channel</code> types and removing a dead Group DM branch in Discord monitor.</li>
<li>Skills: switch imsg installer to brew tap formula.</li>
<li>Skills: gate macOS-only skills by OS and surface block reasons in the Skills UI.</li>
<li>Onboarding: show skill descriptions in the macOS setup flow and surface clearer Gateway/skills error messages.</li>
<li>Onboarding: auto-verify Claude OAuth tokens, show “verified” when detected working, and avoid re-auth prompts unless verification fails.</li>
<li>CLI onboarding: include exit code + a useful one-line summary when skill dependency installs fail.</li>
<li>CLI onboarding: explain Tailscale exposure options (Off/Serve/Funnel) and colorize provider status (linked/configured/needs setup).</li>
<li>CLI onboarding: add provider primers (WhatsApp/Telegram/Discord/Signal) incl. Discord bot token setup steps.</li>
<li>CLI onboarding: allow skipping the “install missing skill dependencies” selection without canceling the wizard.</li>
<li>CLI onboarding: always prompt for WhatsApp <code>whatsapp.allowFrom</code> and print (optionally open) the Control UI URL when done.</li>
<li>CLI onboarding: detect gateway reachability and annotate Local/Remote choices (helps pick the right mode).</li>
<li>macOS settings: colorize provider status subtitles to distinguish healthy vs degraded states.</li>
<li>macOS: keep config writes on the main actor to satisfy Swift concurrency rules.</li>
<li>macOS menu: show multi-line gateway error details, add an always-visible gateway row, avoid duplicate gateway status rows, suppress transient <code>cancelled</code> device refresh errors, and auto-recover the control channel on disconnect.</li>
<li>macOS menu: show session last-used timestamps in the list and add recent-message previews in session submenus.</li>
<li>macOS menu: tighten session row padding and time out session preview loading with cached fallback.</li>
<li>macOS: log health refresh failures and recovery to make gateway issues easier to diagnose.</li>
<li>macOS codesign: skip hardened runtime for ad-hoc signing and avoid empty options args (#70) — thanks @petter-b</li>
<li>macOS codesign: include camera entitlement so permission prompts work in the menu bar app.</li>
<li>Agent tools: bash tool supports real TTY via <code>stdinMode: "pty"</code> with node-pty, warning + fallback on load/start failure.</li>
<li>Agent tools: map <code>camera.snap</code> JPEG payloads to <code>image/jpeg</code> to avoid MIME mismatch errors.</li>
<li>Tests: cover <code>camera.snap</code> MIME mapping to prevent image/png vs image/jpeg mismatches.</li>
<li>macOS camera: wait for exposure/white balance to settle before capturing a snap to avoid dark images.</li>
<li>Camera snap: add <code>delayMs</code> parameter (default 2000ms on macOS) to improve exposure reliability.</li>
<li>Camera: add <code>camera.list</code> and optional <code>deviceId</code> selection for snaps/clips.</li>
<li>Tests: cover camera device selection params in CLI + agent tools.</li>
<li>macOS packaging: move rpath config into swift build for reliability (#69) — thanks @petter-b</li>
<li>macOS: prioritize main bundle for device resources to prevent crash (#73) — thanks @petter-b</li>
<li>macOS remote: route settings through gateway config and avoid local config reads in remote mode.</li>
<li>Telegram: align token resolution for cron/agent/CLI sends (env/config/tokenFile) to prevent isolated delivery failures (#76).</li>
<li>Telegram: honor per-group mention gating defaults/overrides via <code>telegram.groups</code> and <code>"*"</code> defaults (thanks @joshp123).</li>
<li>Chat UI: clear composer input immediately and allow clear while editing to prevent duplicate sends (#72) — thanks @hrdwdmrbl</li>
<li>Restart: use systemd on Linux (and report actual restart method) instead of always launchctl.</li>
<li>Gateway relay: detect Bun binaries via execPath to resolve packaged assets on macOS.</li>
<li>Cron: prevent <code>every</code> schedules without an anchor from firing in a tight loop (thanks @jamesgroat).</li>
<li>Docs: add manual OAuth setup for remote/headless deployments (#67) — thanks @wstock</li>
<li>Docs/agent tools: clarify that browser <code>wait</code> should be avoided by default and used only in exceptional cases.</li>
<li>Docs: clarify self-chat mode and group mention gating config (#111) — thanks @rafaelreis-r.</li>
<li>Browser tools: <code>upload</code> supports auto-click refs, direct <code>inputRef</code>/<code>element</code> file inputs, and emits input/change after <code>setFiles</code> so JS-heavy sites pick up attachments.</li>
<li>Browser tools: 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>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>
<li>CLI: tighten onboarding prompt typing to keep bun builds green.</li>
<li>macOS: Voice Wake now fully tears down the Speech pipeline when disabled (cancel pending restarts, drop stale callbacks) to avoid high CPU in the background.</li>
<li>macOS menu: add a Talk Mode action alongside the Open Dashboard/Chat/Canvas entries.</li>
<li>macOS Debug: hide “Restart Gateway” when the app won’t start a local gateway (remote mode / attach-only).</li>
<li>macOS Debug: add an icon for the App Logging submenu.</li>
<li>macOS Talk Mode: orb overlay refresh, ElevenLabs request logging, API key status in settings, and auto-select first voice when none is configured.</li>
<li>macOS Talk Mode: add hard timeout around ElevenLabs TTS synthesis to avoid getting stuck “speaking” forever on hung requests.</li>
<li>macOS Talk Mode: avoid stuck playback when the audio player never starts (fail-fast + watchdog).</li>
<li>macOS Talk Mode: fix audio stop ordering so disabling Talk Mode always stops in-flight playback.</li>
<li>macOS Talk Mode: throttle audio-level updates (avoid per-buffer task creation) to reduce CPU/task churn.</li>
<li>macOS Talk Mode: increase overlay window size so wave rings don’t clip; close button is hover-only and closer to the orb.</li>
<li>WebChat: preserve chat run ordering per session so concurrent runs don’t strand the typing indicator.</li>
<li>Talk Mode: fall back to system TTS when ElevenLabs is unavailable, returns non-audio, or playback fails (macOS/iOS/Android).</li>
<li>Talk Mode: stream PCM on macOS/iOS for lower latency (incremental playback); Android continues MP3 streaming.</li>
<li>Talk Mode: validate ElevenLabs v3 stability and latency tier directives before sending requests.</li>
<li>iOS/Android Talk Mode: auto-select the first ElevenLabs voice when none is configured.</li>
<li>ElevenLabs: add retry/backoff for 429/5xx and include content-type in errors for debugging.</li>
<li>Talk Mode: align to the gateway’s main session key and fall back to history polling when chat events drop (prevents stuck “thinking” / missing messages).</li>
<li>Talk Mode: treat history timestamps as seconds or milliseconds to avoid stale assistant picks (macOS/iOS/Android).</li>
<li>Chat UI: clear streaming/tool bubbles when external runs finish, preventing duplicate assistant bubbles.</li>
<li>Chat UI: user bubbles use <code>ui.seamColor</code> (fallback to a calmer default blue).</li>
<li>Android Chat UI: use <code>onPrimary</code> for user bubble text to preserve contrast (thanks @Syhids).</li>
<li>Control UI: sync sidebar navigation with the URL for deep-linking, and auto-scroll chat to the latest message.</li>
<li>Control UI: disable Web Chat + Talk when no iOS/Android node is connected; refreshed Web Chat styling and keyboard send.</li>
<li>Control UI: keep chat pinned to the latest message while typing/sending and restore drafts on send failures.</li>
<li>Control UI: soften chat bubble text opacity for calmer readability.</li>
<li>macOS Web Chat: improve empty/error states, focus message field on open, keep pill/send inside the input field, and make the composer pill edge-to-edge with square top corners.</li>
<li>macOS: bundle Control UI assets into the app relay so the packaged app can serve them (thanks @mbelinky).</li>
<li>Talk Mode: wait for chat history to surface the assistant reply before starting TTS (macOS/iOS/Android).</li>
<li>iOS Talk Mode: fix chat completion wait to time out even if no events arrive (prevents “Thinking…” hangs).</li>
<li>iOS Talk Mode: keep recognition running during playback to support interrupt-on-speech.</li>
<li>iOS Talk Mode: preserve directive voice/model overrides across config reloads and add ElevenLabs request timeouts.</li>
<li>iOS/Android Talk Mode: explicitly <code>chat.subscribe</code> when Talk Mode is active, so completion events arrive even if the Chat UI isn’t open.</li>
<li>Chat UI: refresh history when another client finishes a run in the same session, so Talk Mode + Voice Wake transcripts appear consistently.</li>
<li>Gateway: <code>voice.transcript</code> now also maps agent bus output to <code>chat</code> events, ensuring chat UIs refresh for voice-triggered runs.</li>
<li>Gateway: auto-migrate legacy config on startup (non-Nix); Nix mode hard-fails with a clear error when legacy keys are present.</li>
<li>iOS/Android: show a centered Talk Mode orb overlay while Talk Mode is enabled.</li>
<li>Gateway config: inject <code>talk.apiKey</code> from <code>ELEVENLABS_API_KEY</code>/shell profile so nodes can fetch it on demand.</li>
<li>Canvas A2UI: tag requests with <code>platform=android|ios|macos</code> and boost Android canvas background contrast.</li>
<li>iOS/Android nodes: enable scrolling for loaded web pages in the Canvas WebView (default scaffold stays touch-first).</li>
<li>macOS menu: device list now uses <code>node.list</code> (devices only; no agent/tool presence entries).</li>
<li>macOS menu: device list now shows connected nodes only.</li>
<li>macOS menu: device rows now pack platform/version on the first line, and command lists wrap in submenus.</li>
<li>macOS menu: split device platform/version across first and second rows for better fit.</li>
<li>macOS Canvas: show remote control status in the debug overlay and log A2UI auto-nav decisions.</li>
<li>Canvas A2UI: polish the debug status HUD styling.</li>
<li>iOS node: fix ReplayKit screen recording crash caused by queue isolation assertions during capture.</li>
<li>iOS Talk Mode: avoid audio tap queue assertions when starting recognition.</li>
<li>macOS: use $HOME/Library/pnpm for SSH PATH exports (thanks @mbelinky).</li>
<li>macOS remote: harden SSH tunnel recovery/logging, honor <code>gateway.remote.url</code> port when forwarding, clarify gateway disconnect status, and add Debug menu tunnel reset.</li>
<li>iOS/Android nodes: bridge auto-connect refreshes stale tokens and settings now show richer bridge/device details.</li>
<li>macOS: bundle device model resources to prevent Instances crashes (thanks @mbelinky).</li>
<li>iOS/Android nodes: status pill now surfaces camera activity instead of overlay toasts.</li>
<li>iOS/Android/macOS nodes: camera snaps recompress to keep base64 payloads under 5 MB.</li>
<li>iOS/Android nodes: status pill now surfaces pairing, screen recording, voice wake, and foreground-required states.</li>
<li>iOS/Android nodes: avoid duplicating “Gateway reconnecting…” when the bridge is already connecting.</li>
<li>iOS/Android nodes: Talk Mode now lives on a side bubble (with an iOS toggle to hide it), and Android settings no longer show the Talk Mode switch.</li>
<li>macOS menu: top status line now shows pending node pairing approvals (incl. repairs).</li>
<li>CLI: avoid spurious gateway close errors after successful request/response cycles.</li>
<li>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>Tests: add Swift Testing coverage for camera errors and Kotest coverage for Android bridge endpoints.</li>
<li>Package contents: include Discord/hooks build outputs in the npm tarball to avoid missing module errors.</li>
</ul>
<p><a href="https://github.com/steipete/clawdis/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/steipete/clawdis/releases/download/v2.0.0-beta4/Clawdis-2.0.0-beta4.zip" length="70407894" type="application/octet-stream" sparkle:edSignature="HmuiC7TnUn80ZApnKfb6w+JGSrjc3uUOndMrtbTp42bkBSVifbttNVazqvJueGBo4MgoJV8CP+zQNzVmtVihAQ=="/>
</item>
</channel>
</rss>
</rss>
+2 -2
View File
@@ -6,7 +6,7 @@ plugins {
}
android {
namespace = "com.clawdis.android"
namespace = "com.steipete.clawdis.node"
compileSdk = 36
sourceSets {
@@ -16,7 +16,7 @@ android {
}
defaultConfig {
applicationId = "com.clawdis.android"
applicationId = "com.steipete.clawdis.node"
minSdk = 31
targetSdk = 36
versionCode = 1
@@ -1,197 +0,0 @@
{
"version": 1,
"fallback": {
"emoji": "🧩",
"detailKeys": [
"command",
"path",
"url",
"targetUrl",
"targetId",
"ref",
"element",
"node",
"nodeId",
"jobId",
"requestId",
"to",
"channelId",
"guildId",
"userId",
"name",
"query",
"pattern",
"messageId"
]
},
"tools": {
"bash": {
"emoji": "🛠️",
"title": "Bash",
"detailKeys": ["command"]
},
"process": {
"emoji": "🧰",
"title": "Process",
"detailKeys": ["sessionId"]
},
"read": {
"emoji": "📖",
"title": "Read",
"detailKeys": ["path"]
},
"write": {
"emoji": "✍️",
"title": "Write",
"detailKeys": ["path"]
},
"edit": {
"emoji": "📝",
"title": "Edit",
"detailKeys": ["path"]
},
"attach": {
"emoji": "📎",
"title": "Attach",
"detailKeys": ["path", "url", "fileName"]
},
"browser": {
"emoji": "🌐",
"title": "Browser",
"actions": {
"status": { "label": "status" },
"start": { "label": "start" },
"stop": { "label": "stop" },
"tabs": { "label": "tabs" },
"open": { "label": "open", "detailKeys": ["targetUrl"] },
"focus": { "label": "focus", "detailKeys": ["targetId"] },
"close": { "label": "close", "detailKeys": ["targetId"] },
"snapshot": {
"label": "snapshot",
"detailKeys": ["targetUrl", "targetId", "ref", "element", "format"]
},
"screenshot": {
"label": "screenshot",
"detailKeys": ["targetUrl", "targetId", "ref", "element"]
},
"navigate": {
"label": "navigate",
"detailKeys": ["targetUrl", "targetId"]
},
"console": { "label": "console", "detailKeys": ["level", "targetId"] },
"pdf": { "label": "pdf", "detailKeys": ["targetId"] },
"upload": {
"label": "upload",
"detailKeys": ["paths", "ref", "inputRef", "element", "targetId"]
},
"dialog": {
"label": "dialog",
"detailKeys": ["accept", "promptText", "targetId"]
},
"act": {
"label": "act",
"detailKeys": ["request.kind", "request.ref", "request.selector", "request.text", "request.value"]
}
}
},
"canvas": {
"emoji": "🖼️",
"title": "Canvas",
"actions": {
"present": { "label": "present", "detailKeys": ["target", "node", "nodeId"] },
"hide": { "label": "hide", "detailKeys": ["node", "nodeId"] },
"navigate": { "label": "navigate", "detailKeys": ["url", "node", "nodeId"] },
"eval": { "label": "eval", "detailKeys": ["javaScript", "node", "nodeId"] },
"snapshot": { "label": "snapshot", "detailKeys": ["format", "node", "nodeId"] },
"a2ui_push": { "label": "A2UI push", "detailKeys": ["jsonlPath", "node", "nodeId"] },
"a2ui_reset": { "label": "A2UI reset", "detailKeys": ["node", "nodeId"] }
}
},
"nodes": {
"emoji": "📱",
"title": "Nodes",
"actions": {
"status": { "label": "status" },
"describe": { "label": "describe", "detailKeys": ["node", "nodeId"] },
"pending": { "label": "pending" },
"approve": { "label": "approve", "detailKeys": ["requestId"] },
"reject": { "label": "reject", "detailKeys": ["requestId"] },
"notify": { "label": "notify", "detailKeys": ["node", "nodeId", "title", "body"] },
"camera_snap": { "label": "camera snap", "detailKeys": ["node", "nodeId", "facing", "deviceId"] },
"camera_list": { "label": "camera list", "detailKeys": ["node", "nodeId"] },
"camera_clip": { "label": "camera clip", "detailKeys": ["node", "nodeId", "facing", "duration", "durationMs"] },
"screen_record": {
"label": "screen record",
"detailKeys": ["node", "nodeId", "duration", "durationMs", "fps", "screenIndex"]
}
}
},
"cron": {
"emoji": "⏰",
"title": "Cron",
"actions": {
"status": { "label": "status" },
"list": { "label": "list" },
"add": {
"label": "add",
"detailKeys": ["job.name", "job.id", "job.schedule", "job.cron"]
},
"update": { "label": "update", "detailKeys": ["jobId"] },
"remove": { "label": "remove", "detailKeys": ["jobId"] },
"run": { "label": "run", "detailKeys": ["jobId"] },
"runs": { "label": "runs", "detailKeys": ["jobId"] },
"wake": { "label": "wake", "detailKeys": ["text", "mode"] }
}
},
"gateway": {
"emoji": "🔌",
"title": "Gateway",
"actions": {
"restart": { "label": "restart", "detailKeys": ["reason", "delayMs"] }
}
},
"whatsapp_login": {
"emoji": "🟢",
"title": "WhatsApp Login",
"actions": {
"start": { "label": "start" },
"wait": { "label": "wait" }
}
},
"discord": {
"emoji": "💬",
"title": "Discord",
"actions": {
"react": { "label": "react", "detailKeys": ["channelId", "messageId", "emoji"] },
"reactions": { "label": "reactions", "detailKeys": ["channelId", "messageId"] },
"sticker": { "label": "sticker", "detailKeys": ["to", "stickerIds"] },
"poll": { "label": "poll", "detailKeys": ["question", "to"] },
"permissions": { "label": "permissions", "detailKeys": ["channelId"] },
"readMessages": { "label": "read messages", "detailKeys": ["channelId", "limit"] },
"sendMessage": { "label": "send", "detailKeys": ["to", "content"] },
"editMessage": { "label": "edit", "detailKeys": ["channelId", "messageId"] },
"deleteMessage": { "label": "delete", "detailKeys": ["channelId", "messageId"] },
"threadCreate": { "label": "thread create", "detailKeys": ["channelId", "name"] },
"threadList": { "label": "thread list", "detailKeys": ["guildId", "channelId"] },
"threadReply": { "label": "thread reply", "detailKeys": ["channelId", "content"] },
"pinMessage": { "label": "pin", "detailKeys": ["channelId", "messageId"] },
"unpinMessage": { "label": "unpin", "detailKeys": ["channelId", "messageId"] },
"listPins": { "label": "list pins", "detailKeys": ["channelId"] },
"searchMessages": { "label": "search", "detailKeys": ["guildId", "content"] },
"memberInfo": { "label": "member", "detailKeys": ["guildId", "userId"] },
"roleInfo": { "label": "roles", "detailKeys": ["guildId"] },
"emojiList": { "label": "emoji list", "detailKeys": ["guildId"] },
"roleAdd": { "label": "role add", "detailKeys": ["guildId", "userId", "roleId"] },
"roleRemove": { "label": "role remove", "detailKeys": ["guildId", "userId", "roleId"] },
"channelInfo": { "label": "channel", "detailKeys": ["channelId"] },
"channelList": { "label": "channels", "detailKeys": ["guildId"] },
"voiceStatus": { "label": "voice", "detailKeys": ["guildId", "userId"] },
"eventList": { "label": "events", "detailKeys": ["guildId"] },
"eventCreate": { "label": "event create", "detailKeys": ["guildId", "name"] },
"timeout": { "label": "timeout", "detailKeys": ["guildId", "userId"] },
"kick": { "label": "kick", "detailKeys": ["guildId", "userId"] },
"ban": { "label": "ban", "detailKeys": ["guildId", "userId"] }
}
}
}
}
@@ -1,222 +0,0 @@
package com.clawdis.android.tools
import android.content.Context
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.contentOrNull
@Serializable
private data class ToolDisplayActionSpec(
val label: String? = null,
val detailKeys: List<String>? = null,
)
@Serializable
private data class ToolDisplaySpec(
val emoji: String? = null,
val title: String? = null,
val label: String? = null,
val detailKeys: List<String>? = null,
val actions: Map<String, ToolDisplayActionSpec>? = null,
)
@Serializable
private data class ToolDisplayConfig(
val version: Int? = null,
val fallback: ToolDisplaySpec? = null,
val tools: Map<String, ToolDisplaySpec>? = null,
)
data class ToolDisplaySummary(
val name: String,
val emoji: String,
val title: String,
val label: String,
val verb: String?,
val detail: String?,
) {
val detailLine: String?
get() {
val parts = mutableListOf<String>()
if (!verb.isNullOrBlank()) parts.add(verb)
if (!detail.isNullOrBlank()) parts.add(detail)
return if (parts.isEmpty()) null else parts.joinToString(" · ")
}
val summaryLine: String
get() = if (detailLine != null) "${emoji} ${label}: ${detailLine}" else "${emoji} ${label}"
}
object ToolDisplayRegistry {
private const val CONFIG_ASSET = "tool-display.json"
private val json = Json { ignoreUnknownKeys = true }
@Volatile private var cachedConfig: ToolDisplayConfig? = null
fun resolve(
context: Context,
name: String?,
args: JsonObject?,
meta: String? = null,
): ToolDisplaySummary {
val trimmedName = name?.trim().orEmpty().ifEmpty { "tool" }
val key = trimmedName.lowercase()
val config = loadConfig(context)
val spec = config.tools?.get(key)
val fallback = config.fallback
val emoji = spec?.emoji ?: fallback?.emoji ?: "🧩"
val title = spec?.title ?: titleFromName(trimmedName)
val label = spec?.label ?: trimmedName
val actionRaw = args?.get("action")?.asStringOrNull()?.trim()
val action = actionRaw?.takeIf { it.isNotEmpty() }
val actionSpec = action?.let { spec?.actions?.get(it) }
val verb = normalizeVerb(actionSpec?.label ?: action)
var detail: String? = null
if (key == "read") {
detail = readDetail(args)
} else if (key == "write" || key == "edit" || key == "attach") {
detail = pathDetail(args)
}
val detailKeys = actionSpec?.detailKeys ?: spec?.detailKeys ?: fallback?.detailKeys ?: emptyList()
if (detail == null) {
detail = firstValue(args, detailKeys)
}
if (detail == null) {
detail = meta
}
if (detail != null) {
detail = shortenHomeInString(detail)
}
return ToolDisplaySummary(
name = trimmedName,
emoji = emoji,
title = title,
label = label,
verb = verb,
detail = detail,
)
}
private fun loadConfig(context: Context): ToolDisplayConfig {
val existing = cachedConfig
if (existing != null) return existing
return try {
val jsonString = context.assets.open(CONFIG_ASSET).bufferedReader().use { it.readText() }
val decoded = json.decodeFromString(ToolDisplayConfig.serializer(), jsonString)
cachedConfig = decoded
decoded
} catch (_: Throwable) {
val fallback = ToolDisplayConfig()
cachedConfig = fallback
fallback
}
}
private fun titleFromName(name: String): String {
val cleaned = name.replace("_", " ").trim()
if (cleaned.isEmpty()) return "Tool"
return cleaned
.split(Regex("\\s+"))
.joinToString(" ") { part ->
val upper = part.uppercase()
if (part.length <= 2 && part == upper) part
else upper.firstOrNull()?.toString().orEmpty() + part.lowercase().drop(1)
}
}
private fun normalizeVerb(value: String?): String? {
val trimmed = value?.trim().orEmpty()
if (trimmed.isEmpty()) return null
return trimmed.replace("_", " ")
}
private fun readDetail(args: JsonObject?): String? {
val path = args?.get("path")?.asStringOrNull() ?: return null
val offset = args["offset"].asNumberOrNull()
val limit = args["limit"].asNumberOrNull()
return if (offset != null && limit != null) {
val end = offset + limit
"${path}:${offset.toInt()}-${end.toInt()}"
} else {
path
}
}
private fun pathDetail(args: JsonObject?): String? {
return args?.get("path")?.asStringOrNull()
}
private fun firstValue(args: JsonObject?, keys: List<String>): String? {
for (key in keys) {
val value = valueForPath(args, key)
val rendered = renderValue(value)
if (!rendered.isNullOrBlank()) return rendered
}
return null
}
private fun valueForPath(args: JsonObject?, path: String): JsonElement? {
var current: JsonElement? = args
for (segment in path.split(".")) {
if (segment.isBlank()) return null
val obj = current as? JsonObject ?: return null
current = obj[segment]
}
return current
}
private fun renderValue(value: JsonElement?): String? {
if (value == null) return null
if (value is JsonPrimitive) {
if (value.isString) {
val trimmed = value.contentOrNull?.trim().orEmpty()
if (trimmed.isEmpty()) return null
val firstLine = trimmed.lineSequence().firstOrNull()?.trim().orEmpty()
if (firstLine.isEmpty()) return null
return if (firstLine.length > 160) "${firstLine.take(157)}…" else firstLine
}
val raw = value.contentOrNull?.trim().orEmpty()
raw.toBooleanStrictOrNull()?.let { return it.toString() }
raw.toLongOrNull()?.let { return it.toString() }
raw.toDoubleOrNull()?.let { return it.toString() }
}
if (value is JsonArray) {
val items = value.mapNotNull { renderValue(it) }
if (items.isEmpty()) return null
val preview = items.take(3).joinToString(", ")
return if (items.size > 3) "${preview}…" else preview
}
return null
}
private fun shortenHomeInString(value: String): String {
val home = System.getProperty("user.home")?.takeIf { it.isNotBlank() }
?: System.getenv("HOME")?.takeIf { it.isNotBlank() }
if (home.isNullOrEmpty()) return value
return value.replace(home, "~")
.replace(Regex("/Users/[^/]+"), "~")
.replace(Regex("/home/[^/]+"), "~")
}
private fun JsonElement?.asStringOrNull(): String? {
val primitive = this as? JsonPrimitive ?: return null
return if (primitive.isString) primitive.contentOrNull else primitive.toString()
}
private fun JsonElement?.asNumberOrNull(): Double? {
val primitive = this as? JsonPrimitive ?: return null
val raw = primitive.contentOrNull ?: return null
return raw.toDoubleOrNull()
}
}
@@ -1,4 +1,4 @@
package com.clawdis.android
package com.steipete.clawdis.node
enum class CameraHudKind {
Photo,
@@ -1,4 +1,4 @@
package com.clawdis.android
package com.steipete.clawdis.node
import android.content.Context
import android.os.Build
@@ -1,4 +1,4 @@
package com.clawdis.android
package com.steipete.clawdis.node
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.steipete.clawdis.node.ui.RootScreen
import com.steipete.clawdis.node.ui.ClawdisTheme
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
@@ -1,12 +1,12 @@
package com.clawdis.android
package com.steipete.clawdis.node
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.steipete.clawdis.node.bridge.BridgeEndpoint
import com.steipete.clawdis.node.chat.OutgoingAttachment
import com.steipete.clawdis.node.node.CameraCaptureManager
import com.steipete.clawdis.node.node.CanvasController
import com.steipete.clawdis.node.node.ScreenRecordManager
import kotlinx.coroutines.flow.StateFlow
class MainViewModel(app: Application) : AndroidViewModel(app) {
@@ -1,4 +1,4 @@
package com.clawdis.android
package com.steipete.clawdis.node
import android.app.Application
@@ -1,4 +1,4 @@
package com.clawdis.android
package com.steipete.clawdis.node
import android.app.Notification
import android.app.NotificationChannel
@@ -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.steipete.clawdis.node.action.STOP"
fun start(context: Context) {
val intent = Intent(context, NodeForegroundService::class.java)
@@ -1,4 +1,4 @@
package com.clawdis.android
package com.steipete.clawdis.node
import android.Manifest
import android.content.Context
@@ -6,27 +6,27 @@ import android.content.pm.PackageManager
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.BuildConfig
import com.clawdis.android.node.CanvasController
import com.clawdis.android.node.ScreenRecordManager
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.voice.TalkModeManager
import com.clawdis.android.voice.VoiceWakeManager
import com.steipete.clawdis.node.chat.ChatController
import com.steipete.clawdis.node.chat.ChatMessage
import com.steipete.clawdis.node.chat.ChatPendingToolCall
import com.steipete.clawdis.node.chat.ChatSessionEntry
import com.steipete.clawdis.node.chat.OutgoingAttachment
import com.steipete.clawdis.node.bridge.BridgeDiscovery
import com.steipete.clawdis.node.bridge.BridgeEndpoint
import com.steipete.clawdis.node.bridge.BridgePairingClient
import com.steipete.clawdis.node.bridge.BridgeSession
import com.steipete.clawdis.node.node.CameraCaptureManager
import com.steipete.clawdis.node.BuildConfig
import com.steipete.clawdis.node.node.CanvasController
import com.steipete.clawdis.node.node.ScreenRecordManager
import com.steipete.clawdis.node.protocol.ClawdisCapability
import com.steipete.clawdis.node.protocol.ClawdisCameraCommand
import com.steipete.clawdis.node.protocol.ClawdisCanvasA2UIAction
import com.steipete.clawdis.node.protocol.ClawdisCanvasA2UICommand
import com.steipete.clawdis.node.protocol.ClawdisCanvasCommand
import com.steipete.clawdis.node.protocol.ClawdisScreenCommand
import com.steipete.clawdis.node.voice.TalkModeManager
import com.steipete.clawdis.node.voice.VoiceWakeManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -1,4 +1,4 @@
package com.clawdis.android
package com.steipete.clawdis.node
import android.content.pm.PackageManager
import android.content.Intent
@@ -1,4 +1,4 @@
package com.clawdis.android
package com.steipete.clawdis.node
import android.app.Activity
import android.content.Context
@@ -1,6 +1,6 @@
@file:Suppress("DEPRECATION")
package com.clawdis.android
package com.steipete.clawdis.node
import android.content.Context
import androidx.core.content.edit
@@ -1,4 +1,4 @@
package com.clawdis.android
package com.steipete.clawdis.node
enum class VoiceWakeMode(val rawValue: String) {
Off("off"),
@@ -1,4 +1,4 @@
package com.clawdis.android
package com.steipete.clawdis.node
object WakeWords {
const val maxWords: Int = 32
@@ -1,4 +1,4 @@
package com.clawdis.android.bridge
package com.steipete.clawdis.node.bridge
object BonjourEscapes {
fun decode(input: String): String {
@@ -1,4 +1,4 @@
package com.clawdis.android.bridge
package com.steipete.clawdis.node.bridge
import android.content.Context
import android.net.ConnectivityManager
@@ -1,4 +1,4 @@
package com.clawdis.android.bridge
package com.steipete.clawdis.node.bridge
data class BridgeEndpoint(
val stableId: String,
@@ -1,4 +1,4 @@
package com.clawdis.android.bridge
package com.steipete.clawdis.node.bridge
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -1,4 +1,4 @@
package com.clawdis.android.bridge
package com.steipete.clawdis.node.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.steipete.clawdis.node.BuildConfig
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
@@ -1,6 +1,6 @@
package com.clawdis.android.chat
package com.steipete.clawdis.node.chat
import com.clawdis.android.bridge.BridgeSession
import com.steipete.clawdis.node.bridge.BridgeSession
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.CoroutineScope
@@ -361,12 +361,10 @@ class ChatController(
val ts = payload["ts"].asLongOrNull() ?: System.currentTimeMillis()
if (phase == "start") {
val args = data?.get("args").asObjectOrNull()
pendingToolCallsById[toolCallId] =
ChatPendingToolCall(
toolCallId = toolCallId,
name = name,
args = args,
startedAtMs = ts,
isError = null,
)
@@ -1,4 +1,4 @@
package com.clawdis.android.chat
package com.steipete.clawdis.node.chat
data class ChatMessage(
val id: String,
@@ -18,7 +18,6 @@ data class ChatMessageContent(
data class ChatPendingToolCall(
val toolCallId: String,
val name: String,
val args: kotlinx.serialization.json.JsonObject? = null,
val startedAtMs: Long,
val isError: Boolean? = null,
)
@@ -1,4 +1,4 @@
package com.clawdis.android.node
package com.steipete.clawdis.node.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.steipete.clawdis.node.PermissionRequester
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeout
@@ -1,4 +1,4 @@
package com.clawdis.android.node
package com.steipete.clawdis.node.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.steipete.clawdis.node.BuildConfig
import kotlin.coroutines.resume
class CanvasController {
@@ -1,4 +1,4 @@
package com.clawdis.android.node
package com.steipete.clawdis.node.node
import kotlin.math.max
import kotlin.math.min
@@ -1,11 +1,11 @@
package com.clawdis.android.node
package com.steipete.clawdis.node.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.steipete.clawdis.node.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.steipete.clawdis.node.PermissionRequester? = null
fun attachScreenCaptureRequester(requester: ScreenCaptureRequester) {
screenCaptureRequester = requester
}
fun attachPermissionRequester(requester: com.clawdis.android.PermissionRequester) {
fun attachPermissionRequester(requester: com.steipete.clawdis.node.PermissionRequester) {
permissionRequester = requester
}
@@ -1,4 +1,4 @@
package com.clawdis.android.protocol
package com.steipete.clawdis.node.protocol
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
@@ -1,4 +1,4 @@
package com.clawdis.android.protocol
package com.steipete.clawdis.node.protocol
enum class ClawdisCapability(val rawValue: String) {
Canvas("canvas"),
@@ -1,4 +1,4 @@
package com.clawdis.android.ui
package com.steipete.clawdis.node.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
@@ -1,8 +1,8 @@
package com.clawdis.android.ui
package com.steipete.clawdis.node.ui
import androidx.compose.runtime.Composable
import com.clawdis.android.MainViewModel
import com.clawdis.android.ui.chat.ChatSheetContent
import com.steipete.clawdis.node.MainViewModel
import com.steipete.clawdis.node.ui.chat.ChatSheetContent
@Composable
fun ChatSheet(viewModel: MainViewModel) {
@@ -1,4 +1,4 @@
package com.clawdis.android.ui
package com.steipete.clawdis.node.ui
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
@@ -1,4 +1,4 @@
package com.clawdis.android.ui
package com.steipete.clawdis.node.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.steipete.clawdis.node.CameraHudKind
import com.steipete.clawdis.node.MainViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -1,4 +1,4 @@
package com.clawdis.android.ui
package com.steipete.clawdis.node.ui
import android.Manifest
import android.content.pm.PackageManager
@@ -47,10 +47,10 @@ 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.MainViewModel
import com.clawdis.android.NodeForegroundService
import com.clawdis.android.VoiceWakeMode
import com.steipete.clawdis.node.BuildConfig
import com.steipete.clawdis.node.MainViewModel
import com.steipete.clawdis.node.NodeForegroundService
import com.steipete.clawdis.node.VoiceWakeMode
@Composable
fun SettingsSheet(viewModel: MainViewModel) {
@@ -381,7 +381,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(
onClick = {
val parsed = com.clawdis.android.WakeWords.parseCommaSeparated(wakeWordsText)
val parsed = com.steipete.clawdis.node.WakeWords.parseCommaSeparated(wakeWordsText)
viewModel.setWakeWords(parsed)
},
enabled = isConnected,
@@ -1,4 +1,4 @@
package com.clawdis.android.ui
package com.steipete.clawdis.node.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
@@ -1,4 +1,4 @@
package com.clawdis.android.ui
package com.steipete.clawdis.node.ui
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
@@ -1,4 +1,4 @@
package com.clawdis.android.ui.chat
package com.steipete.clawdis.node.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.steipete.clawdis.node.chat.ChatSessionEntry
@Composable
fun ChatComposer(
@@ -148,7 +148,7 @@ fun ChatComposer(
)
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
ConnectionPill(sessionLabel = currentSessionLabel, healthOk = healthOk)
ConnectionPill(sessionKey = sessionKey, healthOk = healthOk)
Spacer(modifier = Modifier.weight(1f))
if (pendingRunCount > 0) {
@@ -186,7 +186,7 @@ fun ChatComposer(
}
@Composable
private fun ConnectionPill(sessionLabel: String, healthOk: Boolean) {
private fun ConnectionPill(sessionKey: String, healthOk: Boolean) {
Surface(
shape = RoundedCornerShape(999.dp),
color = MaterialTheme.colorScheme.surfaceContainerHighest,
@@ -201,7 +201,7 @@ private fun ConnectionPill(sessionLabel: String, healthOk: Boolean) {
shape = androidx.compose.foundation.shape.CircleShape,
color = if (healthOk) Color(0xFF2ECC71) else Color(0xFFF39C12),
) {}
Text(sessionLabel, style = MaterialTheme.typography.labelSmall)
Text(sessionKey, style = MaterialTheme.typography.labelSmall)
Text(
if (healthOk) "Connected" else "Connecting…",
style = MaterialTheme.typography.labelSmall,
@@ -1,4 +1,4 @@
package com.clawdis.android.ui.chat
package com.steipete.clawdis.node.ui.chat
import android.graphics.BitmapFactory
import android.util.Base64
@@ -1,4 +1,4 @@
package com.clawdis.android.ui.chat
package com.steipete.clawdis.node.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.steipete.clawdis.node.chat.ChatMessage
import com.steipete.clawdis.node.chat.ChatPendingToolCall
@Composable
fun ChatMessageListCard(
@@ -1,4 +1,4 @@
package com.clawdis.android.ui.chat
package com.steipete.clawdis.node.ui.chat
import android.graphics.BitmapFactory
import android.util.Base64
@@ -31,13 +31,11 @@ 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.steipete.clawdis.node.chat.ChatMessage
import com.steipete.clawdis.node.chat.ChatMessageContent
import com.steipete.clawdis.node.chat.ChatPendingToolCall
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import androidx.compose.ui.platform.LocalContext
@Composable
fun ChatMessageBubble(message: ChatMessage) {
@@ -106,42 +104,18 @@ fun ChatTypingIndicatorBubble() {
@Composable
fun ChatPendingToolsBubble(toolCalls: List<ChatPendingToolCall>) {
val context = LocalContext.current
val displays =
remember(toolCalls, context) {
toolCalls.map { ToolDisplayRegistry.resolve(context, it.name, it.args) }
}
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Start) {
Surface(
shape = RoundedCornerShape(16.dp),
color = MaterialTheme.colorScheme.surfaceContainer,
) {
Column(modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
Text("Running tools…", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurface)
for (display in displays.take(6)) {
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(
"${display.emoji} ${display.label}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontFamily = FontFamily.Monospace,
)
display.detailLine?.let { detail ->
Text(
detail,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontFamily = FontFamily.Monospace,
)
}
}
Text("Tools", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurface)
for (t in toolCalls.take(6)) {
Text("· ${t.name}", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
if (toolCalls.size > 6) {
Text(
"… +${toolCalls.size - 6} more",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Text("… +${toolCalls.size - 6} more", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
}
@@ -1,4 +1,4 @@
package com.clawdis.android.ui.chat
package com.steipete.clawdis.node.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.steipete.clawdis.node.chat.ChatSessionEntry
@Composable
fun ChatSessionsDialog(
@@ -1,4 +1,4 @@
package com.clawdis.android.ui.chat
package com.steipete.clawdis.node.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.steipete.clawdis.node.MainViewModel
import com.steipete.clawdis.node.chat.OutgoingAttachment
import java.io.ByteArrayOutputStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -1,6 +1,6 @@
package com.clawdis.android.ui.chat
package com.steipete.clawdis.node.ui.chat
import com.clawdis.android.chat.ChatSessionEntry
import com.steipete.clawdis.node.chat.ChatSessionEntry
private const val MAIN_SESSION_KEY = "main"
private const val RECENT_WINDOW_MS = 24 * 60 * 60 * 1000L
@@ -1,4 +1,4 @@
package com.clawdis.android.voice
package com.steipete.clawdis.node.voice
import android.media.MediaDataSource
import kotlin.math.min
@@ -1,4 +1,4 @@
package com.clawdis.android.voice
package com.steipete.clawdis.node.voice
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
@@ -1,4 +1,4 @@
package com.clawdis.android.voice
package com.steipete.clawdis.node.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.steipete.clawdis.node.bridge.BridgeSession
import java.net.HttpURLConnection
import java.net.URL
import java.util.UUID
@@ -1,4 +1,4 @@
package com.clawdis.android.voice
package com.steipete.clawdis.node.voice
object VoiceWakeCommandExtractor {
fun extractCommand(text: String, triggerWords: List<String>): String? {
@@ -1,4 +1,4 @@
package com.clawdis.android.voice
package com.steipete.clawdis.node.voice
import android.content.Context
import android.content.Intent
@@ -1,4 +1,4 @@
package com.clawdis.android
package com.steipete.clawdis.node
import org.junit.Assert.assertEquals
import org.junit.Test
@@ -1,4 +1,4 @@
package com.clawdis.android.bridge
package com.steipete.clawdis.node.bridge
import org.junit.Assert.assertEquals
import org.junit.Test
@@ -1,4 +1,4 @@
package com.clawdis.android.bridge
package com.steipete.clawdis.node.bridge
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
@@ -1,4 +1,4 @@
package com.clawdis.android.bridge
package com.steipete.clawdis.node.bridge
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
@@ -1,4 +1,4 @@
package com.clawdis.android.bridge
package com.steipete.clawdis.node.bridge
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
@@ -1,4 +1,4 @@
package com.clawdis.android.node
package com.steipete.clawdis.node.node
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
@@ -1,4 +1,4 @@
package com.clawdis.android.node
package com.steipete.clawdis.node.node
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
@@ -1,4 +1,4 @@
package com.clawdis.android.protocol
package com.steipete.clawdis.node.protocol
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
@@ -1,4 +1,4 @@
package com.clawdis.android.protocol
package com.steipete.clawdis.node.protocol
import org.junit.Assert.assertEquals
import org.junit.Test
@@ -1,6 +1,6 @@
package com.clawdis.android.ui.chat
package com.steipete.clawdis.node.ui.chat
import com.clawdis.android.chat.ChatSessionEntry
import com.steipete.clawdis.node.chat.ChatSessionEntry
import org.junit.Assert.assertEquals
import org.junit.Test
@@ -1,4 +1,4 @@
package com.clawdis.android.voice
package com.steipete.clawdis.node.voice
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
@@ -1,4 +1,4 @@
package com.clawdis.android.voice
package com.steipete.clawdis.node.voice
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
+1 -1
View File
@@ -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.steipete.clawdis.ios.bridge-client")
defer { connection.cancel() }
try await self.withTimeout(seconds: 8, purpose: "connect") {
try await self.startAndWaitForReady(connection, queue: queue)
@@ -99,7 +99,7 @@ final class BridgeConnectionController {
guard !instanceId.isEmpty else { return }
let token = KeychainStore.loadString(
service: "com.clawdis.bridge",
service: "com.steipete.clawdis.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.steipete.clawdis.bridge",
account: self.keychainAccount(instanceId: instanceId))
}
appModel.connectToBridge(endpoint: endpoint, hello: self.makeHello(token: resolvedToken))
@@ -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.steipete.clawdis.ios.bridge-discovery.\(domain)"))
}
}
+1 -1
View File
@@ -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.steipete.clawdis.ios.bridge-session")
self.connection = connection
self.queue = queue
@@ -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.steipete.clawdis.bridge"
private static let nodeService = "com.steipete.clawdis.node"
private static let instanceIdDefaultsKey = "node.instanceId"
private static let preferredBridgeStableIDDefaultsKey = "bridge.preferredStableID"
@@ -68,7 +68,7 @@ final class ScreenRecordService: @unchecked Sendable {
try? FileManager.default.removeItem(at: outURL)
let state = CaptureState()
let recordQueue = DispatchQueue(label: "com.clawdis.screenrecord")
let recordQueue = DispatchQueue(label: "com.steipete.clawdis.screenrecord")
try await withCheckedThrowingContinuation { (cont: CheckedContinuation<Void, Error>) in
let handler: @Sendable (CMSampleBuffer, RPSampleBufferType, Error?) -> Void = { sample, type, error in
+4 -4
View File
@@ -353,7 +353,7 @@ struct SettingsTab: View {
do {
let statusStore = self.connectStatus
let existing = KeychainStore.loadString(
service: "com.clawdis.bridge",
service: "com.steipete.clawdis.bridge",
account: self.keychainAccount())
let existingToken = (existing?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false) ?
existing :
@@ -381,7 +381,7 @@ struct SettingsTab: View {
if !token.isEmpty, token != existingToken {
_ = KeychainStore.saveString(
token,
service: "com.clawdis.bridge",
service: "com.steipete.clawdis.bridge",
account: self.keychainAccount())
}
@@ -427,7 +427,7 @@ struct SettingsTab: View {
do {
let statusStore = self.connectStatus
let existing = KeychainStore.loadString(
service: "com.clawdis.bridge",
service: "com.steipete.clawdis.bridge",
account: self.keychainAccount())
let existingToken = (existing?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false) ?
existing :
@@ -455,7 +455,7 @@ struct SettingsTab: View {
if !token.isEmpty, token != existingToken {
_ = KeychainStore.saveString(
token,
service: "com.clawdis.bridge",
service: "com.steipete.clawdis.bridge",
account: self.keychainAccount())
}
+1 -1
View File
@@ -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.steipete.clawdis", category: "TalkMode")
func attachBridge(_ bridge: BridgeSession) {
self.bridge = bridge
+1 -1
View File
@@ -6,7 +6,7 @@ import Testing
@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.steipete.clawdis.tests.bridge-client-server")
private let listener: NWListener
private var connection: NWConnection?
private var buffer = Data()
@@ -10,8 +10,8 @@ private struct KeychainEntry: Hashable {
let account: String
}
private let bridgeService = "com.clawdis.bridge"
private let nodeService = "com.clawdis.node"
private let bridgeService = "com.steipete.clawdis.bridge"
private let nodeService = "com.steipete.clawdis.node"
private let instanceIdEntry = KeychainEntry(service: nodeService, account: "instanceId")
private let preferredBridgeEntry = KeychainEntry(service: bridgeService, account: "preferredStableID")
private let lastBridgeEntry = KeychainEntry(service: bridgeService, account: "lastDiscoveredStableID")
@@ -7,8 +7,8 @@ private struct KeychainEntry: Hashable {
let account: String
}
private let bridgeService = "com.clawdis.bridge"
private let nodeService = "com.clawdis.node"
private let bridgeService = "com.steipete.clawdis.bridge"
private let nodeService = "com.steipete.clawdis.node"
private let instanceIdEntry = KeychainEntry(service: nodeService, account: "instanceId")
private let preferredBridgeEntry = KeychainEntry(service: bridgeService, account: "preferredStableID")
private let lastBridgeEntry = KeychainEntry(service: bridgeService, account: "lastDiscoveredStableID")
+1 -1
View File
@@ -4,7 +4,7 @@ import Testing
@Suite struct KeychainStoreTests {
@Test func saveLoadUpdateDeleteRoundTrip() {
let service = "com.clawdis.tests.\(UUID().uuidString)"
let service = "com.steipete.clawdis.tests.\(UUID().uuidString)"
let account = "value"
#expect(KeychainStore.delete(service: service, account: account))
+1 -1
View File
@@ -1,4 +1,4 @@
app_identifier("com.clawdis.ios")
app_identifier("com.steipete.clawdis.ios")
# Auth is expected via App Store Connect API key.
# Provide either:
+4 -4
View File
@@ -1,6 +1,6 @@
name: Clawdis
options:
bundleIdPrefix: com.clawdis
bundleIdPrefix: com.steipete.clawdis
deploymentTarget:
iOS: "17.0"
xcodeVersion: "16.0"
@@ -65,8 +65,8 @@ targets:
CODE_SIGN_IDENTITY: "Apple Development"
CODE_SIGN_STYLE: Manual
DEVELOPMENT_TEAM: Y5PE65HELJ
PRODUCT_BUNDLE_IDENTIFIER: com.clawdis.ios
PROVISIONING_PROFILE_SPECIFIER: "com.clawdis.ios Development"
PRODUCT_BUNDLE_IDENTIFIER: com.steipete.clawdis.ios
PROVISIONING_PROFILE_SPECIFIER: "com.steipete.clawdis.ios Development"
SWIFT_VERSION: "6.0"
info:
path: Sources/Info.plist
@@ -98,7 +98,7 @@ targets:
product: SwabbleKit
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.clawdis.ios.tests
PRODUCT_BUNDLE_IDENTIFIER: com.steipete.clawdis.ios.tests
SWIFT_VERSION: "6.0"
TEST_HOST: "$(BUILT_PRODUCTS_DIR)/Clawdis.app/Clawdis"
BUNDLE_LOADER: "$(TEST_HOST)"
@@ -2,7 +2,7 @@ import Foundation
import OSLog
enum AgentWorkspace {
private static let logger = Logger(subsystem: "com.clawdis", category: "workspace")
private static let logger = Logger(subsystem: "com.steipete.clawdis", category: "workspace")
static let agentsFilename = "AGENTS.md"
static let soulFilename = "SOUL.md"
static let identityFilename = "IDENTITY.md"
@@ -58,7 +58,7 @@ enum AnthropicAuthResolver {
}
enum AnthropicOAuth {
private static let logger = Logger(subsystem: "com.clawdis", category: "anthropic-oauth")
private static let logger = Logger(subsystem: "com.steipete.clawdis", category: "anthropic-oauth")
private static let clientId = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
private static let authorizeURL = URL(string: "https://claude.ai/oauth/authorize")!
@@ -19,7 +19,7 @@ actor BridgeConnectionHandler {
private let logger: Logger
private let decoder = JSONDecoder()
private let encoder = JSONEncoder()
private let queue = DispatchQueue(label: "com.clawdis.bridge.connection")
private let queue = DispatchQueue(label: "com.steipete.clawdis.bridge.connection")
private var buffer = Data()
private var isAuthenticated = false
@@ -8,7 +8,7 @@ import OSLog
actor BridgeServer {
static let shared = BridgeServer()
private let logger = Logger(subsystem: "com.clawdis", category: "bridge")
private let logger = Logger(subsystem: "com.steipete.clawdis", category: "bridge")
private var listener: NWListener?
private var isRunning = false
private var store: PairedNodesStore?
@@ -42,7 +42,7 @@ actor BridgeServer {
Task { await self.handleListenerState(state) }
}
listener.start(queue: DispatchQueue(label: "com.clawdis.bridge"))
listener.start(queue: DispatchQueue(label: "com.steipete.clawdis.bridge"))
self.listener = listener
} catch {
self.logger.error("bridge start failed: \(error.localizedDescription, privacy: .public)")
@@ -36,7 +36,7 @@ actor CameraCaptureService {
}
}
private let logger = Logger(subsystem: "com.clawdis", category: "camera")
private let logger = Logger(subsystem: "com.steipete.clawdis", category: "camera")
func listDevices() -> [CameraDeviceInfo] {
Self.availableCameras().map { device in
@@ -10,7 +10,7 @@ final class CanvasFileWatcher: @unchecked Sendable {
init(url: URL, onChange: @escaping () -> Void) {
self.url = url
self.queue = DispatchQueue(label: "com.clawdis.canvaswatcher")
self.queue = DispatchQueue(label: "com.steipete.clawdis.canvaswatcher")
self.onChange = onChange
}
@@ -7,7 +7,7 @@ import OSLog
final class CanvasManager {
static let shared = CanvasManager()
private static let logger = Logger(subsystem: "com.clawdis", category: "CanvasManager")
private static let logger = Logger(subsystem: "com.steipete.clawdis", category: "CanvasManager")
private var panelController: CanvasWindowController?
private var panelSessionKey: String?
@@ -3,7 +3,7 @@ import Foundation
import OSLog
import WebKit
private let canvasLogger = Logger(subsystem: "com.clawdis", category: "Canvas")
private let canvasLogger = Logger(subsystem: "com.steipete.clawdis", category: "Canvas")
final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
private let root: URL
@@ -1,6 +1,6 @@
import AppKit
let canvasWindowLogger = Logger(subsystem: "com.clawdis", category: "Canvas")
let canvasWindowLogger = Logger(subsystem: "com.steipete.clawdis", category: "Canvas")
enum CanvasLayout {
static let panelSize = NSSize(width: 520, height: 680)
@@ -1,7 +1,7 @@
import Foundation
enum ClawdisConfigFile {
private static let logger = Logger(subsystem: "com.clawdis", category: "config")
private static let logger = Logger(subsystem: "com.steipete.clawdis", category: "config")
static func url() -> URL {
ClawdisPaths.configURL
@@ -106,20 +106,4 @@ enum ClawdisConfigFile {
return remote["password"] as? String
}
static func gatewayPort() -> Int? {
let root = self.loadDict()
guard let gateway = root["gateway"] as? [String: Any] else { return nil }
if let port = gateway["port"] as? Int, port > 0 { return port }
if let number = gateway["port"] as? NSNumber, number.intValue > 0 {
return number.intValue
}
if let raw = gateway["port"] as? String,
let parsed = Int(raw.trimmingCharacters(in: .whitespacesAndNewlines)),
parsed > 0
{
return parsed
}
return nil
}
}
@@ -5,7 +5,7 @@ import OSLog
final class ConnectionModeCoordinator {
static let shared = ConnectionModeCoordinator()
private let logger = Logger(subsystem: "com.clawdis", category: "connection")
private let logger = Logger(subsystem: "com.steipete.clawdis", category: "connection")
/// Apply the requested connection mode by starting/stopping local gateway,
/// managing the control-channel SSH tunnel, and cleaning up chat windows/panels.
File diff suppressed because it is too large. Load diff
@@ -144,45 +144,6 @@ struct ConfigSnapshot: Codable {
let issues: [Issue]?
}
struct DiscordGuildChannelForm: Identifiable {
let id = UUID()
var key: String
var allow: Bool
var requireMention: Bool
init(key: String = "", allow: Bool = true, requireMention: Bool = false) {
self.key = key
self.allow = allow
self.requireMention = requireMention
}
}
struct DiscordGuildForm: Identifiable {
let id = UUID()
var key: String
var slug: String
var requireMention: Bool
var reactionNotifications: String
var users: String
var channels: [DiscordGuildChannelForm]
init(
key: String = "",
slug: String = "",
requireMention: Bool = false,
reactionNotifications: String = "own",
users: String = "",
channels: [DiscordGuildChannelForm] = []
) {
self.key = key
self.slug = slug
self.requireMention = requireMention
self.reactionNotifications = reactionNotifications
self.users = users
self.channels = channels
}
}
@MainActor
@Observable
final class ConnectionsStore {
@@ -208,30 +169,12 @@ final class ConnectionsStore {
var telegramBusy = false
var discordEnabled = true
var discordToken: String = ""
var discordDmEnabled = true
var discordAllowFrom: String = ""
var discordGroupEnabled = false
var discordGroupChannels: String = ""
var discordMediaMaxMb: String = ""
var discordHistoryLimit: String = ""
var discordTextChunkLimit: String = ""
var discordReplyToMode: String = "off"
var discordGuilds: [DiscordGuildForm] = []
var discordActionReactions = true
var discordActionStickers = true
var discordActionPolls = true
var discordActionPermissions = true
var discordActionMessages = true
var discordActionThreads = true
var discordActionPins = true
var discordActionSearch = true
var discordActionMemberInfo = true
var discordActionRoleInfo = true
var discordActionChannelInfo = true
var discordActionVoiceStatus = true
var discordActionEvents = true
var discordActionRoles = false
var discordActionModeration = false
var discordEnableReactions = true
var discordSlashEnabled = false
var discordSlashName: String = ""
var discordSlashSessionPrefix: String = ""
@@ -444,7 +387,6 @@ final class ConnectionsStore {
self.discordEnabled = discord?["enabled"]?.boolValue ?? true
self.discordToken = discord?["token"]?.stringValue ?? ""
let discordDm = discord?["dm"]?.dictionaryValue
self.discordDmEnabled = discordDm?["enabled"]?.boolValue ?? true
if let allow = discordDm?["allowFrom"]?.arrayValue {
let strings = allow.compactMap { entry -> String? in
if let str = entry.stringValue { return str }
@@ -478,77 +420,7 @@ final class ConnectionsStore {
} else {
self.discordHistoryLimit = ""
}
if let limit = discord?["textChunkLimit"]?.doubleValue ?? discord?["textChunkLimit"]?.intValue.map(Double.init) {
self.discordTextChunkLimit = String(Int(limit))
} else {
self.discordTextChunkLimit = ""
}
if let mode = discord?["replyToMode"]?.stringValue, ["off", "first", "all"].contains(mode) {
self.discordReplyToMode = mode
} else {
self.discordReplyToMode = "off"
}
if let guilds = discord?["guilds"]?.dictionaryValue {
self.discordGuilds = guilds
.map { key, value in
let entry = value.dictionaryValue ?? [:]
let slug = entry["slug"]?.stringValue ?? ""
let requireMention = entry["requireMention"]?.boolValue ?? false
let reactionModeRaw = entry["reactionNotifications"]?.stringValue ?? ""
let reactionNotifications = ["off", "own", "all", "allowlist"].contains(reactionModeRaw)
? reactionModeRaw
: "own"
let users = entry["users"]?.arrayValue?
.compactMap { item -> String? in
if let str = item.stringValue { return str }
if let intVal = item.intValue { return String(intVal) }
if let doubleVal = item.doubleValue { return String(Int(doubleVal)) }
return nil
}
.joined(separator: ", ") ?? ""
let channels: [DiscordGuildChannelForm]
if let channelMap = entry["channels"]?.dictionaryValue {
channels = channelMap.map { channelKey, channelValue in
let channelEntry = channelValue.dictionaryValue ?? [:]
let allow = channelEntry["allow"]?.boolValue ?? true
let channelRequireMention =
channelEntry["requireMention"]?.boolValue ?? false
return DiscordGuildChannelForm(
key: channelKey,
allow: allow,
requireMention: channelRequireMention)
}
} else {
channels = []
}
return DiscordGuildForm(
key: key,
slug: slug,
requireMention: requireMention,
reactionNotifications: reactionNotifications,
users: users,
channels: channels)
}
.sorted { $0.key < $1.key }
} else {
self.discordGuilds = []
}
let discordActions = discord?["actions"]?.dictionaryValue
self.discordActionReactions = discordActions?["reactions"]?.boolValue ?? true
self.discordActionStickers = discordActions?["stickers"]?.boolValue ?? true
self.discordActionPolls = discordActions?["polls"]?.boolValue ?? true
self.discordActionPermissions = discordActions?["permissions"]?.boolValue ?? true
self.discordActionMessages = discordActions?["messages"]?.boolValue ?? true
self.discordActionThreads = discordActions?["threads"]?.boolValue ?? true
self.discordActionPins = discordActions?["pins"]?.boolValue ?? true
self.discordActionSearch = discordActions?["search"]?.boolValue ?? true
self.discordActionMemberInfo = discordActions?["memberInfo"]?.boolValue ?? true
self.discordActionRoleInfo = discordActions?["roleInfo"]?.boolValue ?? true
self.discordActionChannelInfo = discordActions?["channelInfo"]?.boolValue ?? true
self.discordActionVoiceStatus = discordActions?["voiceStatus"]?.boolValue ?? true
self.discordActionEvents = discordActions?["events"]?.boolValue ?? true
self.discordActionRoles = discordActions?["roles"]?.boolValue ?? false
self.discordActionModeration = discordActions?["moderation"]?.boolValue ?? false
self.discordEnableReactions = discord?["enableReactions"]?.boolValue ?? true
let slash = discord?["slashCommand"]?.dictionaryValue
self.discordSlashEnabled = slash?["enabled"]?.boolValue ?? false
self.discordSlashName = slash?["name"]?.stringValue ?? ""
@@ -724,11 +596,6 @@ final class ConnectionsStore {
}
var dm: [String: Any] = (discord["dm"] as? [String: Any]) ?? [:]
if self.discordDmEnabled {
dm.removeValue(forKey: "enabled")
} else {
dm["enabled"] = false
}
let allow = self.discordAllowFrom
.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
@@ -777,83 +644,10 @@ final class ConnectionsStore {
discord.removeValue(forKey: "historyLimit")
}
let chunkLimit = self.discordTextChunkLimit.trimmingCharacters(in: .whitespacesAndNewlines)
if chunkLimit.isEmpty {
discord.removeValue(forKey: "textChunkLimit")
} else if let value = Int(chunkLimit), value > 0 {
discord["textChunkLimit"] = value
if self.discordEnableReactions {
discord.removeValue(forKey: "enableReactions")
} else {
discord.removeValue(forKey: "textChunkLimit")
}
let replyToMode = self.discordReplyToMode.trimmingCharacters(in: .whitespacesAndNewlines)
if replyToMode.isEmpty || replyToMode == "off" {
discord.removeValue(forKey: "replyToMode")
} else if ["first", "all"].contains(replyToMode) {
discord["replyToMode"] = replyToMode
} else {
discord.removeValue(forKey: "replyToMode")
}
let guilds: [String: Any] = self.discordGuilds.reduce(into: [:]) { result, entry in
let key = entry.key.trimmingCharacters(in: .whitespacesAndNewlines)
guard !key.isEmpty else { return }
var payload: [String: Any] = [:]
let slug = entry.slug.trimmingCharacters(in: .whitespacesAndNewlines)
if !slug.isEmpty { payload["slug"] = slug }
if entry.requireMention { payload["requireMention"] = true }
if ["off", "own", "all", "allowlist"].contains(entry.reactionNotifications) {
payload["reactionNotifications"] = entry.reactionNotifications
}
let users = entry.users
.split(separator: ",")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
if !users.isEmpty { payload["users"] = users }
let channels: [String: Any] = entry.channels.reduce(into: [:]) { channelsResult, channel in
let channelKey = channel.key.trimmingCharacters(in: .whitespacesAndNewlines)
guard !channelKey.isEmpty else { return }
var channelPayload: [String: Any] = [:]
if !channel.allow { channelPayload["allow"] = false }
if channel.requireMention { channelPayload["requireMention"] = true }
channelsResult[channelKey] = channelPayload
}
if !channels.isEmpty { payload["channels"] = channels }
result[key] = payload
}
if guilds.isEmpty {
discord.removeValue(forKey: "guilds")
} else {
discord["guilds"] = guilds
}
var actions: [String: Any] = (discord["actions"] as? [String: Any]) ?? [:]
func setAction(_ key: String, value: Bool, defaultValue: Bool) {
if value == defaultValue {
actions.removeValue(forKey: key)
} else {
actions[key] = value
}
}
setAction("reactions", value: self.discordActionReactions, defaultValue: true)
setAction("stickers", value: self.discordActionStickers, defaultValue: true)
setAction("polls", value: self.discordActionPolls, defaultValue: true)
setAction("permissions", value: self.discordActionPermissions, defaultValue: true)
setAction("messages", value: self.discordActionMessages, defaultValue: true)
setAction("threads", value: self.discordActionThreads, defaultValue: true)
setAction("pins", value: self.discordActionPins, defaultValue: true)
setAction("search", value: self.discordActionSearch, defaultValue: true)
setAction("memberInfo", value: self.discordActionMemberInfo, defaultValue: true)
setAction("roleInfo", value: self.discordActionRoleInfo, defaultValue: true)
setAction("channelInfo", value: self.discordActionChannelInfo, defaultValue: true)
setAction("voiceStatus", value: self.discordActionVoiceStatus, defaultValue: true)
setAction("events", value: self.discordActionEvents, defaultValue: true)
setAction("roles", value: self.discordActionRoles, defaultValue: false)
setAction("moderation", value: self.discordActionModeration, defaultValue: false)
if actions.isEmpty {
discord.removeValue(forKey: "actions")
} else {
discord["actions"] = actions
discord["enableReactions"] = false
}
var slash: [String: Any] = (discord["slashCommand"] as? [String: Any]) ?? [:]
+2 -2
View File
@@ -1,7 +1,7 @@
import Foundation
let launchdLabel = "com.clawdis.mac"
let gatewayLaunchdLabel = "com.clawdis.gateway"
let launchdLabel = "com.steipete.clawdis"
let gatewayLaunchdLabel = "com.steipete.clawdis.gateway"
let onboardingVersionKey = "clawdis.onboardingVersion"
let currentOnboardingVersion = 7
let pauseDefaultsKey = "clawdis.pauseEnabled"
@@ -73,7 +73,7 @@ final class ControlChannel {
}
private(set) var lastPingMs: Double?
private let logger = Logger(subsystem: "com.clawdis", category: "control")
private let logger = Logger(subsystem: "com.steipete.clawdis", category: "control")
private var eventTask: Task<Void, Never>?
private var recoveryTask: Task<Void, Never>?
@@ -185,7 +185,7 @@ final class ControlChannel {
"Reason: \(reason)"
}
// Common misfire: we connected to the configured localhost port but it is occupied
// Common misfire: we connected to localhost:18789 but the port is occupied
// by some other process (e.g. a local dev gateway or a stuck SSH forward).
// The gateway handshake returns something we can't parse, which currently
// surfaces as "hello failed (unexpected response)". Give the user a pointer
@@ -21,7 +21,7 @@ final class CronJobsStore {
var lastError: String?
var statusMessage: String?
private let logger = Logger(subsystem: "com.clawdis", category: "cron.ui")
private let logger = Logger(subsystem: "com.steipete.clawdis", category: "cron.ui")
private var refreshTask: Task<Void, Never>?
private var runsTask: Task<Void, Never>?
private var eventTask: Task<Void, Never>?
@@ -306,7 +306,7 @@ struct DebugSettings: View {
}
if self.portReports.isEmpty, !self.portCheckInFlight {
Text("Check which process owns \(GatewayEnvironment.gatewayPort()) and suggest fixes.")
Text("Check which process owns 18789 and suggest fixes.")
.font(.caption2)
.foregroundStyle(.secondary)
} else {
@@ -946,7 +946,7 @@ extension DebugSettings {
view.portCheckInFlight = true
view.portReports = [
DebugActions.PortReport(
port: GatewayEnvironment.gatewayPort(),
port: 18789,
expected: "Gateway websocket (node/tsx)",
status: .missing("Missing"),
listeners: []),
+1 -1
View File
@@ -4,7 +4,7 @@ import Foundation
import OSLog
import Security
private let deepLinkLogger = Logger(subsystem: "com.clawdis", category: "DeepLink")
private let deepLinkLogger = Logger(subsystem: "com.steipete.clawdis", category: "DeepLink")
@MainActor
final class DeepLinkHandler {
@@ -6,7 +6,7 @@ final class DockIconManager: NSObject, @unchecked Sendable {
static let shared = DockIconManager()
private var windowsObservation: NSKeyValueObservation?
private let logger = Logger(subsystem: "com.clawdis", category: "DockIconManager")
private let logger = Logger(subsystem: "com.steipete.clawdis", category: "DockIconManager")
override private init() {
super.init()
@@ -58,7 +58,7 @@ struct WebSocketSessionBox: @unchecked Sendable {
private typealias ProtoAnyCodable = ClawdisProtocol.AnyCodable
actor GatewayChannelActor {
private let logger = Logger(subsystem: "com.clawdis", category: "gateway")
private let logger = Logger(subsystem: "com.steipete.clawdis", category: "gateway")
private var task: WebSocketTaskBox?
private var pending: [String: CheckedContinuation<GatewayFrame, Error>] = [:]
private var connected = false
@@ -3,7 +3,7 @@ import ClawdisProtocol
import Foundation
import OSLog
private let gatewayConnectionLogger = Logger(subsystem: "com.clawdis", category: "gateway.connection")
private let gatewayConnectionLogger = Logger(subsystem: "com.steipete.clawdis", category: "gateway.connection")
enum GatewayAgentChannel: String, Codable, CaseIterable, Sendable {
case last
@@ -51,10 +51,6 @@ actor GatewayConnection {
case providersStatus = "providers.status"
case configGet = "config.get"
case configSet = "config.set"
case wizardStart = "wizard.start"
case wizardNext = "wizard.next"
case wizardCancel = "wizard.cancel"
case wizardStatus = "wizard.status"
case talkMode = "talk.mode"
case webLoginStart = "web.login.start"
case webLoginWait = "web.login.wait"
Loaded 100 of 378 files, more files were not shown because too many files have changed in this diff. Show more