Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31b5b45581 | ||
|
|
33cdb16b9e | ||
|
|
53fd7a4473 | ||
|
|
10d56d31e9 | ||
|
|
3633c829ae | ||
|
|
6cda84432e | ||
|
|
b914eaa6fa | ||
|
|
988b67aa30 | ||
|
|
0ed5b82389 | ||
|
|
b417fe5727 | ||
|
|
fabad7aa7a | ||
|
|
3c54da952a | ||
|
|
2ef2646b31 | ||
|
|
82ad7e29a6 | ||
|
|
2290a3c8af | ||
|
|
d216cebff5 | ||
|
|
05bd345828 |
No files matched your search
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 2.0.0-beta5 — Unreleased
|
## 2.0.0-beta5 — 2026-01-03
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Media: preserve GIF animation when uploading to Discord/other providers (skip JPEG optimization for image/gif).
|
- 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).
|
- 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.
|
- 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: 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.
|
- 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).
|
- 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).
|
- Providers: make outbound text chunk limits configurable via `*.textChunkLimit` (defaults remain 4000/Discord 2000).
|
||||||
|
|||||||
+203
-14
@@ -2,30 +2,219 @@
|
|||||||
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
||||||
<channel>
|
<channel>
|
||||||
<title>Clawdis</title>
|
<title>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>
|
<item>
|
||||||
<title>2.0.0-beta4</title>
|
<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>
|
<link>https://raw.githubusercontent.com/steipete/clawdis/main/appcast.xml</link>
|
||||||
<sparkle:version>2.0.0-beta4</sparkle:version>
|
<sparkle:version>2.0.0-beta4</sparkle:version>
|
||||||
<sparkle:shortVersionString>2.0.0-beta4</sparkle:shortVersionString>
|
<sparkle:shortVersionString>2.0.0-beta4</sparkle:shortVersionString>
|
||||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
<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>
|
<h3>Fixes</h3>
|
||||||
<ul>
|
<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>
|
</ul>
|
||||||
<p><a href="https://github.com/steipete/clawdis/blob/main/CHANGELOG.md">View full changelog</a></p>
|
<p><a href="https://github.com/steipete/clawdis/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||||
]]></description>
|
]]></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>
|
</item>
|
||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
@@ -148,7 +148,7 @@ fun ChatComposer(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||||
ConnectionPill(sessionKey = sessionKey, healthOk = healthOk)
|
ConnectionPill(sessionLabel = currentSessionLabel, healthOk = healthOk)
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
if (pendingRunCount > 0) {
|
if (pendingRunCount > 0) {
|
||||||
@@ -186,7 +186,7 @@ fun ChatComposer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ConnectionPill(sessionKey: String, healthOk: Boolean) {
|
private fun ConnectionPill(sessionLabel: String, healthOk: Boolean) {
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(999.dp),
|
shape = RoundedCornerShape(999.dp),
|
||||||
color = MaterialTheme.colorScheme.surfaceContainerHighest,
|
color = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||||
@@ -201,7 +201,7 @@ private fun ConnectionPill(sessionKey: String, healthOk: Boolean) {
|
|||||||
shape = androidx.compose.foundation.shape.CircleShape,
|
shape = androidx.compose.foundation.shape.CircleShape,
|
||||||
color = if (healthOk) Color(0xFF2ECC71) else Color(0xFFF39C12),
|
color = if (healthOk) Color(0xFF2ECC71) else Color(0xFFF39C12),
|
||||||
) {}
|
) {}
|
||||||
Text(sessionKey, style = MaterialTheme.typography.labelSmall)
|
Text(sessionLabel, style = MaterialTheme.typography.labelSmall)
|
||||||
Text(
|
Text(
|
||||||
if (healthOk) "Connected" else "Connecting…",
|
if (healthOk) "Connected" else "Connecting…",
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ struct ClawdisChatComposer: View {
|
|||||||
Circle()
|
Circle()
|
||||||
.fill(self.viewModel.healthOK ? .green : .orange)
|
.fill(self.viewModel.healthOK ? .green : .orange)
|
||||||
.frame(width: 7, height: 7)
|
.frame(width: 7, height: 7)
|
||||||
Text(self.viewModel.sessionKey)
|
Text(self.activeSessionLabel)
|
||||||
.font(.caption2.weight(.semibold))
|
.font(.caption2.weight(.semibold))
|
||||||
Text(self.viewModel.healthOK ? "Connected" : "Connecting…")
|
Text(self.viewModel.healthOK ? "Connected" : "Connecting…")
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
@@ -221,6 +221,12 @@ struct ClawdisChatComposer: View {
|
|||||||
.clipShape(Capsule())
|
.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 {
|
private var editorOverlay: some View {
|
||||||
ZStack(alignment: .topLeading) {
|
ZStack(alignment: .topLeading) {
|
||||||
if self.viewModel.input.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
if self.viewModel.input.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
|
|||||||
@@ -86,6 +86,7 @@
|
|||||||
"commander": "^14.0.2",
|
"commander": "^14.0.2",
|
||||||
"croner": "^9.1.0",
|
"croner": "^9.1.0",
|
||||||
"detect-libc": "^2.1.2",
|
"detect-libc": "^2.1.2",
|
||||||
|
"discord-api-types": "^0.38.33",
|
||||||
"discord.js": "^14.25.1",
|
"discord.js": "^14.25.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ diff --git a/dist/providers/google-shared.js b/dist/providers/google-shared.js
|
|||||||
index ff9cbcfebfac6b4370d85dc838f5cacf2a60ed64..42096c82aec925b412258348a36ba4a7025b283b 100644
|
index ff9cbcfebfac6b4370d85dc838f5cacf2a60ed64..42096c82aec925b412258348a36ba4a7025b283b 100644
|
||||||
--- a/dist/providers/google-shared.js
|
--- a/dist/providers/google-shared.js
|
||||||
+++ b/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;
|
return contents;
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+8
-5
@@ -9,7 +9,7 @@ overrides:
|
|||||||
|
|
||||||
patchedDependencies:
|
patchedDependencies:
|
||||||
'@mariozechner/pi-ai':
|
'@mariozechner/pi-ai':
|
||||||
hash: 717192ba4aea08520822591984491823add75c2f88037188acc7a916b52326f4
|
hash: 969db6f3f4cc91fec48124e1f5e515b386b1f1bed807769d0a80c28abadbaaae
|
||||||
path: patches/@mariozechner__pi-ai.patch
|
path: patches/@mariozechner__pi-ai.patch
|
||||||
'@mariozechner/pi-coding-agent@0.31.1':
|
'@mariozechner/pi-coding-agent@0.31.1':
|
||||||
hash: d0d5ffa1bfda8a0f9d14a5e73a074014346d3edbdb2ffc91444d3be5119f5745
|
hash: d0d5ffa1bfda8a0f9d14a5e73a074014346d3edbdb2ffc91444d3be5119f5745
|
||||||
@@ -33,7 +33,7 @@ importers:
|
|||||||
version: 0.31.1(ws@8.18.3)(zod@4.3.4)
|
version: 0.31.1(ws@8.18.3)(zod@4.3.4)
|
||||||
'@mariozechner/pi-ai':
|
'@mariozechner/pi-ai':
|
||||||
specifier: ^0.31.1
|
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':
|
'@mariozechner/pi-coding-agent':
|
||||||
specifier: ^0.31.1
|
specifier: ^0.31.1
|
||||||
version: 0.31.1(patch_hash=d0d5ffa1bfda8a0f9d14a5e73a074014346d3edbdb2ffc91444d3be5119f5745)(ws@8.18.3)(zod@4.3.4)
|
version: 0.31.1(patch_hash=d0d5ffa1bfda8a0f9d14a5e73a074014346d3edbdb2ffc91444d3be5119f5745)(ws@8.18.3)(zod@4.3.4)
|
||||||
@@ -70,6 +70,9 @@ importers:
|
|||||||
detect-libc:
|
detect-libc:
|
||||||
specifier: ^2.1.2
|
specifier: ^2.1.2
|
||||||
version: 2.1.2
|
version: 2.1.2
|
||||||
|
discord-api-types:
|
||||||
|
specifier: ^0.38.33
|
||||||
|
version: 0.38.37
|
||||||
discord.js:
|
discord.js:
|
||||||
specifier: ^14.25.1
|
specifier: ^14.25.1
|
||||||
version: 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)':
|
'@mariozechner/pi-agent-core@0.31.1(ws@8.18.3)(zod@4.3.4)':
|
||||||
dependencies:
|
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
|
'@mariozechner/pi-tui': 0.31.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@modelcontextprotocol/sdk'
|
- '@modelcontextprotocol/sdk'
|
||||||
@@ -3360,7 +3363,7 @@ snapshots:
|
|||||||
- ws
|
- ws
|
||||||
- zod
|
- 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:
|
dependencies:
|
||||||
'@anthropic-ai/sdk': 0.71.2(zod@4.3.4)
|
'@anthropic-ai/sdk': 0.71.2(zod@4.3.4)
|
||||||
'@google/genai': 1.34.0
|
'@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)':
|
'@mariozechner/pi-coding-agent@0.31.1(patch_hash=d0d5ffa1bfda8a0f9d14a5e73a074014346d3edbdb2ffc91444d3be5119f5745)(ws@8.18.3)(zod@4.3.4)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@mariozechner/pi-agent-core': 0.31.1(ws@8.18.3)(zod@4.3.4)
|
'@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
|
'@mariozechner/pi-tui': 0.31.1
|
||||||
chalk: 5.6.2
|
chalk: 5.6.2
|
||||||
cli-highlight: 2.1.11
|
cli-highlight: 2.1.11
|
||||||
|
|||||||
@@ -197,7 +197,10 @@ export function createBashTool(
|
|||||||
rows: 30,
|
rows: 30,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (ptyShell !== DEFAULT_SHELL_PATH && existsSync(DEFAULT_SHELL_PATH)) {
|
if (
|
||||||
|
ptyShell !== DEFAULT_SHELL_PATH &&
|
||||||
|
existsSync(DEFAULT_SHELL_PATH)
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
pty = ptyModule.spawn(
|
pty = ptyModule.spawn(
|
||||||
DEFAULT_SHELL_PATH,
|
DEFAULT_SHELL_PATH,
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import {
|
|||||||
writeScreenRecordToFile,
|
writeScreenRecordToFile,
|
||||||
} from "../cli/nodes-screen.js";
|
} from "../cli/nodes-screen.js";
|
||||||
import { parseDurationMs } from "../cli/parse-duration.js";
|
import { parseDurationMs } from "../cli/parse-duration.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { type DiscordActionConfig, loadConfig } from "../config/config.js";
|
||||||
import {
|
import {
|
||||||
addRoleDiscord,
|
addRoleDiscord,
|
||||||
banMemberDiscord,
|
banMemberDiscord,
|
||||||
@@ -1745,7 +1745,7 @@ function createDiscordTool(): AnyAgentTool {
|
|||||||
const action = readStringParam(params, "action", { required: true });
|
const action = readStringParam(params, "action", { required: true });
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
const isActionEnabled = (
|
const isActionEnabled = (
|
||||||
key: keyof NonNullable<typeof cfg.discord>["actions"],
|
key: keyof DiscordActionConfig,
|
||||||
defaultValue = true,
|
defaultValue = true,
|
||||||
) => {
|
) => {
|
||||||
const value = cfg.discord?.actions?.[key];
|
const value = cfg.discord?.actions?.[key];
|
||||||
|
|||||||
@@ -116,7 +116,11 @@ describe("directive parsing", () => {
|
|||||||
vi.mocked(runEmbeddedPiAgent).mockReset();
|
vi.mocked(runEmbeddedPiAgent).mockReset();
|
||||||
|
|
||||||
const res = await getReplyFromConfig(
|
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: {
|
agent: {
|
||||||
|
|||||||
@@ -42,9 +42,8 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
|||||||
process.env.HOME = previousHome;
|
process.env.HOME = previousHome;
|
||||||
try {
|
try {
|
||||||
await fs.rm(base, { recursive: true, force: true });
|
await fs.rm(base, { recursive: true, force: true });
|
||||||
} catch (err) {
|
} catch {
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
// ignore cleanup failures in tests
|
||||||
if (!msg.includes("ENOTEMPTY")) throw err;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,9 +104,7 @@ describe("queue followups", () => {
|
|||||||
cfg,
|
cfg,
|
||||||
);
|
);
|
||||||
|
|
||||||
const secondText = Array.isArray(second)
|
const secondText = Array.isArray(second) ? second[0]?.text : second?.text;
|
||||||
? second[0]?.text
|
|
||||||
: second?.text;
|
|
||||||
expect(secondText).toBe("main");
|
expect(secondText).toBe("main");
|
||||||
|
|
||||||
await vi.runAllTimersAsync();
|
await vi.runAllTimersAsync();
|
||||||
|
|||||||
@@ -450,6 +450,8 @@ describe("trigger handling", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("group intro prompts", () => {
|
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 () => {
|
it("labels Discord groups using the surface metadata", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
||||||
@@ -479,7 +481,7 @@ describe("group intro prompts", () => {
|
|||||||
vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0]
|
vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0]
|
||||||
?.extraSystemPrompt ?? "";
|
?.extraSystemPrompt ?? "";
|
||||||
expect(extraSystemPrompt).toBe(
|
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]
|
vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0]
|
||||||
?.extraSystemPrompt ?? "";
|
?.extraSystemPrompt ?? "";
|
||||||
expect(extraSystemPrompt).toBe(
|
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]
|
vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0]
|
||||||
?.extraSystemPrompt ?? "";
|
?.extraSystemPrompt ?? "";
|
||||||
expect(extraSystemPrompt).toBe(
|
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
@@ -30,6 +30,7 @@ import {
|
|||||||
DEFAULT_AGENT_WORKSPACE_DIR,
|
DEFAULT_AGENT_WORKSPACE_DIR,
|
||||||
ensureAgentWorkspace,
|
ensureAgentWorkspace,
|
||||||
} from "../agents/workspace.js";
|
} from "../agents/workspace.js";
|
||||||
|
import { parseDurationMs } from "../cli/parse-duration.js";
|
||||||
import { type ClawdisConfig, loadConfig } from "../config/config.js";
|
import { type ClawdisConfig, loadConfig } from "../config/config.js";
|
||||||
import {
|
import {
|
||||||
buildGroupDisplayName,
|
buildGroupDisplayName,
|
||||||
@@ -54,7 +55,6 @@ import {
|
|||||||
import { clearCommandLane, getQueueSize } from "../process/command-queue.js";
|
import { clearCommandLane, getQueueSize } from "../process/command-queue.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
import { normalizeE164 } from "../utils.js";
|
import { normalizeE164 } from "../utils.js";
|
||||||
import { parseDurationMs } from "../cli/parse-duration.js";
|
|
||||||
import { resolveHeartbeatSeconds } from "../web/reconnect.js";
|
import { resolveHeartbeatSeconds } from "../web/reconnect.js";
|
||||||
import { getWebAuthAgeMs, webAuthExists } from "../web/session.js";
|
import { getWebAuthAgeMs, webAuthExists } from "../web/session.js";
|
||||||
import {
|
import {
|
||||||
@@ -192,10 +192,18 @@ function normalizeQueueMode(raw?: string): QueueMode | undefined {
|
|||||||
if (!raw) return undefined;
|
if (!raw) return undefined;
|
||||||
const cleaned = raw.trim().toLowerCase();
|
const cleaned = raw.trim().toLowerCase();
|
||||||
if (cleaned === "queue" || cleaned === "queued") return "steer";
|
if (cleaned === "queue" || cleaned === "queued") return "steer";
|
||||||
if (cleaned === "interrupt" || cleaned === "interrupts" || cleaned === "abort")
|
if (
|
||||||
|
cleaned === "interrupt" ||
|
||||||
|
cleaned === "interrupts" ||
|
||||||
|
cleaned === "abort"
|
||||||
|
)
|
||||||
return "interrupt";
|
return "interrupt";
|
||||||
if (cleaned === "steer" || cleaned === "steering") return "steer";
|
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";
|
return "followup";
|
||||||
if (cleaned === "collect" || cleaned === "coalesce") return "collect";
|
if (cleaned === "collect" || cleaned === "coalesce") return "collect";
|
||||||
if (
|
if (
|
||||||
@@ -536,10 +544,7 @@ function buildSummaryPrompt(queue: FollowupQueueState): string | undefined {
|
|||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCollectPrompt(
|
function buildCollectPrompt(items: FollowupRun[], summary?: string): string {
|
||||||
items: FollowupRun[],
|
|
||||||
summary?: string,
|
|
||||||
): string {
|
|
||||||
const blocks: string[] = ["[Queued messages while agent was busy]"];
|
const blocks: string[] = ["[Queued messages while agent was busy]"];
|
||||||
if (summary) {
|
if (summary) {
|
||||||
blocks.push(summary);
|
blocks.push(summary);
|
||||||
@@ -706,7 +711,8 @@ function resolveQueueSettings(params: {
|
|||||||
mode: resolvedMode,
|
mode: resolvedMode,
|
||||||
debounceMs:
|
debounceMs:
|
||||||
typeof debounceRaw === "number" ? Math.max(0, debounceRaw) : undefined,
|
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,
|
dropPolicy: dropRaw,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1954,9 +1960,7 @@ export async function getReplyFromConfig(
|
|||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
defaultRuntime.error?.(
|
defaultRuntime.error?.(`Followup agent failed before reply: ${message}`);
|
||||||
`Followup agent failed before reply: ${message}`,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2010,7 +2014,8 @@ export async function getReplyFromConfig(
|
|||||||
...entry,
|
...entry,
|
||||||
inputTokens: input,
|
inputTokens: input,
|
||||||
outputTokens: output,
|
outputTokens: output,
|
||||||
totalTokens: promptTokens > 0 ? promptTokens : usage.total ?? input,
|
totalTokens:
|
||||||
|
promptTokens > 0 ? promptTokens : (usage.total ?? input),
|
||||||
model: modelUsed,
|
model: modelUsed,
|
||||||
contextTokens: contextTokensUsed ?? entry.contextTokens,
|
contextTokens: contextTokensUsed ?? entry.contextTokens,
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
|
|||||||
+8
-2
@@ -22,8 +22,14 @@ export function registerTuiCli(program: Command) {
|
|||||||
.option("--history-limit <n>", "History entries to load", "200")
|
.option("--history-limit <n>", "History entries to load", "200")
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
try {
|
try {
|
||||||
const timeoutMs = Number.parseInt(String(opts.timeoutMs ?? "30000"), 10);
|
const timeoutMs = Number.parseInt(
|
||||||
const historyLimit = Number.parseInt(String(opts.historyLimit ?? "200"), 10);
|
String(opts.timeoutMs ?? "30000"),
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
const historyLimit = Number.parseInt(
|
||||||
|
String(opts.historyLimit ?? "200"),
|
||||||
|
10,
|
||||||
|
);
|
||||||
await runTui({
|
await runTui({
|
||||||
url: opts.url as string | undefined,
|
url: opts.url as string | undefined,
|
||||||
token: opts.token as string | undefined,
|
token: opts.token as string | undefined,
|
||||||
|
|||||||
@@ -5,16 +5,20 @@
|
|||||||
* On VPS/SSH/headless: Shows URL and prompts user to paste the callback URL manually.
|
* On VPS/SSH/headless: Shows URL and prompts user to paste the callback URL manually.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createInterface } from "node:readline/promises";
|
import { createHash, randomBytes } from "node:crypto";
|
||||||
import { stdin, stdout } from "node:process";
|
|
||||||
import { readFileSync } from "node:fs";
|
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";
|
import { loginAntigravity, type OAuthCredentials } from "@mariozechner/pi-ai";
|
||||||
|
|
||||||
// OAuth constants - decoded from pi-ai's base64 encoded values to stay in sync
|
// OAuth constants - decoded from pi-ai's base64 encoded values to stay in sync
|
||||||
const decode = (s: string) => Buffer.from(s, "base64").toString();
|
const decode = (s: string) => Buffer.from(s, "base64").toString();
|
||||||
const CLIENT_ID = decode("MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ==");
|
const CLIENT_ID = decode(
|
||||||
const CLIENT_SECRET = decode("R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=");
|
"MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ==",
|
||||||
|
);
|
||||||
|
const CLIENT_SECRET = decode(
|
||||||
|
"R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=",
|
||||||
|
);
|
||||||
const REDIRECT_URI = "http://localhost:51121/oauth-callback";
|
const REDIRECT_URI = "http://localhost:51121/oauth-callback";
|
||||||
const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
||||||
const TOKEN_URL = "https://oauth2.googleapis.com/token";
|
const TOKEN_URL = "https://oauth2.googleapis.com/token";
|
||||||
@@ -98,9 +102,7 @@ export function shouldUseManualOAuthFlow(): boolean {
|
|||||||
*/
|
*/
|
||||||
function generatePKCESync(): { verifier: string; challenge: string } {
|
function generatePKCESync(): { verifier: string; challenge: string } {
|
||||||
const verifier = randomBytes(32).toString("hex");
|
const verifier = randomBytes(32).toString("hex");
|
||||||
const challenge = createHash("sha256")
|
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
||||||
.update(verifier)
|
|
||||||
.digest("base64url");
|
|
||||||
return { verifier, challenge };
|
return { verifier, challenge };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +198,7 @@ async function exchangeCodeForTokens(
|
|||||||
|
|
||||||
// Fetch user email
|
// Fetch user email
|
||||||
const email = await getUserEmail(data.access_token);
|
const email = await getUserEmail(data.access_token);
|
||||||
|
|
||||||
// Fetch project ID
|
// Fetch project ID
|
||||||
const projectId = await fetchProjectId(data.access_token);
|
const projectId = await fetchProjectId(data.access_token);
|
||||||
|
|
||||||
@@ -288,7 +290,7 @@ async function fetchProjectId(accessToken: string): Promise<string> {
|
|||||||
return data.cloudaicompanionProject.id;
|
return data.cloudaicompanionProject.id;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
continue;
|
// ignore failed endpoint, try next
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,7 +372,9 @@ export async function loginAntigravityManual(
|
|||||||
console.log("=".repeat(60));
|
console.log("=".repeat(60));
|
||||||
console.log("\n1. Open the URL above in your LOCAL browser");
|
console.log("\n1. Open the URL above in your LOCAL browser");
|
||||||
console.log("2. Complete the Google sign-in");
|
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("4. Copy the ENTIRE URL from your browser's address bar");
|
||||||
console.log("5. Paste it below\n");
|
console.log("5. Paste it below\n");
|
||||||
console.log("The URL will look like:");
|
console.log("The URL will look like:");
|
||||||
|
|||||||
@@ -10,16 +10,7 @@ import {
|
|||||||
spinner,
|
spinner,
|
||||||
text,
|
text,
|
||||||
} from "@clack/prompts";
|
} from "@clack/prompts";
|
||||||
import {
|
import { loginAnthropic, type OAuthCredentials } from "@mariozechner/pi-ai";
|
||||||
loginAnthropic,
|
|
||||||
type OAuthCredentials,
|
|
||||||
} from "@mariozechner/pi-ai";
|
|
||||||
|
|
||||||
import {
|
|
||||||
loginAntigravityVpsAware,
|
|
||||||
isRemoteEnvironment,
|
|
||||||
} from "./antigravity-oauth.js";
|
|
||||||
|
|
||||||
import type { ClawdisConfig } from "../config/config.js";
|
import type { ClawdisConfig } from "../config/config.js";
|
||||||
import {
|
import {
|
||||||
CONFIG_PATH_CLAWDIS,
|
CONFIG_PATH_CLAWDIS,
|
||||||
@@ -32,6 +23,10 @@ import { resolveGatewayService } from "../daemon/service.js";
|
|||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
import { resolveUserPath, sleep } from "../utils.js";
|
import { resolveUserPath, sleep } from "../utils.js";
|
||||||
|
import {
|
||||||
|
isRemoteEnvironment,
|
||||||
|
loginAntigravityVpsAware,
|
||||||
|
} from "./antigravity-oauth.js";
|
||||||
import { healthCommand } from "./health.js";
|
import { healthCommand } from "./health.js";
|
||||||
import {
|
import {
|
||||||
applyMinimaxConfig,
|
applyMinimaxConfig,
|
||||||
|
|||||||
@@ -9,16 +9,7 @@ import {
|
|||||||
spinner,
|
spinner,
|
||||||
text,
|
text,
|
||||||
} from "@clack/prompts";
|
} from "@clack/prompts";
|
||||||
import {
|
import { loginAnthropic, type OAuthCredentials } from "@mariozechner/pi-ai";
|
||||||
loginAnthropic,
|
|
||||||
type OAuthCredentials,
|
|
||||||
} from "@mariozechner/pi-ai";
|
|
||||||
|
|
||||||
import {
|
|
||||||
loginAntigravityVpsAware,
|
|
||||||
isRemoteEnvironment,
|
|
||||||
} from "./antigravity-oauth.js";
|
|
||||||
|
|
||||||
import type { ClawdisConfig } from "../config/config.js";
|
import type { ClawdisConfig } from "../config/config.js";
|
||||||
import {
|
import {
|
||||||
CONFIG_PATH_CLAWDIS,
|
CONFIG_PATH_CLAWDIS,
|
||||||
@@ -31,6 +22,10 @@ import { resolveGatewayService } from "../daemon/service.js";
|
|||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
import { resolveUserPath, sleep } from "../utils.js";
|
import { resolveUserPath, sleep } from "../utils.js";
|
||||||
|
import {
|
||||||
|
isRemoteEnvironment,
|
||||||
|
loginAntigravityVpsAware,
|
||||||
|
} from "./antigravity-oauth.js";
|
||||||
import { healthCommand } from "./health.js";
|
import { healthCommand } from "./health.js";
|
||||||
import {
|
import {
|
||||||
applyMinimaxConfig,
|
applyMinimaxConfig,
|
||||||
|
|||||||
@@ -99,7 +99,9 @@ export async function runNonInteractiveOnboarding(
|
|||||||
} else if (authChoice === "minimax") {
|
} else if (authChoice === "minimax") {
|
||||||
nextConfig = applyMinimaxConfig(nextConfig);
|
nextConfig = applyMinimaxConfig(nextConfig);
|
||||||
} else if (authChoice === "oauth" || authChoice === "antigravity") {
|
} 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);
|
runtime.exit(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
export type OnboardMode = "local" | "remote";
|
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 GatewayAuthChoice = "off" | "token" | "password";
|
||||||
export type ResetScope = "config" | "config+creds+sessions" | "full";
|
export type ResetScope = "config" | "config+creds+sessions" | "full";
|
||||||
export type GatewayBind = "loopback" | "lan" | "tailnet" | "auto";
|
export type GatewayBind = "loopback" | "lan" | "tailnet" | "auto";
|
||||||
|
|||||||
@@ -114,11 +114,11 @@ export function resolveHookMappings(
|
|||||||
): HookMappingResolved[] {
|
): HookMappingResolved[] {
|
||||||
const presets = hooks?.presets ?? [];
|
const presets = hooks?.presets ?? [];
|
||||||
const mappings: HookMappingConfig[] = [];
|
const mappings: HookMappingConfig[] = [];
|
||||||
|
if (hooks?.mappings) mappings.push(...hooks.mappings);
|
||||||
for (const preset of presets) {
|
for (const preset of presets) {
|
||||||
const presetMappings = hookPresetMappings[preset];
|
const presetMappings = hookPresetMappings[preset];
|
||||||
if (presetMappings) mappings.push(...presetMappings);
|
if (presetMappings) mappings.push(...presetMappings);
|
||||||
}
|
}
|
||||||
if (hooks?.mappings) mappings.push(...hooks.mappings);
|
|
||||||
if (mappings.length === 0) return [];
|
if (mappings.length === 0) return [];
|
||||||
|
|
||||||
const configDir = path.dirname(CONFIG_PATH_CLAWDIS);
|
const configDir = path.dirname(CONFIG_PATH_CLAWDIS);
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ import {
|
|||||||
} from "../discord/index.js";
|
} from "../discord/index.js";
|
||||||
import { type DiscordProbe, probeDiscord } from "../discord/probe.js";
|
import { type DiscordProbe, probeDiscord } from "../discord/probe.js";
|
||||||
import { isVerbose } from "../globals.js";
|
import { isVerbose } from "../globals.js";
|
||||||
|
import { startGmailWatcher, stopGmailWatcher } from "../hooks/gmail-watcher.js";
|
||||||
import {
|
import {
|
||||||
monitorIMessageProvider,
|
monitorIMessageProvider,
|
||||||
sendMessageIMessage,
|
sendMessageIMessage,
|
||||||
@@ -174,10 +175,6 @@ import {
|
|||||||
type HookMappingResolved,
|
type HookMappingResolved,
|
||||||
resolveHookMappings,
|
resolveHookMappings,
|
||||||
} from "./hooks-mapping.js";
|
} from "./hooks-mapping.js";
|
||||||
import {
|
|
||||||
startGmailWatcher,
|
|
||||||
stopGmailWatcher,
|
|
||||||
} from "../hooks/gmail-watcher.js";
|
|
||||||
|
|
||||||
ensureClawdisCliOnPath();
|
ensureClawdisCliOnPath();
|
||||||
|
|
||||||
@@ -6869,7 +6866,11 @@ export async function startGatewayServer(
|
|||||||
const gmailResult = await startGmailWatcher(cfgAtStart);
|
const gmailResult = await startGmailWatcher(cfgAtStart);
|
||||||
if (gmailResult.started) {
|
if (gmailResult.started) {
|
||||||
logHooks.info("gmail watcher 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}`);
|
logHooks.warn(`gmail watcher not started: ${gmailResult.reason}`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* if hooks.gmail is configured with an account.
|
* 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 { hasBinary } from "../agents/skills.js";
|
||||||
import type { ClawdisConfig } from "../config/config.js";
|
import type { ClawdisConfig } from "../config/config.js";
|
||||||
import { createSubsystemLogger } from "../logging.js";
|
import { createSubsystemLogger } from "../logging.js";
|
||||||
@@ -13,8 +13,8 @@ import { runCommandWithTimeout } from "../process/exec.js";
|
|||||||
import {
|
import {
|
||||||
buildGogWatchServeArgs,
|
buildGogWatchServeArgs,
|
||||||
buildGogWatchStartArgs,
|
buildGogWatchStartArgs,
|
||||||
resolveGmailHookRuntimeConfig,
|
|
||||||
type GmailHookRuntimeConfig,
|
type GmailHookRuntimeConfig,
|
||||||
|
resolveGmailHookRuntimeConfig,
|
||||||
} from "./gmail.js";
|
} from "./gmail.js";
|
||||||
import { ensureTailscaleEndpoint } from "./gmail-setup-utils.js";
|
import { ensureTailscaleEndpoint } from "./gmail-setup-utils.js";
|
||||||
|
|
||||||
@@ -42,7 +42,8 @@ async function startGmailWatch(
|
|||||||
try {
|
try {
|
||||||
const result = await runCommandWithTimeout(args, { timeoutMs: 120_000 });
|
const result = await runCommandWithTimeout(args, { timeoutMs: 120_000 });
|
||||||
if (result.code !== 0) {
|
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}`);
|
log.error(`watch start failed: ${message}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -60,7 +61,7 @@ async function startGmailWatch(
|
|||||||
function spawnGogServe(cfg: GmailHookRuntimeConfig): ChildProcess {
|
function spawnGogServe(cfg: GmailHookRuntimeConfig): ChildProcess {
|
||||||
const args = buildGogWatchServeArgs(cfg);
|
const args = buildGogWatchServeArgs(cfg);
|
||||||
log.info(`starting gog ${args.join(" ")}`);
|
log.info(`starting gog ${args.join(" ")}`);
|
||||||
|
|
||||||
const child = spawn("gog", args, {
|
const child = spawn("gog", args, {
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
detached: false,
|
detached: false,
|
||||||
@@ -142,7 +143,10 @@ export async function startGmailWatcher(
|
|||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error(`tailscale setup failed: ${String(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) {
|
if (watcherProcess) {
|
||||||
log.info("stopping gmail watcher");
|
log.info("stopping gmail watcher");
|
||||||
watcherProcess.kill("SIGTERM");
|
watcherProcess.kill("SIGTERM");
|
||||||
|
|
||||||
// Wait a bit for graceful shutdown
|
// Wait a bit for graceful shutdown
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
|
|||||||
@@ -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"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -81,8 +81,7 @@ export class GatewayChatClient {
|
|||||||
await this.readyPromise;
|
await this.readyPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendChat(opts: ChatSendOptions): Promise<{ runId: string }>
|
async sendChat(opts: ChatSendOptions): Promise<{ runId: string }> {
|
||||||
{
|
|
||||||
const runId = randomUUID();
|
const runId = randomUUID();
|
||||||
await this.client.request("chat.send", {
|
await this.client.request("chat.send", {
|
||||||
sessionKey: opts.sessionKey,
|
sessionKey: opts.sessionKey,
|
||||||
|
|||||||
+8
-2
@@ -21,7 +21,8 @@ export class ChatLayout implements Component {
|
|||||||
const statusLines = this.status.render(width);
|
const statusLines = this.status.render(width);
|
||||||
const inputLines = this.input.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 available = Math.max(rows - reserved, 0);
|
||||||
|
|
||||||
const messageLines = this.messages.render(width);
|
const messageLines = this.messages.render(width);
|
||||||
@@ -30,7 +31,12 @@ export class ChatLayout implements Component {
|
|||||||
? messageLines.slice(Math.max(0, messageLines.length - available))
|
? messageLines.slice(Math.max(0, messageLines.length - available))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const lines = [...headerLines, ...slicedMessages, ...statusLines, ...inputLines];
|
const lines = [
|
||||||
|
...headerLines,
|
||||||
|
...slicedMessages,
|
||||||
|
...statusLines,
|
||||||
|
...inputLines,
|
||||||
|
];
|
||||||
if (lines.length < rows) {
|
if (lines.length < rows) {
|
||||||
const padding = Array.from({ length: rows - lines.length }, () => "");
|
const padding = Array.from({ length: rows - lines.length }, () => "");
|
||||||
return [...lines, ...padding];
|
return [...lines, ...padding];
|
||||||
|
|||||||
+11
-9
@@ -1,8 +1,10 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
|
import type { DefaultTextStyle, MarkdownTheme } from "@mariozechner/pi-tui";
|
||||||
import { Container, Markdown, Spacer, Text } 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";
|
import { theme } from "./theme.js";
|
||||||
|
|
||||||
|
type MessageRole = "user" | "system" | "tool";
|
||||||
|
|
||||||
export class MessageList extends Container {
|
export class MessageList extends Container {
|
||||||
private assistantById = new Map<string, Markdown>();
|
private assistantById = new Map<string, Markdown>();
|
||||||
|
|
||||||
@@ -38,7 +40,13 @@ export class MessageList extends Container {
|
|||||||
addAssistant(text: string, id?: string): string {
|
addAssistant(text: string, id?: string): string {
|
||||||
const messageId = id ?? crypto.randomUUID();
|
const messageId = id ?? crypto.randomUUID();
|
||||||
const label = new Text(theme.assistant("clawd"), 1, 0);
|
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();
|
const group = new Container();
|
||||||
group.addChild(label);
|
group.addChild(label);
|
||||||
group.addChild(body);
|
group.addChild(body);
|
||||||
@@ -55,12 +63,7 @@ export class MessageList extends Container {
|
|||||||
component.setText(text);
|
component.setText(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
private addMessage(
|
private addMessage(role: MessageRole, text: string, style: DefaultTextStyle) {
|
||||||
role: MessageEntry["role"],
|
|
||||||
text: string,
|
|
||||||
style: DefaultTextStyle,
|
|
||||||
) {
|
|
||||||
const messageId = crypto.randomUUID();
|
|
||||||
const label = new Text(
|
const label = new Text(
|
||||||
role === "user"
|
role === "user"
|
||||||
? theme.user("you")
|
? theme.user("you")
|
||||||
@@ -76,6 +79,5 @@ export class MessageList extends Container {
|
|||||||
group.addChild(body);
|
group.addChild(body);
|
||||||
this.addChild(group);
|
this.addChild(group);
|
||||||
this.addChild(new Spacer(1));
|
this.addChild(new Spacer(1));
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import chalk from "chalk";
|
|
||||||
import type { MarkdownTheme } from "@mariozechner/pi-tui";
|
import type { MarkdownTheme } from "@mariozechner/pi-tui";
|
||||||
|
import chalk from "chalk";
|
||||||
|
|
||||||
export const markdownTheme: MarkdownTheme = {
|
export const markdownTheme: MarkdownTheme = {
|
||||||
heading: (text) => chalk.bold.cyan(text),
|
heading: (text) => chalk.bold.cyan(text),
|
||||||
|
|||||||
+26
-10
@@ -1,11 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
type Component,
|
type Component,
|
||||||
Input,
|
Input,
|
||||||
|
isCtrlC,
|
||||||
|
isEscape,
|
||||||
ProcessTerminal,
|
ProcessTerminal,
|
||||||
Text,
|
Text,
|
||||||
TUI,
|
TUI,
|
||||||
isCtrlC,
|
|
||||||
isEscape,
|
|
||||||
} from "@mariozechner/pi-tui";
|
} from "@mariozechner/pi-tui";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
import { GatewayChatClient } from "./gateway-chat.js";
|
import { GatewayChatClient } from "./gateway-chat.js";
|
||||||
@@ -37,8 +37,7 @@ class InputWrapper implements Component {
|
|||||||
private input: Input,
|
private input: Input,
|
||||||
private onAbort: () => void,
|
private onAbort: () => void,
|
||||||
private onExit: () => void,
|
private onExit: () => void,
|
||||||
) {
|
) {}
|
||||||
}
|
|
||||||
|
|
||||||
handleInput(data: string): void {
|
handleInput(data: string): void {
|
||||||
if (isCtrlC(data)) {
|
if (isCtrlC(data)) {
|
||||||
@@ -76,10 +75,13 @@ function extractText(message?: unknown): string {
|
|||||||
return parts.join("\n").trim();
|
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;
|
if (!entry || typeof entry !== "object") return null;
|
||||||
const record = entry as Record<string, unknown>;
|
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;
|
if (!role) return null;
|
||||||
const text = extractText(record);
|
const text = extractText(record);
|
||||||
if (!text) return null;
|
if (!text) return null;
|
||||||
@@ -112,7 +114,10 @@ export async function runTui(opts: TuiOptions) {
|
|||||||
async () => {
|
async () => {
|
||||||
if (!activeRunId) return;
|
if (!activeRunId) return;
|
||||||
try {
|
try {
|
||||||
await client.abortChat({ sessionKey: currentSession, runId: activeRunId });
|
await client.abortChat({
|
||||||
|
sessionKey: currentSession,
|
||||||
|
runId: activeRunId,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
messages.addSystem(`Abort failed: ${String(err)}`);
|
messages.addSystem(`Abort failed: ${String(err)}`);
|
||||||
}
|
}
|
||||||
@@ -245,14 +250,23 @@ export async function runTui(opts: TuiOptions) {
|
|||||||
messages.addSystem("no active run");
|
messages.addSystem("no active run");
|
||||||
break;
|
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;
|
break;
|
||||||
}
|
}
|
||||||
case "exit":
|
|
||||||
case "quit": {
|
case "quit": {
|
||||||
client.stop();
|
client.stop();
|
||||||
tui.stop();
|
tui.stop();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
messages.addSystem(`unknown command: /${command}`);
|
messages.addSystem(`unknown command: /${command}`);
|
||||||
@@ -319,7 +333,9 @@ export async function runTui(opts: TuiOptions) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
client.onGap = (info) => {
|
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();
|
tui.requestRender();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
|
|
||||||
vi.mock("../agents/pi-embedded.js", () => ({
|
vi.mock("../agents/pi-embedded.js", () => ({
|
||||||
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
abortEmbeddedPiRun: vi.fn().mockReturnValue(false),
|
||||||
|
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
|
||||||
|
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||||
runEmbeddedPiAgent: vi.fn(),
|
runEmbeddedPiAgent: vi.fn(),
|
||||||
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
queueEmbeddedPiMessage: vi.fn().mockReturnValue(false),
|
||||||
resolveEmbeddedSessionLane: (key: string) =>
|
resolveEmbeddedSessionLane: (key: string) =>
|
||||||
|
|||||||
@@ -43,7 +43,10 @@ export function renderChat(props: ChatProps) {
|
|||||||
props.onSessionKeyChange((e.target as HTMLSelectElement).value)}
|
props.onSessionKeyChange((e.target as HTMLSelectElement).value)}
|
||||||
>
|
>
|
||||||
${sessionOptions.map(
|
${sessionOptions.map(
|
||||||
(entry) => html`<option value=${entry.key}>${entry.key}</option>`,
|
(entry) =>
|
||||||
|
html`<option value=${entry.key}>
|
||||||
|
${entry.displayName ?? entry.key}
|
||||||
|
</option>`,
|
||||||
)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
@@ -115,6 +118,7 @@ export function renderChat(props: ChatProps) {
|
|||||||
type SessionOption = {
|
type SessionOption = {
|
||||||
key: string;
|
key: string;
|
||||||
updatedAt?: number | null;
|
updatedAt?: number | null;
|
||||||
|
displayName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function resolveSessionOptions(
|
function resolveSessionOptions(
|
||||||
|
|||||||
Reference in new issue
Block a user