Compare commits

..
Author SHA1 Message Date
Peter Steinberger 31b5b45581 docs: refresh appcast for notarized beta5 2026-01-03 06:04:20 +01:00
Peter Steinberger 33cdb16b9e docs: update appcast for 2.0.0-beta5 2026-01-03 05:55:31 +01:00
Peter Steinberger 53fd7a4473 chore: fix lint ordering 2026-01-03 05:38:29 +01:00
Peter Steinberger 10d56d31e9 docs: date 2.0.0-beta5 changelog 2026-01-03 05:37:04 +01:00
Peter Steinberger 3633c829ae fix: repair discord action typing 2026-01-03 05:33:57 +01:00
Peter Steinberger 6cda84432e fix: stabilize pi-ai patch + tests 2026-01-03 05:22:09 +01:00
Peter Steinberger b914eaa6fa chore: apply biome lint fixes 2026-01-03 05:10:09 +01:00
Peter Steinberger 988b67aa30 test: refresh auto-reply expectations 2026-01-03 05:09:59 +01:00
Peter Steinberger 0ed5b82389 fix: prefer explicit hook mappings 2026-01-03 05:09:54 +01:00
Peter Steinberger b417fe5727 fix: show rich session names in chat UIs 2026-01-03 05:07:13 +01:00
Peter Steinberger fabad7aa7a docs: update changelog for antigravity oauth 2026-01-03 05:01:42 +01:00
Peter Steinberger 3c54da952a chore: refresh pi-ai patch hash 2026-01-03 05:01:42 +01:00
Peter Steinberger 2ef2646b31 chore: note lossy google schema scrub 2026-01-03 05:01:42 +01:00
mukhtharcm 82ad7e29a6 fix: reject antigravity auth in non-interactive onboarding mode 2026-01-03 05:01:42 +01:00
mukhtharcm 2290a3c8af feat: add VPS-aware Antigravity OAuth with manual URL paste fallback
Detects SSH/VPS/headless environments and prompts user to paste
the OAuth callback URL instead of relying on localhost server.

- Add antigravity-oauth.ts with VPS detection and manual OAuth flow
- Update onboard-interactive.ts to use VPS-aware flow
- Update configure.ts to use VPS-aware flow
2026-01-03 05:01:42 +01:00
mukhtharcm d216cebff5 fix: use claude-opus-4-5-thinking as default antigravity model 2026-01-03 05:01:42 +01:00
mukhtharcm 05bd345828 feat: add Google Antigravity authentication support
- Add 'antigravity' as new auth choice in onboard and configure wizards
- Implement Google Antigravity OAuth flow using loginAntigravity from pi-ai
- Update writeOAuthCredentials to accept any OAuthProvider (not just 'anthropic')
- Add schema sanitization for Google Cloud Code Assist API to fix tool call errors
- Default model set to google-antigravity/claude-opus-4-5 after successful auth

The schema sanitization removes unsupported JSON Schema keywords (patternProperties,
const, anyOf, etc.) that Google's Cloud Code Assist API doesn't understand.
2026-01-03 05:01:42 +01:00
30 changed files with 480 additions and 122 deletions

No files matched your search

+2 -1
View File
@@ -1,6 +1,6 @@
# Changelog
## 2.0.0-beta5 — Unreleased
## 2.0.0-beta5 — 2026-01-03
### Fixed
- Media: preserve GIF animation when uploading to Discord/other providers (skip JPEG optimization for image/gif).
@@ -62,6 +62,7 @@
- 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).
+203 -14
View File
@@ -2,30 +2,219 @@
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>Clawdis</title>
<item>
<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>2.0.0-beta3</sparkle:version>
<sparkle:shortVersionString>2.0.0-beta3</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<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>
<pubDate>Sat, 03 Jan 2026 06:04:02 +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>
<enclosure url="https://github.com/steipete/clawdis/releases/download/v2.0.0-beta5/Clawdis-2.0.0-beta4.zip" length="70407894" type="application/octet-stream" sparkle:edSignature="HmuiC7TnUn80ZApnKfb6w+JGSrjc3uUOndMrtbTp42bkBSVifbttNVazqvJueGBo4MgoJV8CP+zQNzVmtVihAQ=="/>
</item>
<item>
<title>2.0.0-beta5</title>
<pubDate>Sat, 03 Jan 2026 06:04:02 +0100</pubDate>
<link>https://raw.githubusercontent.com/steipete/clawdis/main/appcast.xml</link>
<sparkle:version>2.0.0-beta5</sparkle:version>
<sparkle:shortVersionString>2.0.0-beta5</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>Clawdis 2.0.0-beta5</h2>
<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: add <code>clawdis tui</code> gateway-connected terminal UI (local or remote).</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>
<h3>Fixes</h3>
<ul>
<li>Package contents: include Discord/hooks build outputs in the npm tarball to avoid missing module errors.</li>
<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>
</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-beta4/Clawdis-2.0.0-beta4.zip" length="70407894" type="application/octet-stream" sparkle:edSignature="HmuiC7TnUn80ZApnKfb6w+JGSrjc3uUOndMrtbTp42bkBSVifbttNVazqvJueGBo4MgoJV8CP+zQNzVmtVihAQ=="/>
<enclosure url="https://github.com/steipete/clawdis/releases/download/v2.0.0-beta5/Clawdis-2.0.0-beta5.zip" length="72078343" type="application/octet-stream" sparkle:edSignature="xSfMHcYYEktlE+EC8cnM7GJP0oqjfN4EK+oIIcTHvXtgXG7LcBqo4kMQEVrr1kIzCIqQ+7/R6HMd/rWtfkc3Cg=="/>
</item>
</channel>
</rss>
</rss>
@@ -148,7 +148,7 @@ fun ChatComposer(
)
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
ConnectionPill(sessionKey = sessionKey, healthOk = healthOk)
ConnectionPill(sessionLabel = currentSessionLabel, healthOk = healthOk)
Spacer(modifier = Modifier.weight(1f))
if (pendingRunCount > 0) {
@@ -186,7 +186,7 @@ fun ChatComposer(
}
@Composable
private fun ConnectionPill(sessionKey: String, healthOk: Boolean) {
private fun ConnectionPill(sessionLabel: String, healthOk: Boolean) {
Surface(
shape = RoundedCornerShape(999.dp),
color = MaterialTheme.colorScheme.surfaceContainerHighest,
@@ -201,7 +201,7 @@ private fun ConnectionPill(sessionKey: String, healthOk: Boolean) {
shape = androidx.compose.foundation.shape.CircleShape,
color = if (healthOk) Color(0xFF2ECC71) else Color(0xFFF39C12),
) {}
Text(sessionKey, style = MaterialTheme.typography.labelSmall)
Text(sessionLabel, style = MaterialTheme.typography.labelSmall)
Text(
if (healthOk) "Connected" else "Connecting…",
style = MaterialTheme.typography.labelSmall,
@@ -209,7 +209,7 @@ struct ClawdisChatComposer: View {
Circle()
.fill(self.viewModel.healthOK ? .green : .orange)
.frame(width: 7, height: 7)
Text(self.viewModel.sessionKey)
Text(self.activeSessionLabel)
.font(.caption2.weight(.semibold))
Text(self.viewModel.healthOK ? "Connected" : "Connecting…")
.font(.caption2)
@@ -221,6 +221,12 @@ struct ClawdisChatComposer: View {
.clipShape(Capsule())
}
private var activeSessionLabel: String {
let match = self.viewModel.sessions.first { $0.key == self.viewModel.sessionKey }
let trimmed = match?.displayName?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return trimmed.isEmpty ? self.viewModel.sessionKey : trimmed
}
private var editorOverlay: some View {
ZStack(alignment: .topLeading) {
if self.viewModel.input.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
+1
View File
@@ -86,6 +86,7 @@
"commander": "^14.0.2",
"croner": "^9.1.0",
"detect-libc": "^2.1.2",
"discord-api-types": "^0.38.33",
"discord.js": "^14.25.1",
"dotenv": "^17.2.3",
"express": "^5.2.1",
+1 -1
View File
@@ -2,7 +2,7 @@ diff --git a/dist/providers/google-shared.js b/dist/providers/google-shared.js
index ff9cbcfebfac6b4370d85dc838f5cacf2a60ed64..42096c82aec925b412258348a36ba4a7025b283b 100644
--- a/dist/providers/google-shared.js
+++ b/dist/providers/google-shared.js
@@ -140,6 +140,71 @@ export function convertMessages(model, context) {
@@ -140,6 +140,72 @@ export function convertMessages(model, context) {
}
return contents;
}
+8 -5
View File
@@ -9,7 +9,7 @@ overrides:
patchedDependencies:
'@mariozechner/pi-ai':
hash: 717192ba4aea08520822591984491823add75c2f88037188acc7a916b52326f4
hash: 969db6f3f4cc91fec48124e1f5e515b386b1f1bed807769d0a80c28abadbaaae
path: patches/@mariozechner__pi-ai.patch
'@mariozechner/pi-coding-agent@0.31.1':
hash: d0d5ffa1bfda8a0f9d14a5e73a074014346d3edbdb2ffc91444d3be5119f5745
@@ -33,7 +33,7 @@ importers:
version: 0.31.1(ws@8.18.3)(zod@4.3.4)
'@mariozechner/pi-ai':
specifier: ^0.31.1
version: 0.31.1(patch_hash=717192ba4aea08520822591984491823add75c2f88037188acc7a916b52326f4)(ws@8.18.3)(zod@4.3.4)
version: 0.31.1(patch_hash=969db6f3f4cc91fec48124e1f5e515b386b1f1bed807769d0a80c28abadbaaae)(ws@8.18.3)(zod@4.3.4)
'@mariozechner/pi-coding-agent':
specifier: ^0.31.1
version: 0.31.1(patch_hash=d0d5ffa1bfda8a0f9d14a5e73a074014346d3edbdb2ffc91444d3be5119f5745)(ws@8.18.3)(zod@4.3.4)
@@ -70,6 +70,9 @@ importers:
detect-libc:
specifier: ^2.1.2
version: 2.1.2
discord-api-types:
specifier: ^0.38.33
version: 0.38.37
discord.js:
specifier: ^14.25.1
version: 14.25.1
@@ -3350,7 +3353,7 @@ snapshots:
'@mariozechner/pi-agent-core@0.31.1(ws@8.18.3)(zod@4.3.4)':
dependencies:
'@mariozechner/pi-ai': 0.31.1(patch_hash=717192ba4aea08520822591984491823add75c2f88037188acc7a916b52326f4)(ws@8.18.3)(zod@4.3.4)
'@mariozechner/pi-ai': 0.31.1(patch_hash=969db6f3f4cc91fec48124e1f5e515b386b1f1bed807769d0a80c28abadbaaae)(ws@8.18.3)(zod@4.3.4)
'@mariozechner/pi-tui': 0.31.1
transitivePeerDependencies:
- '@modelcontextprotocol/sdk'
@@ -3360,7 +3363,7 @@ snapshots:
- ws
- zod
'@mariozechner/pi-ai@0.31.1(patch_hash=717192ba4aea08520822591984491823add75c2f88037188acc7a916b52326f4)(ws@8.18.3)(zod@4.3.4)':
'@mariozechner/pi-ai@0.31.1(patch_hash=969db6f3f4cc91fec48124e1f5e515b386b1f1bed807769d0a80c28abadbaaae)(ws@8.18.3)(zod@4.3.4)':
dependencies:
'@anthropic-ai/sdk': 0.71.2(zod@4.3.4)
'@google/genai': 1.34.0
@@ -3383,7 +3386,7 @@ snapshots:
'@mariozechner/pi-coding-agent@0.31.1(patch_hash=d0d5ffa1bfda8a0f9d14a5e73a074014346d3edbdb2ffc91444d3be5119f5745)(ws@8.18.3)(zod@4.3.4)':
dependencies:
'@mariozechner/pi-agent-core': 0.31.1(ws@8.18.3)(zod@4.3.4)
'@mariozechner/pi-ai': 0.31.1(patch_hash=717192ba4aea08520822591984491823add75c2f88037188acc7a916b52326f4)(ws@8.18.3)(zod@4.3.4)
'@mariozechner/pi-ai': 0.31.1(patch_hash=969db6f3f4cc91fec48124e1f5e515b386b1f1bed807769d0a80c28abadbaaae)(ws@8.18.3)(zod@4.3.4)
'@mariozechner/pi-tui': 0.31.1
chalk: 5.6.2
cli-highlight: 2.1.11
+4 -1
View File
@@ -197,7 +197,10 @@ export function createBashTool(
rows: 30,
});
} catch (error) {
if (ptyShell !== DEFAULT_SHELL_PATH && existsSync(DEFAULT_SHELL_PATH)) {
if (
ptyShell !== DEFAULT_SHELL_PATH &&
existsSync(DEFAULT_SHELL_PATH)
) {
try {
pty = ptyModule.spawn(
DEFAULT_SHELL_PATH,
+2 -2
View File
@@ -40,7 +40,7 @@ import {
writeScreenRecordToFile,
} from "../cli/nodes-screen.js";
import { parseDurationMs } from "../cli/parse-duration.js";
import { loadConfig } from "../config/config.js";
import { type DiscordActionConfig, loadConfig } from "../config/config.js";
import {
addRoleDiscord,
banMemberDiscord,
@@ -1745,7 +1745,7 @@ function createDiscordTool(): AnyAgentTool {
const action = readStringParam(params, "action", { required: true });
const cfg = loadConfig();
const isActionEnabled = (
key: keyof NonNullable<typeof cfg.discord>["actions"],
key: keyof DiscordActionConfig,
defaultValue = true,
) => {
const value = cfg.discord?.actions?.[key];
+5 -1
View File
@@ -116,7 +116,11 @@ describe("directive parsing", () => {
vi.mocked(runEmbeddedPiAgent).mockReset();
const res = await getReplyFromConfig(
{ Body: "/queue collect debounce:bogus cap:zero drop:maybe", From: "+1222", To: "+1222" },
{
Body: "/queue collect debounce:bogus cap:zero drop:maybe",
From: "+1222",
To: "+1222",
},
{},
{
agent: {
+3 -6
View File
@@ -42,9 +42,8 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
process.env.HOME = previousHome;
try {
await fs.rm(base, { recursive: true, force: true });
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
if (!msg.includes("ENOTEMPTY")) throw err;
} catch {
// ignore cleanup failures in tests
}
}
}
@@ -105,9 +104,7 @@ describe("queue followups", () => {
cfg,
);
const secondText = Array.isArray(second)
? second[0]?.text
: second?.text;
const secondText = Array.isArray(second) ? second[0]?.text : second?.text;
expect(secondText).toBe("main");
await vi.runAllTimersAsync();
+5 -3
View File
@@ -450,6 +450,8 @@ describe("trigger handling", () => {
});
describe("group intro prompts", () => {
const groupParticipationNote =
"Be a good group participant: lurk and follow the conversation, but only chime in when you have something genuinely helpful or relevant to add. Don't feel obligated to respond to every message — quality over quantity. Even when lurking silently, you can use emoji reactions to acknowledge messages, show support, or react to humor — reactions are always appreciated and don't clutter the chat.";
it("labels Discord groups using the surface metadata", async () => {
await withTempHome(async (home) => {
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
@@ -479,7 +481,7 @@ describe("group intro prompts", () => {
vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0]
?.extraSystemPrompt ?? "";
expect(extraSystemPrompt).toBe(
'You are replying inside the Discord group "Release Squad". Group members: Alice, Bob. Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). Address the specific sender noted in the message context.',
`You are replying inside the Discord group "Release Squad". Group members: Alice, Bob. Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). ${groupParticipationNote} Address the specific sender noted in the message context.`,
);
});
});
@@ -512,7 +514,7 @@ describe("group intro prompts", () => {
vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0]
?.extraSystemPrompt ?? "";
expect(extraSystemPrompt).toBe(
'You are replying inside the WhatsApp group "Ops". Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). Address the specific sender noted in the message context.',
`You are replying inside the WhatsApp group "Ops". Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). ${groupParticipationNote} Address the specific sender noted in the message context.`,
);
});
});
@@ -545,7 +547,7 @@ describe("group intro prompts", () => {
vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0]
?.extraSystemPrompt ?? "";
expect(extraSystemPrompt).toBe(
'You are replying inside the Telegram group "Dev Chat". Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). Address the specific sender noted in the message context.',
`You are replying inside the Telegram group "Dev Chat". Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). ${groupParticipationNote} Address the specific sender noted in the message context.`,
);
});
});
+17 -12
View File
@@ -30,6 +30,7 @@ import {
DEFAULT_AGENT_WORKSPACE_DIR,
ensureAgentWorkspace,
} from "../agents/workspace.js";
import { parseDurationMs } from "../cli/parse-duration.js";
import { type ClawdisConfig, loadConfig } from "../config/config.js";
import {
buildGroupDisplayName,
@@ -54,7 +55,6 @@ import {
import { clearCommandLane, getQueueSize } from "../process/command-queue.js";
import { defaultRuntime } from "../runtime.js";
import { normalizeE164 } from "../utils.js";
import { parseDurationMs } from "../cli/parse-duration.js";
import { resolveHeartbeatSeconds } from "../web/reconnect.js";
import { getWebAuthAgeMs, webAuthExists } from "../web/session.js";
import {
@@ -192,10 +192,18 @@ function normalizeQueueMode(raw?: string): QueueMode | undefined {
if (!raw) return undefined;
const cleaned = raw.trim().toLowerCase();
if (cleaned === "queue" || cleaned === "queued") return "steer";
if (cleaned === "interrupt" || cleaned === "interrupts" || cleaned === "abort")
if (
cleaned === "interrupt" ||
cleaned === "interrupts" ||
cleaned === "abort"
)
return "interrupt";
if (cleaned === "steer" || cleaned === "steering") return "steer";
if (cleaned === "followup" || cleaned === "follow-ups" || cleaned === "followups")
if (
cleaned === "followup" ||
cleaned === "follow-ups" ||
cleaned === "followups"
)
return "followup";
if (cleaned === "collect" || cleaned === "coalesce") return "collect";
if (
@@ -536,10 +544,7 @@ function buildSummaryPrompt(queue: FollowupQueueState): string | undefined {
return lines.join("\n");
}
function buildCollectPrompt(
items: FollowupRun[],
summary?: string,
): string {
function buildCollectPrompt(items: FollowupRun[], summary?: string): string {
const blocks: string[] = ["[Queued messages while agent was busy]"];
if (summary) {
blocks.push(summary);
@@ -706,7 +711,8 @@ function resolveQueueSettings(params: {
mode: resolvedMode,
debounceMs:
typeof debounceRaw === "number" ? Math.max(0, debounceRaw) : undefined,
cap: typeof capRaw === "number" ? Math.max(1, Math.floor(capRaw)) : undefined,
cap:
typeof capRaw === "number" ? Math.max(1, Math.floor(capRaw)) : undefined,
dropPolicy: dropRaw,
};
}
@@ -1954,9 +1960,7 @@ export async function getReplyFromConfig(
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
defaultRuntime.error?.(
`Followup agent failed before reply: ${message}`,
);
defaultRuntime.error?.(`Followup agent failed before reply: ${message}`);
return;
}
@@ -2010,7 +2014,8 @@ export async function getReplyFromConfig(
...entry,
inputTokens: input,
outputTokens: output,
totalTokens: promptTokens > 0 ? promptTokens : usage.total ?? input,
totalTokens:
promptTokens > 0 ? promptTokens : (usage.total ?? input),
model: modelUsed,
contextTokens: contextTokensUsed ?? entry.contextTokens,
updatedAt: Date.now(),
+8 -2
View File
@@ -22,8 +22,14 @@ export function registerTuiCli(program: Command) {
.option("--history-limit <n>", "History entries to load", "200")
.action(async (opts) => {
try {
const timeoutMs = Number.parseInt(String(opts.timeoutMs ?? "30000"), 10);
const historyLimit = Number.parseInt(String(opts.historyLimit ?? "200"), 10);
const timeoutMs = Number.parseInt(
String(opts.timeoutMs ?? "30000"),
10,
);
const historyLimit = Number.parseInt(
String(opts.historyLimit ?? "200"),
10,
);
await runTui({
url: opts.url as string | undefined,
token: opts.token as string | undefined,
+15 -11
View File
@@ -5,16 +5,20 @@
* On VPS/SSH/headless: Shows URL and prompts user to paste the callback URL manually.
*/
import { createInterface } from "node:readline/promises";
import { stdin, stdout } from "node:process";
import { createHash, randomBytes } from "node:crypto";
import { readFileSync } from "node:fs";
import { randomBytes, createHash } from "node:crypto";
import { stdin, stdout } from "node:process";
import { createInterface } from "node:readline/promises";
import { loginAntigravity, type OAuthCredentials } from "@mariozechner/pi-ai";
// OAuth constants - decoded from pi-ai's base64 encoded values to stay in sync
const decode = (s: string) => Buffer.from(s, "base64").toString();
const CLIENT_ID = decode("MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ==");
const CLIENT_SECRET = decode("R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=");
const CLIENT_ID = decode(
"MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ==",
);
const CLIENT_SECRET = decode(
"R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=",
);
const REDIRECT_URI = "http://localhost:51121/oauth-callback";
const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
const TOKEN_URL = "https://oauth2.googleapis.com/token";
@@ -98,9 +102,7 @@ export function shouldUseManualOAuthFlow(): boolean {
*/
function generatePKCESync(): { verifier: string; challenge: string } {
const verifier = randomBytes(32).toString("hex");
const challenge = createHash("sha256")
.update(verifier)
.digest("base64url");
const challenge = createHash("sha256").update(verifier).digest("base64url");
return { verifier, challenge };
}
@@ -196,7 +198,7 @@ async function exchangeCodeForTokens(
// Fetch user email
const email = await getUserEmail(data.access_token);
// Fetch project ID
const projectId = await fetchProjectId(data.access_token);
@@ -288,7 +290,7 @@ async function fetchProjectId(accessToken: string): Promise<string> {
return data.cloudaicompanionProject.id;
}
} catch {
continue;
// ignore failed endpoint, try next
}
}
@@ -370,7 +372,9 @@ export async function loginAntigravityManual(
console.log("=".repeat(60));
console.log("\n1. Open the URL above in your LOCAL browser");
console.log("2. Complete the Google sign-in");
console.log("3. Your browser will redirect to a localhost URL that won't load");
console.log(
"3. Your browser will redirect to a localhost URL that won't load",
);
console.log("4. Copy the ENTIRE URL from your browser's address bar");
console.log("5. Paste it below\n");
console.log("The URL will look like:");
+5 -10
View File
@@ -10,16 +10,7 @@ import {
spinner,
text,
} from "@clack/prompts";
import {
loginAnthropic,
type OAuthCredentials,
} from "@mariozechner/pi-ai";
import {
loginAntigravityVpsAware,
isRemoteEnvironment,
} from "./antigravity-oauth.js";
import { loginAnthropic, type OAuthCredentials } from "@mariozechner/pi-ai";
import type { ClawdisConfig } from "../config/config.js";
import {
CONFIG_PATH_CLAWDIS,
@@ -32,6 +23,10 @@ import { resolveGatewayService } from "../daemon/service.js";
import type { RuntimeEnv } from "../runtime.js";
import { defaultRuntime } from "../runtime.js";
import { resolveUserPath, sleep } from "../utils.js";
import {
isRemoteEnvironment,
loginAntigravityVpsAware,
} from "./antigravity-oauth.js";
import { healthCommand } from "./health.js";
import {
applyMinimaxConfig,
+5 -10
View File
@@ -9,16 +9,7 @@ import {
spinner,
text,
} from "@clack/prompts";
import {
loginAnthropic,
type OAuthCredentials,
} from "@mariozechner/pi-ai";
import {
loginAntigravityVpsAware,
isRemoteEnvironment,
} from "./antigravity-oauth.js";
import { loginAnthropic, type OAuthCredentials } from "@mariozechner/pi-ai";
import type { ClawdisConfig } from "../config/config.js";
import {
CONFIG_PATH_CLAWDIS,
@@ -31,6 +22,10 @@ import { resolveGatewayService } from "../daemon/service.js";
import type { RuntimeEnv } from "../runtime.js";
import { defaultRuntime } from "../runtime.js";
import { resolveUserPath, sleep } from "../utils.js";
import {
isRemoteEnvironment,
loginAntigravityVpsAware,
} from "./antigravity-oauth.js";
import { healthCommand } from "./health.js";
import {
applyMinimaxConfig,
+3 -1
View File
@@ -99,7 +99,9 @@ export async function runNonInteractiveOnboarding(
} else if (authChoice === "minimax") {
nextConfig = applyMinimaxConfig(nextConfig);
} else if (authChoice === "oauth" || authChoice === "antigravity") {
runtime.error(`${authChoice === "oauth" ? "OAuth" : "Antigravity"} requires interactive mode.`);
runtime.error(
`${authChoice === "oauth" ? "OAuth" : "Antigravity"} requires interactive mode.`,
);
runtime.exit(1);
return;
}
+6 -1
View File
@@ -1,5 +1,10 @@
export type OnboardMode = "local" | "remote";
export type AuthChoice = "oauth" | "antigravity" | "apiKey" | "minimax" | "skip";
export type AuthChoice =
| "oauth"
| "antigravity"
| "apiKey"
| "minimax"
| "skip";
export type GatewayAuthChoice = "off" | "token" | "password";
export type ResetScope = "config" | "config+creds+sessions" | "full";
export type GatewayBind = "loopback" | "lan" | "tailnet" | "auto";
+1 -1
View File
@@ -114,11 +114,11 @@ export function resolveHookMappings(
): HookMappingResolved[] {
const presets = hooks?.presets ?? [];
const mappings: HookMappingConfig[] = [];
if (hooks?.mappings) mappings.push(...hooks.mappings);
for (const preset of presets) {
const presetMappings = hookPresetMappings[preset];
if (presetMappings) mappings.push(...presetMappings);
}
if (hooks?.mappings) mappings.push(...hooks.mappings);
if (mappings.length === 0) return [];
const configDir = path.dirname(CONFIG_PATH_CLAWDIS);
+6 -5
View File
@@ -77,6 +77,7 @@ import {
} from "../discord/index.js";
import { type DiscordProbe, probeDiscord } from "../discord/probe.js";
import { isVerbose } from "../globals.js";
import { startGmailWatcher, stopGmailWatcher } from "../hooks/gmail-watcher.js";
import {
monitorIMessageProvider,
sendMessageIMessage,
@@ -174,10 +175,6 @@ import {
type HookMappingResolved,
resolveHookMappings,
} from "./hooks-mapping.js";
import {
startGmailWatcher,
stopGmailWatcher,
} from "../hooks/gmail-watcher.js";
ensureClawdisCliOnPath();
@@ -6869,7 +6866,11 @@ export async function startGatewayServer(
const gmailResult = await startGmailWatcher(cfgAtStart);
if (gmailResult.started) {
logHooks.info("gmail watcher started");
} else if (gmailResult.reason && gmailResult.reason !== "hooks not enabled" && gmailResult.reason !== "no gmail account configured") {
} else if (
gmailResult.reason &&
gmailResult.reason !== "hooks not enabled" &&
gmailResult.reason !== "no gmail account configured"
) {
logHooks.warn(`gmail watcher not started: ${gmailResult.reason}`);
}
} catch (err) {
+10 -6
View File
@@ -5,7 +5,7 @@
* if hooks.gmail is configured with an account.
*/
import { spawn, type ChildProcess } from "node:child_process";
import { type ChildProcess, spawn } from "node:child_process";
import { hasBinary } from "../agents/skills.js";
import type { ClawdisConfig } from "../config/config.js";
import { createSubsystemLogger } from "../logging.js";
@@ -13,8 +13,8 @@ import { runCommandWithTimeout } from "../process/exec.js";
import {
buildGogWatchServeArgs,
buildGogWatchStartArgs,
resolveGmailHookRuntimeConfig,
type GmailHookRuntimeConfig,
resolveGmailHookRuntimeConfig,
} from "./gmail.js";
import { ensureTailscaleEndpoint } from "./gmail-setup-utils.js";
@@ -42,7 +42,8 @@ async function startGmailWatch(
try {
const result = await runCommandWithTimeout(args, { timeoutMs: 120_000 });
if (result.code !== 0) {
const message = result.stderr || result.stdout || "gog watch start failed";
const message =
result.stderr || result.stdout || "gog watch start failed";
log.error(`watch start failed: ${message}`);
return false;
}
@@ -60,7 +61,7 @@ async function startGmailWatch(
function spawnGogServe(cfg: GmailHookRuntimeConfig): ChildProcess {
const args = buildGogWatchServeArgs(cfg);
log.info(`starting gog ${args.join(" ")}`);
const child = spawn("gog", args, {
stdio: ["ignore", "pipe", "pipe"],
detached: false,
@@ -142,7 +143,10 @@ export async function startGmailWatcher(
);
} catch (err) {
log.error(`tailscale setup failed: ${String(err)}`);
return { started: false, reason: `tailscale setup failed: ${String(err)}` };
return {
started: false,
reason: `tailscale setup failed: ${String(err)}`,
};
}
}
@@ -184,7 +188,7 @@ export async function stopGmailWatcher(): Promise<void> {
if (watcherProcess) {
log.info("stopping gmail watcher");
watcherProcess.kill("SIGTERM");
// Wait a bit for graceful shutdown
await new Promise<void>((resolve) => {
const timeout = setTimeout(() => {
+106
View File
@@ -0,0 +1,106 @@
import { convertTools } from "@mariozechner/pi-ai/dist/providers/google-shared.js";
import type { Tool } from "@mariozechner/pi-ai/dist/types.js";
import { describe, expect, it } from "vitest";
const asRecord = (value: unknown): Record<string, unknown> => {
expect(value).toBeTruthy();
expect(typeof value).toBe("object");
expect(Array.isArray(value)).toBe(false);
return value as Record<string, unknown>;
};
describe("google-shared convertTools", () => {
it("strips unsupported JSON Schema keywords", () => {
const tools: Tool[] = [
{
name: "example",
description: "Example tool",
parameters: {
type: "object",
patternProperties: {
"^x-": { type: "string" },
},
additionalProperties: false,
properties: {
mode: {
type: "string",
const: "fast",
},
options: {
anyOf: [{ type: "string" }, { type: "number" }],
},
list: {
type: "array",
items: {
type: "string",
const: "item",
},
},
},
required: ["mode"],
},
},
];
const converted = convertTools(tools);
const params = asRecord(
converted?.[0]?.functionDeclarations?.[0]?.parameters,
);
const properties = asRecord(params.properties);
const mode = asRecord(properties.mode);
const options = asRecord(properties.options);
const list = asRecord(properties.list);
const items = asRecord(list.items);
expect(params).not.toHaveProperty("patternProperties");
expect(params).not.toHaveProperty("additionalProperties");
expect(mode).not.toHaveProperty("const");
expect(options).not.toHaveProperty("anyOf");
expect(items).not.toHaveProperty("const");
expect(params.required).toEqual(["mode"]);
});
it("keeps supported schema fields", () => {
const tools: Tool[] = [
{
name: "settings",
description: "Settings tool",
parameters: {
type: "object",
properties: {
config: {
type: "object",
properties: {
retries: { type: "number", minimum: 1 },
tags: {
type: "array",
items: { type: "string" },
},
},
required: ["retries"],
},
},
required: ["config"],
},
},
];
const converted = convertTools(tools);
const params = asRecord(
converted?.[0]?.functionDeclarations?.[0]?.parameters,
);
const config = asRecord(asRecord(params.properties).config);
const configProps = asRecord(config.properties);
const retries = asRecord(configProps.retries);
const tags = asRecord(configProps.tags);
const items = asRecord(tags.items);
expect(params.type).toBe("object");
expect(config.type).toBe("object");
expect(retries.minimum).toBe(1);
expect(tags.type).toBe("array");
expect(items.type).toBe("string");
expect(config.required).toEqual(["retries"]);
expect(params.required).toEqual(["config"]);
});
});
+1 -2
View File
@@ -81,8 +81,7 @@ export class GatewayChatClient {
await this.readyPromise;
}
async sendChat(opts: ChatSendOptions): Promise<{ runId: string }>
{
async sendChat(opts: ChatSendOptions): Promise<{ runId: string }> {
const runId = randomUUID();
await this.client.request("chat.send", {
sessionKey: opts.sessionKey,
+8 -2
View File
@@ -21,7 +21,8 @@ export class ChatLayout implements Component {
const statusLines = this.status.render(width);
const inputLines = this.input.render(width);
const reserved = headerLines.length + statusLines.length + inputLines.length;
const reserved =
headerLines.length + statusLines.length + inputLines.length;
const available = Math.max(rows - reserved, 0);
const messageLines = this.messages.render(width);
@@ -30,7 +31,12 @@ export class ChatLayout implements Component {
? messageLines.slice(Math.max(0, messageLines.length - available))
: [];
const lines = [...headerLines, ...slicedMessages, ...statusLines, ...inputLines];
const lines = [
...headerLines,
...slicedMessages,
...statusLines,
...inputLines,
];
if (lines.length < rows) {
const padding = Array.from({ length: rows - lines.length }, () => "");
return [...lines, ...padding];
+11 -9
View File
@@ -1,8 +1,10 @@
import crypto from "node:crypto";
import type { DefaultTextStyle, MarkdownTheme } from "@mariozechner/pi-tui";
import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
import type { MarkdownTheme, DefaultTextStyle } from "@mariozechner/pi-tui";
import { theme } from "./theme.js";
type MessageRole = "user" | "system" | "tool";
export class MessageList extends Container {
private assistantById = new Map<string, Markdown>();
@@ -38,7 +40,13 @@ export class MessageList extends Container {
addAssistant(text: string, id?: string): string {
const messageId = id ?? crypto.randomUUID();
const label = new Text(theme.assistant("clawd"), 1, 0);
const body = new Markdown(text, 1, 0, this.markdownTheme, this.styles.assistant);
const body = new Markdown(
text,
1,
0,
this.markdownTheme,
this.styles.assistant,
);
const group = new Container();
group.addChild(label);
group.addChild(body);
@@ -55,12 +63,7 @@ export class MessageList extends Container {
component.setText(text);
}
private addMessage(
role: MessageEntry["role"],
text: string,
style: DefaultTextStyle,
) {
const messageId = crypto.randomUUID();
private addMessage(role: MessageRole, text: string, style: DefaultTextStyle) {
const label = new Text(
role === "user"
? theme.user("you")
@@ -76,6 +79,5 @@ export class MessageList extends Container {
group.addChild(body);
this.addChild(group);
this.addChild(new Spacer(1));
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
import chalk from "chalk";
import type { MarkdownTheme } from "@mariozechner/pi-tui";
import chalk from "chalk";
export const markdownTheme: MarkdownTheme = {
heading: (text) => chalk.bold.cyan(text),
+26 -10
View File
@@ -1,11 +1,11 @@
import {
type Component,
Input,
isCtrlC,
isEscape,
ProcessTerminal,
Text,
TUI,
isCtrlC,
isEscape,
} from "@mariozechner/pi-tui";
import { loadConfig } from "../config/config.js";
import { GatewayChatClient } from "./gateway-chat.js";
@@ -37,8 +37,7 @@ class InputWrapper implements Component {
private input: Input,
private onAbort: () => void,
private onExit: () => void,
) {
}
) {}
handleInput(data: string): void {
if (isCtrlC(data)) {
@@ -76,10 +75,13 @@ function extractText(message?: unknown): string {
return parts.join("\n").trim();
}
function renderHistoryEntry(entry: unknown): { role: "user" | "assistant"; text: string } | null {
function renderHistoryEntry(
entry: unknown,
): { role: "user" | "assistant"; text: string } | null {
if (!entry || typeof entry !== "object") return null;
const record = entry as Record<string, unknown>;
const role = record.role === "user" || record.role === "assistant" ? record.role : null;
const role =
record.role === "user" || record.role === "assistant" ? record.role : null;
if (!role) return null;
const text = extractText(record);
if (!text) return null;
@@ -112,7 +114,10 @@ export async function runTui(opts: TuiOptions) {
async () => {
if (!activeRunId) return;
try {
await client.abortChat({ sessionKey: currentSession, runId: activeRunId });
await client.abortChat({
sessionKey: currentSession,
runId: activeRunId,
});
} catch (err) {
messages.addSystem(`Abort failed: ${String(err)}`);
}
@@ -245,14 +250,23 @@ export async function runTui(opts: TuiOptions) {
messages.addSystem("no active run");
break;
}
await client.abortChat({ sessionKey: currentSession, runId: activeRunId });
await client.abortChat({
sessionKey: currentSession,
runId: activeRunId,
});
break;
}
case "exit": {
client.stop();
tui.stop();
process.exit(0);
break;
}
case "exit":
case "quit": {
client.stop();
tui.stop();
process.exit(0);
break;
}
default:
messages.addSystem(`unknown command: /${command}`);
@@ -319,7 +333,9 @@ export async function runTui(opts: TuiOptions) {
};
client.onGap = (info) => {
messages.addSystem(`event gap: expected ${info.expected}, got ${info.received}`);
messages.addSystem(
`event gap: expected ${info.expected}, got ${info.received}`,
);
tui.requestRender();
};
+2
View File
@@ -8,6 +8,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../agents/pi-embedded.js", () => ({
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
runEmbeddedPiAgent: vi.fn(),
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
resolveEmbeddedSessionLane: (key: string) =>
+5 -1
View File
@@ -43,7 +43,10 @@ export function renderChat(props: ChatProps) {
props.onSessionKeyChange((e.target as HTMLSelectElement).value)}
>
${sessionOptions.map(
(entry) => html`<option value=${entry.key}>${entry.key}</option>`,
(entry) =>
html`<option value=${entry.key}>
${entry.displayName ?? entry.key}
</option>`,
)}
</select>
</label>
@@ -115,6 +118,7 @@ export function renderChat(props: ChatProps) {
type SessionOption = {
key: string;
updatedAt?: number | null;
displayName?: string;
};
function resolveSessionOptions(