Compare commits

..

254 Commits

Author SHA1 Message Date
Peter Steinberger
aaf6a37385 chore: update changelog for llm-task (#1498) (thanks @vignesh07) 2026-01-24 01:18:13 +00:00
Peter Steinberger
250210d32b fix: harden llm-task plugin 2026-01-24 01:11:40 +00:00
Vignesh Natarajan
764d79dadf fix(llm-task): load embedded runner from src or bundled dist 2026-01-24 01:11:16 +00:00
Vignesh Natarajan
bd98589b39 fix(llm-task): fix index.ts import quoting 2026-01-24 01:11:16 +00:00
Vignesh Natarajan
81cc6aefaf fix(llm-task): fix invalid plugin manifest JSON 2026-01-24 01:11:16 +00:00
Vignesh Natarajan
da3a56708f fix(llm-task): fix invalid package.json 2026-01-24 01:11:16 +00:00
Vignesh Natarajan
c5356e121a feat(llm-task): add optional JSON-only LLM task tool 2026-01-24 01:11:16 +00:00
Peter Steinberger
c66b1fd18b docs: add changelog entry for sidebar fix (#1515) (thanks @pookNast) 2026-01-24 01:00:19 +00:00
pookNast
c04f8ba1ea fix(ui): Make sidebar sticky while scrolling content (#1515)
The left navigation sidebar now stays fixed when scrolling through
long content pages like /skills. Changed .shell from min-height to
fixed height with overflow: hidden, allowing nav and content to
scroll independently within their grid cells.

Co-authored-by: pookNast <pook@nast.local>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 00:58:37 +00:00
Peter Steinberger
c1b7f6b6ba Merge pull request #1544 from wca4a/feature/add-tlon-plugin
Add Tlon/Urbit channel plugin
2026-01-24 00:57:44 +00:00
Peter Steinberger
e4708b3b99 test: relax tailscale binary expectations 2026-01-24 00:49:04 +00:00
Peter Steinberger
f938f6617b docs: extend cron vs heartbeat guide 2026-01-24 00:42:56 +00:00
justyannicc
e882f7d207 docs: add cron vs heartbeat decision guide
- New docs/automation/cron-vs-heartbeat.md with complete guidance
- Cross-links from heartbeat.md and cron-jobs.md
- Updated AGENTS.md template with practical guidance
- Added navigation entry in docs.json
2026-01-24 00:42:56 +00:00
AJ (@techfren)
e38fd8603f docs: remove misplaced Google Docs Editor from showcase (#1547)
- Was incorrectly placed in Voice & Phone section
- Not a Clawdbot project (Claude Code skill)
- No valid link available
2026-01-24 00:42:48 +00:00
Travis
89283aa788 Plugins: move clawdbot to devDependencies + add zod 2026-01-24 00:42:33 +00:00
Peter Steinberger
f7dc27f2d0 fix: move probe errors below table 2026-01-24 00:32:49 +00:00
google-labs-jules[bot]
ed560e466f fix(doctor): align sandbox image check with main logic
Updated `dockerImageExists` in `src/commands/doctor-sandbox.ts` to mirror the logic in `src/agents/sandbox/docker.ts`. It now re-throws errors unless they are explicitly "No such image" errors.
2026-01-24 00:30:24 +00:00
google-labs-jules[bot]
b5f1dc9d95 chore(tests): remove reproduction test
Removed the test file `src/agents/sandbox/docker.test.ts` as requested in code review.
2026-01-24 00:30:24 +00:00
google-labs-jules[bot]
f58ad7625f fix(sandbox): simplify docker image check
Simplify the stderr check in `dockerImageExists` to only look for "No such image", as requested in code review.
2026-01-24 00:30:24 +00:00
google-labs-jules[bot]
49c6d8019f fix(sandbox): improve docker image existence check error handling
Previously, `dockerImageExists` assumed any error from `docker image inspect` meant the image did not exist. This masked other errors like socket permission issues.

This change:
- Modifies `dockerImageExists` to inspect stderr when the exit code is non-zero.
- Returns `false` only if the error explicitly indicates "No such image" or "No such object".
- Throws an error with the stderr content for all other failures.
- Adds a reproduction test in `src/agents/sandbox/docker.test.ts`.
2026-01-24 00:30:24 +00:00
Peter Steinberger
86db180a17 docs: clarify PR merge preference 2026-01-24 00:30:11 +00:00
Peter Steinberger
c69111a4e6 Merge branch 'main' into feature/add-tlon-plugin 2026-01-24 00:27:24 +00:00
Peter Steinberger
31e59cd583 fix: hide probe logs without verbose 2026-01-24 00:27:05 +00:00
Peter Steinberger
d2bfcd70e7 fix: stabilize tests and sync protocol models 2026-01-24 00:25:58 +00:00
Peter Steinberger
12d22e1c89 chore: update clawtributors 2026-01-24 00:25:39 +00:00
Peter Steinberger
75cb78a5b1 chore: drop tlon node_modules 2026-01-24 00:25:39 +00:00
Peter Steinberger
791b568f78 feat: add tlon channel plugin 2026-01-24 00:25:39 +00:00
william arzt
d46642319b Add Tlon/Urbit channel plugin
Adds built-in Tlon (Urbit) channel plugin to support decentralized messaging on the Urbit network.

Features:
- DM and group chat support
- SSE-based real-time message monitoring
- Auto-discovery of group channels
- Thread replies and reactions
- Integration with Urbit's HTTP API

This resolves cron delivery issues with external Tlon plugins by making it a first-class built-in channel alongside Telegram, Signal, and other messaging platforms.

Implementation includes:
- Plugin registration via ClawdbotPluginApi
- Outbound delivery with sendText and sendMedia
- Gateway adapter for inbound message handling
- Urbit SSE client for event streaming
- Core bridge for Clawdbot runtime integration

Co-authored-by: William Arzt <william@arzt.co>
2026-01-24 00:25:38 +00:00
Peter Steinberger
a96d37ca69 docs: clarify plugin dependency rules 2026-01-24 00:23:21 +00:00
Peter Steinberger
f8046268bc chore: drop tlon node_modules 2026-01-24 00:18:58 +00:00
Peter Steinberger
9cdd0c28be feat: add tlon channel plugin 2026-01-24 00:17:58 +00:00
Peter Steinberger
05b0b82937 fix: guard tailscale sudo fallback (#1551) (thanks @sweepies) 2026-01-24 00:17:20 +00:00
google-labs-jules[bot]
908d9331af feat: use sudo fallback for tailscale configuration commands
To avoid permission denied errors when modifying Tailscale configuration (serve/funnel),
we now attempt the command directly first. If it fails, we catch the error and retry
with `sudo -n`. This preserves existing behavior for users where it works, but
attempts to escalate privileges (non-interactively) if needed.

- Added `execWithSudoFallback` helper in `src/infra/tailscale.ts`.
- Updated `ensureFunnel`, `enableTailscaleServe`, `disableTailscaleServe`,
  `enableTailscaleFunnel`, and `disableTailscaleFunnel` to use the fallback helper.
- Added tests in `src/infra/tailscale.test.ts` to verify fallback behavior.
2026-01-24 00:17:20 +00:00
google-labs-jules[bot]
29f0463f65 feat: use sudo for tailscale configuration commands
To avoid permission denied errors when modifying Tailscale configuration (serve/funnel),
we now prepend `sudo -n` to these commands. This ensures that if the user has appropriate
sudo privileges (specifically passwordless for these commands or generally), the operation
succeeds. If sudo fails (e.g. requires password non-interactively), it will throw an error
which is caught and logged as a warning, preserving existing behavior but attempting to escalate privileges first.

- Updated `ensureFunnel` to use `sudo -n` for the enabling step.
- Updated `enableTailscaleServe`, `disableTailscaleServe`, `enableTailscaleFunnel`, `disableTailscaleFunnel` to use `sudo -n`.
2026-01-24 00:17:20 +00:00
google-labs-jules[bot]
66f353fe7a feat: use sudo for tailscale configuration commands
To avoid permission denied errors when modifying Tailscale configuration (serve/funnel),
we now prepend `sudo -n` to these commands. This ensures that if the user has appropriate
sudo privileges (specifically passwordless for these commands or generally), the operation
succeeds. If sudo fails (e.g. requires password non-interactively), it will throw an error
which is caught and logged as a warning, preserving existing behavior but attempting to escalate privileges first.

- Updated `ensureFunnel` to use `sudo -n` for the enabling step.
- Updated `enableTailscaleServe`, `disableTailscaleServe`, `enableTailscaleFunnel`, `disableTailscaleFunnel` to use `sudo -n`.
- Added tests in `src/infra/tailscale.test.ts` to verify `sudo` usage.
2026-01-24 00:17:20 +00:00
Robby
511a0c22b7 fix(sessions): reset token counts to 0 on /new (#1523)
- Set inputTokens, outputTokens, totalTokens to 0 in sessions.reset
- Clear TUI sessionInfo tokens immediately before async reset
- Prevents stale token display after session reset

Fixes #1523
2026-01-24 00:15:42 +00:00
Peter Steinberger
da3f2b4898 fix: table auth probe output 2026-01-24 00:11:04 +00:00
Peter Steinberger
438e782f81 fix: silence probe timeouts 2026-01-24 00:11:04 +00:00
Peter Steinberger
d354030974 docs: changelog for MS Teams scopes (#1507) (thanks @Evizero) 2026-01-24 00:08:10 +00:00
Christof
ef777d6bb6 fix(msteams): remove .default suffix from graph scopes (#1507)
The @microsoft/agents-hosting SDK's MsalTokenProvider automatically
appends `/.default` to all scope strings in its token acquisition
methods (acquireAccessTokenViaSecret, acquireAccessTokenViaFIC,
acquireAccessTokenViaWID, acquireTokenWithCertificate in
msalTokenProvider.ts). This is consistent SDK behavior, not a recent
change.

Our code was including `.default` in scope URLs, resulting in invalid
double suffixes like `https://graph.microsoft.com/.default/.default`.

This was confirmed to cause Graph API authentication errors. Removing
the `.default` suffix from our scope strings allows the SDK to append
it correctly, resolving the issue.

Before: we pass `.default` -> SDK appends -> double `.default` (broken)
After:  we pass base URL  -> SDK appends -> single `.default` (works)

Co-authored-by: Christof Salis <c.salis@vertifymed.com>
2026-01-24 00:07:22 +00:00
Peter Steinberger
b9c35d9fdc docs: add Comcast SSL troubleshooting note 2026-01-24 00:01:20 +00:00
Peter Steinberger
69f645c662 fix: auto-save voice wake words across apps 2026-01-23 23:59:08 +00:00
Peter Steinberger
efec5fc751 docs: remove channel unify checklist 2026-01-23 23:37:04 +00:00
Peter Steinberger
bf4544784a fix: stabilize typing + summary merge 2026-01-23 23:34:30 +00:00
Peter Steinberger
c9a7c77b24 test: cover typing and history helpers 2026-01-23 23:34:30 +00:00
Peter Steinberger
aeb6b2ffad refactor: standardize channel logging 2026-01-23 23:34:30 +00:00
Peter Steinberger
07ce1d73ff refactor: standardize control command gating 2026-01-23 23:34:30 +00:00
Peter Steinberger
1113f17d4c refactor: share reply prefix context 2026-01-23 23:34:30 +00:00
Peter Steinberger
8252ae2da1 refactor: unify typing callbacks 2026-01-23 23:33:32 +00:00
Peter Steinberger
d82ecaf9dc refactor: centralize inbound session updates 2026-01-23 23:33:32 +00:00
Peter Steinberger
521ea4ae5b refactor: unify pending history helpers 2026-01-23 23:33:32 +00:00
Peter Steinberger
05e7e06146 docs: add channel unification checklist 2026-01-23 23:32:14 +00:00
Peter Steinberger
cb8c8fee9a refactor: centralize ack reaction removal 2026-01-23 23:32:14 +00:00
Peter Steinberger
ed05152cb1 fix: align compaction summary message types 2026-01-23 23:03:04 +00:00
Peter Steinberger
a8054d1e83 fix: complete inbound dispatch refactor 2026-01-23 22:58:54 +00:00
Peter Steinberger
2e0a835e07 fix: unify inbound dispatch pipeline 2026-01-23 22:58:54 +00:00
Peter Steinberger
da26954dd0 test(compaction): cover staged pruning 2026-01-23 22:25:07 +00:00
Peter Steinberger
892197c43e refactor: reuse ack reaction helper for whatsapp 2026-01-23 22:24:31 +00:00
Peter Steinberger
02bd6e4a24 refactor: centralize ack reaction gating 2026-01-23 22:24:31 +00:00
Peter Steinberger
99d4820b39 docs: clarify exe.dev ops 2026-01-23 22:23:23 +00:00
Peter Steinberger
022aa10063 feat(compaction): apply staged pruning 2026-01-23 22:23:23 +00:00
Peter Steinberger
ae0741a346 feat(compaction): add staged helpers 2026-01-23 22:23:23 +00:00
Peter Steinberger
4ee70be690 chore: bump version to 2026.1.23 2026-01-23 22:14:56 +00:00
william arzt
24de8cecf6 Add Tlon/Urbit channel plugin
Adds built-in Tlon (Urbit) channel plugin to support decentralized messaging on the Urbit network.

Features:
- DM and group chat support
- SSE-based real-time message monitoring
- Auto-discovery of group channels
- Thread replies and reactions
- Integration with Urbit's HTTP API

This resolves cron delivery issues with external Tlon plugins by making it a first-class built-in channel alongside Telegram, Signal, and other messaging platforms.

Implementation includes:
- Plugin registration via ClawdbotPluginApi
- Outbound delivery with sendText and sendMedia
- Gateway adapter for inbound message handling
- Urbit SSE client for event streaming
- Core bridge for Clawdbot runtime integration

Co-authored-by: William Arzt <william@arzt.co>
2026-01-23 15:17:07 -05:00
Shiva Prasad
fdbaae6a33 macOS: fix trigger word input disappearing when typing and on add (#1506)
Fixed issue where trigger words would disappear when typing or when adding new trigger words. The problem was that `swabbleTriggerWords` changes were triggering `VoiceWakeRuntime.refresh()` which sanitized the array by removing empty strings in real-time.

Solution: Introduced local `@State` buffer `triggerEntries` with stable UUID identifiers for each trigger word entry. User edits now only affect the local state buffer and are synced back to `AppState` on explicit actions (submit, remove, disappear). This prevents premature sanitization during editing.

The local state is loaded on view appear and when the view becomes active, ensuring it stays in sync with `AppState`.
2026-01-23 20:08:12 +00:00
Paul van Oorschot
7d0a0ae3ba fix(discord): autoThread ack reactions + exec approval null handling (#1511)
* fix(discord): gate autoThread by thread owner

* fix(discord): ack bot-owned autoThreads

* fix(discord): ack mentions in open channels

- Ack reactions in bot-owned autoThreads
- Ack reactions in open channels (no mention required)
- DRY: Pass pre-computed isAutoThreadOwnedByBot to avoid redundant checks
- Consolidate ack logic with explanatory comment

* fix: allow null values in exec.approval.request schema

The ExecApprovalRequestParamsSchema was rejecting null values for optional
fields like resolvedPath, but the calling code in bash-tools.exec.ts passes
null. This caused intermittent 'invalid exec.approval.request params'
validation errors.

Fix: Accept Type.Union([Type.String(), Type.Null()]) for all optional string
fields in the schema. Update test to reflect new behavior.

* fix: align discord ack reactions with mention gating (#1511) (thanks @pvoo)

---------

Co-authored-by: Wimmie <wimmie@tameson.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-01-23 20:01:15 +00:00
Peter Steinberger
242add587f fix: quiet auth probe diagnostics 2026-01-23 19:53:01 +00:00
Peter Steinberger
6fba598eaf fix: handle gateway slash command replies in TUI 2026-01-23 19:48:22 +00:00
Peter Steinberger
75a54f0259 docs: note models usage suppression 2026-01-23 19:43:26 +00:00
Peter Steinberger
c63144ab14 fix: hide usage errors in status 2026-01-23 19:43:26 +00:00
Peter Steinberger
f07c39b265 docs: handle lint/format churn 2026-01-23 19:37:33 +00:00
Peter Steinberger
40181afded feat: add models status auth probes 2026-01-23 19:28:55 +00:00
Peter Steinberger
2f1b9efe9a style: wrap service path helpers 2026-01-23 19:17:57 +00:00
Peter Steinberger
ff30cef8a4 fix: expand linux service PATH handling 2026-01-23 19:16:41 +00:00
Robby
3d958d5466 fix(linux): add user bin directories to systemd service PATH for skill installation (#1512)
* fix(linux): add user bin directories to systemd service PATH

Fixes #1503

On Linux, the systemd service PATH was hardcoded to only include system
directories (/usr/local/bin, /usr/bin, /bin), causing binaries installed
via npm global with custom prefix or node version managers to not be found.

This adds common Linux user bin directories to the PATH:
- ~/.local/bin (XDG standard, pip, etc.)
- ~/.npm-global/bin (npm custom prefix)
- ~/bin (user's personal bin)
- Node version manager paths (nvm, fnm, volta, asdf)
- ~/.local/share/pnpm (pnpm global)
- ~/.bun/bin (Bun)

User directories are added before system directories so user-installed
binaries take precedence.

🤖 AI-assisted (Claude Opus 4.5 via Clawdbot)
📋 Testing: Existing unit tests pass (7/7)

* test: add comprehensive tests for Linux user bin directory resolution

- Add dedicated tests for resolveLinuxUserBinDirs() function
- Test path ordering (extraDirs > user dirs > system dirs)
- Test buildMinimalServicePath() with HOME set/unset
- Test platform-specific behavior (Linux vs macOS vs Windows)

Test count: 7 → 20 (+13 tests)

* test: add comprehensive tests for Linux user bin directory handling

- Test Linux user directories included when HOME is set
- Test Linux user directories excluded when HOME is missing
- Test path ordering (extraDirs > user dirs > system dirs)
- Test platform-specific behavior (Linux vs macOS vs Windows)
- Test buildMinimalServicePath() with HOME in env

Covers getMinimalServicePathParts() and buildMinimalServicePath()
for all Linux user bin directory edge cases.

Test count: 7 → 16 (+9 tests)
2026-01-23 19:06:14 +00:00
Peter Steinberger
cad7ed1cb8 fix(exec-approvals): stabilize allowlist ids (#1521) 2026-01-23 19:00:45 +00:00
Peter Steinberger
8195497cec fix: surface gateway slash commands in TUI 2026-01-23 18:58:41 +00:00
Peter Steinberger
1af227b619 fix: forward unknown TUI slash commands 2026-01-23 18:41:02 +00:00
Peter Steinberger
b77e730657 fix: add per-channel markdown table conversion (#1495) (thanks @odysseus0) 2026-01-23 18:39:25 +00:00
Peter Steinberger
37e5f077b8 test: move gateway server coverage to e2e 2026-01-23 18:34:33 +00:00
Peter Steinberger
0eb7e1864c test: move auto-reply directive coverage to e2e 2026-01-23 18:34:33 +00:00
Peter Steinberger
0d336272f9 test: consolidate auto-reply unit coverage 2026-01-23 18:34:33 +00:00
Peter Steinberger
ace6a42ea6 test: dedupe CLI onboard auth cases 2026-01-23 18:34:33 +00:00
Peter Steinberger
6d2a1ce217 test: trim async waits in webhook tests 2026-01-23 18:34:33 +00:00
Peter Steinberger
c9d73469c3 test: stub heavy tools in agent tests 2026-01-23 18:34:33 +00:00
Peter Steinberger
29353e2e81 test: speed up default test env 2026-01-23 18:34:33 +00:00
Peter Steinberger
fdc50a0feb fix: normalize session lock path 2026-01-23 18:34:33 +00:00
George Zhang
a1413a011e feat(telegram): convert markdown tables to bullet points (#1495)
Tables render poorly in Telegram (pipes stripped, whitespace collapses).
This adds a 'tableMode' option to markdownToIR that converts tables to
nested bullet points, which render cleanly on mobile.

- Add tableMode: 'flat' | 'bullets' to MarkdownParseOptions
- Track table state during token rendering
- Render tables as bullet points with first column as row labels
- Apply bold styling to row labels for visual hierarchy
- Enable tableMode: 'bullets' for Telegram formatter

Closes #TBD
2026-01-23 18:00:51 +00:00
George Zhang
bfbeea0f20 daemon: prefer symlinked paths over realpath for stable service configs (#1505)
When installing the LaunchAgent/systemd service, the CLI was using
fs.realpath() to resolve the entry.js path, which converted stable
symlinked paths (e.g. node_modules/clawdbot) into version-specific
paths (e.g. .pnpm/clawdbot@X.Y.Z/...).

This caused the service to break after pnpm updates because the old
versioned path no longer exists, even though the symlink still works.

Now we prefer the original (symlinked) path when it's valid, keeping
service configs stable across package version updates.
2026-01-23 11:52:26 +00:00
Peter Steinberger
2c85b1b409 fix: restart gateway after update by default 2026-01-23 11:50:19 +00:00
Peter Steinberger
8b7b7e154f chore: speed up tests and update opencode models 2026-01-23 11:36:32 +00:00
Peter Steinberger
bb9bddebb4 fix: stabilize ci tests 2026-01-23 09:52:22 +00:00
Peter Steinberger
6e570561b6 docs: prefer fast install smoke for release 2026-01-23 09:18:15 +00:00
Peter Steinberger
fb6363ae58 Merge pull request #1492 from svkozak/fix-discord-accountId
Discord: preserve accountId in message actions (refs #1489)
2026-01-23 09:15:54 +00:00
Peter Steinberger
1b77e086d4 Merge origin/main into fix-discord-accountId 2026-01-23 09:15:44 +00:00
Sergii Kozak
d371a4c8c3 Discord Actions: Update tests for optional config parameter 2026-01-23 01:11:54 -08:00
Peter Steinberger
03e8b7c4ba fix: always offer TUI hatch 2026-01-23 09:07:43 +00:00
Peter Steinberger
8aadcaa1bd test: fix discord action mocks 2026-01-23 09:06:04 +00:00
Peter Steinberger
96800c27ec docs: update changelog for #1492 2026-01-23 09:06:04 +00:00
Peter Steinberger
13d1712850 fix: honor accountId in message actions 2026-01-23 09:06:04 +00:00
Sergii Kozak
c5546f0d5b Discord: preserve accountId in message actions (refs #1489) 2026-01-23 09:06:04 +00:00
Peter Steinberger
3de5ea818d ci: speed up install smoke on PRs 2026-01-23 09:05:15 +00:00
Peter Steinberger
dc07f1e021 fix: keep core tools when allowlist is plugin-only 2026-01-23 09:02:17 +00:00
Peter Steinberger
310a248a44 docs: add exe.dev ops note 2026-01-23 09:01:02 +00:00
Peter Steinberger
88e7684258 chore: update appcast for 2026.1.22 2026-01-23 08:59:04 +00:00
Sergii Kozak
716f901504 Discord: honor accountId across channel actions (refs #1489) 2026-01-23 00:50:50 -08:00
Peter Steinberger
e817c0cee5 fix: preserve PNG alpha fallback (#1491) (thanks @robbyczgw-cla) 2026-01-23 08:45:50 +00:00
Robby
e634791585 fix(media): preserve alpha channel for transparent PNGs (#1473) 2026-01-23 08:43:01 +00:00
Peter Steinberger
78071f8ec4 docs: note SPARKLE_PRIVATE_KEY_FILE in profile 2026-01-23 08:25:20 +00:00
Peter Steinberger
c48751a99c chore: sync plugin versions for 2026.1.22 2026-01-23 08:18:55 +00:00
Peter Steinberger
86e0916fa3 fix: allow windows spawn in test parallel 2026-01-23 07:52:04 +00:00
Sergii Kozak
dc89bc4004 Discord: preserve accountId in message actions (refs #1489) 2026-01-22 23:51:58 -08:00
Peter Steinberger
0c7e649676 docs: fix 2026.1.21 changelog placement 2026-01-23 07:51:40 +00:00
Peter Steinberger
45ce07a098 test: split vitest into unit and gateway 2026-01-23 07:34:57 +00:00
Peter Steinberger
aed8dc1ade test: consolidate pi-tools shards 2026-01-23 07:34:57 +00:00
Peter Steinberger
86a341be62 test: speed up history and cron suites 2026-01-23 07:34:57 +00:00
Ian Hildebrand
ff78e9a564 fix: support direct token and provider in auth apply commands (#1485) 2026-01-23 07:27:52 +00:00
Peter Steinberger
60a60779d7 test: streamline slow suites 2026-01-23 07:26:19 +00:00
Peter Steinberger
32da00cb2f docs: note vitest worker cap 2026-01-23 07:26:19 +00:00
Peter Steinberger
0420f2804c fix: log config update in copilot auth 2026-01-23 07:23:52 +00:00
Hiren Patel
4de660bec6 [AI Assisted] Usage: add Google Antigravity usage tracking (#1490)
* Usage: add Google Antigravity usage tracking

- Add dedicated fetcher for google-antigravity provider
- Fetch credits and per-model quotas from Cloud Code API
- Report individual model IDs sorted by usage (top 10)
- Include comprehensive debug logging with [antigravity] prefix

* fix: refine antigravity usage tracking (#1490) (thanks @patelhiren)

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-01-23 07:17:59 +00:00
Peter Steinberger
58f638463f fix: stop gateway before uninstall 2026-01-23 07:17:42 +00:00
Peter Steinberger
f1afc722da Revert "fix: improve GitHub Copilot integration"
This reverts commit 21a9b3b66f.
2026-01-23 07:14:00 +00:00
Peter Steinberger
bc75d58e9e Revert "fix: set Copilot user agent header"
This reverts commit cfcc4548bb.
2026-01-23 07:14:00 +00:00
Peter Steinberger
2efd265697 Revert "fix: treat copilot oauth tokens as non-expiring"
This reverts commit 35228ecae9.
2026-01-23 07:14:00 +00:00
Peter Steinberger
9c1f1476bc docs: fix Lobster changelog placement 2026-01-23 07:12:13 +00:00
Peter Steinberger
e8352c8d21 fix: stabilize cron log wait 2026-01-23 07:11:01 +00:00
Peter Steinberger
551685351f fix: sanitize assistant session text (#1456) (thanks @zerone0x) 2026-01-23 07:05:31 +00:00
Peter Steinberger
3fbbac07fe fix: prioritize Anthropic token auth option 2026-01-23 07:04:18 +00:00
zerone0x
03bec49299 fix: sanitize tool call text in sessions-helpers extractAssistantText
Adds sanitization to extractAssistantText in sessions-helpers.ts to
prevent tool call text from leaking to users. Previously, messages
retrieved from chat history via sessions-helpers.ts could expose:

- Minimax XML tool calls (<invoke>...</invoke>)
- Downgraded tool call markers ([Tool Call: name (ID: ...)])
- Thinking tags (<think>...</think>)

This fix:
- Exports the stripping functions from pi-embedded-utils.ts
- Adds a new sanitizeTextContent helper in sessions-helpers.ts
- Updates extractAssistantText to sanitize before returning
- Updates extractMessageText in commands-subagents.ts to sanitize

Fixes #1269

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-23 07:03:26 +00:00
Peter Steinberger
6779ba2367 fix(tui): hide off think/verbose in footer 2026-01-23 07:02:56 +00:00
Peter Steinberger
8598e906ef docs: highlight compaction safeguards in changelog 2026-01-23 06:41:23 +00:00
Peter Steinberger
300fc486a4 test: avoid double cron finish wait 2026-01-23 06:40:14 +00:00
Peter Steinberger
f014b46b56 test: harden onboarding/discord/telegram test setup 2026-01-23 06:38:16 +00:00
Peter Steinberger
833f5acda1 test: stabilize cron + async search timings 2026-01-23 06:38:16 +00:00
Dave Lauer
d03c404cb4 feat(compaction): add adaptive chunk sizing, progressive fallback, and UI indicator (#1466)
* fix(ui): allow relative URLs in avatar validation

The isAvatarUrl check only accepted http://, https://, or data: URLs,
but the /avatar/{agentId} endpoint returns relative paths like /avatar/main.
This caused local file avatars to display as text instead of images.

Fixes avatar display for locally configured avatar files.

* fix(gateway): resolve local avatars to URL in HTML injection and RPC

The frontend fix alone wasn't enough because:
1. serveIndexHtml() was injecting the raw avatar filename into HTML
2. agent.identity.get RPC was returning raw filename, overwriting the
   HTML-injected value

Now both paths resolve local file avatars (*.png, *.jpg, etc.) to the
/avatar/{agentId} endpoint URL.

* feat(compaction): add adaptive chunk sizing and progressive fallback

- Add computeAdaptiveChunkRatio() to reduce chunk size for large messages
- Add isOversizedForSummary() to detect messages too large to summarize
- Add summarizeWithFallback() with progressive fallback:
  - Tries full summarization first
  - Falls back to partial summarization excluding oversized messages
  - Notes oversized messages in the summary output
- Add SAFETY_MARGIN (1.2x) buffer for token estimation inaccuracy
- Reduce MIN_CHUNK_RATIO to 0.15 for very large messages

This prevents compaction failures when conversations contain
unusually large tool outputs or responses that exceed the
summarization model's context window.

* feat(ui): add compaction indicator and improve event error handling

Compaction indicator:
- Add CompactionStatus type and handleCompactionEvent() in app-tool-stream.ts
- Show '🧹 Compacting context...' toast while active (with pulse animation)
- Show '🧹 Context compacted' briefly after completion
- Auto-clear toast after 5 seconds
- Add CSS styles for .callout.info, .callout.success, .compaction-indicator

Error handling improvements:
- Wrap onEvent callback in try/catch in gateway.ts to prevent errors
  from breaking the WebSocket message handler
- Wrap handleGatewayEvent in try/catch with console.error logging
  to isolate errors and make them visible in devtools

These changes address UI freezes during heavy agent activity by:
1. Showing users when compaction is happening
2. Preventing uncaught errors from silently breaking the event loop

* fix(control-ui): add agentId to DEFAULT_ASSISTANT_IDENTITY

TypeScript inferred the union type without agentId when falling back to
DEFAULT_ASSISTANT_IDENTITY, causing build errors at control-ui.ts:222-223.
2026-01-23 06:32:30 +00:00
Peter Steinberger
68ea6e521b fix: reduce Slack WebClient retries 2026-01-23 06:31:53 +00:00
Peter Steinberger
4912e85ac8 fix: fall back to non-PTY exec 2026-01-23 06:27:26 +00:00
Peter Steinberger
39d8ff59aa test: trim plugin + telegram test setup 2026-01-23 06:22:09 +00:00
Peter Steinberger
070944f64f test(memory): speed up batch coverage 2026-01-23 06:22:09 +00:00
Peter Steinberger
d4db45e8a9 test(agents): merge sessions_spawn group announce coverage 2026-01-23 06:22:09 +00:00
Peter Steinberger
451792d326 test(commands): streamline onboarding tests 2026-01-23 06:22:09 +00:00
Peter Steinberger
c7ca312f97 test(gateway): consolidate server suites for speed 2026-01-23 06:22:09 +00:00
ganghyun kim
1e6e58b23b fix: clarify Discord onboarding hint (#1487)
Thanks @kyleok.

Co-authored-by: Ganghyun Kim <58307870+kyleok@users.noreply.github.com>
2026-01-23 06:11:41 +00:00
Peter Steinberger
e98e71401a fix: always skip browser opens in tests 2026-01-23 06:00:21 +00:00
Peter Steinberger
bec1d0d3d4 fix: extend gateway chat test timeout on windows 2026-01-23 05:55:35 +00:00
Peter Steinberger
9f6ea67415 fix: gateway summary lookup + test browser opens 2026-01-23 05:54:51 +00:00
Peter Steinberger
bd7443b39b docs: update media auto-detect 2026-01-23 05:47:16 +00:00
Peter Steinberger
93bef830ce test: add media auto-detect coverage 2026-01-23 05:47:13 +00:00
Peter Steinberger
2dfbd1c1f6 feat: improve media auto-detect 2026-01-23 05:47:09 +00:00
Peter Steinberger
1d9f230be4 docs: expand slack replyToModeByChatType examples 2026-01-23 05:38:28 +00:00
Peter Steinberger
9bf295da48 feat: add slack replyToModeByChatType overrides 2026-01-23 05:38:28 +00:00
Peter Steinberger
eebd750781 fix: improve matrix direct room resolution (#1436) (thanks @sibbl) (#1486)
* fix: improve matrix direct room resolution (#1436) (thanks @sibbl)

* docs: update changelog for matrix fix (#1486) (thanks @sibbl)
2026-01-23 05:38:04 +00:00
Sebastian Schubotz
aa11300175 fix(matrix): broken import and enhance direct room resolve logic (#1436)
* fix(matrix): fix broken import again

* fix(matrix): improve error handling and fallback logic in resolveDirectRoomId
2026-01-23 05:35:01 +00:00
Stefan Galescu
7b40d1b261 feat(slack): add dm-specific replyToMode configuration (#1442)
Adds support for separate replyToMode settings for DMs vs channels:

- Add channels.slack.dm.replyToMode for DM-specific threading
- Keep channels.slack.replyToMode as default for channels
- Add resolveSlackReplyToMode helper to centralize logic
- Pass chatType through threading resolution chain

Usage:
```json5
{
  channels: {
    slack: {
      replyToMode: "off",     // channels
      dm: {
        replyToMode: "all"    // DMs always thread
      }
    }
  }
}
```

When dm.replyToMode is set, DMs use that mode; channels use the
top-level replyToMode. Backward compatible when not configured.
2026-01-23 05:13:23 +00:00
Peter Steinberger
2c10c601a8 test: harden docker onboarding waits 2026-01-23 05:10:59 +00:00
Travis Irby
578ac9f1a9 hydrate files from thread root message on replies
When replying to a Slack thread, files attached to the root message were
  not being fetched. The existing `resolveSlackThreadStarter()` fetched the
  root message text via `conversations.replies` but ignored the `files[]`
  array in the response.

  Changes:
  - Add `files` to `SlackThreadStarter` type and extract from API response
  - Download thread starter files when the reply message has no attachments
  - Add verbose log for thread starter file hydration

  Fixes issue where asking about a PDF in a thread reply would fail because
  the model never received the file content from the root message.
2026-01-23 05:10:36 +00:00
Neo
2accb47e4d fix: follow soul.md more closely (#1434)
* Agents: honor SOUL.md persona guidance

* fix: harden SOUL.md detection (#1434) (thanks @neooriginal)

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-01-23 05:00:13 +00:00
Tak hoffman
b65916e0d1 CLI: fix Windows gateway startup 2026-01-23 04:47:01 +00:00
Peter Steinberger
9207840db4 docs: note #1482 in changelog 2026-01-23 04:38:08 +00:00
Peter Steinberger
784468d6c3 fix: harden BlueBubbles voice memos (#1477) (thanks @Nicell) 2026-01-23 04:38:08 +00:00
Clawd
02b5f403db feat(bluebubbles): add asVoice support for voice memos
Add asVoice parameter to sendBlueBubblesAttachment that converts audio
to iMessage voice memo format (Opus CAF at 48kHz) and sets isAudioMessage
flag in the BlueBubbles API.

This follows the existing asVoice pattern used by Telegram.

- Convert audio to Opus CAF format using ffmpeg when asVoice=true
- Set isAudioMessage=true in BlueBubbles attachment API
- Pass asVoice through action handler and media-send
2026-01-23 04:34:19 +00:00
Peter Steinberger
5d0d9e6323 feat: refine onboarding hatch flow 2026-01-23 04:32:23 +00:00
Peter Steinberger
64be2b2cd1 test: speed up gateway suite setup 2026-01-23 04:28:02 +00:00
Rodrigo Uroz
dd2400fb2a fix: read Slack thread replies for message reads (#1450) (#1450)
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Rodrigo Uroz <rodrigouroz@users.noreply.github.com>
2026-01-23 04:17:45 +00:00
Peter Steinberger
5d001cb953 refactor: add config logging helpers 2026-01-23 04:16:39 +00:00
Peter Steinberger
d23c4a3f10 fix: put plugin descriptions under source 2026-01-23 04:02:42 +00:00
Peter Steinberger
e750ad5e75 refactor: centralize config update logging 2026-01-23 04:01:26 +00:00
Paulo Portella
246ee490f6 docs: add pauloportella to clawtributors 2026-01-23 03:58:57 +00:00
Peter Steinberger
d62a20fba9 chore: add open-prose license 2026-01-23 03:53:03 +00:00
Peter Steinberger
7f68bf79b6 fix: prefer ~ for home paths in output 2026-01-23 03:44:31 +00:00
Peter Steinberger
34bb7250f8 fix: resolve changelog merge markers 2026-01-23 03:44:14 +00:00
Peter Steinberger
34696dc8b9 Merge pull request #1432 from tobiasbischoff/main
fix(auth): skip auth profiles in cooldown during selection and rotation
2026-01-23 03:35:25 +00:00
Peter Steinberger
9a9afb389a Merge origin/main into pr-1432 2026-01-23 03:35:16 +00:00
Peter Steinberger
1e9ae7649d docs: add changelog entry for #1432 2026-01-23 03:31:42 +00:00
Peter Steinberger
5cb9026541 fix: honor user-pinned profiles and search ranking 2026-01-23 03:28:47 +00:00
Tobias Bischoff
81e78dced5 perf(tui): optimize searchable select list filtering
- Add regex caching to avoid creating new RegExp objects on each render
- Optimize smartFilter to use single array with tier-based scoring
- Replace non-existent fuzzyFilter import with local fuzzyFilterLower
- Reduces from 4 array allocations and 4 sorts to 1 array and 1 sort

Fixes pre-existing bug where fuzzyFilter was imported from pi-tui but not exported.
2026-01-23 03:28:18 +00:00
Tobias Bischoff
565944ec71 fix(auth): skip auth profiles in cooldown during selection and rotation
Auth profiles in cooldown (due to rate limiting) were being attempted,
causing unnecessary retries and delays. This fix ensures:

1. Initial profile selection skips profiles in cooldown
2. Profile rotation (after failures) skips cooldown profiles
3. Clear error message when all profiles are unavailable

Tests added:
- Skips profiles in cooldown during initial selection
- Skips profiles in cooldown when rotating after failure

Fixes #1316
2026-01-23 03:28:18 +00:00
Peter Steinberger
ec2c69c230 fix: honor gateway env token for doctor/security
Co-authored-by: azade-c <azade-c@users.noreply.github.com>
2026-01-23 03:16:52 +00:00
Peter Steinberger
f1deffa681 fix: repair docs redirects 2026-01-23 03:13:12 +00:00
Peter Steinberger
4b19066cc1 fix: normalize Windows exec allowlist paths 2026-01-23 03:11:41 +00:00
Peter Steinberger
ea79b26b79 feat: extend lobster tool run args 2026-01-23 03:09:59 +00:00
Peter Steinberger
6eb355954c docs: add changelog entry for #1432 2026-01-23 03:06:10 +00:00
Peter Steinberger
91ca52d3c5 fix: honor user-pinned profiles and search ranking 2026-01-23 03:05:01 +00:00
Peter Steinberger
0149d2b678 test: speed up test suite 2026-01-23 02:55:38 +00:00
Peter Steinberger
ecfddb7807 docs: fix lobster links 2026-01-23 02:51:33 +00:00
Peter Steinberger
35228ecae9 fix: treat copilot oauth tokens as non-expiring 2026-01-23 02:51:33 +00:00
Peter Steinberger
cfcc4548bb fix: set Copilot user agent header 2026-01-23 02:51:33 +00:00
Peter Steinberger
21a9b3b66f fix: improve GitHub Copilot integration 2026-01-23 02:51:33 +00:00
Peter Steinberger
837749dced fix: honor send path/filePath inputs (#1444) (thanks @hopyky) 2026-01-23 02:27:47 +00:00
Peter Steinberger
59a8eecd7e test: speed up test suite 2026-01-23 02:22:02 +00:00
Peter Steinberger
542cf011a0 Merge pull request #1444 from hopyky/fix-message-path-parameter
Fix: Support path and filePath parameters in message send action
2026-01-23 02:10:54 +00:00
Peter Steinberger
4355d9acca fix: resolve heartbeat sender and Slack thread_ts 2026-01-23 02:05:34 +00:00
Peter Steinberger
712bc74c30 docs: highlight mattermost plugin 2026-01-23 01:39:36 +00:00
Peter Steinberger
0396b678fa docs: note transcript hygiene sync 2026-01-23 01:38:05 +00:00
Peter Steinberger
eaf1b6bfee docs: simplify OpenProse install 2026-01-23 01:37:54 +00:00
Peter Steinberger
06cb2bf58d docs: expand mattermost intro 2026-01-23 01:35:50 +00:00
Peter Steinberger
8fdb3b38eb docs: add mattermost redirect 2026-01-23 01:35:15 +00:00
Peter Steinberger
5689d7fb98 refactor: remove transcript sanitize extension 2026-01-23 01:34:33 +00:00
Peter Steinberger
2424404fb4 docs: add transcript hygiene reference 2026-01-23 01:34:21 +00:00
Peter Steinberger
17a09cc721 Merge pull request #1472 from czekaj/fix/logs-follow-spinner
fix: suppress spinner in logs --follow mode
2026-01-23 01:29:30 +00:00
Peter Steinberger
bc4d8ce398 docs: link Lobster and OpenProse 2026-01-23 01:29:17 +00:00
Peter Steinberger
279f799388 fix: harden Mattermost plugin gating (#1428) (thanks @damoahdominic) 2026-01-23 01:23:23 +00:00
Peter Steinberger
1d658109a8 docs: remove OpenProse telemetry mentions 2026-01-23 01:20:30 +00:00
Peter Steinberger
5a446f3a21 docs: expand OpenProse guide 2026-01-23 01:08:55 +00:00
Peter Steinberger
52b6bf04af fix: improve tool summaries 2026-01-23 01:00:24 +00:00
Lucas Czekaj
76a42da676 fix: suppress spinner in logs --follow mode
The progress spinner was being shown for each gateway RPC call during
log tailing, causing repeated spinner frames (◇ │) to appear every
polling interval.

Add a `progress` option to `callGatewayFromCli` and disable the spinner
during follow mode polling to keep output clean.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 16:58:42 -08:00
Peter Steinberger
51a9053387 feat: add OpenProse plugin skills 2026-01-23 00:49:40 +00:00
Peter Steinberger
db0235a26a fix: gate transcript sanitization by provider 2026-01-23 00:42:45 +00:00
Peter Steinberger
fac21e6eb4 Merge pull request #1428 from bestparents/feat/mattermost-channel
feat: add Mattermost channel support
2026-01-23 00:24:47 +00:00
Peter Steinberger
e872f5335b fix: allow chained exec allowlists
Co-authored-by: Lucas Czekaj <1464539+czekaj@users.noreply.github.com>
2026-01-23 00:11:58 +00:00
Peter Steinberger
a23e272877 Merge pull request #1440 from robbyczgw-cla/fix/token-count-after-compaction
fix: update token count display after compaction
2026-01-23 00:10:46 +00:00
Peter Steinberger
870bfa94ed fix: skip tool id sanitization for openai responses 2026-01-22 23:51:59 +00:00
Peter Steinberger
d297e17958 refactor: centralize control ui avatar helpers 2026-01-22 23:41:36 +00:00
Peter Steinberger
6a25e23909 fix: tui local shell consent UX (#1463)
- add local shell runner + denial notice + tests
- docs: describe ! local shell usage
- lint: drop unused Slack upload contentType
- cleanup: remove stray Swabble pins

Thanks @vignesh07.
Co-authored-by: Vignesh Natarajan <vigneshnatarajan92@gmail.com>
2026-01-22 23:38:44 +00:00
Vignesh Natarajan
dc66527114 tui: clarify local shell exec consent prompt 2026-01-22 23:26:01 +00:00
Vignesh Natarajan
110b5dafee tui: keep trimming for normal submits; only raw ! triggers bash 2026-01-22 23:26:01 +00:00
Vignesh Natarajan
5fd699d0bf tui: add local shell execution for !-prefixed lines 2026-01-22 23:26:01 +00:00
Peter Steinberger
c1e50b7184 docs: clarify node service commands 2026-01-22 23:22:56 +00:00
Peter Steinberger
c7e0dc10fc docs: fix remaining node ws references 2026-01-22 23:22:56 +00:00
Dominic Damoah
01579aa7d7 Merge branch 'main' into feat/mattermost-channel 2026-01-22 18:17:40 -05:00
Peter Steinberger
42cd8a02bb Merge pull request #1447 from jdrhyne/fix/slack-filetype-deprecation
fix(slack): remove deprecated filetype field from files.uploadV2 [AI]
2026-01-22 23:16:26 +00:00
Peter Steinberger
96f1846c2c docs: align node transport with gateway ws 2026-01-22 23:10:09 +00:00
Peter Steinberger
7c336588ea chore: drop tty from install e2e docker 2026-01-22 23:09:28 +00:00
Peter Steinberger
814e9a500e feat: add manual onboarding flow alias 2026-01-22 23:09:28 +00:00
Peter Steinberger
370896e994 fix(macos): prefer linked channel in health summaries 2026-01-22 23:09:28 +00:00
Peter Steinberger
573354f5e4 chore(dev): default restart-mac to attach-only 2026-01-22 23:08:56 +00:00
Peter Steinberger
c721947346 feat(macos): add attach-only launchd override 2026-01-22 23:08:56 +00:00
Peter Steinberger
56339a17cc fix: correct gog auth services example (#1454) (thanks @zerone0x) 2026-01-22 22:51:59 +00:00
Peter Steinberger
567d8e5aa4 Merge pull request #1454 from zerone0x/docs/fix-gog-auth-services-example
docs(gog): fix invalid service name in auth example
2026-01-22 22:50:48 +00:00
Peter Steinberger
da3a141c58 refactor: require session state for directive handling 2026-01-22 22:42:46 +00:00
Peter Steinberger
c0c8ee217f fix: clarify session_status model-use guidance 2026-01-22 22:42:37 +00:00
Peter Steinberger
411ce7e231 fix: surface concrete ai error details 2026-01-22 22:24:25 +00:00
Peter Steinberger
b709898fb3 Merge pull request #1461 from ameno-/fix/node-daemon-run
Fix node daemon command
2026-01-22 22:02:19 +00:00
Peter Steinberger
826013c990 docs: refresh nodes + pairing docs 2026-01-22 22:02:06 +00:00
Peter Steinberger
482fcd2f2c fix: resolve control UI avatar URLs (#1457) (thanks @dlauer) 2026-01-22 21:58:46 +00:00
Peter Steinberger
6c7f224ce1 Merge pull request #1457 from dlauer/fix/avatar-relative-url-validation
fix(ui): allow relative URLs in avatar validation
2026-01-22 21:57:27 +00:00
Peter Steinberger
db146837a1 fix: move session-memory changelog entry 2026-01-22 21:55:10 +00:00
Dave Lauer
ffca65d15f fix(gateway): resolve local avatars to URL in HTML injection and RPC
The frontend fix alone wasn't enough because:
1. serveIndexHtml() was injecting the raw avatar filename into HTML
2. agent.identity.get RPC was returning raw filename, overwriting the
   HTML-injected value

Now both paths resolve local file avatars (*.png, *.jpg, etc.) to the
/avatar/{agentId} endpoint URL.
2026-01-22 15:16:31 -05:00
Ameno Osman
654b6a943b fix(node): use node run for node daemon 2026-01-22 11:15:51 -08:00
Robby
768d5ccafe style: fix formatting 2026-01-22 17:47:52 +00:00
Dominic Damoah
8b3cb373d4 fix: remove unused originatingChannel variable
Remove unused originatingChannel variable from runPreparedReply function that was assigned but never referenced.
2026-01-22 12:11:05 -05:00
Dave Lauer
9d09a7879c fix(ui): allow relative URLs in avatar validation
The isAvatarUrl check only accepted http://, https://, or data: URLs,
but the /avatar/{agentId} endpoint returns relative paths like /avatar/main.
This caused local file avatars to display as text instead of images.

Fixes avatar display for locally configured avatar files.
2026-01-22 12:09:27 -05:00
Dominic Damoah
495a39b5a9 refactor: extract mattermost channel plugin to extension
Move mattermost channel implementation from core to extensions/mattermost plugin. Extract config schema, group mentions, normalize utilities, and all mattermost-specific logic (accounts, client, monitor, probe, send) into the extension. Update imports to use plugin SDK and local modules. Add channel metadata directly in plugin definition instead of using getChatChannelMeta. Update package.json with channel and install configuration.
2026-01-22 12:02:30 -05:00
zerone0x
ba824a4b2d docs(gog): fix invalid service name in auth example
Replace invalid "docs" service with the correct "tasks,people" services
in the setup example. The gog CLI does not have a "docs" service - docs
commands (export/cat) use Drive authentication instead.

Fixes #1433

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-23 00:45:20 +08:00
Jonathan Rhyne
8b6b97c3f6 docs: add changelog entry for PR #1447 2026-01-22 08:39:54 -05:00
Jonathan Rhyne
47e440f73a fix(slack): remove deprecated filetype field from files.uploadV2
Slack's files.uploadV2 API no longer supports the filetype field and logs
deprecation warnings when it's included. Slack auto-detects the file type
from the file content, so this field is unnecessary.

This removes the warning:
[WARN] web-api:WebClient filetype is no longer a supported field in files.uploadV2.
2026-01-22 08:33:13 -05:00
Matt mini
57e81d3c24 Fix: Support path and filePath parameters in message send action
The message tool accepts path and filePath parameters in its schema,
but these were never converted to mediaUrl, causing local files to
be ignored when sending messages.

Changes:
- src/agents/tools/message-tool.ts: Convert path/filePath to media with file:// URL
- src/infra/outbound/message-action-runner.ts: Allow hydrateSendAttachmentParams for "send" action

Fixes issue where local audio files (and other media) couldn't be sent
via the message tool with the path parameter.

Users can now use:
  message({ path: "/tmp/file.ogg" })
  message({ filePath: "/tmp/file.ogg" })
2026-01-22 13:15:48 +01:00
Robby
0873351401 fix: update token count display after compaction (#1299) 2026-01-22 09:58:07 +00:00
Tobias Bischoff
917bcb714e perf(tui): optimize searchable select list filtering
- Add regex caching to avoid creating new RegExp objects on each render
- Optimize smartFilter to use single array with tier-based scoring
- Replace non-existent fuzzyFilter import with local fuzzyFilterLower
- Reduces from 4 array allocations and 4 sorts to 1 array and 1 sort

Fixes pre-existing bug where fuzzyFilter was imported from pi-tui but not exported.
2026-01-22 10:29:37 +01:00
Tobias Bischoff
3d8a759eba fix(auth): skip auth profiles in cooldown during selection and rotation
Auth profiles in cooldown (due to rate limiting) were being attempted,
causing unnecessary retries and delays. This fix ensures:

1. Initial profile selection skips profiles in cooldown
2. Profile rotation (after failures) skips cooldown profiles
3. Clear error message when all profiles are unavailable

Tests added:
- Skips profiles in cooldown during initial selection
- Skips profiles in cooldown when rotating after failure

Fixes #1316
2026-01-22 10:04:56 +01:00
Dominic Damoah
91278d8b4e Merge branch 'main' into feat/mattermost-channel 2026-01-22 03:11:53 -05:00
Dominic Damoah
fe77d3eb56 Merge branch 'main' into feat/mattermost-channel 2026-01-22 02:49:17 -05:00
Dominic Damoah
bf6df6d6b7 feat: add Mattermost channel support
Add Mattermost as a supported messaging channel with bot API and WebSocket integration. Includes channel state tracking (tint, summary, details), multi-account support, and delivery target routing. Update documentation and tests to include Mattermost alongside existing channels.
2026-01-21 18:40:56 -05:00
825 changed files with 50841 additions and 12392 deletions

View File

@@ -29,5 +29,6 @@ jobs:
CLAWDBOT_INSTALL_CLI_URL: https://clawd.bot/install-cli.sh
CLAWDBOT_NO_ONBOARD: "1"
CLAWDBOT_INSTALL_SMOKE_SKIP_CLI: "1"
CLAWDBOT_INSTALL_SMOKE_SKIP_NONROOT: ${{ github.event_name == 'pull_request' && '1' || '0' }}
CLAWDBOT_INSTALL_SMOKE_PREVIOUS: "2026.1.11-4"
run: pnpm test:install:smoke

View File

@@ -7,6 +7,7 @@
- Tests: colocated `*.test.ts`.
- Docs: `docs/` (images, queue, Pi config). Built output lives in `dist/`.
- Plugins/extensions: live under `extensions/*` (workspace packages). Keep plugin-only deps in the extension `package.json`; do not add them to the root `package.json` unless core uses them.
- Plugins: install runs `npm install --omit=dev` in plugin dir; runtime deps must live in `dependencies`. Avoid `workspace:*` in `dependencies` (npm install breaks); put `clawdbot` in `devDependencies` or `peerDependencies` instead (runtime resolves `clawdbot/plugin-sdk` via jiti alias).
- Installers served from `https://clawd.bot/*`: live in the sibling repo `../clawd.bot` (`public/install.sh`, `public/install-cli.sh`, `public/install.ps1`).
- Messaging channels: always consider **all** built-in + extension channels when refactoring shared logic (routing, allowlists, pairing, command gating, onboarding, docs).
- Core channel docs: `docs/channels/`
@@ -22,6 +23,16 @@
- README (GitHub): keep absolute docs URLs (`https://docs.clawd.bot/...`) so links work on GitHub.
- Docs content must be generic: no personal device names/hostnames/paths; use placeholders like `user@gateway-host` and “gateway host”.
## exe.dev VM ops (general)
- Access: stable path is `ssh exe.dev` then `ssh vm-name` (assume SSH key already set).
- SSH flaky: use exe.dev web terminal or Shelley (web agent); keep a tmux session for long ops.
- Update: `sudo npm i -g clawdbot@latest` (global install needs root on `/usr/lib/node_modules`).
- Config: use `clawdbot config set ...`; ensure `gateway.mode=local` is set.
- Discord: store raw token only (no `DISCORD_BOT_TOKEN=` prefix).
- Restart: stop old gateway and run:
`pkill -9 -f clawdbot-gateway || true; nohup clawdbot gateway run --bind loopback --port 18789 --force > /tmp/clawdbot-gateway.log 2>&1 &`
- Verify: `clawdbot channels status --probe`, `ss -ltnp | rg 18789`, `tail -n 120 /tmp/clawdbot-gateway.log`.
## Build, Test, and Development Commands
- Runtime baseline: Node **22+** (keep Node + Bun paths working).
- Install deps: `pnpm install`
@@ -51,6 +62,7 @@
- Framework: Vitest with V8 coverage thresholds (70% lines/branches/functions/statements).
- Naming: match source names with `*.test.ts`; e2e in `*.e2e.test.ts`.
- Run `pnpm test` (or `pnpm test:coverage`) before pushing when you touch logic.
- Do not set test workers above 16; tried already.
- Live tests (real keys): `CLAWDBOT_LIVE_TEST=1 pnpm test:live` (Clawdbot-only) or `LIVE=1 pnpm test:live` (includes provider live tests). Docker: `pnpm test:docker:live-models`, `pnpm test:docker:live-gateway`. Onboarding Docker E2E: `pnpm test:docker:onboard`.
- Full kit + whats covered: `docs/testing.md`.
- Pure test additions/fixes generally do **not** need a changelog entry unless they alter user-facing behavior or the user asks for one.
@@ -65,6 +77,7 @@
- PR review flow: when given a PR link, review via `gh pr view`/`gh pr diff` and do **not** change branches.
- PR review calls: prefer a single `gh pr view --json ...` to batch metadata/comments; run `gh pr diff` only when needed.
- Before starting a review when a GH Issue/PR is pasted: run `git pull`; if there are local changes or unpushed commits, stop and alert the user before reviewing.
- Goal: merge PRs. Prefer **rebase** when commits are clean; **squash** when history is messy.
- PR merge flow: create a temp branch from `main`, merge the PR branch into it (prefer squash unless commit history is important; use rebase/merge when it is). Always try to merge the PR unless its truly difficult, then use another approach. If we squash, add the PR author as a co-contributor. Apply fixes, add changelog entry (include PR # + thanks), run full gate before the final commit, commit, merge back to `main`, delete the temp branch, and end on `main`.
- If you review a PR and later do work on it, land via merge/squash (no direct-main commits) and always add the PR author as a co-contributor.
- When working on a PR: add a changelog entry with the PR number and thank the contributor.
@@ -118,6 +131,10 @@
- **Multi-agent safety:** do **not** switch branches / check out a different branch unless explicitly requested.
- **Multi-agent safety:** running multiple agents is OK as long as each agent has its own session.
- **Multi-agent safety:** when you see unrecognized files, keep going; focus on your changes and commit only those.
- Lint/format churn:
- If staged+unstaged diffs are formatting-only, auto-resolve without asking.
- If commit/push already requested, auto-stage and include formatting-only follow-ups in the same commit (or a tiny follow-up commit if needed), no extra confirmation.
- Only ask when changes are semantic (logic/data/behavior).
- Lobster seam: use the shared CLI palette in `src/terminal/palette.ts` (no hardcoded colors); apply palette to onboarding/config prompts and other TTY UI output as needed.
- **Multi-agent safety:** focus reports on your edits; avoid guard-rail disclaimers unless truly blocked; when multiple agents touch the same file, continue if safe; end with a brief “other files present” note only if relevant.
- Bug investigations: read source code of relevant npm dependencies and all related local code before concluding; aim for high-confidence root cause.

View File

@@ -2,77 +2,127 @@
Docs: https://docs.clawd.bot
## 2026.1.22 (Unreleased)
## 2026.1.23 (Unreleased)
### Changes
- Plugins: add optional llm-task JSON-only tool for workflows. (#1498) Thanks @vignesh07.
- CLI: restart the gateway by default after `clawdbot update`; add `--no-restart` to skip it.
- CLI: add live auth probes to `clawdbot models status` for per-profile verification.
- Docs: add cron vs heartbeat decision guide (with Lobster workflow notes). (#1533) Thanks @JustYannicc.
- Markdown: add per-channel table conversion (bullets for Signal/WhatsApp, code blocks elsewhere). (#1495) Thanks @odysseus0.
- Tlon: add Urbit channel plugin (DMs, group mentions, thread replies). (#1544) Thanks @wca4a.
### Fixes
- Voice wake: auto-save wake words on blur/submit across iOS/Android and align limits with macOS.
- UI: keep the Control UI sidebar visible while scrolling long pages. (#1515) Thanks @pookNast.
- Tailscale: retry serve/funnel with sudo only for permission errors and keep original failure details. (#1551) Thanks @sweepies.
- Discord: limit autoThread mention bypass to bot-owned threads; keep ack reactions mention-gated. (#1511) Thanks @pvoo.
- Gateway: accept null optional fields in exec approval requests. (#1511) Thanks @pvoo.
- TUI: forward unknown slash commands (for example, `/context`) to the Gateway.
- TUI: include Gateway slash commands in autocomplete and `/help`.
- CLI: skip usage lines in `clawdbot models status` when provider usage is unavailable.
- CLI: suppress diagnostic session/run noise during auth probes.
- CLI: hide auth probe timeout warnings from embedded runs.
- CLI: render auth probe results as a table in `clawdbot models status`.
- CLI: suppress probe-only embedded logs unless `--verbose` is set.
- CLI: move auth probe errors below the table to reduce wrapping.
- Linux: include env-configured user bin roots in systemd PATH and align PATH audits. (#1512) Thanks @robbyczgw-cla.
- TUI: render Gateway slash-command replies as system output (for example, `/context`).
- Media: preserve PNG alpha when possible; fall back to JPEG when still over size cap. (#1491) Thanks @robbyczgw-cla.
- Agents: treat plugin-only tool allowlists as opt-ins; keep core tools enabled. (#1467)
- Exec approvals: persist allowlist entry ids to keep macOS allowlist rows stable. (#1521) Thanks @ngutman.
- MS Teams (plugin): remove `.default` suffix from Graph scopes to avoid double-appending. (#1507) Thanks @Evizero.
## 2026.1.22
### Changes
- Highlight: Compaction safeguard now uses adaptive chunking, progressive fallback, and UI status + retries. (#1466) Thanks @dlauer.
- Providers: add Antigravity usage tracking to status output. (#1490) Thanks @patelhiren.
- Slack: add chat-type reply threading overrides via `replyToModeByChatType`. (#1442) Thanks @stefangalescu.
- BlueBubbles: add `asVoice` support for MP3/CAF voice memos in sendAttachment. (#1477, #1482) Thanks @Nicell.
- Onboarding: add hatch choice (TUI/Web/Later), token explainer, background dashboard seed on macOS, and showcase link.
### Fixes
- BlueBubbles: stop typing indicator on idle/no-reply. (#1439) Thanks @Nicell.
- Message tool: keep path/filePath as-is for send; hydrate buffers only for sendAttachment. (#1444) Thanks @hopyky.
- Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla.
- Node: launch node host service with `node run` (not `node start`) and align node host hints/docs. (#1461) Thanks @ameno-.
- Control UI: resolve local avatar URLs with basePath across injection + identity RPC. (#1457) Thanks @dlauer.
- Agents: sanitize assistant history text to strip tool-call markers. (#1456) Thanks @zerone0x.
- Discord: clarify Message Content Intent onboarding hint. (#1487) Thanks @kyleok.
- Gateway: stop the service before uninstalling and fail if it remains loaded.
- Agents: surface concrete API error details instead of generic AI service errors.
- Exec: fall back to non-PTY when PTY spawn fails (EBADF). (#1484)
- Exec approvals: allow per-segment allowlists for chained shell commands on gateway + node hosts. (#1458) Thanks @czekaj.
- Agents: make OpenAI sessions image-sanitize-only; gate tool-id/repair sanitization by provider.
- Doctor: honor CLAWDBOT_GATEWAY_TOKEN for auth checks and security audit token reuse. (#1448) Thanks @azade-c.
- Agents: make tool summaries more readable and only show optional params when set.
- Agents: honor SOUL.md guidance even when the file is nested or path-qualified. (#1434) Thanks @neooriginal.
- Matrix (plugin): persist m.direct for resolved DMs and harden room fallback. (#1436, #1486) Thanks @sibbl.
- CLI: prefer `~` for home paths in output.
- Mattermost (plugin): enforce pairing/allowlist gating, keep @username targets, and clarify plugin-only docs. (#1428) Thanks @damoahdominic.
- Agents: centralize transcript sanitization in the runner; keep <final> tags and error turns intact.
- Auth: skip auth profiles in cooldown during initial selection and rotation. (#1316) Thanks @odrobnik.
- Agents/TUI: honor user-pinned auth profiles during cooldown and preserve search picker ranking. (#1432) Thanks @tobiasbischoff.
- Docs: fix gog auth services example to include docs scope. (#1454) Thanks @zerone0x.
- Slack: reduce WebClient retries to avoid duplicate sends. (#1481)
- Slack: read thread replies for message reads when threadId is provided (replies-only). (#1450) Thanks @rodrigouroz.
- Discord: honor accountId across message actions and cron deliveries. (#1492) Thanks @svkozak.
- macOS: prefer linked channels in gateway summary to avoid false “not linked” status.
- macOS/tests: fix gateway summary lookup after guard unwrap; prevent browser opens during tests. (ECID-1483)
## 2026.1.21-2
### Fixes
- Control UI: ignore bootstrap identity placeholder text for avatar values and fall back to the default avatar. https://docs.clawd.bot/cli/agents https://docs.clawd.bot/web/control-ui
- Slack: remove deprecated `filetype` field from `files.uploadV2` to eliminate API warnings. (#1447)
## 2026.1.21
### Highlights
- Lobster optional plugin tool for typed workflows + approval gates. https://docs.clawd.bot/tools/lobster
- Custom assistant identity + avatars in the Control UI. https://docs.clawd.bot/cli/agents https://docs.clawd.bot/web/control-ui
- Cache optimizations: cache-ttl pruning + defaults reduce token spend on cold requests. https://docs.clawd.bot/concepts/session-pruning
- Exec approvals + elevated ask/full modes. https://docs.clawd.bot/tools/exec-approvals https://docs.clawd.bot/tools/elevated
- Signal typing/read receipts + MSTeams attachments. https://docs.clawd.bot/channels/signal https://docs.clawd.bot/channels/msteams
- `/models` UX refresh + `clawdbot update wizard`. https://docs.clawd.bot/cli/models https://docs.clawd.bot/cli/update
### Changes
- Highlight: Lobster optional plugin tool for typed workflows + approval gates. https://docs.clawd.bot/tools/lobster (#1152) Thanks @vignesh07.
- Agents/UI: add identity avatar config support and Control UI avatar rendering. (#1329, #1424) Thanks @dlauer. https://docs.clawd.bot/gateway/configuration https://docs.clawd.bot/cli/agents
- Control UI: add custom assistant identity support and per-session identity display. (#1420) Thanks @robbyczgw-cla. https://docs.clawd.bot/web/control-ui
- CLI: add `clawdbot update wizard` with interactive channel selection + restart prompts, plus preflight checks before rebasing. https://docs.clawd.bot/cli/update
- Models/Commands: add `/models`, improve `/model` listing UX, and expand `clawdbot models` paging. (#1398) Thanks @vignesh07. https://docs.clawd.bot/cli/models
- CLI: move gateway service commands under `clawdbot gateway`, flatten node service commands under `clawdbot node`, and add `gateway probe` for reachability. https://docs.clawd.bot/cli/gateway https://docs.clawd.bot/cli/node
- Exec: add elevated ask/full modes, tighten allowlist gating, and render approvals tables on write. https://docs.clawd.bot/tools/elevated https://docs.clawd.bot/tools/exec-approvals
- Exec approvals: default to local host, add gateway/node targeting + target details, support wildcard agent allowlists, and tighten allowlist parsing/safe bins. https://docs.clawd.bot/cli/approvals https://docs.clawd.bot/tools/exec-approvals
- Heartbeat: allow explicit session keys and active hours. (#1256) Thanks @zknicker. https://docs.clawd.bot/gateway/heartbeat
- Sessions: add per-channel idle durations via `sessions.channelIdleMinutes`. (#1353) Thanks @cash-echo-bot.
- Nodes: run exec-style, expose PATH in status/describe, and bootstrap PATH for node-host execution. https://docs.clawd.bot/cli/node
- Cache: add `cache.ttlPrune` mode and auth-aware defaults for cache TTL behavior.
- Queue: add per-channel debounce overrides for auto-reply. https://docs.clawd.bot/concepts/queue
- Discord: add wildcard channel config support. (#1334) Thanks @pvoo. https://docs.clawd.bot/channels/discord
- Signal: add typing indicators and DM read receipts via signal-cli. https://docs.clawd.bot/channels/signal
- MSTeams: add file uploads, adaptive cards, and attachment handling improvements. (#1410) Thanks @Evizero. https://docs.clawd.bot/channels/msteams
- Highlight: Lobster optional plugin tool for typed workflows + approval gates. https://docs.clawd.bot/tools/lobster
- Lobster: allow workflow file args via `argsJson` in the plugin tool. https://docs.clawd.bot/tools/lobster
- Heartbeat: allow running heartbeats in an explicit session key. (#1256) Thanks @zknicker.
- CLI: default exec approvals to the local host, add gateway/node targeting flags, and show target details in allowlist output.
- CLI: exec approvals mutations render tables instead of raw JSON.
- Exec approvals: support wildcard agent allowlists (`*`) across all agents.
- Exec approvals: allowlist matches resolved binary paths only, add safe stdin-only bins, and tighten allowlist shell parsing.
- Nodes: expose node PATH in status/describe and bootstrap PATH for node-host execution.
- CLI: flatten node service commands under `clawdbot node` and remove `service node` docs.
- CLI: move gateway service commands under `clawdbot gateway` and add `gateway probe` for reachability.
- Sessions: add per-channel reset overrides via `session.resetByChannel`. (#1353) Thanks @cash-echo-bot.
- Agents: add identity avatar config support and Control UI avatar rendering. (#1329, #1424) Thanks @dlauer.
- UI: show per-session assistant identity in the Control UI. (#1420) Thanks @robbyczgw-cla.
- CLI: add `clawdbot update wizard` for interactive channel selection and restart prompts. https://docs.clawd.bot/cli/update
- Signal: add typing indicators and DM read receipts via signal-cli.
- MSTeams: add file uploads, adaptive cards, and attachment handling improvements. (#1410) Thanks @Evizero.
- Onboarding: remove the run setup-token auth option (paste setup-token or reuse CLI creds instead).
- macOS: refresh Settings (location access in Permissions, connection mode in menu, remove CLI install UI).
- Diagnostics: add cache trace config for debugging. (#1370) Thanks @parubets.
- Docs: Lobster guides + org URL updates, /model allowlist troubleshooting, Gmail message search examples, gateway.mode troubleshooting, prompt injection guidance, npm prefix/node CLI notes, control UI dev gatewayUrl note, tool_use FAQ, showcase video, and sharp/node-gyp workaround. (#1427, #1220, #1405) Thanks @vignesh07, @mbelinky.
- Docs: add troubleshooting entry for gateway.mode blocking gateway start. https://docs.clawd.bot/gateway/troubleshooting
- Docs: add /model allowlist troubleshooting note. (#1405)
- Docs: add per-message Gmail search example for gog. (#1220) Thanks @mbelinky.
### Breaking
- **BREAKING:** Control UI now rejects insecure HTTP without device identity by default. Use HTTPS (Tailscale Serve) or set `gateway.controlUi.allowInsecureAuth: true` to allow token-only auth. https://docs.clawd.bot/web/control-ui#insecure-http
- **BREAKING:** Envelope and system event timestamps now default to host-local time (was UTC) so agents dont have to constantly convert.
### Fixes
- Streaming/Typing/Media: keep reply tags across streamed chunks, start typing indicators at run start, and accept MEDIA paths with spaces/tilde while preferring the message tool hint for image replies.
- Agents/Providers: drop unsigned thinking blocks for Claude models (Google Antigravity) and enforce alphanumeric tool call ids for strict providers (Mistral/OpenRouter). (#1372) Thanks @zerone0x.
- Exec approvals: treat main as the default agent, align node/gateway allowlist prechecks, validate resolved paths, avoid allowlist resolve races, and avoid null optional params. (#1417, #1414, #1425) Thanks @czekaj.
- Exec/Windows: resolve Windows exec paths with extensions and handle safe-bin exe names.
- Nodes/macOS: prompt on allowlist miss for node exec approvals, persist allowlist decisions, and flatten node invoke errors. (#1394) Thanks @ngutman.
- Gateway: prevent multiple gateways from sharing the same config/state (singleton lock), keep auto bind loopback-first with explicit tailnet binding, and improve SSH auth handling. (#1380)
- Control UI: remove the chat stop button, keep the composer aligned to the bottom edge, stabilize session previews, and refresh the debug panel on route-driven tab changes. (#1373) Thanks @yazinsai.
- UI/config: export `SECTION_META` for config form modules. (#1418) Thanks @MaudeBot.
- macOS: keep chat pinned during streaming replies, include Textual resources, respect wildcard exec approvals, allow SSH agent auth, and default distribution builds to universal binaries. (#1279, #1362, #1384, #1396) Thanks @ameno-, @JustYannicc.
- BlueBubbles: resolve short message IDs safely, expose full IDs in templates, and harden short-id fetch wrappers. (#1369, #1387) Thanks @tyler6204.
- Models/Configure: inherit session model overrides in threads/topics, map OpenCode Zen models to the correct APIs, narrow Anthropic OAuth allowlist handling, seed allowlist fallbacks, list the full catalog when no allowlist is set, and limit `/model` list output. (#1376, #1416)
- Memory: prevent CLI hangs by deferring vector probes, add sqlite-vec/embedding timeouts, and make session memory indexing async.
- Cron: cap reminder context history to 10 messages and honor `contextMessages`. (#1103) Thanks @mkbehr.
- Cache: restore the 1h cache TTL option and reset the pruning window.
- Zalo Personal: tolerate ANSI/log-prefixed JSON output from `zca`. (#1379) Thanks @ptn1411.
- Browser: suppress Chrome restore prompts for managed profiles. (#1419) Thanks @jamesgroat.
- Infra: preserve fetch helper methods/preconnect when wrapping abort signals and normalize Telegram fetch aborts.
- Config/Doctor: avoid stack traces for invalid configs, log the config path, avoid WhatsApp config resurrection, and warn when `gateway.mode` is unset. (#900)
- CLI: read Codex CLI account_id for workspace billing. (#1422) Thanks @aj47.
- Logs/Status: align rolling log filenames with local time and report sandboxed runtime in `clawdbot status`. (#1343)
- Gateway: keep auto bind loopback-first and add explicit tailnet binding to avoid Tailscale taking over local UI. (#1380)
- Memory: prevent CLI hangs by deferring vector probes, adding sqlite-vec/embedding timeouts, and showing sync progress early.
- Agents: enforce 9-char alphanumeric tool call ids for Mistral providers. (#1372) Thanks @zerone0x.
- Embedded runner: persist injected history images so attachments arent reloaded each turn. (#1374) Thanks @Nicell.
- Nodes/Subagents: include agent/node/gateway context in tool failure logs and ensure subagent list uses the command session.
- Nodes tool: include agent/node/gateway context in tool failure logs to speed approval debugging.
- macOS: exec approvals now respect wildcard agent allowlists (`*`).
- macOS: allow SSH agent auth when no identity file is set. (#1384) Thanks @ameno-.
- Gateway: prevent multiple gateways from sharing the same config/state at once (singleton lock).
- UI: remove the chat stop button and keep the composer aligned to the bottom edge.
- Typing: start instant typing indicators at run start so DMs and mentions show immediately.
- Configure: restrict the model allowlist picker to OAuth-compatible Anthropic models and preselect Opus 4.5.
- Configure: seed model fallbacks from the allowlist selection when multiple models are chosen.
- Model picker: list the full catalog when no model allowlist is configured.
- Discord: honor wildcard channel configs via shared match helpers. (#1334) Thanks @pvoo.
- BlueBubbles: resolve short message IDs safely and expose full IDs in templates. (#1387) Thanks @tyler6204.
- Infra: preserve fetch helper methods when wrapping abort signals. (#1387)
- macOS: default distribution packaging to universal binaries. (#1396) Thanks @JustYannicc.
## 2026.1.20

View File

@@ -477,28 +477,30 @@ Special thanks to [Mario Zechner](https://mariozechner.at/) for his support and
Thanks to all clawtributors:
<p align="left">
<a href="https://github.com/steipete"><img src="https://avatars.githubusercontent.com/u/58493?v=4&s=48" width="48" height="48" alt="steipete" title="steipete"/></a> <a href="https://github.com/bohdanpodvirnyi"><img src="https://avatars.githubusercontent.com/u/31819391?v=4&s=48" width="48" height="48" alt="bohdanpodvirnyi" title="bohdanpodvirnyi"/></a> <a href="https://github.com/joaohlisboa"><img src="https://avatars.githubusercontent.com/u/8200873?v=4&s=48" width="48" height="48" alt="joaohlisboa" title="joaohlisboa"/></a> <a href="https://github.com/mneves75"><img src="https://avatars.githubusercontent.com/u/2423436?v=4&s=48" width="48" height="48" alt="mneves75" title="mneves75"/></a> <a href="https://github.com/MatthieuBizien"><img src="https://avatars.githubusercontent.com/u/173090?v=4&s=48" width="48" height="48" alt="MatthieuBizien" title="MatthieuBizien"/></a> <a href="https://github.com/MaudeBot"><img src="https://avatars.githubusercontent.com/u/255777700?v=4&s=48" width="48" height="48" alt="MaudeBot" title="MaudeBot"/></a> <a href="https://github.com/rahthakor"><img src="https://avatars.githubusercontent.com/u/8470553?v=4&s=48" width="48" height="48" alt="rahthakor" title="rahthakor"/></a> <a href="https://github.com/vrknetha"><img src="https://avatars.githubusercontent.com/u/20596261?v=4&s=48" width="48" height="48" alt="vrknetha" title="vrknetha"/></a> <a href="https://github.com/radek-paclt"><img src="https://avatars.githubusercontent.com/u/50451445?v=4&s=48" width="48" height="48" alt="radek-paclt" title="radek-paclt"/></a> <a href="https://github.com/joshp123"><img src="https://avatars.githubusercontent.com/u/1497361?v=4&s=48" width="48" height="48" alt="joshp123" title="joshp123"/></a>
<a href="https://github.com/mukhtharcm"><img src="https://avatars.githubusercontent.com/u/56378562?v=4&s=48" width="48" height="48" alt="mukhtharcm" title="mukhtharcm"/></a> <a href="https://github.com/maxsumrall"><img src="https://avatars.githubusercontent.com/u/628843?v=4&s=48" width="48" height="48" alt="maxsumrall" title="maxsumrall"/></a> <a href="https://github.com/xadenryan"><img src="https://avatars.githubusercontent.com/u/165437834?v=4&s=48" width="48" height="48" alt="xadenryan" title="xadenryan"/></a> <a href="https://github.com/tobiasbischoff"><img src="https://avatars.githubusercontent.com/u/711564?v=4&s=48" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></a> <a href="https://github.com/juanpablodlc"><img src="https://avatars.githubusercontent.com/u/92012363?v=4&s=48" width="48" height="48" alt="juanpablodlc" title="juanpablodlc"/></a> <a href="https://github.com/hsrvc"><img src="https://avatars.githubusercontent.com/u/129702169?v=4&s=48" width="48" height="48" alt="hsrvc" title="hsrvc"/></a> <a href="https://github.com/magimetal"><img src="https://avatars.githubusercontent.com/u/36491250?v=4&s=48" width="48" height="48" alt="magimetal" title="magimetal"/></a> <a href="https://github.com/meaningfool"><img src="https://avatars.githubusercontent.com/u/2862331?v=4&s=48" width="48" height="48" alt="meaningfool" title="meaningfool"/></a> <a href="https://github.com/NicholasSpisak"><img src="https://avatars.githubusercontent.com/u/129075147?v=4&s=48" width="48" height="48" alt="NicholasSpisak" title="NicholasSpisak"/></a> <a href="https://github.com/sebslight"><img src="https://avatars.githubusercontent.com/u/19554889?v=4&s=48" width="48" height="48" alt="sebslight" title="sebslight"/></a>
<a href="https://github.com/AbhisekBasu1"><img src="https://avatars.githubusercontent.com/u/40645221?v=4&s=48" width="48" height="48" alt="abhisekbasu1" title="abhisekbasu1"/></a> <a href="https://github.com/jamesgroat"><img src="https://avatars.githubusercontent.com/u/2634024?v=4&s=48" width="48" height="48" alt="jamesgroat" title="jamesgroat"/></a> <a href="https://github.com/zerone0x"><img src="https://avatars.githubusercontent.com/u/39543393?v=4&s=48" width="48" height="48" alt="zerone0x" title="zerone0x"/></a> <a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude" title="claude"/></a> <a href="https://github.com/SocialNerd42069"><img src="https://avatars.githubusercontent.com/u/118244303?v=4&s=48" width="48" height="48" alt="SocialNerd42069" title="SocialNerd42069"/></a> <a href="https://github.com/Hyaxia"><img src="https://avatars.githubusercontent.com/u/36747317?v=4&s=48" width="48" height="48" alt="Hyaxia" title="Hyaxia"/></a> <a href="https://github.com/dantelex"><img src="https://avatars.githubusercontent.com/u/631543?v=4&s=48" width="48" height="48" alt="dantelex" title="dantelex"/></a> <a href="https://github.com/daveonkels"><img src="https://avatars.githubusercontent.com/u/533642?v=4&s=48" width="48" height="48" alt="daveonkels" title="daveonkels"/></a> <a href="https://github.com/mteam88"><img src="https://avatars.githubusercontent.com/u/84196639?v=4&s=48" width="48" height="48" alt="mteam88" title="mteam88"/></a> <a href="https://github.com/omniwired"><img src="https://avatars.githubusercontent.com/u/322761?v=4&s=48" width="48" height="48" alt="Eng. Juan Combetto" title="Eng. Juan Combetto"/></a>
<a href="https://github.com/mbelinky"><img src="https://avatars.githubusercontent.com/u/132747814?v=4&s=48" width="48" height="48" alt="Mariano Belinky" title="Mariano Belinky"/></a> <a href="https://github.com/dbhurley"><img src="https://avatars.githubusercontent.com/u/5251425?v=4&s=48" width="48" height="48" alt="dbhurley" title="dbhurley"/></a> <a href="https://github.com/TSavo"><img src="https://avatars.githubusercontent.com/u/877990?v=4&s=48" width="48" height="48" alt="TSavo" title="TSavo"/></a> <a href="https://github.com/julianengel"><img src="https://avatars.githubusercontent.com/u/10634231?v=4&s=48" width="48" height="48" alt="julianengel" title="julianengel"/></a> <a href="https://github.com/benithors"><img src="https://avatars.githubusercontent.com/u/20652882?v=4&s=48" width="48" height="48" alt="benithors" title="benithors"/></a> <a href="https://github.com/bradleypriest"><img src="https://avatars.githubusercontent.com/u/167215?v=4&s=48" width="48" height="48" alt="bradleypriest" title="bradleypriest"/></a> <a href="https://github.com/timolins"><img src="https://avatars.githubusercontent.com/u/1440854?v=4&s=48" width="48" height="48" alt="timolins" title="timolins"/></a> <a href="https://github.com/Nachx639"><img src="https://avatars.githubusercontent.com/u/71144023?v=4&s=48" width="48" height="48" alt="nachx639" title="nachx639"/></a> <a href="https://github.com/sreekaransrinath"><img src="https://avatars.githubusercontent.com/u/50989977?v=4&s=48" width="48" height="48" alt="sreekaransrinath" title="sreekaransrinath"/></a> <a href="https://github.com/gupsammy"><img src="https://avatars.githubusercontent.com/u/20296019?v=4&s=48" width="48" height="48" alt="gupsammy" title="gupsammy"/></a>
<a href="https://github.com/cristip73"><img src="https://avatars.githubusercontent.com/u/24499421?v=4&s=48" width="48" height="48" alt="cristip73" title="cristip73"/></a> <a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a> <a href="https://github.com/vsabavat"><img src="https://avatars.githubusercontent.com/u/50385532?v=4&s=48" width="48" height="48" alt="Vasanth Rao Naik Sabavat" title="Vasanth Rao Naik Sabavat"/></a> <a href="https://github.com/cpojer"><img src="https://avatars.githubusercontent.com/u/13352?v=4&s=48" width="48" height="48" alt="cpojer" title="cpojer"/></a> <a href="https://github.com/lc0rp"><img src="https://avatars.githubusercontent.com/u/2609441?v=4&s=48" width="48" height="48" alt="lc0rp" title="lc0rp"/></a> <a href="https://github.com/scald"><img src="https://avatars.githubusercontent.com/u/1215913?v=4&s=48" width="48" height="48" alt="scald" title="scald"/></a> <a href="https://github.com/gumadeiras"><img src="https://avatars.githubusercontent.com/u/5599352?v=4&s=48" width="48" height="48" alt="gumadeiras" title="gumadeiras"/></a> <a href="https://github.com/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a> <a href="https://github.com/davidguttman"><img src="https://avatars.githubusercontent.com/u/431696?v=4&s=48" width="48" height="48" alt="davidguttman" title="davidguttman"/></a> <a href="https://github.com/sleontenko"><img src="https://avatars.githubusercontent.com/u/7135949?v=4&s=48" width="48" height="48" alt="sleontenko" title="sleontenko"/></a>
<a href="https://github.com/sircrumpet"><img src="https://avatars.githubusercontent.com/u/4436535?v=4&s=48" width="48" height="48" alt="sircrumpet" title="sircrumpet"/></a> <a href="https://github.com/peschee"><img src="https://avatars.githubusercontent.com/u/63866?v=4&s=48" width="48" height="48" alt="peschee" title="peschee"/></a> <a href="https://github.com/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/thewilloftheshadow"><img src="https://avatars.githubusercontent.com/u/35580099?v=4&s=48" width="48" height="48" alt="thewilloftheshadow" title="thewilloftheshadow"/></a> <a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/lutr0"><img src="https://avatars.githubusercontent.com/u/76906369?v=4&s=48" width="48" height="48" alt="lutr0" title="lutr0"/></a> <a href="https://github.com/danielz1z"><img src="https://avatars.githubusercontent.com/u/235270390?v=4&s=48" width="48" height="48" alt="danielz1z" title="danielz1z"/></a> <a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a> <a href="https://github.com/KristijanJovanovski"><img src="https://avatars.githubusercontent.com/u/8942284?v=4&s=48" width="48" height="48" alt="KristijanJovanovski" title="KristijanJovanovski"/></a> <a href="https://github.com/CashWilliams"><img src="https://avatars.githubusercontent.com/u/613573?v=4&s=48" width="48" height="48" alt="CashWilliams" title="CashWilliams"/></a>
<a href="https://github.com/rdev"><img src="https://avatars.githubusercontent.com/u/8418866?v=4&s=48" width="48" height="48" alt="rdev" title="rdev"/></a> <a href="https://github.com/osolmaz"><img src="https://avatars.githubusercontent.com/u/2453968?v=4&s=48" width="48" height="48" alt="osolmaz" title="osolmaz"/></a> <a href="https://github.com/joshrad-dev"><img src="https://avatars.githubusercontent.com/u/62785552?v=4&s=48" width="48" height="48" alt="joshrad-dev" title="joshrad-dev"/></a> <a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a> <a href="https://github.com/adityashaw2"><img src="https://avatars.githubusercontent.com/u/41204444?v=4&s=48" width="48" height="48" alt="adityashaw2" title="adityashaw2"/></a> <a href="https://github.com/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></a> <a href="https://github.com/artuskg"><img src="https://avatars.githubusercontent.com/u/11966157?v=4&s=48" width="48" height="48" alt="artuskg" title="artuskg"/></a> <a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/tyler6204"><img src="https://avatars.githubusercontent.com/u/64381258?v=4&s=48" width="48" height="48" alt="tyler6204" title="tyler6204"/></a> <a href="https://github.com/ManuelHettich"><img src="https://avatars.githubusercontent.com/u/17690367?v=4&s=48" width="48" height="48" alt="manuelhettich" title="manuelhettich"/></a>
<a href="https://github.com/minghinmatthewlam"><img src="https://avatars.githubusercontent.com/u/14224566?v=4&s=48" width="48" height="48" alt="minghinmatthewlam" title="minghinmatthewlam"/></a> <a href="https://github.com/myfunc"><img src="https://avatars.githubusercontent.com/u/19294627?v=4&s=48" width="48" height="48" alt="myfunc" title="myfunc"/></a> <a href="https://github.com/buddyh"><img src="https://avatars.githubusercontent.com/u/31752869?v=4&s=48" width="48" height="48" alt="buddyh" title="buddyh"/></a> <a href="https://github.com/connorshea"><img src="https://avatars.githubusercontent.com/u/2977353?v=4&s=48" width="48" height="48" alt="connorshea" title="connorshea"/></a> <a href="https://github.com/vignesh07"><img src="https://avatars.githubusercontent.com/u/1436853?v=4&s=48" width="48" height="48" alt="vignesh07" title="vignesh07"/></a> <a href="https://github.com/mcinteerj"><img src="https://avatars.githubusercontent.com/u/3613653?v=4&s=48" width="48" height="48" alt="mcinteerj" title="mcinteerj"/></a> <a href="https://github.com/apps/dependabot"><img src="https://avatars.githubusercontent.com/in/29110?v=4&s=48" width="48" height="48" alt="dependabot[bot]" title="dependabot[bot]"/></a> <a href="https://github.com/John-Rood"><img src="https://avatars.githubusercontent.com/u/62669593?v=4&s=48" width="48" height="48" alt="John-Rood" title="John-Rood"/></a> <a href="https://github.com/timkrase"><img src="https://avatars.githubusercontent.com/u/38947626?v=4&s=48" width="48" height="48" alt="timkrase" title="timkrase"/></a> <a href="https://github.com/gerardward2007"><img src="https://avatars.githubusercontent.com/u/3002155?v=4&s=48" width="48" height="48" alt="gerardward2007" title="gerardward2007"/></a>
<a href="https://github.com/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a> <a href="https://github.com/tosh-hamburg"><img src="https://avatars.githubusercontent.com/u/58424326?v=4&s=48" width="48" height="48" alt="tosh-hamburg" title="tosh-hamburg"/></a> <a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/roshanasingh4"><img src="https://avatars.githubusercontent.com/u/88576930?v=4&s=48" width="48" height="48" alt="roshanasingh4" title="roshanasingh4"/></a> <a href="https://github.com/bjesuiter"><img src="https://avatars.githubusercontent.com/u/2365676?v=4&s=48" width="48" height="48" alt="bjesuiter" title="bjesuiter"/></a> <a href="https://github.com/cheeeee"><img src="https://avatars.githubusercontent.com/u/21245729?v=4&s=48" width="48" height="48" alt="cheeeee" title="cheeeee"/></a> <a href="https://github.com/j1philli"><img src="https://avatars.githubusercontent.com/u/3744255?v=4&s=48" width="48" height="48" alt="Josh Phillips" title="Josh Phillips"/></a> <a href="https://github.com/Whoaa512"><img src="https://avatars.githubusercontent.com/u/1581943?v=4&s=48" width="48" height="48" alt="Whoaa512" title="Whoaa512"/></a> <a href="https://github.com/YuriNachos"><img src="https://avatars.githubusercontent.com/u/19365375?v=4&s=48" width="48" height="48" alt="YuriNachos" title="YuriNachos"/></a> <a href="https://github.com/chriseidhof"><img src="https://avatars.githubusercontent.com/u/5382?v=4&s=48" width="48" height="48" alt="chriseidhof" title="chriseidhof"/></a>
<a href="https://github.com/ysqander"><img src="https://avatars.githubusercontent.com/u/80843820?v=4&s=48" width="48" height="48" alt="ysqander" title="ysqander"/></a> <a href="https://github.com/superman32432432"><img src="https://avatars.githubusercontent.com/u/7228420?v=4&s=48" width="48" height="48" alt="superman32432432" title="superman32432432"/></a> <a href="https://github.com/search?q=Yurii%20Chukhlib"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yurii Chukhlib" title="Yurii Chukhlib"/></a> <a href="https://github.com/grp06"><img src="https://avatars.githubusercontent.com/u/1573959?v=4&s=48" width="48" height="48" alt="grp06" title="grp06"/></a> <a href="https://github.com/antons"><img src="https://avatars.githubusercontent.com/u/129705?v=4&s=48" width="48" height="48" alt="antons" title="antons"/></a> <a href="https://github.com/austinm911"><img src="https://avatars.githubusercontent.com/u/31991302?v=4&s=48" width="48" height="48" alt="austinm911" title="austinm911"/></a> <a href="https://github.com/apps/blacksmith-sh"><img src="https://avatars.githubusercontent.com/in/807020?v=4&s=48" width="48" height="48" alt="blacksmith-sh[bot]" title="blacksmith-sh[bot]"/></a> <a href="https://github.com/dan-dr"><img src="https://avatars.githubusercontent.com/u/6669808?v=4&s=48" width="48" height="48" alt="dan-dr" title="dan-dr"/></a> <a href="https://github.com/dlauer"><img src="https://avatars.githubusercontent.com/u/757041?v=4&s=48" width="48" height="48" alt="dlauer" title="dlauer"/></a> <a href="https://github.com/HeimdallStrategy"><img src="https://avatars.githubusercontent.com/u/223014405?v=4&s=48" width="48" height="48" alt="HeimdallStrategy" title="HeimdallStrategy"/></a>
<a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/jarvis-medmatic"><img src="https://avatars.githubusercontent.com/u/252428873?v=4&s=48" width="48" height="48" alt="jarvis-medmatic" title="jarvis-medmatic"/></a> <a href="https://github.com/kkarimi"><img src="https://avatars.githubusercontent.com/u/875218?v=4&s=48" width="48" height="48" alt="kkarimi" title="kkarimi"/></a> <a href="https://github.com/mahmoudashraf93"><img src="https://avatars.githubusercontent.com/u/9130129?v=4&s=48" width="48" height="48" alt="mahmoudashraf93" title="mahmoudashraf93"/></a> <a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a> <a href="https://github.com/petter-b"><img src="https://avatars.githubusercontent.com/u/62076402?v=4&s=48" width="48" height="48" alt="petter-b" title="petter-b"/></a> <a href="https://github.com/pkrmf"><img src="https://avatars.githubusercontent.com/u/1714267?v=4&s=48" width="48" height="48" alt="pkrmf" title="pkrmf"/></a> <a href="https://github.com/RandyVentures"><img src="https://avatars.githubusercontent.com/u/149904821?v=4&s=48" width="48" height="48" alt="RandyVentures" title="RandyVentures"/></a> <a href="https://github.com/search?q=Ryan%20Lisse"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ryan Lisse" title="Ryan Lisse"/></a>
<a href="https://github.com/dougvk"><img src="https://avatars.githubusercontent.com/u/401660?v=4&s=48" width="48" height="48" alt="dougvk" title="dougvk"/></a> <a href="https://github.com/erikpr1994"><img src="https://avatars.githubusercontent.com/u/6299331?v=4&s=48" width="48" height="48" alt="erikpr1994" title="erikpr1994"/></a> <a href="https://github.com/search?q=Ghost"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ghost" title="Ghost"/></a> <a href="https://github.com/jonasjancarik"><img src="https://avatars.githubusercontent.com/u/2459191?v=4&s=48" width="48" height="48" alt="jonasjancarik" title="jonasjancarik"/></a> <a href="https://github.com/search?q=Keith%20the%20Silly%20Goose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Keith the Silly Goose" title="Keith the Silly Goose"/></a> <a href="https://github.com/search?q=L36%20Server"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="L36 Server" title="L36 Server"/></a> <a href="https://github.com/search?q=Marc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc" title="Marc"/></a> <a href="https://github.com/mitschabaude-bot"><img src="https://avatars.githubusercontent.com/u/247582884?v=4&s=48" width="48" height="48" alt="mitschabaude-bot" title="mitschabaude-bot"/></a> <a href="https://github.com/mkbehr"><img src="https://avatars.githubusercontent.com/u/1285?v=4&s=48" width="48" height="48" alt="mkbehr" title="mkbehr"/></a> <a href="https://github.com/neist"><img src="https://avatars.githubusercontent.com/u/1029724?v=4&s=48" width="48" height="48" alt="neist" title="neist"/></a>
<a href="https://github.com/chrisrodz"><img src="https://avatars.githubusercontent.com/u/2967620?v=4&s=48" width="48" height="48" alt="chrisrodz" title="chrisrodz"/></a> <a href="https://github.com/search?q=Friederike%20Seiler"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Friederike Seiler" title="Friederike Seiler"/></a> <a href="https://github.com/gabriel-trigo"><img src="https://avatars.githubusercontent.com/u/38991125?v=4&s=48" width="48" height="48" alt="gabriel-trigo" title="gabriel-trigo"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="iamadig" title="iamadig"/></a> <a href="https://github.com/search?q=Kit"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kit" title="Kit"/></a> <a href="https://github.com/koala73"><img src="https://avatars.githubusercontent.com/u/996596?v=4&s=48" width="48" height="48" alt="koala73" title="koala73"/></a> <a href="https://github.com/manmal"><img src="https://avatars.githubusercontent.com/u/142797?v=4&s=48" width="48" height="48" alt="manmal" title="manmal"/></a> <a href="https://github.com/ogulcancelik"><img src="https://avatars.githubusercontent.com/u/7064011?v=4&s=48" width="48" height="48" alt="ogulcancelik" title="ogulcancelik"/></a> <a href="https://github.com/pasogott"><img src="https://avatars.githubusercontent.com/u/23458152?v=4&s=48" width="48" height="48" alt="pasogott" title="pasogott"/></a> <a href="https://github.com/petradonka"><img src="https://avatars.githubusercontent.com/u/7353770?v=4&s=48" width="48" height="48" alt="petradonka" title="petradonka"/></a>
<a href="https://github.com/rubyrunsstuff"><img src="https://avatars.githubusercontent.com/u/246602379?v=4&s=48" width="48" height="48" alt="rubyrunsstuff" title="rubyrunsstuff"/></a> <a href="https://github.com/sibbl"><img src="https://avatars.githubusercontent.com/u/866535?v=4&s=48" width="48" height="48" alt="sibbl" title="sibbl"/></a> <a href="https://github.com/siddhantjain"><img src="https://avatars.githubusercontent.com/u/4835232?v=4&s=48" width="48" height="48" alt="siddhantjain" title="siddhantjain"/></a> <a href="https://github.com/suminhthanh"><img src="https://avatars.githubusercontent.com/u/2907636?v=4&s=48" width="48" height="48" alt="suminhthanh" title="suminhthanh"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/wes-davis"><img src="https://avatars.githubusercontent.com/u/16506720?v=4&s=48" width="48" height="48" alt="wes-davis" title="wes-davis"/></a> <a href="https://github.com/zats"><img src="https://avatars.githubusercontent.com/u/2688806?v=4&s=48" width="48" height="48" alt="zats" title="zats"/></a> <a href="https://github.com/24601"><img src="https://avatars.githubusercontent.com/u/1157207?v=4&s=48" width="48" height="48" alt="24601" title="24601"/></a> <a href="https://github.com/search?q=Chris%20Taylor"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Chris Taylor" title="Chris Taylor"/></a> <a href="https://github.com/czekaj"><img src="https://avatars.githubusercontent.com/u/1464539?v=4&s=48" width="48" height="48" alt="czekaj" title="czekaj"/></a>
<a href="https://github.com/djangonavarro220"><img src="https://avatars.githubusercontent.com/u/251162586?v=4&s=48" width="48" height="48" alt="Django Navarro" title="Django Navarro"/></a> <a href="https://github.com/evalexpr"><img src="https://avatars.githubusercontent.com/u/23485511?v=4&s=48" width="48" height="48" alt="evalexpr" title="evalexpr"/></a> <a href="https://github.com/henrino3"><img src="https://avatars.githubusercontent.com/u/4260288?v=4&s=48" width="48" height="48" alt="henrino3" title="henrino3"/></a> <a href="https://github.com/humanwritten"><img src="https://avatars.githubusercontent.com/u/206531610?v=4&s=48" width="48" height="48" alt="humanwritten" title="humanwritten"/></a> <a href="https://github.com/larlyssa"><img src="https://avatars.githubusercontent.com/u/13128869?v=4&s=48" width="48" height="48" alt="larlyssa" title="larlyssa"/></a> <a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a> <a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a> <a href="https://github.com/Syhids"><img src="https://avatars.githubusercontent.com/u/671202?v=4&s=48" width="48" height="48" alt="Syhids" title="Syhids"/></a> <a href="https://github.com/search?q=Aaron%20Konyer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Aaron Konyer" title="Aaron Konyer"/></a> <a href="https://github.com/aaronveklabs"><img src="https://avatars.githubusercontent.com/u/225997828?v=4&s=48" width="48" height="48" alt="aaronveklabs" title="aaronveklabs"/></a>
<a href="https://github.com/adam91holt"><img src="https://avatars.githubusercontent.com/u/9592417?v=4&s=48" width="48" height="48" alt="adam91holt" title="adam91holt"/></a> <a href="https://github.com/ameno-"><img src="https://avatars.githubusercontent.com/u/2416135?v=4&s=48" width="48" height="48" alt="ameno-" title="ameno-"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a> <a href="https://github.com/search?q=ClawdFx"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ClawdFx" title="ClawdFx"/></a> <a href="https://github.com/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></a> <a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a> <a href="https://github.com/ivanrvpereira"><img src="https://avatars.githubusercontent.com/u/183991?v=4&s=48" width="48" height="48" alt="ivanrvpereira" title="ivanrvpereira"/></a> <a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a> <a href="https://github.com/jeffersonwarrior"><img src="https://avatars.githubusercontent.com/u/89030989?v=4&s=48" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a>
<a href="https://github.com/search?q=jeffersonwarrior"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/jdrhyne"><img src="https://avatars.githubusercontent.com/u/7828464?v=4&s=48" width="48" height="48" alt="Jonathan D. Rhyne (DJ-D)" title="Jonathan D. Rhyne (DJ-D)"/></a> <a href="https://github.com/jverdi"><img src="https://avatars.githubusercontent.com/u/345050?v=4&s=48" width="48" height="48" alt="jverdi" title="jverdi"/></a> <a href="https://github.com/longmaba"><img src="https://avatars.githubusercontent.com/u/9361500?v=4&s=48" width="48" height="48" alt="longmaba" title="longmaba"/></a> <a href="https://github.com/mickahouan"><img src="https://avatars.githubusercontent.com/u/31423109?v=4&s=48" width="48" height="48" alt="mickahouan" title="mickahouan"/></a> <a href="https://github.com/mjrussell"><img src="https://avatars.githubusercontent.com/u/1641895?v=4&s=48" width="48" height="48" alt="mjrussell" title="mjrussell"/></a> <a href="https://github.com/p6l-richard"><img src="https://avatars.githubusercontent.com/u/18185649?v=4&s=48" width="48" height="48" alt="p6l-richard" title="p6l-richard"/></a> <a href="https://github.com/philipp-spiess"><img src="https://avatars.githubusercontent.com/u/458591?v=4&s=48" width="48" height="48" alt="philipp-spiess" title="philipp-spiess"/></a> <a href="https://github.com/robaxelsen"><img src="https://avatars.githubusercontent.com/u/13132899?v=4&s=48" width="48" height="48" alt="robaxelsen" title="robaxelsen"/></a> <a href="https://github.com/search?q=Sash%20Catanzarite"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sash Catanzarite" title="Sash Catanzarite"/></a>
<a href="https://github.com/T5-AndyML"><img src="https://avatars.githubusercontent.com/u/22801233?v=4&s=48" width="48" height="48" alt="T5-AndyML" title="T5-AndyML"/></a> <a href="https://github.com/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></a> <a href="https://github.com/zknicker"><img src="https://avatars.githubusercontent.com/u/1164085?v=4&s=48" width="48" height="48" alt="zknicker" title="zknicker"/></a> <a href="https://github.com/aj47"><img src="https://avatars.githubusercontent.com/u/8023513?v=4&s=48" width="48" height="48" alt="aj47" title="aj47"/></a> <a href="https://github.com/search?q=alejandro%20maza"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="alejandro maza" title="alejandro maza"/></a> <a href="https://github.com/andrewting19"><img src="https://avatars.githubusercontent.com/u/10536704?v=4&s=48" width="48" height="48" alt="andrewting19" title="andrewting19"/></a> <a href="https://github.com/search?q=Andrii"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Andrii" title="Andrii"/></a> <a href="https://github.com/anpoirier"><img src="https://avatars.githubusercontent.com/u/1245729?v=4&s=48" width="48" height="48" alt="anpoirier" title="anpoirier"/></a> <a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a> <a href="https://github.com/bolismauro"><img src="https://avatars.githubusercontent.com/u/771999?v=4&s=48" width="48" height="48" alt="bolismauro" title="bolismauro"/></a>
<a href="https://github.com/conhecendoia"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendoia" title="conhecendoia"/></a> <a href="https://github.com/search?q=Dimitrios%20Ploutarchos"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Dimitrios Ploutarchos" title="Dimitrios Ploutarchos"/></a> <a href="https://github.com/search?q=Drake%20Thomsen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Drake Thomsen" title="Drake Thomsen"/></a> <a href="https://github.com/search?q=Felix%20Krause"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Felix Krause" title="Felix Krause"/></a> <a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a> <a href="https://github.com/HazAT"><img src="https://avatars.githubusercontent.com/u/363802?v=4&s=48" width="48" height="48" alt="HazAT" title="HazAT"/></a> <a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a> <a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a> <a href="https://github.com/search?q=Jamie%20Openshaw"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jamie Openshaw" title="Jamie Openshaw"/></a> <a href="https://github.com/search?q=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></a>
<a href="https://github.com/search?q=Jefferson%20Nunn"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jefferson Nunn" title="Jefferson Nunn"/></a> <a href="https://github.com/search?q=Kevin%20Lin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kevin Lin" title="Kevin Lin"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/levifig"><img src="https://avatars.githubusercontent.com/u/1605?v=4&s=48" width="48" height="48" alt="levifig" title="levifig"/></a> <a href="https://github.com/search?q=Lloyd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Lloyd" title="Lloyd"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/martinpucik"><img src="https://avatars.githubusercontent.com/u/5503097?v=4&s=48" width="48" height="48" alt="martinpucik" title="martinpucik"/></a> <a href="https://github.com/search?q=Miles"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Miles" title="Miles"/></a> <a href="https://github.com/mrdbstn"><img src="https://avatars.githubusercontent.com/u/58957632?v=4&s=48" width="48" height="48" alt="mrdbstn" title="mrdbstn"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a>
<a href="https://github.com/search?q=Mustafa%20Tag%20Eldeen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mustafa Tag Eldeen" title="Mustafa Tag Eldeen"/></a> <a href="https://github.com/ndraiman"><img src="https://avatars.githubusercontent.com/u/12609607?v=4&s=48" width="48" height="48" alt="ndraiman" title="ndraiman"/></a> <a href="https://github.com/nexty5870"><img src="https://avatars.githubusercontent.com/u/3869659?v=4&s=48" width="48" height="48" alt="nexty5870" title="nexty5870"/></a> <a href="https://github.com/odysseus0"><img src="https://avatars.githubusercontent.com/u/8635094?v=4&s=48" width="48" height="48" alt="odysseus0" title="odysseus0"/></a> <a href="https://github.com/prathamdby"><img src="https://avatars.githubusercontent.com/u/134331217?v=4&s=48" width="48" height="48" alt="prathamdby" title="prathamdby"/></a> <a href="https://github.com/ptn1411"><img src="https://avatars.githubusercontent.com/u/57529765?v=4&s=48" width="48" height="48" alt="ptn1411" title="ptn1411"/></a> <a href="https://github.com/reeltimeapps"><img src="https://avatars.githubusercontent.com/u/637338?v=4&s=48" width="48" height="48" alt="reeltimeapps" title="reeltimeapps"/></a> <a href="https://github.com/RLTCmpe"><img src="https://avatars.githubusercontent.com/u/10762242?v=4&s=48" width="48" height="48" alt="RLTCmpe" title="RLTCmpe"/></a> <a href="https://github.com/robbyczgw-cla"><img src="https://avatars.githubusercontent.com/u/239660374?v=4&s=48" width="48" height="48" alt="robbyczgw-cla" title="robbyczgw-cla"/></a> <a href="https://github.com/rodrigouroz"><img src="https://avatars.githubusercontent.com/u/384037?v=4&s=48" width="48" height="48" alt="rodrigouroz" title="rodrigouroz"/></a>
<a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/search?q=Rony%20Kelner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rony Kelner" title="Rony Kelner"/></a> <a href="https://github.com/search?q=Samrat%20Jha"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Samrat Jha" title="Samrat Jha"/></a> <a href="https://github.com/siraht"><img src="https://avatars.githubusercontent.com/u/73152895?v=4&s=48" width="48" height="48" alt="siraht" title="siraht"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/search?q=The%20Admiral"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="The Admiral" title="The Admiral"/></a> <a href="https://github.com/thesash"><img src="https://avatars.githubusercontent.com/u/1166151?v=4&s=48" width="48" height="48" alt="thesash" title="thesash"/></a> <a href="https://github.com/search?q=Ubuntu"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ubuntu" title="Ubuntu"/></a> <a href="https://github.com/voidserf"><img src="https://avatars.githubusercontent.com/u/477673?v=4&s=48" width="48" height="48" alt="voidserf" title="voidserf"/></a> <a href="https://github.com/search?q=Vultr-Clawd%20Admin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vultr-Clawd Admin" title="Vultr-Clawd Admin"/></a>
<a href="https://github.com/search?q=Wimmie"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Wimmie" title="Wimmie"/></a> <a href="https://github.com/wstock"><img src="https://avatars.githubusercontent.com/u/1394687?v=4&s=48" width="48" height="48" alt="wstock" title="wstock"/></a> <a href="https://github.com/yazinsai"><img src="https://avatars.githubusercontent.com/u/1846034?v=4&s=48" width="48" height="48" alt="yazinsai" title="yazinsai"/></a> <a href="https://github.com/search?q=Zach%20Knickerbocker"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Zach Knickerbocker" title="Zach Knickerbocker"/></a> <a href="https://github.com/Alphonse-arianee"><img src="https://avatars.githubusercontent.com/u/254457365?v=4&s=48" width="48" height="48" alt="Alphonse-arianee" title="Alphonse-arianee"/></a> <a href="https://github.com/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a> <a href="https://github.com/carlulsoe"><img src="https://avatars.githubusercontent.com/u/34673973?v=4&s=48" width="48" height="48" alt="carlulsoe" title="carlulsoe"/></a> <a href="https://github.com/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a> <a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></a> <a href="https://github.com/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></a>
<a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/odrobnik"><img src="https://avatars.githubusercontent.com/u/333270?v=4&s=48" width="48" height="48" alt="odrobnik" title="odrobnik"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a> <a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a> <a href="https://github.com/rhjoh"><img src="https://avatars.githubusercontent.com/u/105699450?v=4&s=48" width="48" height="48" alt="rhjoh" title="rhjoh"/></a> <a href="https://github.com/ronak-guliani"><img src="https://avatars.githubusercontent.com/u/23518228?v=4&s=48" width="48" height="48" alt="ronak-guliani" title="ronak-guliani"/></a> <a href="https://github.com/search?q=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
<a href="https://github.com/steipete"><img src="https://avatars.githubusercontent.com/u/58493?v=4&s=48" width="48" height="48" alt="steipete" title="steipete"/></a> <a href="https://github.com/bohdanpodvirnyi"><img src="https://avatars.githubusercontent.com/u/31819391?v=4&s=48" width="48" height="48" alt="bohdanpodvirnyi" title="bohdanpodvirnyi"/></a> <a href="https://github.com/joaohlisboa"><img src="https://avatars.githubusercontent.com/u/8200873?v=4&s=48" width="48" height="48" alt="joaohlisboa" title="joaohlisboa"/></a> <a href="https://github.com/mneves75"><img src="https://avatars.githubusercontent.com/u/2423436?v=4&s=48" width="48" height="48" alt="mneves75" title="mneves75"/></a> <a href="https://github.com/MatthieuBizien"><img src="https://avatars.githubusercontent.com/u/173090?v=4&s=48" width="48" height="48" alt="MatthieuBizien" title="MatthieuBizien"/></a> <a href="https://github.com/MaudeBot"><img src="https://avatars.githubusercontent.com/u/255777700?v=4&s=48" width="48" height="48" alt="MaudeBot" title="MaudeBot"/></a> <a href="https://github.com/rahthakor"><img src="https://avatars.githubusercontent.com/u/8470553?v=4&s=48" width="48" height="48" alt="rahthakor" title="rahthakor"/></a> <a href="https://github.com/vrknetha"><img src="https://avatars.githubusercontent.com/u/20596261?v=4&s=48" width="48" height="48" alt="vrknetha" title="vrknetha"/></a> <a href="https://github.com/radek-paclt"><img src="https://avatars.githubusercontent.com/u/50451445?v=4&s=48" width="48" height="48" alt="radek-paclt" title="radek-paclt"/></a> <a href="https://github.com/tobiasbischoff"><img src="https://avatars.githubusercontent.com/u/711564?v=4&s=48" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></a>
<a href="https://github.com/joshp123"><img src="https://avatars.githubusercontent.com/u/1497361?v=4&s=48" width="48" height="48" alt="joshp123" title="joshp123"/></a> <a href="https://github.com/mukhtharcm"><img src="https://avatars.githubusercontent.com/u/56378562?v=4&s=48" width="48" height="48" alt="mukhtharcm" title="mukhtharcm"/></a> <a href="https://github.com/maxsumrall"><img src="https://avatars.githubusercontent.com/u/628843?v=4&s=48" width="48" height="48" alt="maxsumrall" title="maxsumrall"/></a> <a href="https://github.com/xadenryan"><img src="https://avatars.githubusercontent.com/u/165437834?v=4&s=48" width="48" height="48" alt="xadenryan" title="xadenryan"/></a> <a href="https://github.com/juanpablodlc"><img src="https://avatars.githubusercontent.com/u/92012363?v=4&s=48" width="48" height="48" alt="juanpablodlc" title="juanpablodlc"/></a> <a href="https://github.com/hsrvc"><img src="https://avatars.githubusercontent.com/u/129702169?v=4&s=48" width="48" height="48" alt="hsrvc" title="hsrvc"/></a> <a href="https://github.com/magimetal"><img src="https://avatars.githubusercontent.com/u/36491250?v=4&s=48" width="48" height="48" alt="magimetal" title="magimetal"/></a> <a href="https://github.com/meaningfool"><img src="https://avatars.githubusercontent.com/u/2862331?v=4&s=48" width="48" height="48" alt="meaningfool" title="meaningfool"/></a> <a href="https://github.com/patelhiren"><img src="https://avatars.githubusercontent.com/u/172098?v=4&s=48" width="48" height="48" alt="patelhiren" title="patelhiren"/></a> <a href="https://github.com/NicholasSpisak"><img src="https://avatars.githubusercontent.com/u/129075147?v=4&s=48" width="48" height="48" alt="NicholasSpisak" title="NicholasSpisak"/></a>
<a href="https://github.com/sebslight"><img src="https://avatars.githubusercontent.com/u/19554889?v=4&s=48" width="48" height="48" alt="sebslight" title="sebslight"/></a> <a href="https://github.com/AbhisekBasu1"><img src="https://avatars.githubusercontent.com/u/40645221?v=4&s=48" width="48" height="48" alt="abhisekbasu1" title="abhisekbasu1"/></a> <a href="https://github.com/zerone0x"><img src="https://avatars.githubusercontent.com/u/39543393?v=4&s=48" width="48" height="48" alt="zerone0x" title="zerone0x"/></a> <a href="https://github.com/jamesgroat"><img src="https://avatars.githubusercontent.com/u/2634024?v=4&s=48" width="48" height="48" alt="jamesgroat" title="jamesgroat"/></a> <a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude" title="claude"/></a> <a href="https://github.com/SocialNerd42069"><img src="https://avatars.githubusercontent.com/u/118244303?v=4&s=48" width="48" height="48" alt="SocialNerd42069" title="SocialNerd42069"/></a> <a href="https://github.com/Hyaxia"><img src="https://avatars.githubusercontent.com/u/36747317?v=4&s=48" width="48" height="48" alt="Hyaxia" title="Hyaxia"/></a> <a href="https://github.com/dantelex"><img src="https://avatars.githubusercontent.com/u/631543?v=4&s=48" width="48" height="48" alt="dantelex" title="dantelex"/></a> <a href="https://github.com/daveonkels"><img src="https://avatars.githubusercontent.com/u/533642?v=4&s=48" width="48" height="48" alt="daveonkels" title="daveonkels"/></a> <a href="https://github.com/apps/google-labs-jules"><img src="https://avatars.githubusercontent.com/in/842251?v=4&s=48" width="48" height="48" alt="google-labs-jules[bot]" title="google-labs-jules[bot]"/></a>
<a href="https://github.com/mteam88"><img src="https://avatars.githubusercontent.com/u/84196639?v=4&s=48" width="48" height="48" alt="mteam88" title="mteam88"/></a> <a href="https://github.com/omniwired"><img src="https://avatars.githubusercontent.com/u/322761?v=4&s=48" width="48" height="48" alt="Eng. Juan Combetto" title="Eng. Juan Combetto"/></a> <a href="https://github.com/mbelinky"><img src="https://avatars.githubusercontent.com/u/132747814?v=4&s=48" width="48" height="48" alt="Mariano Belinky" title="Mariano Belinky"/></a> <a href="https://github.com/dbhurley"><img src="https://avatars.githubusercontent.com/u/5251425?v=4&s=48" width="48" height="48" alt="dbhurley" title="dbhurley"/></a> <a href="https://github.com/TSavo"><img src="https://avatars.githubusercontent.com/u/877990?v=4&s=48" width="48" height="48" alt="TSavo" title="TSavo"/></a> <a href="https://github.com/julianengel"><img src="https://avatars.githubusercontent.com/u/10634231?v=4&s=48" width="48" height="48" alt="julianengel" title="julianengel"/></a> <a href="https://github.com/benithors"><img src="https://avatars.githubusercontent.com/u/20652882?v=4&s=48" width="48" height="48" alt="benithors" title="benithors"/></a> <a href="https://github.com/bradleypriest"><img src="https://avatars.githubusercontent.com/u/167215?v=4&s=48" width="48" height="48" alt="bradleypriest" title="bradleypriest"/></a> <a href="https://github.com/timolins"><img src="https://avatars.githubusercontent.com/u/1440854?v=4&s=48" width="48" height="48" alt="timolins" title="timolins"/></a> <a href="https://github.com/Nachx639"><img src="https://avatars.githubusercontent.com/u/71144023?v=4&s=48" width="48" height="48" alt="nachx639" title="nachx639"/></a>
<a href="https://github.com/pvoo"><img src="https://avatars.githubusercontent.com/u/20116814?v=4&s=48" width="48" height="48" alt="pvoo" title="pvoo"/></a> <a href="https://github.com/sreekaransrinath"><img src="https://avatars.githubusercontent.com/u/50989977?v=4&s=48" width="48" height="48" alt="sreekaransrinath" title="sreekaransrinath"/></a> <a href="https://github.com/gupsammy"><img src="https://avatars.githubusercontent.com/u/20296019?v=4&s=48" width="48" height="48" alt="gupsammy" title="gupsammy"/></a> <a href="https://github.com/cristip73"><img src="https://avatars.githubusercontent.com/u/24499421?v=4&s=48" width="48" height="48" alt="cristip73" title="cristip73"/></a> <a href="https://github.com/stefangalescu"><img src="https://avatars.githubusercontent.com/u/52995748?v=4&s=48" width="48" height="48" alt="stefangalescu" title="stefangalescu"/></a> <a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a> <a href="https://github.com/vsabavat"><img src="https://avatars.githubusercontent.com/u/50385532?v=4&s=48" width="48" height="48" alt="Vasanth Rao Naik Sabavat" title="Vasanth Rao Naik Sabavat"/></a> <a href="https://github.com/iHildy"><img src="https://avatars.githubusercontent.com/u/25069719?v=4&s=48" width="48" height="48" alt="iHildy" title="iHildy"/></a> <a href="https://github.com/cpojer"><img src="https://avatars.githubusercontent.com/u/13352?v=4&s=48" width="48" height="48" alt="cpojer" title="cpojer"/></a> <a href="https://github.com/lc0rp"><img src="https://avatars.githubusercontent.com/u/2609441?v=4&s=48" width="48" height="48" alt="lc0rp" title="lc0rp"/></a>
<a href="https://github.com/scald"><img src="https://avatars.githubusercontent.com/u/1215913?v=4&s=48" width="48" height="48" alt="scald" title="scald"/></a> <a href="https://github.com/gumadeiras"><img src="https://avatars.githubusercontent.com/u/5599352?v=4&s=48" width="48" height="48" alt="gumadeiras" title="gumadeiras"/></a> <a href="https://github.com/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a> <a href="https://github.com/davidguttman"><img src="https://avatars.githubusercontent.com/u/431696?v=4&s=48" width="48" height="48" alt="davidguttman" title="davidguttman"/></a> <a href="https://github.com/sleontenko"><img src="https://avatars.githubusercontent.com/u/7135949?v=4&s=48" width="48" height="48" alt="sleontenko" title="sleontenko"/></a> <a href="https://github.com/rodrigouroz"><img src="https://avatars.githubusercontent.com/u/384037?v=4&s=48" width="48" height="48" alt="rodrigouroz" title="rodrigouroz"/></a> <a href="https://github.com/sircrumpet"><img src="https://avatars.githubusercontent.com/u/4436535?v=4&s=48" width="48" height="48" alt="sircrumpet" title="sircrumpet"/></a> <a href="https://github.com/peschee"><img src="https://avatars.githubusercontent.com/u/63866?v=4&s=48" width="48" height="48" alt="peschee" title="peschee"/></a> <a href="https://github.com/rafaelreis-r"><img src="https://avatars.githubusercontent.com/u/57492577?v=4&s=48" width="48" height="48" alt="rafaelreis-r" title="rafaelreis-r"/></a> <a href="https://github.com/thewilloftheshadow"><img src="https://avatars.githubusercontent.com/u/35580099?v=4&s=48" width="48" height="48" alt="thewilloftheshadow" title="thewilloftheshadow"/></a>
<a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/lutr0"><img src="https://avatars.githubusercontent.com/u/76906369?v=4&s=48" width="48" height="48" alt="lutr0" title="lutr0"/></a> <a href="https://github.com/danielz1z"><img src="https://avatars.githubusercontent.com/u/235270390?v=4&s=48" width="48" height="48" alt="danielz1z" title="danielz1z"/></a> <a href="https://github.com/emanuelst"><img src="https://avatars.githubusercontent.com/u/9994339?v=4&s=48" width="48" height="48" alt="emanuelst" title="emanuelst"/></a> <a href="https://github.com/KristijanJovanovski"><img src="https://avatars.githubusercontent.com/u/8942284?v=4&s=48" width="48" height="48" alt="KristijanJovanovski" title="KristijanJovanovski"/></a> <a href="https://github.com/CashWilliams"><img src="https://avatars.githubusercontent.com/u/613573?v=4&s=48" width="48" height="48" alt="CashWilliams" title="CashWilliams"/></a> <a href="https://github.com/rdev"><img src="https://avatars.githubusercontent.com/u/8418866?v=4&s=48" width="48" height="48" alt="rdev" title="rdev"/></a> <a href="https://github.com/osolmaz"><img src="https://avatars.githubusercontent.com/u/2453968?v=4&s=48" width="48" height="48" alt="osolmaz" title="osolmaz"/></a> <a href="https://github.com/joshrad-dev"><img src="https://avatars.githubusercontent.com/u/62785552?v=4&s=48" width="48" height="48" alt="joshrad-dev" title="joshrad-dev"/></a> <a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a>
<a href="https://github.com/adityashaw2"><img src="https://avatars.githubusercontent.com/u/41204444?v=4&s=48" width="48" height="48" alt="adityashaw2" title="adityashaw2"/></a> <a href="https://github.com/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></a> <a href="https://github.com/artuskg"><img src="https://avatars.githubusercontent.com/u/11966157?v=4&s=48" width="48" height="48" alt="artuskg" title="artuskg"/></a> <a href="https://github.com/onutc"><img src="https://avatars.githubusercontent.com/u/152018508?v=4&s=48" width="48" height="48" alt="onutc" title="onutc"/></a> <a href="https://github.com/pauloportella"><img src="https://avatars.githubusercontent.com/u/22947229?v=4&s=48" width="48" height="48" alt="pauloportella" title="pauloportella"/></a> <a href="https://github.com/tyler6204"><img src="https://avatars.githubusercontent.com/u/64381258?v=4&s=48" width="48" height="48" alt="tyler6204" title="tyler6204"/></a> <a href="https://github.com/neooriginal"><img src="https://avatars.githubusercontent.com/u/54811660?v=4&s=48" width="48" height="48" alt="neooriginal" title="neooriginal"/></a> <a href="https://github.com/ManuelHettich"><img src="https://avatars.githubusercontent.com/u/17690367?v=4&s=48" width="48" height="48" alt="manuelhettich" title="manuelhettich"/></a> <a href="https://github.com/minghinmatthewlam"><img src="https://avatars.githubusercontent.com/u/14224566?v=4&s=48" width="48" height="48" alt="minghinmatthewlam" title="minghinmatthewlam"/></a> <a href="https://github.com/myfunc"><img src="https://avatars.githubusercontent.com/u/19294627?v=4&s=48" width="48" height="48" alt="myfunc" title="myfunc"/></a>
<a href="https://github.com/travisirby"><img src="https://avatars.githubusercontent.com/u/5958376?v=4&s=48" width="48" height="48" alt="travisirby" title="travisirby"/></a> <a href="https://github.com/vignesh07"><img src="https://avatars.githubusercontent.com/u/1436853?v=4&s=48" width="48" height="48" alt="vignesh07" title="vignesh07"/></a> <a href="https://github.com/buddyh"><img src="https://avatars.githubusercontent.com/u/31752869?v=4&s=48" width="48" height="48" alt="buddyh" title="buddyh"/></a> <a href="https://github.com/connorshea"><img src="https://avatars.githubusercontent.com/u/2977353?v=4&s=48" width="48" height="48" alt="connorshea" title="connorshea"/></a> <a href="https://github.com/mcinteerj"><img src="https://avatars.githubusercontent.com/u/3613653?v=4&s=48" width="48" height="48" alt="mcinteerj" title="mcinteerj"/></a> <a href="https://github.com/apps/dependabot"><img src="https://avatars.githubusercontent.com/in/29110?v=4&s=48" width="48" height="48" alt="dependabot[bot]" title="dependabot[bot]"/></a> <a href="https://github.com/John-Rood"><img src="https://avatars.githubusercontent.com/u/62669593?v=4&s=48" width="48" height="48" alt="John-Rood" title="John-Rood"/></a> <a href="https://github.com/timkrase"><img src="https://avatars.githubusercontent.com/u/38947626?v=4&s=48" width="48" height="48" alt="timkrase" title="timkrase"/></a> <a href="https://github.com/gerardward2007"><img src="https://avatars.githubusercontent.com/u/3002155?v=4&s=48" width="48" height="48" alt="gerardward2007" title="gerardward2007"/></a> <a href="https://github.com/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a>
<a href="https://github.com/tosh-hamburg"><img src="https://avatars.githubusercontent.com/u/58424326?v=4&s=48" width="48" height="48" alt="tosh-hamburg" title="tosh-hamburg"/></a> <a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/roshanasingh4"><img src="https://avatars.githubusercontent.com/u/88576930?v=4&s=48" width="48" height="48" alt="roshanasingh4" title="roshanasingh4"/></a> <a href="https://github.com/bjesuiter"><img src="https://avatars.githubusercontent.com/u/2365676?v=4&s=48" width="48" height="48" alt="bjesuiter" title="bjesuiter"/></a> <a href="https://github.com/cheeeee"><img src="https://avatars.githubusercontent.com/u/21245729?v=4&s=48" width="48" height="48" alt="cheeeee" title="cheeeee"/></a> <a href="https://github.com/j1philli"><img src="https://avatars.githubusercontent.com/u/3744255?v=4&s=48" width="48" height="48" alt="Josh Phillips" title="Josh Phillips"/></a> <a href="https://github.com/Whoaa512"><img src="https://avatars.githubusercontent.com/u/1581943?v=4&s=48" width="48" height="48" alt="Whoaa512" title="Whoaa512"/></a> <a href="https://github.com/YuriNachos"><img src="https://avatars.githubusercontent.com/u/19365375?v=4&s=48" width="48" height="48" alt="YuriNachos" title="YuriNachos"/></a> <a href="https://github.com/chriseidhof"><img src="https://avatars.githubusercontent.com/u/5382?v=4&s=48" width="48" height="48" alt="chriseidhof" title="chriseidhof"/></a> <a href="https://github.com/dlauer"><img src="https://avatars.githubusercontent.com/u/757041?v=4&s=48" width="48" height="48" alt="dlauer" title="dlauer"/></a>
<a href="https://github.com/robbyczgw-cla"><img src="https://avatars.githubusercontent.com/u/239660374?v=4&s=48" width="48" height="48" alt="robbyczgw-cla" title="robbyczgw-cla"/></a> <a href="https://github.com/ysqander"><img src="https://avatars.githubusercontent.com/u/80843820?v=4&s=48" width="48" height="48" alt="ysqander" title="ysqander"/></a> <a href="https://github.com/superman32432432"><img src="https://avatars.githubusercontent.com/u/7228420?v=4&s=48" width="48" height="48" alt="superman32432432" title="superman32432432"/></a> <a href="https://github.com/search?q=Yurii%20Chukhlib"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yurii Chukhlib" title="Yurii Chukhlib"/></a> <a href="https://github.com/grp06"><img src="https://avatars.githubusercontent.com/u/1573959?v=4&s=48" width="48" height="48" alt="grp06" title="grp06"/></a> <a href="https://github.com/antons"><img src="https://avatars.githubusercontent.com/u/129705?v=4&s=48" width="48" height="48" alt="antons" title="antons"/></a> <a href="https://github.com/austinm911"><img src="https://avatars.githubusercontent.com/u/31991302?v=4&s=48" width="48" height="48" alt="austinm911" title="austinm911"/></a> <a href="https://github.com/apps/blacksmith-sh"><img src="https://avatars.githubusercontent.com/in/807020?v=4&s=48" width="48" height="48" alt="blacksmith-sh[bot]" title="blacksmith-sh[bot]"/></a> <a href="https://github.com/damoahdominic"><img src="https://avatars.githubusercontent.com/u/4623434?v=4&s=48" width="48" height="48" alt="damoahdominic" title="damoahdominic"/></a> <a href="https://github.com/dan-dr"><img src="https://avatars.githubusercontent.com/u/6669808?v=4&s=48" width="48" height="48" alt="dan-dr" title="dan-dr"/></a>
<a href="https://github.com/HeimdallStrategy"><img src="https://avatars.githubusercontent.com/u/223014405?v=4&s=48" width="48" height="48" alt="HeimdallStrategy" title="HeimdallStrategy"/></a> <a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/jarvis-medmatic"><img src="https://avatars.githubusercontent.com/u/252428873?v=4&s=48" width="48" height="48" alt="jarvis-medmatic" title="jarvis-medmatic"/></a> <a href="https://github.com/kkarimi"><img src="https://avatars.githubusercontent.com/u/875218?v=4&s=48" width="48" height="48" alt="kkarimi" title="kkarimi"/></a> <a href="https://github.com/mahmoudashraf93"><img src="https://avatars.githubusercontent.com/u/9130129?v=4&s=48" width="48" height="48" alt="mahmoudashraf93" title="mahmoudashraf93"/></a> <a href="https://github.com/ngutman"><img src="https://avatars.githubusercontent.com/u/1540134?v=4&s=48" width="48" height="48" alt="ngutman" title="ngutman"/></a> <a href="https://github.com/petter-b"><img src="https://avatars.githubusercontent.com/u/62076402?v=4&s=48" width="48" height="48" alt="petter-b" title="petter-b"/></a> <a href="https://github.com/pkrmf"><img src="https://avatars.githubusercontent.com/u/1714267?v=4&s=48" width="48" height="48" alt="pkrmf" title="pkrmf"/></a> <a href="https://github.com/RandyVentures"><img src="https://avatars.githubusercontent.com/u/149904821?v=4&s=48" width="48" height="48" alt="RandyVentures" title="RandyVentures"/></a>
<a href="https://github.com/search?q=Ryan%20Lisse"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ryan Lisse" title="Ryan Lisse"/></a> <a href="https://github.com/dougvk"><img src="https://avatars.githubusercontent.com/u/401660?v=4&s=48" width="48" height="48" alt="dougvk" title="dougvk"/></a> <a href="https://github.com/erikpr1994"><img src="https://avatars.githubusercontent.com/u/6299331?v=4&s=48" width="48" height="48" alt="erikpr1994" title="erikpr1994"/></a> <a href="https://github.com/search?q=Ghost"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ghost" title="Ghost"/></a> <a href="https://github.com/jonasjancarik"><img src="https://avatars.githubusercontent.com/u/2459191?v=4&s=48" width="48" height="48" alt="jonasjancarik" title="jonasjancarik"/></a> <a href="https://github.com/search?q=Keith%20the%20Silly%20Goose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Keith the Silly Goose" title="Keith the Silly Goose"/></a> <a href="https://github.com/search?q=L36%20Server"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="L36 Server" title="L36 Server"/></a> <a href="https://github.com/search?q=Marc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc" title="Marc"/></a> <a href="https://github.com/mitschabaude-bot"><img src="https://avatars.githubusercontent.com/u/247582884?v=4&s=48" width="48" height="48" alt="mitschabaude-bot" title="mitschabaude-bot"/></a> <a href="https://github.com/mkbehr"><img src="https://avatars.githubusercontent.com/u/1285?v=4&s=48" width="48" height="48" alt="mkbehr" title="mkbehr"/></a>
<a href="https://github.com/neist"><img src="https://avatars.githubusercontent.com/u/1029724?v=4&s=48" width="48" height="48" alt="neist" title="neist"/></a> <a href="https://github.com/sibbl"><img src="https://avatars.githubusercontent.com/u/866535?v=4&s=48" width="48" height="48" alt="sibbl" title="sibbl"/></a> <a href="https://github.com/chrisrodz"><img src="https://avatars.githubusercontent.com/u/2967620?v=4&s=48" width="48" height="48" alt="chrisrodz" title="chrisrodz"/></a> <a href="https://github.com/czekaj"><img src="https://avatars.githubusercontent.com/u/1464539?v=4&s=48" width="48" height="48" alt="czekaj" title="czekaj"/></a> <a href="https://github.com/search?q=Friederike%20Seiler"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Friederike Seiler" title="Friederike Seiler"/></a> <a href="https://github.com/gabriel-trigo"><img src="https://avatars.githubusercontent.com/u/38991125?v=4&s=48" width="48" height="48" alt="gabriel-trigo" title="gabriel-trigo"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="iamadig" title="iamadig"/></a> <a href="https://github.com/jdrhyne"><img src="https://avatars.githubusercontent.com/u/7828464?v=4&s=48" width="48" height="48" alt="Jonathan D. Rhyne (DJ-D)" title="Jonathan D. Rhyne (DJ-D)"/></a> <a href="https://github.com/search?q=Kit"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kit" title="Kit"/></a> <a href="https://github.com/koala73"><img src="https://avatars.githubusercontent.com/u/996596?v=4&s=48" width="48" height="48" alt="koala73" title="koala73"/></a>
<a href="https://github.com/manmal"><img src="https://avatars.githubusercontent.com/u/142797?v=4&s=48" width="48" height="48" alt="manmal" title="manmal"/></a> <a href="https://github.com/ogulcancelik"><img src="https://avatars.githubusercontent.com/u/7064011?v=4&s=48" width="48" height="48" alt="ogulcancelik" title="ogulcancelik"/></a> <a href="https://github.com/pasogott"><img src="https://avatars.githubusercontent.com/u/23458152?v=4&s=48" width="48" height="48" alt="pasogott" title="pasogott"/></a> <a href="https://github.com/petradonka"><img src="https://avatars.githubusercontent.com/u/7353770?v=4&s=48" width="48" height="48" alt="petradonka" title="petradonka"/></a> <a href="https://github.com/rubyrunsstuff"><img src="https://avatars.githubusercontent.com/u/246602379?v=4&s=48" width="48" height="48" alt="rubyrunsstuff" title="rubyrunsstuff"/></a> <a href="https://github.com/siddhantjain"><img src="https://avatars.githubusercontent.com/u/4835232?v=4&s=48" width="48" height="48" alt="siddhantjain" title="siddhantjain"/></a> <a href="https://github.com/suminhthanh"><img src="https://avatars.githubusercontent.com/u/2907636?v=4&s=48" width="48" height="48" alt="suminhthanh" title="suminhthanh"/></a> <a href="https://github.com/svkozak"><img src="https://avatars.githubusercontent.com/u/31941359?v=4&s=48" width="48" height="48" alt="svkozak" title="svkozak"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/wes-davis"><img src="https://avatars.githubusercontent.com/u/16506720?v=4&s=48" width="48" height="48" alt="wes-davis" title="wes-davis"/></a>
<a href="https://github.com/zats"><img src="https://avatars.githubusercontent.com/u/2688806?v=4&s=48" width="48" height="48" alt="zats" title="zats"/></a> <a href="https://github.com/24601"><img src="https://avatars.githubusercontent.com/u/1157207?v=4&s=48" width="48" height="48" alt="24601" title="24601"/></a> <a href="https://github.com/ameno-"><img src="https://avatars.githubusercontent.com/u/2416135?v=4&s=48" width="48" height="48" alt="ameno-" title="ameno-"/></a> <a href="https://github.com/search?q=Chris%20Taylor"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Chris Taylor" title="Chris Taylor"/></a> <a href="https://github.com/djangonavarro220"><img src="https://avatars.githubusercontent.com/u/251162586?v=4&s=48" width="48" height="48" alt="Django Navarro" title="Django Navarro"/></a> <a href="https://github.com/evalexpr"><img src="https://avatars.githubusercontent.com/u/23485511?v=4&s=48" width="48" height="48" alt="evalexpr" title="evalexpr"/></a> <a href="https://github.com/henrino3"><img src="https://avatars.githubusercontent.com/u/4260288?v=4&s=48" width="48" height="48" alt="henrino3" title="henrino3"/></a> <a href="https://github.com/humanwritten"><img src="https://avatars.githubusercontent.com/u/206531610?v=4&s=48" width="48" height="48" alt="humanwritten" title="humanwritten"/></a> <a href="https://github.com/larlyssa"><img src="https://avatars.githubusercontent.com/u/13128869?v=4&s=48" width="48" height="48" alt="larlyssa" title="larlyssa"/></a> <a href="https://github.com/odysseus0"><img src="https://avatars.githubusercontent.com/u/8635094?v=4&s=48" width="48" height="48" alt="odysseus0" title="odysseus0"/></a>
<a href="https://github.com/oswalpalash"><img src="https://avatars.githubusercontent.com/u/6431196?v=4&s=48" width="48" height="48" alt="oswalpalash" title="oswalpalash"/></a> <a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a> <a href="https://github.com/Syhids"><img src="https://avatars.githubusercontent.com/u/671202?v=4&s=48" width="48" height="48" alt="Syhids" title="Syhids"/></a> <a href="https://github.com/search?q=Aaron%20Konyer"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Aaron Konyer" title="Aaron Konyer"/></a> <a href="https://github.com/aaronveklabs"><img src="https://avatars.githubusercontent.com/u/225997828?v=4&s=48" width="48" height="48" alt="aaronveklabs" title="aaronveklabs"/></a> <a href="https://github.com/adam91holt"><img src="https://avatars.githubusercontent.com/u/9592417?v=4&s=48" width="48" height="48" alt="adam91holt" title="adam91holt"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/search?q=Clawd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Clawd" title="Clawd"/></a> <a href="https://github.com/search?q=ClawdFx"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ClawdFx" title="ClawdFx"/></a> <a href="https://github.com/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></a>
<a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a> <a href="https://github.com/ivanrvpereira"><img src="https://avatars.githubusercontent.com/u/183991?v=4&s=48" width="48" height="48" alt="ivanrvpereira" title="ivanrvpereira"/></a> <a href="https://github.com/search?q=jeffersonwarrior"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/jverdi"><img src="https://avatars.githubusercontent.com/u/345050?v=4&s=48" width="48" height="48" alt="jverdi" title="jverdi"/></a> <a href="https://github.com/longmaba"><img src="https://avatars.githubusercontent.com/u/9361500?v=4&s=48" width="48" height="48" alt="longmaba" title="longmaba"/></a> <a href="https://github.com/mickahouan"><img src="https://avatars.githubusercontent.com/u/31423109?v=4&s=48" width="48" height="48" alt="mickahouan" title="mickahouan"/></a> <a href="https://github.com/mjrussell"><img src="https://avatars.githubusercontent.com/u/1641895?v=4&s=48" width="48" height="48" alt="mjrussell" title="mjrussell"/></a> <a href="https://github.com/p6l-richard"><img src="https://avatars.githubusercontent.com/u/18185649?v=4&s=48" width="48" height="48" alt="p6l-richard" title="p6l-richard"/></a> <a href="https://github.com/philipp-spiess"><img src="https://avatars.githubusercontent.com/u/458591?v=4&s=48" width="48" height="48" alt="philipp-spiess" title="philipp-spiess"/></a> <a href="https://github.com/robaxelsen"><img src="https://avatars.githubusercontent.com/u/13132899?v=4&s=48" width="48" height="48" alt="robaxelsen" title="robaxelsen"/></a>
<a href="https://github.com/search?q=Sash%20Catanzarite"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Sash Catanzarite" title="Sash Catanzarite"/></a> <a href="https://github.com/T5-AndyML"><img src="https://avatars.githubusercontent.com/u/22801233?v=4&s=48" width="48" height="48" alt="T5-AndyML" title="T5-AndyML"/></a> <a href="https://github.com/search?q=VAC"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="VAC" title="VAC"/></a> <a href="https://github.com/zknicker"><img src="https://avatars.githubusercontent.com/u/1164085?v=4&s=48" width="48" height="48" alt="zknicker" title="zknicker"/></a> <a href="https://github.com/aj47"><img src="https://avatars.githubusercontent.com/u/8023513?v=4&s=48" width="48" height="48" alt="aj47" title="aj47"/></a> <a href="https://github.com/search?q=alejandro%20maza"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="alejandro maza" title="alejandro maza"/></a> <a href="https://github.com/andrewting19"><img src="https://avatars.githubusercontent.com/u/10536704?v=4&s=48" width="48" height="48" alt="andrewting19" title="andrewting19"/></a> <a href="https://github.com/search?q=Andrii"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Andrii" title="Andrii"/></a> <a href="https://github.com/anpoirier"><img src="https://avatars.githubusercontent.com/u/1245729?v=4&s=48" width="48" height="48" alt="anpoirier" title="anpoirier"/></a> <a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a>
<a href="https://github.com/bolismauro"><img src="https://avatars.githubusercontent.com/u/771999?v=4&s=48" width="48" height="48" alt="bolismauro" title="bolismauro"/></a> <a href="https://github.com/conhecendoia"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendoia" title="conhecendoia"/></a> <a href="https://github.com/search?q=Dimitrios%20Ploutarchos"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Dimitrios Ploutarchos" title="Dimitrios Ploutarchos"/></a> <a href="https://github.com/search?q=Drake%20Thomsen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Drake Thomsen" title="Drake Thomsen"/></a> <a href="https://github.com/search?q=Felix%20Krause"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Felix Krause" title="Felix Krause"/></a> <a href="https://github.com/search?q=ganghyun%20kim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ganghyun kim" title="ganghyun kim"/></a> <a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a> <a href="https://github.com/HazAT"><img src="https://avatars.githubusercontent.com/u/363802?v=4&s=48" width="48" height="48" alt="HazAT" title="HazAT"/></a> <a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a> <a href="https://github.com/hugobarauna"><img src="https://avatars.githubusercontent.com/u/2719?v=4&s=48" width="48" height="48" alt="hugobarauna" title="hugobarauna"/></a>
<a href="https://github.com/search?q=Jamie%20Openshaw"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jamie Openshaw" title="Jamie Openshaw"/></a> <a href="https://github.com/search?q=Jarvis"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jarvis" title="Jarvis"/></a> <a href="https://github.com/search?q=Jefferson%20Nunn"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Jefferson Nunn" title="Jefferson Nunn"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/levifig"><img src="https://avatars.githubusercontent.com/u/1605?v=4&s=48" width="48" height="48" alt="levifig" title="levifig"/></a> <a href="https://github.com/search?q=Lloyd"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Lloyd" title="Lloyd"/></a> <a href="https://github.com/loukotal"><img src="https://avatars.githubusercontent.com/u/18210858?v=4&s=48" width="48" height="48" alt="loukotal" title="loukotal"/></a> <a href="https://github.com/martinpucik"><img src="https://avatars.githubusercontent.com/u/5503097?v=4&s=48" width="48" height="48" alt="martinpucik" title="martinpucik"/></a> <a href="https://github.com/search?q=Matt%20mini"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Matt mini" title="Matt mini"/></a> <a href="https://github.com/search?q=Miles"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Miles" title="Miles"/></a>
<a href="https://github.com/mrdbstn"><img src="https://avatars.githubusercontent.com/u/58957632?v=4&s=48" width="48" height="48" alt="mrdbstn" title="mrdbstn"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a> <a href="https://github.com/search?q=Mustafa%20Tag%20Eldeen"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mustafa Tag Eldeen" title="Mustafa Tag Eldeen"/></a> <a href="https://github.com/ndraiman"><img src="https://avatars.githubusercontent.com/u/12609607?v=4&s=48" width="48" height="48" alt="ndraiman" title="ndraiman"/></a> <a href="https://github.com/nexty5870"><img src="https://avatars.githubusercontent.com/u/3869659?v=4&s=48" width="48" height="48" alt="nexty5870" title="nexty5870"/></a> <a href="https://github.com/odnxe"><img src="https://avatars.githubusercontent.com/u/403141?v=4&s=48" width="48" height="48" alt="odnxe" title="odnxe"/></a> <a href="https://github.com/prathamdby"><img src="https://avatars.githubusercontent.com/u/134331217?v=4&s=48" width="48" height="48" alt="prathamdby" title="prathamdby"/></a> <a href="https://github.com/ptn1411"><img src="https://avatars.githubusercontent.com/u/57529765?v=4&s=48" width="48" height="48" alt="ptn1411" title="ptn1411"/></a> <a href="https://github.com/reeltimeapps"><img src="https://avatars.githubusercontent.com/u/637338?v=4&s=48" width="48" height="48" alt="reeltimeapps" title="reeltimeapps"/></a> <a href="https://github.com/RLTCmpe"><img src="https://avatars.githubusercontent.com/u/10762242?v=4&s=48" width="48" height="48" alt="RLTCmpe" title="RLTCmpe"/></a>
<a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/search?q=Rony%20Kelner"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rony Kelner" title="Rony Kelner"/></a> <a href="https://github.com/search?q=Samrat%20Jha"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Samrat Jha" title="Samrat Jha"/></a> <a href="https://github.com/shiv19"><img src="https://avatars.githubusercontent.com/u/9407019?v=4&s=48" width="48" height="48" alt="shiv19" title="shiv19"/></a> <a href="https://github.com/siraht"><img src="https://avatars.githubusercontent.com/u/73152895?v=4&s=48" width="48" height="48" alt="siraht" title="siraht"/></a> <a href="https://github.com/snopoke"><img src="https://avatars.githubusercontent.com/u/249606?v=4&s=48" width="48" height="48" alt="snopoke" title="snopoke"/></a> <a href="https://github.com/testingabc321"><img src="https://avatars.githubusercontent.com/u/8577388?v=4&s=48" width="48" height="48" alt="testingabc321" title="testingabc321"/></a> <a href="https://github.com/search?q=The%20Admiral"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="The Admiral" title="The Admiral"/></a> <a href="https://github.com/thesash"><img src="https://avatars.githubusercontent.com/u/1166151?v=4&s=48" width="48" height="48" alt="thesash" title="thesash"/></a> <a href="https://github.com/search?q=Ubuntu"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Ubuntu" title="Ubuntu"/></a>
<a href="https://github.com/voidserf"><img src="https://avatars.githubusercontent.com/u/477673?v=4&s=48" width="48" height="48" alt="voidserf" title="voidserf"/></a> <a href="https://github.com/search?q=Vultr-Clawd%20Admin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vultr-Clawd Admin" title="Vultr-Clawd Admin"/></a> <a href="https://github.com/search?q=william%20arzt"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="william arzt" title="william arzt"/></a> <a href="https://github.com/search?q=Wimmie"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Wimmie" title="Wimmie"/></a> <a href="https://github.com/wstock"><img src="https://avatars.githubusercontent.com/u/1394687?v=4&s=48" width="48" height="48" alt="wstock" title="wstock"/></a> <a href="https://github.com/yazinsai"><img src="https://avatars.githubusercontent.com/u/1846034?v=4&s=48" width="48" height="48" alt="yazinsai" title="yazinsai"/></a> <a href="https://github.com/search?q=Zach%20Knickerbocker"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Zach Knickerbocker" title="Zach Knickerbocker"/></a> <a href="https://github.com/Alphonse-arianee"><img src="https://avatars.githubusercontent.com/u/254457365?v=4&s=48" width="48" height="48" alt="Alphonse-arianee" title="Alphonse-arianee"/></a> <a href="https://github.com/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a> <a href="https://github.com/carlulsoe"><img src="https://avatars.githubusercontent.com/u/34673973?v=4&s=48" width="48" height="48" alt="carlulsoe" title="carlulsoe"/></a>
<a href="https://github.com/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a> <a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></a> <a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a> <a href="https://github.com/jeffersonwarrior"><img src="https://avatars.githubusercontent.com/u/89030989?v=4&s=48" width="48" height="48" alt="jeffersonwarrior" title="jeffersonwarrior"/></a> <a href="https://github.com/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></a> <a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/odrobnik"><img src="https://avatars.githubusercontent.com/u/333270?v=4&s=48" width="48" height="48" alt="odrobnik" title="odrobnik"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a>
<a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a> <a href="https://github.com/rhjoh"><img src="https://avatars.githubusercontent.com/u/105699450?v=4&s=48" width="48" height="48" alt="rhjoh" title="rhjoh"/></a> <a href="https://github.com/ronak-guliani"><img src="https://avatars.githubusercontent.com/u/23518228?v=4&s=48" width="48" height="48" alt="ronak-guliani" title="ronak-guliani"/></a> <a href="https://github.com/search?q=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
</p>

View File

@@ -2,6 +2,54 @@
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>Clawdbot</title>
<item>
<title>2026.1.22</title>
<pubDate>Fri, 23 Jan 2026 08:58:14 +0000</pubDate>
<link>https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml</link>
<sparkle:version>7530</sparkle:version>
<sparkle:shortVersionString>2026.1.22</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>Clawdbot 2026.1.22</h2>
<h3>Changes</h3>
<ul>
<li>Highlight: Compaction safeguard now uses adaptive chunking, progressive fallback, and UI status + retries. (#1466) Thanks @dlauer.</li>
<li>Providers: add Antigravity usage tracking to status output. (#1490) Thanks @patelhiren.</li>
<li>Slack: add chat-type reply threading overrides via <code>replyToModeByChatType</code>. (#1442) Thanks @stefangalescu.</li>
<li>BlueBubbles: add <code>asVoice</code> support for MP3/CAF voice memos in sendAttachment. (#1477, #1482) Thanks @Nicell.</li>
<li>Onboarding: add hatch choice (TUI/Web/Later), token explainer, background dashboard seed on macOS, and showcase link.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>BlueBubbles: stop typing indicator on idle/no-reply. (#1439) Thanks @Nicell.</li>
<li>Message tool: keep path/filePath as-is for send; hydrate buffers only for sendAttachment. (#1444) Thanks @hopyky.</li>
<li>Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla.</li>
<li>Control UI: resolve local avatar URLs with basePath across injection + identity RPC. (#1457) Thanks @dlauer.</li>
<li>Agents: sanitize assistant history text to strip tool-call markers. (#1456) Thanks @zerone0x.</li>
<li>Discord: clarify Message Content Intent onboarding hint. (#1487) Thanks @kyleok.</li>
<li>Gateway: stop the service before uninstalling and fail if it remains loaded.</li>
<li>Agents: surface concrete API error details instead of generic AI service errors.</li>
<li>Exec: fall back to non-PTY when PTY spawn fails (EBADF). (#1484)</li>
<li>Exec approvals: allow per-segment allowlists for chained shell commands on gateway + node hosts. (#1458) Thanks @czekaj.</li>
<li>Agents: make OpenAI sessions image-sanitize-only; gate tool-id/repair sanitization by provider.</li>
<li>Doctor: honor CLAWDBOT_GATEWAY_TOKEN for auth checks and security audit token reuse. (#1448) Thanks @azade-c.</li>
<li>Agents: make tool summaries more readable and only show optional params when set.</li>
<li>Agents: honor SOUL.md guidance even when the file is nested or path-qualified. (#1434) Thanks @neooriginal.</li>
<li>Matrix (plugin): persist m.direct for resolved DMs and harden room fallback. (#1436, #1486) Thanks @sibbl.</li>
<li>CLI: prefer <code>~</code> for home paths in output.</li>
<li>Mattermost (plugin): enforce pairing/allowlist gating, keep @username targets, and clarify plugin-only docs. (#1428) Thanks @damoahdominic.</li>
<li>Agents: centralize transcript sanitization in the runner; keep <final> tags and error turns intact.</li>
<li>Auth: skip auth profiles in cooldown during initial selection and rotation. (#1316) Thanks @odrobnik.</li>
<li>Agents/TUI: honor user-pinned auth profiles during cooldown and preserve search picker ranking. (#1432) Thanks @tobiasbischoff.</li>
<li>Docs: fix gog auth services example to include docs scope. (#1454) Thanks @zerone0x.</li>
<li>Slack: reduce WebClient retries to avoid duplicate sends. (#1481)</li>
<li>Slack: read thread replies for message reads when threadId is provided (replies-only). (#1450) Thanks @rodrigouroz.</li>
<li>macOS: prefer linked channels in gateway summary to avoid false “not linked” status.</li>
<li>macOS/tests: fix gateway summary lookup after guard unwrap; prevent browser opens during tests. (ECID-1483)</li>
</ul>
<p><a href="https://github.com/clawdbot/clawdbot/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.22/Clawdbot-2026.1.22.zip" length="22302446" type="application/octet-stream" sparkle:edSignature="w/EzfwGBCRRuCg5vz8enIfYujxOZJWRw9PaunQ7gIafKwnBJSTtxcnkvMVwQsnBwB6VN5Tu2MPij7PjDFFX+CA=="/>
</item>
<item>
<title>2026.1.21</title>
<pubDate>Thu, 22 Jan 2026 12:22:35 +0000</pubDate>
@@ -266,21 +314,5 @@ Thanks @AlexMikhalev, @CoreyH, @John-Rood, @KrauseFx, @MaudeBot, @Nachx639, @Nic
]]></description>
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.21/Clawdbot-2026.1.21.zip" length="12208102" type="application/octet-stream" sparkle:edSignature="hU495Eii8O3qmmUnxYFhXyEGv+qan6KL+GpeuBhPIXf+7B5F/gBh5Oz9cHaqaAPoZ4/3Bo6xgvic0HTkbz6gDw=="/>
</item>
<item>
<title>2026.1.16-2</title>
<pubDate>Sat, 17 Jan 2026 12:46:22 +0000</pubDate>
<link>https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml</link>
<sparkle:version>6273</sparkle:version>
<sparkle:shortVersionString>2026.1.16-2</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>Clawdbot 2026.1.16-2</h2>
<h3>Changes</h3>
<ul>
<li>CLI: stamp build commit into dist metadata so banners show the commit in npm installs.</li>
</ul>
<p><a href="https://github.com/clawdbot/clawdbot/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.16-2/Clawdbot-2026.1.16-2.zip" length="21399591" type="application/octet-stream" sparkle:edSignature="zelT+KzN32cXsihbFniPF5Heq0hkwFfL3Agrh/AaoKUkr7kJAFarkGSOZRTWZ9y+DvOluzn2wHHjVigRjMzrBA=="/>
</item>
</channel>
</rss>

View File

@@ -21,8 +21,8 @@ android {
applicationId = "com.clawdbot.android"
minSdk = 31
targetSdk = 36
versionCode = 202601210
versionName = "2026.1.21"
versionCode = 202601230
versionName = "2026.1.23"
}
buildTypes {

View File

@@ -8,10 +8,14 @@ object WakeWords {
return input.split(",").map { it.trim() }.filter { it.isNotEmpty() }
}
fun parseIfChanged(input: String, current: List<String>): List<String>? {
val parsed = parseCommaSeparated(input)
return if (parsed == current) null else parsed
}
fun sanitize(words: List<String>, defaults: List<String>): List<String> {
val cleaned =
words.map { it.trim() }.filter { it.isNotEmpty() }.take(maxWords).map { it.take(maxWordLength) }
return cleaned.ifEmpty { defaults }
}
}

View File

@@ -28,6 +28,8 @@ import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
@@ -49,7 +51,10 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
@@ -58,6 +63,7 @@ import com.clawdbot.android.LocationMode
import com.clawdbot.android.MainViewModel
import com.clawdbot.android.NodeForegroundService
import com.clawdbot.android.VoiceWakeMode
import com.clawdbot.android.WakeWords
@Composable
fun SettingsSheet(viewModel: MainViewModel) {
@@ -86,6 +92,8 @@ fun SettingsSheet(viewModel: MainViewModel) {
val listState = rememberLazyListState()
val (wakeWordsText, setWakeWordsText) = remember { mutableStateOf("") }
val (advancedExpanded, setAdvancedExpanded) = remember { mutableStateOf(false) }
val focusManager = LocalFocusManager.current
var wakeWordsHadFocus by remember { mutableStateOf(false) }
val deviceModel =
remember {
listOfNotNull(Build.MANUFACTURER, Build.MODEL)
@@ -104,6 +112,12 @@ fun SettingsSheet(viewModel: MainViewModel) {
}
LaunchedEffect(wakeWords) { setWakeWordsText(wakeWords.joinToString(", ")) }
val commitWakeWords = {
val parsed = WakeWords.parseIfChanged(wakeWordsText, wakeWords)
if (parsed != null) {
viewModel.setWakeWords(parsed)
}
}
val permissionLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { perms ->
@@ -481,25 +495,27 @@ fun SettingsSheet(viewModel: MainViewModel) {
value = wakeWordsText,
onValueChange = setWakeWordsText,
label = { Text("Wake Words (comma-separated)") },
modifier = Modifier.fillMaxWidth(),
modifier =
Modifier.fillMaxWidth().onFocusChanged { focusState ->
if (focusState.isFocused) {
wakeWordsHadFocus = true
} else if (wakeWordsHadFocus) {
wakeWordsHadFocus = false
commitWakeWords()
}
},
singleLine = true,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions =
KeyboardActions(
onDone = {
commitWakeWords()
focusManager.clearFocus()
},
),
)
}
item {
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(
onClick = {
val parsed = com.clawdbot.android.WakeWords.parseCommaSeparated(wakeWordsText)
viewModel.setWakeWords(parsed)
},
enabled = isConnected,
) {
Text("Save + Sync")
}
Button(onClick = viewModel::resetWakeWordsDefaults) { Text("Reset defaults") }
}
}
item { Button(onClick = viewModel::resetWakeWordsDefaults) { Text("Reset defaults") } }
item {
Text(
if (isConnected) {

View File

@@ -1,6 +1,7 @@
package com.clawdbot.android
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
class WakeWordsTest {
@@ -32,5 +33,18 @@ class WakeWordsTest {
assertEquals("w1", sanitized.first())
assertEquals("w${WakeWords.maxWords}", sanitized.last())
}
}
@Test
fun parseIfChangedSkipsWhenUnchanged() {
val current = listOf("clawd", "claude")
val parsed = WakeWords.parseIfChanged(" clawd , claude ", current)
assertNull(parsed)
}
@Test
fun parseIfChangedReturnsUpdatedList() {
val current = listOf("clawd")
val parsed = WakeWords.parseIfChanged(" clawd , jarvis ", current)
assertEquals(listOf("clawd", "jarvis"), parsed)
}
}

View File

@@ -19,9 +19,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.1.21</string>
<string>2026.1.23</string>
<key>CFBundleVersion</key>
<string>20260121</string>
<string>20260123</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoadsInWebContent</key>

View File

@@ -1,8 +1,10 @@
import SwiftUI
import Combine
struct VoiceWakeWordsSettingsView: View {
@Environment(NodeAppModel.self) private var appModel
@State private var triggerWords: [String] = VoiceWakePreferences.loadTriggerWords()
@FocusState private var focusedTriggerIndex: Int?
@State private var syncTask: Task<Void, Never>?
var body: some View {
@@ -12,6 +14,10 @@ struct VoiceWakeWordsSettingsView: View {
TextField("Wake word", text: self.binding(for: index))
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.focused(self.$focusedTriggerIndex, equals: index)
.onSubmit {
self.commitTriggerWords()
}
}
.onDelete(perform: self.removeWords)
@@ -39,17 +45,18 @@ struct VoiceWakeWordsSettingsView: View {
.onAppear {
if self.triggerWords.isEmpty {
self.triggerWords = VoiceWakePreferences.defaultTriggerWords
self.commitTriggerWords()
}
}
.onChange(of: self.triggerWords) { _, newValue in
// Keep local voice wake responsive even if the gateway isn't connected yet.
VoiceWakePreferences.saveTriggerWords(newValue)
let snapshot = VoiceWakePreferences.sanitizeTriggerWords(newValue)
self.syncTask?.cancel()
self.syncTask = Task { [snapshot, weak appModel = self.appModel] in
try? await Task.sleep(nanoseconds: 650_000_000)
await appModel?.setGlobalWakeWords(snapshot)
.onChange(of: self.focusedTriggerIndex) { oldValue, newValue in
guard oldValue != nil, oldValue != newValue else { return }
self.commitTriggerWords()
}
.onReceive(NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification)) { _ in
guard self.focusedTriggerIndex == nil else { return }
let updated = VoiceWakePreferences.loadTriggerWords()
if updated != self.triggerWords {
self.triggerWords = updated
}
}
}
@@ -63,6 +70,7 @@ struct VoiceWakeWordsSettingsView: View {
if self.triggerWords.isEmpty {
self.triggerWords = VoiceWakePreferences.defaultTriggerWords
}
self.commitTriggerWords()
}
private func binding(for index: Int) -> Binding<String> {
@@ -76,4 +84,15 @@ struct VoiceWakeWordsSettingsView: View {
self.triggerWords[index] = newValue
})
}
private func commitTriggerWords() {
VoiceWakePreferences.saveTriggerWords(self.triggerWords)
let snapshot = VoiceWakePreferences.sanitizeTriggerWords(self.triggerWords)
self.syncTask?.cancel()
self.syncTask = Task { [snapshot, weak appModel = self.appModel] in
try? await Task.sleep(nanoseconds: 650_000_000)
await appModel?.setGlobalWakeWords(snapshot)
}
}
}

View File

@@ -6,6 +6,8 @@ enum VoiceWakePreferences {
// Keep defaults aligned with the mac app.
static let defaultTriggerWords: [String] = ["clawd", "claude"]
static let maxWords = 32
static let maxWordLength = 64
static func decodeGatewayTriggers(from payloadJSON: String) -> [String]? {
guard let data = payloadJSON.data(using: .utf8) else { return nil }
@@ -30,6 +32,8 @@ enum VoiceWakePreferences {
let cleaned = words
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
.prefix(Self.maxWords)
.map { String($0.prefix(Self.maxWordLength)) }
return cleaned.isEmpty ? Self.defaultTriggerWords : cleaned
}

View File

@@ -17,8 +17,8 @@
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>2026.1.21</string>
<string>2026.1.23</string>
<key>CFBundleVersion</key>
<string>20260121</string>
<string>20260123</string>
</dict>
</plist>

View File

@@ -11,6 +11,18 @@ import Testing
#expect(VoiceWakePreferences.sanitizeTriggerWords(["", " "]) == VoiceWakePreferences.defaultTriggerWords)
}
@Test func sanitizeTriggerWordsLimitsWordLength() {
let long = String(repeating: "x", count: VoiceWakePreferences.maxWordLength + 5)
let cleaned = VoiceWakePreferences.sanitizeTriggerWords(["ok", long])
#expect(cleaned[1].count == VoiceWakePreferences.maxWordLength)
}
@Test func sanitizeTriggerWordsLimitsWordCount() {
let words = (1...VoiceWakePreferences.maxWords + 3).map { "w\($0)" }
let cleaned = VoiceWakePreferences.sanitizeTriggerWords(words)
#expect(cleaned.count == VoiceWakePreferences.maxWords)
}
@Test func displayStringUsesSanitizedWords() {
#expect(VoiceWakePreferences.displayString(for: ["", " "]) == "clawd, claude")
}

View File

@@ -81,8 +81,8 @@ targets:
properties:
CFBundleDisplayName: Clawdbot
CFBundleIconName: AppIcon
CFBundleShortVersionString: "2026.1.21"
CFBundleVersion: "20260121"
CFBundleShortVersionString: "2026.1.23"
CFBundleVersion: "20260123"
UILaunchScreen: {}
UIApplicationSceneManifest:
UIApplicationSupportsMultipleScenes: false
@@ -130,5 +130,5 @@ targets:
path: Tests/Info.plist
properties:
CFBundleDisplayName: ClawdbotTests
CFBundleShortVersionString: "2026.1.21"
CFBundleVersion: "20260121"
CFBundleShortVersionString: "2026.1.23"
CFBundleVersion: "20260123"

View File

@@ -12,6 +12,8 @@ let voiceWakeTriggerChimeKey = "clawdbot.voiceWakeTriggerChime"
let voiceWakeSendChimeKey = "clawdbot.voiceWakeSendChime"
let showDockIconKey = "clawdbot.showDockIcon"
let defaultVoiceWakeTriggers = ["clawd", "claude"]
let voiceWakeMaxWords = 32
let voiceWakeMaxWordLength = 64
let voiceWakeMicKey = "clawdbot.voiceWakeMicID"
let voiceWakeMicNameKey = "clawdbot.voiceWakeMicName"
let voiceWakeLocaleKey = "clawdbot.voiceWakeLocaleID"

View File

@@ -16,6 +16,8 @@ struct DebugSettings: View {
@State private var modelsError: String?
private let gatewayManager = GatewayProcessManager.shared
private let healthStore = HealthStore.shared
@State private var launchAgentWriteDisabled = GatewayLaunchAgentManager.isLaunchAgentWriteDisabled()
@State private var launchAgentWriteError: String?
@State private var gatewayRootInput: String = GatewayProcessManager.shared.projectRootPath()
@State private var sessionStorePath: String = SessionLoader.defaultStorePath
@State private var sessionStoreSaveError: String?
@@ -47,6 +49,7 @@ struct DebugSettings: View {
VStack(alignment: .leading, spacing: 14) {
self.header
self.launchdSection
self.appInfoSection
self.gatewaySection
self.logsSection
@@ -79,6 +82,41 @@ struct DebugSettings: View {
}
}
private var launchdSection: some View {
GroupBox("Gateway startup") {
VStack(alignment: .leading, spacing: 8) {
Toggle("Attach only (skip launchd install)", isOn: self.$launchAgentWriteDisabled)
.onChange(of: self.launchAgentWriteDisabled) { _, newValue in
self.launchAgentWriteError = GatewayLaunchAgentManager.setLaunchAgentWriteDisabled(newValue)
if self.launchAgentWriteError != nil {
self.launchAgentWriteDisabled = GatewayLaunchAgentManager.isLaunchAgentWriteDisabled()
return
}
if newValue {
Task {
_ = await GatewayLaunchAgentManager.set(
enabled: false,
bundlePath: Bundle.main.bundlePath,
port: GatewayEnvironment.gatewayPort())
}
}
}
Text(
"When enabled, Clawdbot won't install or manage \(gatewayLaunchdLabel). " +
"It will only attach to an existing Gateway.")
.font(.caption)
.foregroundStyle(.secondary)
if let launchAgentWriteError {
Text(launchAgentWriteError)
.font(.caption)
.foregroundStyle(.red)
}
}
}
}
private var header: some View {
VStack(alignment: .leading, spacing: 6) {
Text("Debug")

View File

@@ -84,11 +84,52 @@ enum ExecApprovalDecision: String, Codable, Sendable {
case deny
}
struct ExecAllowlistEntry: Codable, Hashable {
struct ExecAllowlistEntry: Codable, Hashable, Identifiable {
var id: UUID
var pattern: String
var lastUsedAt: Double?
var lastUsedCommand: String?
var lastResolvedPath: String?
init(
id: UUID = UUID(),
pattern: String,
lastUsedAt: Double? = nil,
lastUsedCommand: String? = nil,
lastResolvedPath: String? = nil)
{
self.id = id
self.pattern = pattern
self.lastUsedAt = lastUsedAt
self.lastUsedCommand = lastUsedCommand
self.lastResolvedPath = lastResolvedPath
}
private enum CodingKeys: String, CodingKey {
case id
case pattern
case lastUsedAt
case lastUsedCommand
case lastResolvedPath
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decodeIfPresent(UUID.self, forKey: .id) ?? UUID()
self.pattern = try container.decode(String.self, forKey: .pattern)
self.lastUsedAt = try container.decodeIfPresent(Double.self, forKey: .lastUsedAt)
self.lastUsedCommand = try container.decodeIfPresent(String.self, forKey: .lastUsedCommand)
self.lastResolvedPath = try container.decodeIfPresent(String.self, forKey: .lastResolvedPath)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.id, forKey: .id)
try container.encode(self.pattern, forKey: .pattern)
try container.encodeIfPresent(self.lastUsedAt, forKey: .lastUsedAt)
try container.encodeIfPresent(self.lastUsedCommand, forKey: .lastUsedCommand)
try container.encodeIfPresent(self.lastResolvedPath, forKey: .lastResolvedPath)
}
}
struct ExecApprovalsDefaults: Codable {
@@ -295,6 +336,7 @@ enum ExecApprovalsStore {
let allowlist = ((wildcardEntry.allowlist ?? []) + (agentEntry.allowlist ?? []))
.map { entry in
ExecAllowlistEntry(
id: entry.id,
pattern: entry.pattern.trimmingCharacters(in: .whitespacesAndNewlines),
lastUsedAt: entry.lastUsedAt,
lastUsedCommand: entry.lastUsedCommand,
@@ -379,6 +421,7 @@ enum ExecApprovalsStore {
let allowlist = (entry.allowlist ?? []).map { item -> ExecAllowlistEntry in
guard item.pattern == pattern else { return item }
return ExecAllowlistEntry(
id: item.id,
pattern: item.pattern,
lastUsedAt: Date().timeIntervalSince1970 * 1000,
lastUsedCommand: command,
@@ -398,6 +441,7 @@ enum ExecApprovalsStore {
let cleaned = allowlist
.map { item in
ExecAllowlistEntry(
id: item.id,
pattern: item.pattern.trimmingCharacters(in: .whitespacesAndNewlines),
lastUsedAt: item.lastUsedAt,
lastUsedCommand: item.lastUsedCommand,

View File

@@ -4,11 +4,46 @@ enum GatewayLaunchAgentManager {
private static let logger = Logger(subsystem: "com.clawdbot", category: "gateway.launchd")
private static let disableLaunchAgentMarker = ".clawdbot/disable-launchagent"
private static var disableLaunchAgentMarkerURL: URL {
FileManager().homeDirectoryForCurrentUser
.appendingPathComponent(self.disableLaunchAgentMarker)
}
private static var plistURL: URL {
FileManager().homeDirectoryForCurrentUser
.appendingPathComponent("Library/LaunchAgents/\(gatewayLaunchdLabel).plist")
}
static func isLaunchAgentWriteDisabled() -> Bool {
FileManager().fileExists(atPath: self.disableLaunchAgentMarkerURL.path)
}
static func setLaunchAgentWriteDisabled(_ disabled: Bool) -> String? {
let marker = self.disableLaunchAgentMarkerURL
if disabled {
do {
try FileManager().createDirectory(
at: marker.deletingLastPathComponent(),
withIntermediateDirectories: true)
if !FileManager().fileExists(atPath: marker.path) {
FileManager().createFile(atPath: marker.path, contents: nil)
}
} catch {
return error.localizedDescription
}
return nil
}
if FileManager().fileExists(atPath: marker.path) {
do {
try FileManager().removeItem(at: marker)
} catch {
return error.localizedDescription
}
}
return nil
}
static func isLoaded() async -> Bool {
guard let loaded = await self.readDaemonLoaded() else { return false }
return loaded
@@ -66,12 +101,6 @@ enum GatewayLaunchAgentManager {
}
extension GatewayLaunchAgentManager {
private static func isLaunchAgentWriteDisabled() -> Bool {
let marker = FileManager().homeDirectoryForCurrentUser
.appendingPathComponent(self.disableLaunchAgentMarker)
return FileManager().fileExists(atPath: marker.path)
}
private static func readDaemonLoaded() async -> Bool? {
let result = await self.runDaemonCommandResult(
["status", "--json", "--no-probe"],

View File

@@ -79,6 +79,11 @@ final class GatewayProcessManager {
func ensureLaunchAgentEnabledIfNeeded() async {
guard !CommandResolver.connectionModeIsRemote() else { return }
if GatewayLaunchAgentManager.isLaunchAgentWriteDisabled() {
self.appendLog("[gateway] launchd auto-enable skipped (attach-only)\n")
self.logger.info("gateway launchd auto-enable skipped (disable marker set)")
return
}
let enabled = await GatewayLaunchAgentManager.isLoaded()
guard !enabled else { return }
let bundlePath = Bundle.main.bundleURL.path
@@ -237,19 +242,17 @@ final class GatewayProcessManager {
private func describe(details instance: String?, port: Int, snap: HealthSnapshot?) -> String {
let instanceText = instance ?? "pid unknown"
if let snap {
let linkId = snap.channelOrder?.first(where: {
if let summary = snap.channels[$0] { return summary.linked != nil }
return false
}) ?? snap.channels.keys.first(where: {
if let summary = snap.channels[$0] { return summary.linked != nil }
return false
})
let linked = linkId.flatMap { snap.channels[$0]?.linked } ?? false
let authAge = linkId.flatMap { snap.channels[$0]?.authAgeMs }.flatMap(msToAge) ?? "unknown age"
let order = snap.channelOrder ?? Array(snap.channels.keys)
let linkId = order.first(where: { snap.channels[$0]?.linked == true })
?? order.first(where: { snap.channels[$0]?.linked != nil })
guard let linkId else {
return "port \(port), health probe succeeded, \(instanceText)"
}
let linked = snap.channels[linkId]?.linked ?? false
let authAge = snap.channels[linkId]?.authAgeMs.flatMap(msToAge) ?? "unknown age"
let label =
linkId.flatMap { snap.channelLabels?[$0] } ??
linkId?.capitalized ??
"channel"
snap.channelLabels?[linkId] ??
linkId.capitalized
let linkText = linked ? "linked" : "not linked"
return "port \(port), \(label) \(linkText), auth \(authAge), \(instanceText)"
}
@@ -308,6 +311,15 @@ final class GatewayProcessManager {
return
}
if GatewayLaunchAgentManager.isLaunchAgentWriteDisabled() {
let message = "Launchd disabled; start the Gateway manually or disable attach-only."
self.status = .failed(message)
self.lastFailureReason = "launchd disabled"
self.appendLog("[gateway] launchd disabled; skipping auto-start\n")
self.logger.info("gateway launchd enable skipped (disable marker set)")
return
}
let bundlePath = Bundle.main.bundleURL.path
let port = GatewayEnvironment.gatewayPort()
self.appendLog("[gateway] enabling launchd job (\(gatewayLaunchdLabel)) on port \(port)\n")

View File

@@ -166,6 +166,11 @@ final class HealthStore {
_ snap: HealthSnapshot) -> (id: String, summary: HealthSnapshot.ChannelSummary)?
{
let order = snap.channelOrder ?? Array(snap.channels.keys)
for id in order {
if let summary = snap.channels[id], summary.linked == true {
return (id: id, summary: summary)
}
}
for id in order {
if let summary = snap.channels[id], summary.linked != nil {
return (id: id, summary: summary)

View File

@@ -3,6 +3,7 @@ import Darwin
import Foundation
import MenuBarExtraAccess
import Observation
import OSLog
import Security
import SwiftUI
@@ -10,6 +11,7 @@ import SwiftUI
struct ClawdbotApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) private var delegate
@State private var state: AppState
private static let logger = Logger(subsystem: "com.clawdbot", category: "app")
private let gatewayManager = GatewayProcessManager.shared
private let controlChannel = ControlChannel.shared
private let activityStore = WorkActivityStore.shared
@@ -31,6 +33,7 @@ struct ClawdbotApp: App {
init() {
ClawdbotLogging.bootstrapIfNeeded()
Self.applyAttachOnlyOverrideIfNeeded()
_state = State(initialValue: AppStateStore.shared)
}
@@ -91,6 +94,22 @@ struct ClawdbotApp: App {
self.statusItem?.button?.appearsDisabled = paused || sleeping
}
private static func applyAttachOnlyOverrideIfNeeded() {
let args = CommandLine.arguments
guard args.contains("--attach-only") || args.contains("--no-launchd") else { return }
if let error = GatewayLaunchAgentManager.setLaunchAgentWriteDisabled(true) {
Self.logger.error("attach-only flag failed: \(error, privacy: .public)")
return
}
Task {
_ = await GatewayLaunchAgentManager.set(
enabled: false,
bundlePath: Bundle.main.bundlePath,
port: GatewayEnvironment.gatewayPort())
}
Self.logger.info("attach-only flag enabled")
}
private var isGatewaySleeping: Bool {
if self.state.isPaused { return false }
switch self.state.connectionMode {

View File

@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.1.21</string>
<string>2026.1.23</string>
<key>CFBundleVersion</key>
<string>202601210</string>
<string>202601230</string>
<key>CFBundleIconFile</key>
<string>Clawdbot</string>
<key>CFBundleURLTypes</key>

View File

@@ -123,12 +123,12 @@ struct SystemRunSettingsView: View {
.foregroundStyle(.secondary)
} else {
VStack(alignment: .leading, spacing: 8) {
ForEach(Array(self.model.entries.enumerated()), id: \.offset) { index, _ in
ForEach(self.model.entries, id: \.id) { entry in
ExecAllowlistRow(
entry: Binding(
get: { self.model.entries[index] },
set: { self.model.updateEntry($0, at: index) }),
onRemove: { self.model.removeEntry(at: index) })
get: { self.model.entry(for: entry.id) ?? entry },
set: { self.model.updateEntry($0, id: entry.id) }),
onRemove: { self.model.removeEntry(id: entry.id) })
}
}
}
@@ -373,20 +373,24 @@ final class ExecApprovalsSettingsModel {
ExecApprovalsStore.updateAllowlist(agentId: self.selectedAgentId, allowlist: self.entries)
}
func updateEntry(_ entry: ExecAllowlistEntry, at index: Int) {
func updateEntry(_ entry: ExecAllowlistEntry, id: UUID) {
guard !self.isDefaultsScope else { return }
guard self.entries.indices.contains(index) else { return }
guard let index = self.entries.firstIndex(where: { $0.id == id }) else { return }
self.entries[index] = entry
ExecApprovalsStore.updateAllowlist(agentId: self.selectedAgentId, allowlist: self.entries)
}
func removeEntry(at index: Int) {
func removeEntry(id: UUID) {
guard !self.isDefaultsScope else { return }
guard self.entries.indices.contains(index) else { return }
guard let index = self.entries.firstIndex(where: { $0.id == id }) else { return }
self.entries.remove(at: index)
ExecApprovalsStore.updateAllowlist(agentId: self.selectedAgentId, allowlist: self.entries)
}
func entry(for id: UUID) -> ExecAllowlistEntry? {
self.entries.first(where: { $0.id == id })
}
func refreshSkillBins(force: Bool = false) async {
guard self.autoAllowSkills else {
self.skillBins = []

View File

@@ -4,6 +4,8 @@ func sanitizeVoiceWakeTriggers(_ words: [String]) -> [String] {
let cleaned = words
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
.prefix(voiceWakeMaxWords)
.map { String($0.prefix(voiceWakeMaxWordLength)) }
return cleaned.isEmpty ? defaultVoiceWakeTriggers : cleaned
}

View File

@@ -21,6 +21,7 @@ struct VoiceWakeSettings: View {
@State private var micObserver = AudioInputDeviceObserver()
@State private var micRefreshTask: Task<Void, Never>?
@State private var availableLocales: [Locale] = []
@State private var triggerEntries: [TriggerEntry] = []
private let fieldLabelWidth: CGFloat = 140
private let controlWidth: CGFloat = 240
private let isPreview = ProcessInfo.processInfo.isPreview
@@ -31,9 +32,9 @@ struct VoiceWakeSettings: View {
var id: String { self.uid }
}
private struct IndexedWord: Identifiable {
let id: Int
let value: String
private struct TriggerEntry: Identifiable {
let id: UUID
var value: String
}
private var voiceWakeBinding: Binding<Bool> {
@@ -105,6 +106,7 @@ struct VoiceWakeSettings: View {
.onAppear {
guard !self.isPreview else { return }
self.startMicObserver()
self.loadTriggerEntries()
}
.onChange(of: self.state.voiceWakeMicID) { _, _ in
guard !self.isPreview else { return }
@@ -122,8 +124,10 @@ struct VoiceWakeSettings: View {
self.micRefreshTask = nil
Task { await self.meter.stop() }
self.micObserver.stop()
self.syncTriggerEntriesToState()
} else {
self.startMicObserver()
self.loadTriggerEntries()
}
}
.onDisappear {
@@ -136,11 +140,16 @@ struct VoiceWakeSettings: View {
self.micRefreshTask = nil
self.micObserver.stop()
Task { await self.meter.stop() }
self.syncTriggerEntriesToState()
}
}
private var indexedWords: [IndexedWord] {
self.state.swabbleTriggerWords.enumerated().map { IndexedWord(id: $0.offset, value: $0.element) }
private func loadTriggerEntries() {
self.triggerEntries = self.state.swabbleTriggerWords.map { TriggerEntry(id: UUID(), value: $0) }
}
private func syncTriggerEntriesToState() {
self.state.swabbleTriggerWords = self.triggerEntries.map(\.value)
}
private var triggerTable: some View {
@@ -154,29 +163,42 @@ struct VoiceWakeSettings: View {
} label: {
Label("Add word", systemImage: "plus")
}
.disabled(self.state.swabbleTriggerWords
.contains(where: { $0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }))
.disabled(self.triggerEntries
.contains(where: { $0.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }))
Button("Reset defaults") { self.state.swabbleTriggerWords = defaultVoiceWakeTriggers }
Button("Reset defaults") {
self.triggerEntries = defaultVoiceWakeTriggers.map { TriggerEntry(id: UUID(), value: $0) }
self.syncTriggerEntriesToState()
}
}
Table(self.indexedWords) {
TableColumn("Word") { row in
TextField("Wake word", text: self.binding(for: row.id))
.textFieldStyle(.roundedBorder)
}
TableColumn("") { row in
Button {
self.removeWord(at: row.id)
} label: {
Image(systemName: "trash")
VStack(spacing: 0) {
ForEach(self.$triggerEntries) { $entry in
HStack(spacing: 8) {
TextField("Wake word", text: $entry.value)
.textFieldStyle(.roundedBorder)
.onSubmit {
self.syncTriggerEntriesToState()
}
Button {
self.removeWord(id: entry.id)
} label: {
Image(systemName: "trash")
}
.buttonStyle(.borderless)
.help("Remove trigger word")
.frame(width: 24)
}
.padding(8)
if entry.id != self.triggerEntries.last?.id {
Divider()
}
.buttonStyle(.borderless)
.help("Remove trigger word")
}
.width(36)
}
.frame(minHeight: 180)
.frame(maxWidth: .infinity, minHeight: 180, alignment: .topLeading)
.background(Color(nsColor: .textBackgroundColor))
.clipShape(RoundedRectangle(cornerRadius: 6))
.overlay(
RoundedRectangle(cornerRadius: 6)
@@ -211,24 +233,12 @@ struct VoiceWakeSettings: View {
}
private func addWord() {
self.state.swabbleTriggerWords.append("")
self.triggerEntries.append(TriggerEntry(id: UUID(), value: ""))
}
private func removeWord(at index: Int) {
guard self.state.swabbleTriggerWords.indices.contains(index) else { return }
self.state.swabbleTriggerWords.remove(at: index)
}
private func binding(for index: Int) -> Binding<String> {
Binding(
get: {
guard self.state.swabbleTriggerWords.indices.contains(index) else { return "" }
return self.state.swabbleTriggerWords[index]
},
set: { newValue in
guard self.state.swabbleTriggerWords.indices.contains(index) else { return }
self.state.swabbleTriggerWords[index] = newValue
})
private func removeWord(id: UUID) {
self.triggerEntries.removeAll { $0.id == id }
self.syncTriggerEntriesToState()
}
private func toggleTest() {
@@ -638,13 +648,14 @@ extension VoiceWakeSettings {
state.voicePushToTalkEnabled = true
state.swabbleTriggerWords = ["Claude", "Hey"]
let view = VoiceWakeSettings(state: state, isActive: true)
var view = VoiceWakeSettings(state: state, isActive: true)
view.availableMics = [AudioInputDevice(uid: "mic-1", name: "Built-in")]
view.availableLocales = [Locale(identifier: "en_US")]
view.meterLevel = 0.42
view.meterError = "No input"
view.testState = .detected("ok")
view.isTesting = true
view.triggerEntries = [TriggerEntry(id: UUID(), value: "Claude")]
_ = view.body
_ = view.localePicker
@@ -654,8 +665,9 @@ extension VoiceWakeSettings {
_ = view.chimeSection
view.addWord()
_ = view.binding(for: 0).wrappedValue
view.removeWord(at: 0)
if let entryId = view.triggerEntries.first?.id {
view.removeWord(id: entryId)
}
}
}
#endif

View File

@@ -1973,25 +1973,25 @@ public struct ExecApprovalsSnapshot: Codable, Sendable {
public struct ExecApprovalRequestParams: Codable, Sendable {
public let id: String?
public let command: String
public let cwd: String?
public let host: String?
public let security: String?
public let ask: String?
public let agentid: String?
public let resolvedpath: String?
public let sessionkey: String?
public let cwd: AnyCodable?
public let host: AnyCodable?
public let security: AnyCodable?
public let ask: AnyCodable?
public let agentid: AnyCodable?
public let resolvedpath: AnyCodable?
public let sessionkey: AnyCodable?
public let timeoutms: Int?
public init(
id: String?,
command: String,
cwd: String?,
host: String?,
security: String?,
ask: String?,
agentid: String?,
resolvedpath: String?,
sessionkey: String?,
cwd: AnyCodable?,
host: AnyCodable?,
security: AnyCodable?,
ask: AnyCodable?,
agentid: AnyCodable?,
resolvedpath: AnyCodable?,
sessionkey: AnyCodable?,
timeoutms: Int?
) {
self.id = id

View File

@@ -12,6 +12,18 @@ struct VoiceWakeHelpersTests {
#expect(cleaned == defaultVoiceWakeTriggers)
}
@Test func sanitizeTriggersLimitsWordLength() {
let long = String(repeating: "x", count: voiceWakeMaxWordLength + 5)
let cleaned = sanitizeVoiceWakeTriggers(["ok", long])
#expect(cleaned[1].count == voiceWakeMaxWordLength)
}
@Test func sanitizeTriggersLimitsWordCount() {
let words = (1...voiceWakeMaxWords + 3).map { "w\($0)" }
let cleaned = sanitizeVoiceWakeTriggers(words)
#expect(cleaned.count == voiceWakeMaxWords)
}
@Test func normalizeLocaleStripsCollation() {
#expect(normalizeLocaleIdentifier("en_US@collation=phonebook") == "en_US")
}

View File

@@ -1973,25 +1973,25 @@ public struct ExecApprovalsSnapshot: Codable, Sendable {
public struct ExecApprovalRequestParams: Codable, Sendable {
public let id: String?
public let command: String
public let cwd: String?
public let host: String?
public let security: String?
public let ask: String?
public let agentid: String?
public let resolvedpath: String?
public let sessionkey: String?
public let cwd: AnyCodable?
public let host: AnyCodable?
public let security: AnyCodable?
public let ask: AnyCodable?
public let agentid: AnyCodable?
public let resolvedpath: AnyCodable?
public let sessionkey: AnyCodable?
public let timeoutms: Int?
public init(
id: String?,
command: String,
cwd: String?,
host: String?,
security: String?,
ask: String?,
agentid: String?,
resolvedpath: String?,
sessionkey: String?,
cwd: AnyCodable?,
host: AnyCodable?,
security: AnyCodable?,
ask: AnyCodable?,
agentid: AnyCodable?,
resolvedpath: AnyCodable?,
sessionkey: AnyCodable?,
timeoutms: Int?
) {
self.id = id

View File

@@ -3,9 +3,12 @@ summary: "Cron jobs + wakeups for the Gateway scheduler"
read_when:
- Scheduling background jobs or wakeups
- Wiring automation that should run with or alongside heartbeats
- Deciding between heartbeat and cron for scheduled tasks
---
# Cron jobs (Gateway scheduler)
> **Cron vs Heartbeat?** See [Cron vs Heartbeat](/automation/cron-vs-heartbeat) for guidance on when to use each.
Cron is the Gateways built-in scheduler. It persists jobs, wakes the agent at
the right time, and can optionally deliver output back to a chat.
@@ -121,7 +124,7 @@ Resolution priority:
### Delivery (channel + target)
Isolated jobs can deliver output to a channel. The job payload can specify:
- `channel`: `whatsapp` / `telegram` / `discord` / `slack` / `signal` / `imessage` / `last`
- `channel`: `whatsapp` / `telegram` / `discord` / `slack` / `mattermost` (plugin) / `signal` / `imessage` / `last`
- `to`: channel-specific recipient target
If `channel` or `to` is omitted, cron can fall back to the main sessions “last route”
@@ -133,7 +136,7 @@ Delivery notes:
- Use `deliver: false` to keep output internal even if a `to` is present.
Target format reminders:
- Slack/Discord targets should use explicit prefixes (e.g. `channel:<id>`, `user:<id>`) to avoid ambiguity.
- Slack/Discord/Mattermost (plugin) targets should use explicit prefixes (e.g. `channel:<id>`, `user:<id>`) to avoid ambiguity.
- Telegram topics should use the `:topic:` form (see below).
#### Telegram delivery targets (topics / forum threads)

View File

@@ -0,0 +1,274 @@
---
summary: "Guidance for choosing between heartbeat and cron jobs for automation"
read_when:
- Deciding how to schedule recurring tasks
- Setting up background monitoring or notifications
- Optimizing token usage for periodic checks
---
# Cron vs Heartbeat: When to Use Each
Both heartbeats and cron jobs let you run tasks on a schedule. This guide helps you choose the right mechanism for your use case.
## Quick Decision Guide
| Use Case | Recommended | Why |
|----------|-------------|-----|
| Check inbox every 30 min | Heartbeat | Batches with other checks, context-aware |
| Send daily report at 9am sharp | Cron (isolated) | Exact timing needed |
| Monitor calendar for upcoming events | Heartbeat | Natural fit for periodic awareness |
| Run weekly deep analysis | Cron (isolated) | Standalone task, can use different model |
| Remind me in 20 minutes | Cron (main, `--at`) | One-shot with precise timing |
| Background project health check | Heartbeat | Piggybacks on existing cycle |
## Heartbeat: Periodic Awareness
Heartbeats run in the **main session** at a regular interval (default: 30 min). They're designed for the agent to check on things and surface anything important.
### When to use heartbeat
- **Multiple periodic checks**: Instead of 5 separate cron jobs checking inbox, calendar, weather, notifications, and project status, a single heartbeat can batch all of these.
- **Context-aware decisions**: The agent has full main-session context, so it can make smart decisions about what's urgent vs. what can wait.
- **Conversational continuity**: Heartbeat runs share the same session, so the agent remembers recent conversations and can follow up naturally.
- **Low-overhead monitoring**: One heartbeat replaces many small polling tasks.
### Heartbeat advantages
- **Batches multiple checks**: One agent turn can review inbox, calendar, and notifications together.
- **Reduces API calls**: A single heartbeat is cheaper than 5 isolated cron jobs.
- **Context-aware**: The agent knows what you've been working on and can prioritize accordingly.
- **Smart suppression**: If nothing needs attention, the agent replies `HEARTBEAT_OK` and no message is delivered.
- **Natural timing**: Drifts slightly based on queue load, which is fine for most monitoring.
### Heartbeat example: HEARTBEAT.md checklist
```md
# Heartbeat checklist
- Check email for urgent messages
- Review calendar for events in next 2 hours
- If a background task finished, summarize results
- If idle for 8+ hours, send a brief check-in
```
The agent reads this on each heartbeat and handles all items in one turn.
### Configuring heartbeat
```json5
{
agents: {
defaults: {
heartbeat: {
every: "30m", // interval
target: "last", // where to deliver alerts
activeHours: { start: "08:00", end: "22:00" } // optional
}
}
}
}
```
See [Heartbeat](/gateway/heartbeat) for full configuration.
## Cron: Precise Scheduling
Cron jobs run at **exact times** and can run in isolated sessions without affecting main context.
### When to use cron
- **Exact timing required**: "Send this at 9:00 AM every Monday" (not "sometime around 9").
- **Standalone tasks**: Tasks that don't need conversational context.
- **Different model/thinking**: Heavy analysis that warrants a more powerful model.
- **One-shot reminders**: "Remind me in 20 minutes" with `--at`.
- **Noisy/frequent tasks**: Tasks that would clutter main session history.
- **External triggers**: Tasks that should run independently of whether the agent is otherwise active.
### Cron advantages
- **Exact timing**: 5-field cron expressions with timezone support.
- **Session isolation**: Runs in `cron:<jobId>` without polluting main history.
- **Model overrides**: Use a cheaper or more powerful model per job.
- **Delivery control**: Can deliver directly to a channel; still posts a summary to main by default (configurable).
- **No agent context needed**: Runs even if main session is idle or compacted.
- **One-shot support**: `--at` for precise future timestamps.
### Cron example: Daily morning briefing
```bash
clawdbot cron add \
--name "Morning briefing" \
--cron "0 7 * * *" \
--tz "America/New_York" \
--session isolated \
--message "Generate today's briefing: weather, calendar, top emails, news summary." \
--model opus \
--deliver \
--channel whatsapp \
--to "+15551234567"
```
This runs at exactly 7:00 AM New York time, uses Opus for quality, and delivers directly to WhatsApp.
### Cron example: One-shot reminder
```bash
clawdbot cron add \
--name "Meeting reminder" \
--at "20m" \
--session main \
--system-event "Reminder: standup meeting starts in 10 minutes." \
--wake now \
--delete-after-run
```
See [Cron jobs](/automation/cron-jobs) for full CLI reference.
## Decision Flowchart
```
Does the task need to run at an EXACT time?
YES -> Use cron
NO -> Continue...
Does the task need isolation from main session?
YES -> Use cron (isolated)
NO -> Continue...
Can this task be batched with other periodic checks?
YES -> Use heartbeat (add to HEARTBEAT.md)
NO -> Use cron
Is this a one-shot reminder?
YES -> Use cron with --at
NO -> Continue...
Does it need a different model or thinking level?
YES -> Use cron (isolated) with --model/--thinking
NO -> Use heartbeat
```
## Combining Both
The most efficient setup uses **both**:
1. **Heartbeat** handles routine monitoring (inbox, calendar, notifications) in one batched turn every 30 minutes.
2. **Cron** handles precise schedules (daily reports, weekly reviews) and one-shot reminders.
### Example: Efficient automation setup
**HEARTBEAT.md** (checked every 30 min):
```md
# Heartbeat checklist
- Scan inbox for urgent emails
- Check calendar for events in next 2h
- Review any pending tasks
- Light check-in if quiet for 8+ hours
```
**Cron jobs** (precise timing):
```bash
# Daily morning briefing at 7am
clawdbot cron add --name "Morning brief" --cron "0 7 * * *" --session isolated --message "..." --deliver
# Weekly project review on Mondays at 9am
clawdbot cron add --name "Weekly review" --cron "0 9 * * 1" --session isolated --message "..." --model opus
# One-shot reminder
clawdbot cron add --name "Call back" --at "2h" --session main --system-event "Call back the client" --wake now
```
## Lobster: Deterministic workflows with approvals
Lobster is the workflow runtime for **multi-step tool pipelines** that need deterministic execution and explicit approvals.
Use it when the task is more than a single agent turn, and you want a resumable workflow with human checkpoints.
### When Lobster fits
- **Multi-step automation**: You need a fixed pipeline of tool calls, not a one-off prompt.
- **Approval gates**: Side effects should pause until you approve, then resume.
- **Resumable runs**: Continue a paused workflow without re-running earlier steps.
### How it pairs with heartbeat and cron
- **Heartbeat/cron** decide *when* a run happens.
- **Lobster** defines *what steps* happen once the run starts.
For scheduled workflows, use cron or heartbeat to trigger an agent turn that calls Lobster.
For ad-hoc workflows, call Lobster directly.
### Operational notes (from the code)
- Lobster runs as a **local subprocess** (`lobster` CLI) in tool mode and returns a **JSON envelope**.
- If the tool returns `needs_approval`, you resume with a `resumeToken` and `approve` flag.
- The tool is an **optional plugin**; you must allowlist `lobster` in `tools.allow`.
- If you pass `lobsterPath`, it must be an **absolute path**.
See [Lobster](/tools/lobster) for full usage and examples.
## Main Session vs Isolated Session
Both heartbeat and cron can interact with the main session, but differently:
| | Heartbeat | Cron (main) | Cron (isolated) |
|---|---|---|---|
| Session | Main | Main (via system event) | `cron:<jobId>` |
| History | Shared | Shared | Fresh each run |
| Context | Full | Full | None (starts clean) |
| Model | Main session model | Main session model | Can override |
| Output | Delivered if not `HEARTBEAT_OK` | Heartbeat prompt + event | Summary posted to main |
### When to use main session cron
Use `--session main` with `--system-event` when you want:
- The reminder/event to appear in main session context
- The agent to handle it during the next heartbeat with full context
- No separate isolated run
```bash
clawdbot cron add \
--name "Check project" \
--every "4h" \
--session main \
--system-event "Time for a project health check" \
--wake now
```
### When to use isolated cron
Use `--session isolated` when you want:
- A clean slate without prior context
- Different model or thinking settings
- Output delivered directly to a channel (summary still posts to main by default)
- History that doesn't clutter main session
```bash
clawdbot cron add \
--name "Deep analysis" \
--cron "0 6 * * 0" \
--session isolated \
--message "Weekly codebase analysis..." \
--model opus \
--thinking high \
--deliver
```
## Cost Considerations
| Mechanism | Cost Profile |
|-----------|--------------|
| Heartbeat | One turn every N minutes; scales with HEARTBEAT.md size |
| Cron (main) | Adds event to next heartbeat (no isolated turn) |
| Cron (isolated) | Full agent turn per job; can use cheaper model |
**Tips**:
- Keep `HEARTBEAT.md` small to minimize token overhead.
- Batch similar checks into heartbeat instead of multiple cron jobs.
- Use `target: "none"` on heartbeat if you only want internal processing.
- Use isolated cron with a cheaper model for routine tasks.
## Related
- [Heartbeat](/gateway/heartbeat) - full heartbeat configuration
- [Cron jobs](/automation/cron-jobs) - full cron CLI and API reference
- [Wake](/cli/wake) - manual wake command

View File

@@ -71,8 +71,8 @@ Payload:
- `sessionKey` optional (string): The key used to identify the agent's session. Defaults to a random `hook:<uuid>`. Using a consistent key allows for a multi-turn conversation within the hook context.
- `wakeMode` optional (`now` | `next-heartbeat`): Whether to trigger an immediate heartbeat (default `now`) or wait for the next periodic check.
- `deliver` optional (boolean): If `true`, the agent's response will be sent to the messaging channel. Defaults to `true`. Responses that are only heartbeat acknowledgments are automatically skipped.
- `channel` optional (string): The messaging channel for delivery. One of: `last`, `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`, `msteams`. Defaults to `last`.
- `to` optional (string): The recipient identifier for the channel (e.g., phone number for WhatsApp/Signal, chat ID for Telegram, channel ID for Discord/Slack, conversation ID for MS Teams). Defaults to the last recipient in the main session.
- `channel` optional (string): The messaging channel for delivery. One of: `last`, `whatsapp`, `telegram`, `discord`, `slack`, `mattermost` (plugin), `signal`, `imessage`, `msteams`. Defaults to `last`.
- `to` optional (string): The recipient identifier for the channel (e.g., phone number for WhatsApp/Signal, chat ID for Telegram, channel ID for Discord/Slack/Mattermost (plugin), conversation ID for MS Teams). Defaults to the last recipient in the main session.
- `model` optional (string): Model override (e.g., `anthropic/claude-3-5-sonnet` or an alias). Must be in the allowed model list if restricted.
- `thinking` optional (string): Thinking level override (e.g., `low`, `medium`, `high`).
- `timeoutSeconds` optional (number): Maximum duration for the agent run in seconds.

View File

@@ -147,7 +147,8 @@ Available actions:
- **addParticipant**: Add someone to a group (`chatGuid`, `address`)
- **removeParticipant**: Remove someone from a group (`chatGuid`, `address`)
- **leaveGroup**: Leave a group chat (`chatGuid`)
- **sendAttachment**: Send media/files (`to`, `buffer`, `filename`)
- **sendAttachment**: Send media/files (`to`, `buffer`, `filename`, `asVoice`)
- Voice memos: set `asVoice: true` with **MP3** or **CAF** audio to send as an iMessage voice message. BlueBubbles converts MP3 → CAF when sending voice memos.
### Message IDs (short vs full)
Clawdbot may surface *short* message IDs (e.g., `1`, `2`) to save tokens.

View File

@@ -15,6 +15,7 @@ Text is supported everywhere; media and reactions vary by channel.
- [Telegram](/channels/telegram) — Bot API via grammY; supports groups.
- [Discord](/channels/discord) — Discord Bot API + Gateway; supports servers, channels, and DMs.
- [Slack](/channels/slack) — Bolt SDK; workspace apps.
- [Mattermost](/channels/mattermost) — Bot API + WebSocket; channels, groups, DMs (plugin, installed separately).
- [Signal](/channels/signal) — signal-cli; privacy-focused.
- [BlueBubbles](/channels/bluebubbles) — **Recommended for iMessage**; uses the BlueBubbles macOS server REST API with full feature support (edit, unsend, effects, reactions, group management — edit currently broken on macOS 26 Tahoe).
- [iMessage](/channels/imessage) — macOS only; native integration via imsg (legacy, consider BlueBubbles for new setups).
@@ -22,6 +23,7 @@ Text is supported everywhere; media and reactions vary by channel.
- [Nextcloud Talk](/channels/nextcloud-talk) — Self-hosted chat via Nextcloud Talk (plugin, installed separately).
- [Matrix](/channels/matrix) — Matrix protocol (plugin, installed separately).
- [Nostr](/channels/nostr) — Decentralized DMs via NIP-04 (plugin, installed separately).
- [Tlon](/channels/tlon) — Urbit-based messenger (plugin, installed separately).
- [Zalo](/channels/zalo) — Zalo Bot API; Vietnam's popular messenger (plugin, installed separately).
- [Zalo Personal](/channels/zalouser) — Zalo personal account via QR login (plugin, installed separately).
- [WebChat](/web/webchat) — Gateway WebChat UI over WebSocket.

123
docs/channels/mattermost.md Normal file
View File

@@ -0,0 +1,123 @@
---
summary: "Mattermost bot setup and Clawdbot config"
read_when:
- Setting up Mattermost
- Debugging Mattermost routing
---
# Mattermost (plugin)
Status: supported via plugin (bot token + WebSocket events). Channels, groups, and DMs are supported.
Mattermost is a self-hostable team messaging platform; see the official site at
[mattermost.com](https://mattermost.com) for product details and downloads.
## Plugin required
Mattermost ships as a plugin and is not bundled with the core install.
Install via CLI (npm registry):
```bash
clawdbot plugins install @clawdbot/mattermost
```
Local checkout (when running from a git repo):
```bash
clawdbot plugins install ./extensions/mattermost
```
If you choose Mattermost during configure/onboarding and a git checkout is detected,
Clawdbot will offer the local install path automatically.
Details: [Plugins](/plugin)
## Quick setup
1) Install the Mattermost plugin.
2) Create a Mattermost bot account and copy the **bot token**.
3) Copy the Mattermost **base URL** (e.g., `https://chat.example.com`).
4) Configure Clawdbot and start the gateway.
Minimal config:
```json5
{
channels: {
mattermost: {
enabled: true,
botToken: "mm-token",
baseUrl: "https://chat.example.com",
dmPolicy: "pairing"
}
}
}
```
## Environment variables (default account)
Set these on the gateway host if you prefer env vars:
- `MATTERMOST_BOT_TOKEN=...`
- `MATTERMOST_URL=https://chat.example.com`
Env vars apply only to the **default** account (`default`). Other accounts must use config values.
## Chat modes
Mattermost responds to DMs automatically. Channel behavior is controlled by `chatmode`:
- `oncall` (default): respond only when @mentioned in channels.
- `onmessage`: respond to every channel message.
- `onchar`: respond when a message starts with a trigger prefix.
Config example:
```json5
{
channels: {
mattermost: {
chatmode: "onchar",
oncharPrefixes: [">", "!"]
}
}
}
```
Notes:
- `onchar` still responds to explicit @mentions.
- `channels.mattermost.requireMention` is honored for legacy configs but `chatmode` is preferred.
## Access control (DMs)
- Default: `channels.mattermost.dmPolicy = "pairing"` (unknown senders get a pairing code).
- Approve via:
- `clawdbot pairing list mattermost`
- `clawdbot pairing approve mattermost <CODE>`
- Public DMs: `channels.mattermost.dmPolicy="open"` plus `channels.mattermost.allowFrom=["*"]`.
## Channels (groups)
- Default: `channels.mattermost.groupPolicy = "allowlist"` (mention-gated).
- Allowlist senders with `channels.mattermost.groupAllowFrom` (user IDs or `@username`).
- Open channels: `channels.mattermost.groupPolicy="open"` (mention-gated).
## Targets for outbound delivery
Use these target formats with `clawdbot message send` or cron/webhooks:
- `channel:<id>` for a channel
- `user:<id>` for a DM
- `@username` for a DM (resolved via the Mattermost API)
Bare IDs are treated as channels.
## Multi-account
Mattermost supports multiple accounts under `channels.mattermost.accounts`:
```json5
{
channels: {
mattermost: {
accounts: {
default: { name: "Primary", botToken: "mm-token", baseUrl: "https://chat.example.com" },
alerts: { name: "Alerts", botToken: "mm-token-2", baseUrl: "https://alerts.example.com" }
}
}
}
}
```
## Troubleshooting
- No replies in channels: ensure the bot is in the channel and mention it (oncall), use a trigger prefix (onchar), or set `chatmode: "onmessage"`.
- Auth errors: check the bot token, base URL, and whether the account is enabled.
- Multi-account issues: env vars only apply to the `default` account.

View File

@@ -304,7 +304,8 @@ Slack uses Socket Mode only (no HTTP webhook server). Provide both tokens:
"policy": "pairing",
"allowFrom": ["U123", "U456", "*"],
"groupEnabled": false,
"groupChannels": ["G123"]
"groupChannels": ["G123"],
"replyToMode": "all"
},
"channels": {
"C123": { "allow": true, "requireMention": true },
@@ -361,6 +362,73 @@ By default, Clawdbot replies in the main channel. Use `channels.slack.replyToMod
The mode applies to both auto-replies and agent tool calls (`slack sendMessage`).
### Per-chat-type threading
You can configure different threading behavior per chat type by setting `channels.slack.replyToModeByChatType`:
```json5
{
channels: {
slack: {
replyToMode: "off", // default for channels
replyToModeByChatType: {
direct: "all", // DMs always thread
group: "first" // group DMs/MPIM thread first reply
},
}
}
}
```
Supported chat types:
- `direct`: 1:1 DMs (Slack `im`)
- `group`: group DMs / MPIMs (Slack `mpim`)
- `channel`: standard channels (public/private)
Precedence:
1) `replyToModeByChatType.<chatType>`
2) `replyToMode`
3) Provider default (`off`)
Legacy `channels.slack.dm.replyToMode` is still accepted as a fallback for `direct` when no chat-type override is set.
Examples:
Thread DMs only:
```json5
{
channels: {
slack: {
replyToMode: "off",
replyToModeByChatType: { direct: "all" }
}
}
}
```
Thread group DMs but keep channels in the root:
```json5
{
channels: {
slack: {
replyToMode: "off",
replyToModeByChatType: { group: "first" }
}
}
}
```
Make channels thread, keep DMs in the root:
```json5
{
channels: {
slack: {
replyToMode: "first",
replyToModeByChatType: { direct: "off", group: "off" }
}
}
}
```
### Manual threading tags
For fine-grained control, use these tags in agent responses:
- `[[reply_to_current]]` — reply to the triggering message (start/continue thread).

133
docs/channels/tlon.md Normal file
View File

@@ -0,0 +1,133 @@
---
summary: "Tlon/Urbit support status, capabilities, and configuration"
read_when:
- Working on Tlon/Urbit channel features
---
# Tlon (plugin)
Tlon is a decentralized messenger built on Urbit. Clawdbot connects to your Urbit ship and can
respond to DMs and group chat messages. Group replies require an @ mention by default and can
be further restricted via allowlists.
Status: supported via plugin. DMs, group mentions, thread replies, and text-only media fallback
(URL appended to caption). Reactions, polls, and native media uploads are not supported.
## Plugin required
Tlon ships as a plugin and is not bundled with the core install.
Install via CLI (npm registry):
```bash
clawdbot plugins install @clawdbot/tlon
```
Local checkout (when running from a git repo):
```bash
clawdbot plugins install ./extensions/tlon
```
Details: [Plugins](/plugin)
## Setup
1) Install the Tlon plugin.
2) Gather your ship URL and login code.
3) Configure `channels.tlon`.
4) Restart the gateway.
5) DM the bot or mention it in a group channel.
Minimal config (single account):
```json5
{
channels: {
tlon: {
enabled: true,
ship: "~sampel-palnet",
url: "https://your-ship-host",
code: "lidlut-tabwed-pillex-ridrup"
}
}
}
```
## Group channels
Auto-discovery is enabled by default. You can also pin channels manually:
```json5
{
channels: {
tlon: {
groupChannels: [
"chat/~host-ship/general",
"chat/~host-ship/support"
]
}
}
}
```
Disable auto-discovery:
```json5
{
channels: {
tlon: {
autoDiscoverChannels: false
}
}
}
```
## Access control
DM allowlist (empty = allow all):
```json5
{
channels: {
tlon: {
dmAllowlist: ["~zod", "~nec"]
}
}
}
```
Group authorization (restricted by default):
```json5
{
channels: {
tlon: {
defaultAuthorizedShips: ["~zod"],
authorization: {
channelRules: {
"chat/~host-ship/general": {
mode: "restricted",
allowedShips: ["~zod", "~nec"]
},
"chat/~host-ship/announcements": {
mode: "open"
}
}
}
}
}
}
```
## Delivery targets (CLI/cron)
Use these with `clawdbot message send` or cron delivery:
- DM: `~sampel-palnet` or `dm/~sampel-palnet`
- Group: `chat/~host-ship/channel` or `group:~host-ship/channel`
## Notes
- Group replies require a mention (e.g. `~your-bot-ship`) to respond.
- Thread replies: if the inbound message is in a thread, Clawdbot replies in-thread.
- Media: `sendMedia` falls back to text + URL (no native upload).

View File

@@ -1,7 +1,7 @@
---
summary: "CLI reference for `clawdbot channels` (accounts, status, login/logout, logs)"
read_when:
- You want to add/remove channel accounts (WhatsApp/Telegram/Discord/Slack/Signal/iMessage)
- You want to add/remove channel accounts (WhatsApp/Telegram/Discord/Slack/Mattermost (plugin)/Signal/iMessage)
- You want to check channel status or tail channel logs
---

View File

@@ -122,7 +122,7 @@ clawdbot gateway probe --ssh user@gateway-host
Options:
- `--ssh <target>`: `user@host` or `user@host:port` (port defaults to `22`).
- `--ssh-identity <path>`: identity file.
- `--ssh-auto`: pick the first discovered bridge host as SSH target (LAN/WAB only).
- `--ssh-auto`: pick the first discovered gateway host as SSH target (LAN/WAB only).
Config (optional, used as defaults):
- `gateway.remote.sshTarget`

View File

@@ -293,7 +293,7 @@ Options:
- `--reset` (reset config + credentials + sessions + workspace before wizard)
- `--non-interactive`
- `--mode <local|remote>`
- `--flow <quickstart|advanced>`
- `--flow <quickstart|advanced|manual>` (manual is an alias for advanced)
- `--auth-choice <setup-token|claude-cli|token|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|codex-cli|gemini-api-key|zai-api-key|apiKey|minimax-api|opencode-zen|skip>`
- `--token-provider <id>` (non-interactive; used with `--auth-choice token`)
- `--token <token>` (non-interactive; used with `--auth-choice token`)
@@ -352,7 +352,7 @@ Options:
## Channel helpers
### `channels`
Manage chat channel accounts (WhatsApp/Telegram/Discord/Slack/Signal/iMessage/MS Teams).
Manage chat channel accounts (WhatsApp/Telegram/Discord/Slack/Mattermost (plugin)/Signal/iMessage/MS Teams).
Subcommands:
- `channels list`: show configured channels and auth profiles (Claude Code + Codex CLI OAuth sync included).
@@ -365,7 +365,7 @@ Subcommands:
- `channels logout`: log out of a channel session (if supported).
Common options:
- `--channel <name>`: `whatsapp|telegram|discord|slack|signal|imessage|msteams`
- `--channel <name>`: `whatsapp|telegram|discord|slack|mattermost|signal|imessage|msteams`
- `--account <id>`: channel account id (default `default`)
- `--name <label>`: display name for the account
@@ -472,7 +472,7 @@ Options:
- `--session-id <id>`
- `--thinking <off|minimal|low|medium|high|xhigh>` (GPT-5.2 + Codex models only)
- `--verbose <on|full|off>`
- `--channel <whatsapp|telegram|discord|slack|signal|imessage>`
- `--channel <whatsapp|telegram|discord|slack|mattermost|signal|imessage|msteams>`
- `--local`
- `--deliver`
- `--json`
@@ -700,8 +700,15 @@ Options:
- `--json`
- `--plain`
- `--check` (exit 1=expired/missing, 2=expiring)
- `--probe` (live probe of configured auth profiles)
- `--probe-provider <name>`
- `--probe-profile <id>` (repeat or comma-separated)
- `--probe-timeout <ms>`
- `--probe-concurrency <n>`
- `--probe-max-tokens <n>`
Always includes the auth overview and OAuth expiry status for profiles in the auth store.
`--probe` runs live requests (may consume tokens and trigger rate limits).
### `models set <model>`
Set `agents.defaults.model.primary`.
@@ -791,11 +798,10 @@ All `cron` commands accept `--url`, `--token`, `--timeout`, `--expect-final`.
[`clawdbot node`](/cli/node).
Subcommands:
- `node run --host <gateway-host> --port 18790`
- `node run --host <gateway-host> --port 18789`
- `node status`
- `node install [--host <gateway-host>] [--port <port>] [--tls] [--tls-fingerprint <sha256>] [--node-id <id>] [--display-name <name>] [--runtime <node|bun>] [--force]`
- `node uninstall`
- `node run`
- `node stop`
- `node restart`

View File

@@ -8,7 +8,7 @@ read_when:
# `clawdbot message`
Single outbound command for sending messages and channel actions
(Discord/Slack/Telegram/WhatsApp/Signal/iMessage/MS Teams).
(Discord/Slack/Mattermost (plugin)/Telegram/WhatsApp/Signal/iMessage/MS Teams).
## Usage
@@ -19,13 +19,14 @@ clawdbot message <subcommand> [flags]
Channel selection:
- `--channel` required if more than one channel is configured.
- If exactly one channel is configured, it becomes the default.
- Values: `whatsapp|telegram|discord|slack|signal|imessage|msteams`
- Values: `whatsapp|telegram|discord|slack|mattermost|signal|imessage|msteams` (Mattermost requires plugin)
Target formats (`--target`):
- WhatsApp: E.164 or group JID
- Telegram: chat id or `@username`
- Discord: `channel:<id>` or `user:<id>` (or `<@id>` mention; raw numeric ids are treated as channels)
- Slack: `channel:<id>` or `user:<id>` (raw channel id is accepted)
- Mattermost (plugin): `channel:<id>`, `user:<id>`, or `@username` (bare ids are treated as channels)
- Signal: `+E.164`, `group:<id>`, `signal:+E.164`, `signal:group:<id>`, or `username:<name>`/`u:<name>`
- iMessage: handle, `chat_id:<id>`, `chat_guid:<guid>`, or `chat_identifier:<id>`
- MS Teams: conversation id (`19:...@thread.tacv2`) or `conversation:<id>` or `user:<aad-object-id>`
@@ -49,7 +50,7 @@ Name lookup:
### Core
- `send`
- Channels: WhatsApp/Telegram/Discord/Slack/Signal/iMessage/MS Teams
- Channels: WhatsApp/Telegram/Discord/Slack/Mattermost (plugin)/Signal/iMessage/MS Teams
- Required: `--target`, plus `--message` or `--media`
- Optional: `--media`, `--reply-to`, `--thread-id`, `--gif-playback`
- Telegram only: `--buttons` (requires `channels.telegram.capabilities.inlineButtons` to allow it)

View File

@@ -25,12 +25,26 @@ clawdbot models scan
`clawdbot models status` shows the resolved default/fallbacks plus an auth overview.
When provider usage snapshots are available, the OAuth/token status section includes
provider usage headers.
Add `--probe` to run live auth probes against each configured provider profile.
Probes are real requests (may consume tokens and trigger rate limits).
Notes:
- `models set <model-or-alias>` accepts `provider/model` or an alias.
- Model refs are parsed by splitting on the **first** `/`. If the model ID includes `/` (OpenRouter-style), include the provider prefix (example: `openrouter/moonshotai/kimi-k2`).
- If you omit the provider, Clawdbot treats the input as an alias or a model for the **default provider** (only works when there is no `/` in the model ID).
### `models status`
Options:
- `--json`
- `--plain`
- `--check` (exit 1=expired/missing, 2=expiring)
- `--probe` (live probe of configured auth profiles)
- `--probe-provider <name>` (probe one provider)
- `--probe-profile <id>` (repeat or comma-separated profile ids)
- `--probe-timeout <ms>`
- `--probe-concurrency <n>`
- `--probe-max-tokens <n>`
## Aliases + fallbacks
```bash

View File

@@ -7,7 +7,7 @@ read_when:
# `clawdbot node`
Run a **headless node host** that connects to the Gateway bridge and exposes
Run a **headless node host** that connects to the Gateway WebSocket and exposes
`system.run` / `system.which` on this machine.
## Why use a node host?
@@ -26,14 +26,14 @@ node host, so you can keep command access scoped and explicit.
## Run (foreground)
```bash
clawdbot node run --host <gateway-host> --port 18790
clawdbot node run --host <gateway-host> --port 18789
```
Options:
- `--host <host>`: Gateway bridge host (default: `127.0.0.1`)
- `--port <port>`: Gateway bridge port (default: `18790`)
- `--tls`: Use TLS for the bridge connection
- `--tls-fingerprint <sha256>`: Pin the bridge certificate fingerprint
- `--host <host>`: Gateway WebSocket host (default: `127.0.0.1`)
- `--port <port>`: Gateway WebSocket port (default: `18789`)
- `--tls`: Use TLS for the gateway connection
- `--tls-fingerprint <sha256>`: Expected TLS certificate fingerprint (sha256)
- `--node-id <id>`: Override node id (clears pairing token)
- `--display-name <name>`: Override the node display name
@@ -42,14 +42,14 @@ Options:
Install a headless node host as a user service.
```bash
clawdbot node install --host <gateway-host> --port 18790
clawdbot node install --host <gateway-host> --port 18789
```
Options:
- `--host <host>`: Gateway bridge host (default: `127.0.0.1`)
- `--port <port>`: Gateway bridge port (default: `18790`)
- `--tls`: Use TLS for the bridge connection
- `--tls-fingerprint <sha256>`: Pin the bridge certificate fingerprint
- `--host <host>`: Gateway WebSocket host (default: `127.0.0.1`)
- `--port <port>`: Gateway WebSocket port (default: `18789`)
- `--tls`: Use TLS for the gateway connection
- `--tls-fingerprint <sha256>`: Expected TLS certificate fingerprint (sha256)
- `--node-id <id>`: Override node id (clears pairing token)
- `--display-name <name>`: Override the node display name
- `--runtime <runtime>`: Service runtime (`node` or `bun`)
@@ -64,6 +64,10 @@ clawdbot node restart
clawdbot node uninstall
```
Use `clawdbot node run` for a foreground node host (no service).
Service commands accept `--json` for machine-readable output.
## Pairing
The first connection creates a pending node pair request on the Gateway.
@@ -74,7 +78,8 @@ clawdbot nodes pending
clawdbot nodes approve <requestId>
```
The node host stores its node id + token in `~/.clawdbot/node.json`.
The node host stores its node id, token, display name, and gateway connection info in
`~/.clawdbot/node.json`.
## Exec approvals

View File

@@ -14,6 +14,9 @@ Related:
- Camera: [Camera nodes](/nodes/camera)
- Images: [Image nodes](/nodes/images)
Common options:
- `--url`, `--token`, `--timeout`, `--json`
## Common commands
```bash
@@ -40,6 +43,11 @@ clawdbot nodes run --raw "git status"
clawdbot nodes run --agent main --node <id|name|ip> --raw "git status"
```
Invoke flags:
- `--params <json>`: JSON object string (default `{}`).
- `--invoke-timeout <ms>`: node invoke timeout (default `15000`).
- `--idempotency-key <key>`: optional idempotency key.
### Exec-style defaults
`nodes run` mirrors the models exec behavior (defaults + approvals):
@@ -47,8 +55,14 @@ clawdbot nodes run --agent main --node <id|name|ip> --raw "git status"
- Reads `tools.exec.*` (plus `agents.list[].tools.exec.*` overrides).
- Uses exec approvals (`exec.approval.request`) before invoking `system.run`.
- `--node` can be omitted when `tools.exec.node` is set.
- Requires a node that advertises `system.run` (macOS companion app or headless node host).
Flags:
- `--cwd <path>`: working directory.
- `--env <key=val>`: env override (repeatable).
- `--command-timeout <ms>`: command timeout.
- `--invoke-timeout <ms>`: node invoke timeout (default `30000`).
- `--needs-screen-recording`: require screen recording permission.
- `--raw <command>`: run a shell string (`/bin/sh -lc` or `cmd.exe /c`).
- `--agent <id>`: agent-scoped approvals/allowlists (defaults to configured agent).
- `--ask <off|on-miss|always>`, `--security <deny|allowlist|full>`: overrides.

View File

@@ -16,6 +16,10 @@ Related:
```bash
clawdbot onboard
clawdbot onboard --flow quickstart
clawdbot onboard --flow manual
clawdbot onboard --mode remote --remote-url ws://gateway-host:18789
```
Flow notes:
- `quickstart`: minimal prompts, auto-generates a gateway token.
- `manual`: full prompts for port/bind/auth (alias of `advanced`).

View File

@@ -1,5 +1,5 @@
---
summary: "CLI reference for `clawdbot update` (safe-ish source update + optional gateway restart)"
summary: "CLI reference for `clawdbot update` (safe-ish source update + gateway auto-restart)"
read_when:
- You want to update a source checkout safely
- You need to understand `--update` shorthand behavior
@@ -20,14 +20,14 @@ clawdbot update wizard
clawdbot update --channel beta
clawdbot update --channel dev
clawdbot update --tag beta
clawdbot update --restart
clawdbot update --no-restart
clawdbot update --json
clawdbot --update
```
## Options
- `--restart`: restart the Gateway service after a successful update.
- `--no-restart`: skip restarting the Gateway service after a successful update.
- `--channel <stable|beta|dev>`: set the update channel (git + npm; persisted in config).
- `--tag <dist-tag|version>`: override the npm dist-tag or version for this update only.
- `--json`: print machine-readable `UpdateRunResult` JSON.
@@ -52,7 +52,8 @@ Options:
## `update wizard`
Interactive flow to pick an update channel and confirm whether to restart the Gateway
after updating. If you select `dev` without a git checkout, it offers to create one.
after updating (default is to restart). If you select `dev` without a git checkout, it
offers to create one.
## What it does

View File

@@ -5,7 +5,7 @@ read_when:
---
# Gateway architecture
Last updated: 2026-01-19
Last updated: 2026-01-22
## Overview
@@ -34,7 +34,8 @@ Last updated: 2026-01-19
### Nodes (macOS / iOS / Android / headless)
- Connect to the **same WS server** with `role: node`.
- Pair with the Gateway to receive a token.
- Provide a device identity in `connect`; pairing is **devicebased** (role `node`) and
approval lives in the device pairing store.
- Expose commands like `canvas.*`, `camera.*`, `screen.record`, `location.get`.
Protocol details:

View File

@@ -25,6 +25,7 @@ stay consistent across channels.
1. **Parse Markdown -> IR**
- IR is plain text plus style spans (bold/italic/strike/code/spoiler) and link spans.
- Offsets are UTF-16 code units so Signal style ranges align with its API.
- Tables are parsed only when a channel opts into table conversion.
2. **Chunk IR (format-first)**
- Chunking happens on the IR text before rendering.
- Inline formatting does not split across chunks; spans are sliced per chunk.
@@ -59,7 +60,30 @@ IR (schematic):
- Slack, Telegram, and Signal outbound adapters render from the IR.
- Other channels (WhatsApp, iMessage, MS Teams, Discord) still use plain text or
their own formatting rules.
their own formatting rules, with Markdown table conversion applied before
chunking when enabled.
## Table handling
Markdown tables are not consistently supported across chat clients. Use
`markdown.tables` to control conversion per channel (and per account).
- `code`: render tables as code blocks (default for most channels).
- `bullets`: convert each row into bullet points (default for Signal + WhatsApp).
- `off`: disable table parsing and conversion; raw table text passes through.
Config keys:
```yaml
channels:
discord:
markdown:
tables: code
accounts:
work:
markdown:
tables: off
```
## Chunking rules

View File

@@ -52,10 +52,9 @@ Instances list, `client.mode === "cli"` is **not** turned into a presence entry.
Clients can send richer periodic beacons via the `system-event` method. The mac
app uses this to report host name, IP, and `lastInputSeconds`.
### 4) Node bridge beacons
When a node bridge connection authenticates, the Gateway emits a presence entry
for that node and refreshes it periodically so it doesnt expire.
### 4) Node connects (role: node)
When a node connects over the Gateway WebSocket with `role: node`, the Gateway
upserts a presence entry for that node (same flow as other WS clients).
## Merge + dedupe rules (why `instanceId` matters)

View File

@@ -19,7 +19,7 @@ Goal: small, hard-to-misuse tool set so agents can list sessions, fetch history,
- Group chats use `agent:<agentId>:<channel>:group:<id>` or `agent:<agentId>:<channel>:channel:<id>` (pass the full key).
- Cron jobs use `cron:<job.id>`.
- Hooks use `hook:<uuid>` unless explicitly set.
- Node bridge uses `node-<nodeId>` unless explicitly set.
- Node sessions use `node-<nodeId>` unless explicitly set.
`global` and `unknown` are reserved values and are never listed. If `session.scope = "global"`, we alias it to `main` for all tools so callers never see `global`.

View File

@@ -52,7 +52,7 @@ the workspace is writable. See [Memory](/concepts/memory) and
- Other sources:
- Cron jobs: `cron:<job.id>`
- Webhooks: `hook:<uuid>` (unless explicitly set by the hook)
- Node bridge runs: `node-<nodeId>`
- Node runs: `node-<nodeId>`
## Lifecycle
- Reset policy: sessions are reused until they expire, and expiry is evaluated on the next inbound message.

View File

@@ -46,7 +46,7 @@ Common methods + events:
| Messaging | `send`, `poll`, `agent`, `agent.wait` | side-effects need `idempotencyKey` |
| Chat | `chat.history`, `chat.send`, `chat.abort`, `chat.inject` | WebChat uses these |
| Sessions | `sessions.list`, `sessions.patch`, `sessions.delete` | session admin |
| Nodes | `node.list`, `node.invoke`, `node.pair.*` | bridge + node actions |
| Nodes | `node.list`, `node.invoke`, `node.pair.*` | Gateway WS + node actions |
| Events | `tick`, `presence`, `agent`, `chat`, `health`, `shutdown` | server push |
Authoritative list lives in `src/gateway/server.ts` (`METHODS`, `EVENTS`).

View File

@@ -70,7 +70,7 @@ What this does:
- `CLAWDBOT_PROFILE=dev`
- `CLAWDBOT_STATE_DIR=~/.clawdbot-dev`
- `CLAWDBOT_CONFIG_PATH=~/.clawdbot-dev/clawdbot.json`
- `CLAWDBOT_GATEWAY_PORT=19001` (bridge/canvas/browser shift accordingly)
- `CLAWDBOT_GATEWAY_PORT=19001` (browser/canvas shift accordingly)
2) **Dev bootstrap** (`gateway --dev`)
- Writes a minimal config if missing (`gateway.mode=local`, bind loopback).

View File

@@ -109,6 +109,14 @@
"source": "/opencode/",
"destination": "/providers/opencode"
},
{
"source": "/mattermost",
"destination": "/channels/mattermost"
},
{
"source": "/mattermost/",
"destination": "/channels/mattermost"
},
{
"source": "/glm",
"destination": "/providers/glm"
@@ -165,6 +173,14 @@
"source": "/providers/location/",
"destination": "/channels/location"
},
{
"source": "/providers/mattermost",
"destination": "/channels/mattermost"
},
{
"source": "/providers/mattermost/",
"destination": "/channels/mattermost"
},
{
"source": "/providers/msteams",
"destination": "/channels/msteams"
@@ -333,6 +349,14 @@
"source": "/cron-jobs",
"destination": "/automation/cron-jobs"
},
{
"source": "/cron-vs-heartbeat",
"destination": "/automation/cron-vs-heartbeat"
},
{
"source": "/cron-vs-heartbeat/",
"destination": "/automation/cron-vs-heartbeat"
},
{
"source": "/dashboard",
"destination": "/web/dashboard"
@@ -930,6 +954,7 @@
"channels/grammy",
"channels/discord",
"channels/slack",
"channels/mattermost",
"channels/signal",
"channels/imessage",
"channels/msteams",
@@ -967,6 +992,7 @@
"automation/webhook",
"automation/gmail-pubsub",
"automation/cron-jobs",
"automation/cron-vs-heartbeat",
"automation/poll"
]
},
@@ -974,6 +1000,8 @@
"group": "Tools & Skills",
"pages": [
"tools",
"tools/lobster",
"tools/llm-task",
"plugin",
"plugins/voice-call",
"plugins/zalouser",

View File

@@ -7,7 +7,7 @@ read_when:
# Bonjour / mDNS discovery
Clawdbot uses Bonjour (mDNS / DNSSD) as a **LANonly convenience** to discover
an active Gateway bridge. It is besteffort and does **not** replace SSH or
an active Gateway (WebSocket endpoint). It is besteffort and does **not** replace SSH or
Tailnet-based connectivity.
## Widearea Bonjour (Unicast DNSSD) over Tailscale
@@ -31,7 +31,7 @@ browse both `local.` and `clawdbot.internal.` automatically.
```json5
{
bridge: { bind: "tailnet" }, // tailnet-only (recommended)
gateway: { bind: "tailnet" }, // tailnet-only (recommended)
discovery: { wideArea: { enabled: true } } // enables clawdbot.internal DNS-SD publishing
}
```
@@ -63,13 +63,13 @@ In the Tailscale admin console:
Once clients accept tailnet DNS, iOS nodes can browse
`_clawdbot-gw._tcp` in `clawdbot.internal.` without multicast.
### Bridge listener security (recommended)
### Gateway listener security (recommended)
The bridge port (default `18790`) is a plain TCP service. By default it binds to
`0.0.0.0`, which makes it reachable from any interface on the gateway host.
The Gateway WS port (default `18789`) binds to loopback by default. For LAN/tailnet
access, bind explicitly and keep auth enabled.
For tailnetonly setups:
- Set `bridge.bind: "tailnet"` in `~/.clawdbot/clawdbot.json`.
- Set `gateway.bind: "tailnet"` in `~/.clawdbot/clawdbot.json`.
- Restart the Gateway (or restart the macOS menubar app).
## What advertises
@@ -87,11 +87,12 @@ The Gateway advertises small nonsecret hints to make UI flows convenient:
- `role=gateway`
- `displayName=<friendly name>`
- `lanHost=<hostname>.local`
- `gatewayPort=<port>` (informational; Gateway WS is usually loopbackonly)
- `bridgePort=<port>` (only when bridge is enabled)
- `gatewayPort=<port>` (Gateway WS + HTTP)
- `gatewayTls=1` (only when TLS is enabled)
- `gatewayTlsSha256=<sha256>` (only when TLS is enabled and fingerprint is available)
- `canvasPort=<port>` (only when the canvas host is enabled; default `18793`)
- `sshPort=<port>` (defaults to 22 when not overridden)
- `transport=bridge`
- `transport=gateway`
- `cliPath=<path>` (optional; absolute path to a runnable `clawdbot` entrypoint)
- `tailnetDns=<magicdns>` (optional hint when Tailnet is available)
@@ -125,8 +126,8 @@ The Gateway writes a rolling log file (printed on startup as
The iOS node uses `NWBrowser` to discover `_clawdbot-gw._tcp`.
To capture logs:
- Settings → Bridge → Advanced → **Discovery Debug Logs**
- Settings → Bridge → Advanced → **Discovery Logs** → reproduce → **Copy**
- Settings → Gateway → Advanced → **Discovery Debug Logs**
- Settings → Gateway → Advanced → **Discovery Logs** → reproduce → **Copy**
The log includes browser state transitions and resultset changes.
@@ -136,7 +137,7 @@ The log includes browser state transitions and resultset changes.
- **Multicast blocked**: some WiFi networks disable mDNS.
- **Sleep / interface churn**: macOS may temporarily drop mDNS results; retry.
- **Browse works but resolve fails**: keep machine names simple (avoid emojis or
punctuation), then restart the Gateway. The bridge instance name derives from
punctuation), then restart the Gateway. The service instance name derives from
the host name, so overly complex names can confuse some resolvers.
## Escaped instance names (`\032`)
@@ -150,9 +151,7 @@ sequences (e.g. spaces become `\032`).
## Disabling / configuration
- `CLAWDBOT_DISABLE_BONJOUR=1` disables advertising.
- `CLAWDBOT_BRIDGE_ENABLED=0` disables the bridge listener (and the bridge beacon).
- `bridge.bind` / `bridge.port` in `~/.clawdbot/clawdbot.json` control bridge bind/port.
- `CLAWDBOT_BRIDGE_HOST` / `CLAWDBOT_BRIDGE_PORT` still work as backcompat overrides.
- `gateway.bind` in `~/.clawdbot/clawdbot.json` controls the Gateway bind mode.
- `CLAWDBOT_SSH_PORT` overrides the SSH port advertised in TXT.
- `CLAWDBOT_TAILNET_DNS` publishes a MagicDNS hint in TXT.
- `CLAWDBOT_CLI_PATH` overrides the advertised CLI path.

View File

@@ -14,6 +14,9 @@ should use the unified Gateway WebSocket protocol instead.
If you are building an operator or node client, use the
[Gateway protocol](/gateway/protocol).
**Note:** Current Clawdbot builds no longer ship the TCP bridge listener; this document is kept for historical reference.
Legacy `bridge.*` config keys are no longer part of the config schema.
## Why we have both
- **Security boundary**: the bridge exposes a small allowlist instead of the
@@ -28,7 +31,7 @@ If you are building an operator or node client, use the
- TCP, one JSON object per line (JSONL).
- Optional TLS (when `bridge.tls.enabled` is true).
- Gateway owns the listener (default `18790`).
- Legacy default listener port was `18790` (current builds do not start a TCP bridge).
When TLS is enabled, discovery TXT records include `bridgeTls=1` plus
`bridgeTlsSha256` so nodes can pin the certificate.
@@ -54,7 +57,7 @@ Gateway → Client:
- `event`: chat updates for subscribed sessions
- `ping` / `pong`: keepalive
Exact allowlist is enforced in `src/gateway/server-bridge.ts`.
Legacy allowlist enforcement lived in `src/gateway/server-bridge.ts` (removed).
## Exec lifecycle events

View File

@@ -129,7 +129,7 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number.
enabled: true,
maxBytes: 20971520,
models: [
{ provider: "openai", model: "whisper-1" },
{ provider: "openai", model: "gpt-4o-mini-transcribe" },
// Optional CLI fallback (Whisper binary):
// { type: "cli", command: "whisper", args: ["--model", "base", "{{MediaPath}}"] }
],
@@ -568,5 +568,5 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number.
- If you set `dmPolicy: "open"`, the matching `allowFrom` list must include `"*"`.
- Provider IDs differ (phone numbers, user IDs, channel IDs). Use the provider docs to confirm the format.
- Optional sections to add later: `web`, `browser`, `ui`, `bridge`, `discovery`, `canvasHost`, `talk`, `signal`, `imessage`.
- Optional sections to add later: `web`, `browser`, `ui`, `discovery`, `canvasHost`, `talk`, `signal`, `imessage`.
- See [Providers](/channels/whatsapp) and [Troubleshooting](/gateway/troubleshooting) for deeper setup notes.

View File

@@ -543,7 +543,7 @@ Notes:
- Outbound commands default to account `default` if present; otherwise the first configured account id (sorted).
- The legacy single-account Baileys auth dir is migrated by `clawdbot doctor` into `whatsapp/default`.
### `channels.telegram.accounts` / `channels.discord.accounts` / `channels.slack.accounts` / `channels.signal.accounts` / `channels.imessage.accounts`
### `channels.telegram.accounts` / `channels.discord.accounts` / `channels.slack.accounts` / `channels.mattermost.accounts` / `channels.signal.accounts` / `channels.imessage.accounts`
Run multiple accounts per channel (each account has its own `accountId` and optional `name`):
@@ -1204,6 +1204,44 @@ Slack action groups (gate `slack` tool actions):
| memberInfo | enabled | Member info |
| emojiList | enabled | Custom emoji list |
### `channels.mattermost` (bot token)
Mattermost ships as a plugin and is not bundled with the core install.
Install it first: `clawdbot plugins install @clawdbot/mattermost` (or `./extensions/mattermost` from a git checkout).
Mattermost requires a bot token plus the base URL for your server:
```json5
{
channels: {
mattermost: {
enabled: true,
botToken: "mm-token",
baseUrl: "https://chat.example.com",
dmPolicy: "pairing",
chatmode: "oncall", // oncall | onmessage | onchar
oncharPrefixes: [">", "!"],
textChunkLimit: 4000
}
}
}
```
Clawdbot starts Mattermost when the account is configured (bot token + base URL) and enabled. The token + base URL are resolved from `channels.mattermost.botToken` + `channels.mattermost.baseUrl` or `MATTERMOST_BOT_TOKEN` + `MATTERMOST_URL` for the default account (unless `channels.mattermost.enabled` is `false`).
Chat modes:
- `oncall` (default): respond to channel messages only when @mentioned.
- `onmessage`: respond to every channel message.
- `onchar`: respond when a message starts with a trigger prefix (`channels.mattermost.oncharPrefixes`, default `[">", "!"]`).
Access control:
- Default DMs: `channels.mattermost.dmPolicy="pairing"` (unknown senders get a pairing code).
- Public DMs: `channels.mattermost.dmPolicy="open"` plus `channels.mattermost.allowFrom=["*"]`.
- Groups: `channels.mattermost.groupPolicy="allowlist"` by default (mention-gated). Use `channels.mattermost.groupAllowFrom` to restrict senders.
Multi-account support lives under `channels.mattermost.accounts` (see the multi-account section above). Env vars only apply to the default account.
Use `channel:<id>` or `user:<id>` (or `@username`) when specifying delivery targets; bare ids are treated as channel ids.
### `channels.signal` (signal-cli)
Signal reactions can emit system events (shared reaction tooling):
@@ -1690,7 +1728,7 @@ auto-compaction, instructing the model to store durable memories on disk (e.g.
`memory/YYYY-MM-DD.md`). It triggers when the session token estimate crosses a
soft threshold below the compaction limit.
Defaults:
Legacy defaults:
- `memoryFlush.enabled`: `true`
- `memoryFlush.softThresholdTokens`: `4000`
- `memoryFlush.prompt` / `memoryFlush.systemPrompt`: built-in defaults with `NO_REPLY`
@@ -1735,8 +1773,9 @@ Block streaming:
with `maxChars` capped to the channel text limit. Signal/Slack/Discord default
to `minChars: 1500` unless overridden.
Channel overrides: `channels.whatsapp.blockStreamingCoalesce`, `channels.telegram.blockStreamingCoalesce`,
`channels.discord.blockStreamingCoalesce`, `channels.slack.blockStreamingCoalesce`, `channels.signal.blockStreamingCoalesce`,
`channels.imessage.blockStreamingCoalesce`, `channels.msteams.blockStreamingCoalesce` (and per-account variants).
`channels.discord.blockStreamingCoalesce`, `channels.slack.blockStreamingCoalesce`, `channels.mattermost.blockStreamingCoalesce`,
`channels.signal.blockStreamingCoalesce`, `channels.imessage.blockStreamingCoalesce`, `channels.msteams.blockStreamingCoalesce`
(and per-account variants).
- `agents.defaults.humanDelay`: randomized pause between **block replies** after the first.
Modes: `off` (default), `natural` (8002500ms), `custom` (use `minMs`/`maxMs`).
Per-agent override: `agents.list[].humanDelay`.
@@ -1826,7 +1865,7 @@ Note: `applyPatch` is only under `tools.exec`.
- Each `models[]` entry:
- Provider entry (`type: "provider"` or omitted):
- `provider`: API provider id (`openai`, `anthropic`, `google`/`gemini`, `groq`, etc).
- `model`: model id override (required for image; defaults to `whisper-1`/`whisper-large-v3-turbo` for audio providers, and `gemini-3-flash-preview` for video).
- `model`: model id override (required for image; defaults to `gpt-4o-mini-transcribe`/`whisper-large-v3-turbo` for audio providers, and `gemini-3-flash-preview` for video).
- `profile` / `preferredProfile`: auth profile selection.
- CLI entry (`type: "cli"`):
- `command`: executable to run.
@@ -1851,7 +1890,7 @@ Example:
rules: [{ action: "allow", match: { chatType: "direct" } }]
},
models: [
{ provider: "openai", model: "whisper-1" },
{ provider: "openai", model: "gpt-4o-mini-transcribe" },
{ type: "cli", command: "whisper", args: ["--model", "base", "{{MediaPath}}"] }
]
},
@@ -2784,7 +2823,7 @@ Hot-applied (no full gateway restart):
Requires full Gateway restart:
- `gateway` (port/bind/auth/control UI/tailscale)
- `bridge`
- `bridge` (legacy)
- `discovery`
- `canvasHost`
- `plugins`
@@ -2802,7 +2841,7 @@ Convenience flags (CLI):
- `clawdbot --dev …` → uses `~/.clawdbot-dev` + shifts ports from base `19001`
- `clawdbot --profile <name> …` → uses `~/.clawdbot-<name>` (port via config/env/flags)
See [Gateway runbook](/gateway) for the derived port mapping (gateway/bridge/browser/canvas).
See [Gateway runbook](/gateway) for the derived port mapping (gateway/browser/canvas).
See [Multiple gateways](/gateway/multiple-gateways) for browser/CDP port isolation details.
Example:
@@ -2921,7 +2960,7 @@ The Gateway serves a directory of HTML/CSS/JS over HTTP so iOS/Android nodes can
Default root: `~/clawd/canvas`
Default port: `18793` (chosen to avoid the clawd browser CDP port `18792`)
The server listens on the **bridge bind host** (LAN or Tailnet) so nodes can reach it.
The server listens on the **gateway bind host** (LAN or Tailnet) so nodes can reach it.
The server:
- serves files under `canvasHost.root`
@@ -2950,9 +2989,13 @@ Disable with:
- config: `canvasHost: { enabled: false }`
- env: `CLAWDBOT_SKIP_CANVAS_HOST=1`
### `bridge` (node bridge server)
### `bridge` (legacy TCP bridge, removed)
The Gateway can expose a simple TCP bridge for nodes (iOS/Android), typically on port `18790`.
Current builds no longer include the TCP bridge listener; `bridge.*` config keys are ignored.
Nodes connect over the Gateway WebSocket. This section is kept for historical reference.
Legacy behavior:
- The Gateway could expose a simple TCP bridge for nodes (iOS/Android), typically on port `18790`.
Defaults:
- enabled: `true`

View File

@@ -3,7 +3,7 @@ summary: "Node discovery and transports (Bonjour, Tailscale, SSH) for finding th
read_when:
- Implementing or changing Bonjour discovery/advertising
- Adjusting remote connection modes (direct vs SSH)
- Designing bridge + pairing for remote nodes
- Designing node discovery + pairing for remote nodes
---
# Discovery & transports
@@ -17,17 +17,18 @@ The design goal is to keep all network discovery/advertising in the **Node Gatew
## Terms
- **Gateway**: a single long-running gateway process that owns state (sessions, pairing, node registry) and runs channels. Most setups use one per host; isolated multi-gateway setups are possible.
- **Gateway WS (loopback)**: the existing gateway WebSocket control endpoint on `127.0.0.1:18789`.
- **Bridge (direct transport)**: a LAN/tailnet-facing endpoint owned by the gateway that allows authenticated clients/nodes to call a scoped subset of gateway methods. The bridge exists so the gateway can remain loopback-only.
- **Gateway WS (control plane)**: the WebSocket endpoint on `127.0.0.1:18789` by default; can be bound to LAN/tailnet via `gateway.bind`.
- **Direct WS transport**: a LAN/tailnet-facing Gateway WS endpoint (no SSH).
- **SSH transport (fallback)**: remote control by forwarding `127.0.0.1:18789` over SSH.
- **Legacy TCP bridge (deprecated/removed)**: older node transport (see [Bridge protocol](/gateway/bridge-protocol)); no longer advertised for discovery.
Protocol details:
- [Gateway protocol](/gateway/protocol)
- [Bridge protocol](/gateway/bridge-protocol)
- [Bridge protocol (legacy)](/gateway/bridge-protocol)
## Why we keep both “direct” and SSH
- **Direct bridge** is the best UX on the same network and within a tailnet:
- **Direct WS** is the best UX on the same network and within a tailnet:
- auto-discovery on LAN via Bonjour
- pairing tokens + ACLs owned by the gateway
- no shell access required; protocol surface can stay tight and auditable
@@ -43,7 +44,7 @@ Protocol details:
Bonjour is best-effort and does not cross networks. It is only used for “same LAN” convenience.
Target direction:
- The **gateway** advertises its bridge via Bonjour.
- The **gateway** advertises its WS endpoint via Bonjour.
- Clients browse and show a “pick a gateway” list, then store the chosen endpoint.
Troubleshooting and beacon details: [Bonjour](/gateway/bonjour).
@@ -56,19 +57,19 @@ Troubleshooting and beacon details: [Bonjour](/gateway/bonjour).
- `role=gateway`
- `lanHost=<hostname>.local`
- `sshPort=22` (or whatever is advertised)
- `gatewayPort=18789` (loopback WS port; informational)
- `bridgePort=18790` (when bridge is enabled)
- `gatewayPort=18789` (Gateway WS + HTTP)
- `gatewayTls=1` (only when TLS is enabled)
- `gatewayTlsSha256=<sha256>` (only when TLS is enabled and fingerprint is available)
- `canvasPort=18793` (default canvas host port; serves `/__clawdbot__/canvas/`)
- `cliPath=<path>` (optional; absolute path to a runnable `clawdbot` entrypoint or binary)
- `tailnetDns=<magicdns>` (optional hint; auto-detected when Tailscale is available)
Disable/override:
- `CLAWDBOT_DISABLE_BONJOUR=1` disables advertising.
- `CLAWDBOT_BRIDGE_ENABLED=0` disables the bridge listener.
- `bridge.bind` / `bridge.port` in `~/.clawdbot/clawdbot.json` control bridge bind/port (preferred).
- `CLAWDBOT_BRIDGE_HOST` / `CLAWDBOT_BRIDGE_PORT` still work as a back-compat override when `bridge.bind` / `bridge.port` are not set.
- `CLAWDBOT_SSH_PORT` overrides the SSH port advertised in the bridge beacon (defaults to 22).
- `CLAWDBOT_TAILNET_DNS` publishes a `tailnetDns` hint (MagicDNS) in the bridge beacon (auto-detected if unset).
- `gateway.bind` in `~/.clawdbot/clawdbot.json` controls the Gateway bind mode.
- `CLAWDBOT_SSH_PORT` overrides the SSH port advertised in TXT (defaults to 22).
- `CLAWDBOT_TAILNET_DNS` publishes a `tailnetDns` hint (MagicDNS).
- `CLAWDBOT_CLI_PATH` overrides the advertised CLI path.
### 2) Tailnet (cross-network)
@@ -97,13 +98,13 @@ Recommended client behavior:
The gateway is the source of truth for node/client admission.
- Pairing requests are created/approved/rejected in the gateway (see [Gateway pairing](/gateway/pairing)).
- The bridge enforces:
- The gateway enforces:
- auth (token / keypair)
- scopes/ACLs (bridge is not a raw proxy to every gateway method)
- scopes/ACLs (the gateway is not a raw proxy to every method)
- rate limits
## Responsibilities by component
- **Gateway**: advertises discovery beacons, owns pairing decisions, runs the bridge listener.
- **Gateway**: advertises discovery beacons, owns pairing decisions, and hosts the WS endpoint.
- **macOS app**: helps you pick a gateway, shows pairing prompts, and uses SSH only as a fallback.
- **iOS/Android nodes**: browse Bonjour as a convenience and connect via the paired bridge.
- **iOS/Android nodes**: browse Bonjour as a convenience and connect to the paired Gateway WS.

View File

@@ -2,9 +2,12 @@
summary: "Heartbeat polling messages and notification rules"
read_when:
- Adjusting heartbeat cadence or messaging
- Deciding between heartbeat and cron for scheduled tasks
---
# Heartbeat (Gateway)
> **Heartbeat vs Cron?** See [Cron vs Heartbeat](/automation/cron-vs-heartbeat) for guidance on when to use each.
Heartbeat runs **periodic agent turns** in the main session so the model can
surface anything that needs attention without spamming you.

View File

@@ -82,14 +82,12 @@ Defaults (can be overridden via env/flags/config):
- `CLAWDBOT_STATE_DIR=~/.clawdbot-dev`
- `CLAWDBOT_CONFIG_PATH=~/.clawdbot-dev/clawdbot.json`
- `CLAWDBOT_GATEWAY_PORT=19001` (Gateway WS + HTTP)
- `bridge.port=19002` (derived: `gateway.port+1`)
- `browser.controlUrl=http://127.0.0.1:19003` (derived: `gateway.port+2`)
- `canvasHost.port=19005` (derived: `gateway.port+4`)
- `agents.defaults.workspace` default becomes `~/clawd-dev` when you run `setup`/`onboard` under `--dev`.
Derived ports (rules of thumb):
- Base port = `gateway.port` (or `CLAWDBOT_GATEWAY_PORT` / `--port`)
- `bridge.port = base + 1` (or `CLAWDBOT_BRIDGE_PORT` / config override)
- `browser.controlUrl port = base + 2` (or `CLAWDBOT_BROWSER_CONTROL_URL` / config override)
- `canvasHost.port = base + 4` (or `CLAWDBOT_CANVAS_HOST_PORT` / config override)
- Browser profile CDP ports auto-allocate from `browser.controlPort + 9 .. + 108` (persisted per profile).
@@ -114,7 +112,7 @@ CLAWDBOT_CONFIG_PATH=~/.clawdbot/b.json CLAWDBOT_STATE_DIR=~/.clawdbot-b clawdbo
```
## Protocol (operator view)
- Full docs: [Gateway protocol](/gateway/protocol) and [Bridge protocol](/gateway/bridge-protocol).
- Full docs: [Gateway protocol](/gateway/protocol) and [Bridge protocol (legacy)](/gateway/bridge-protocol).
- Mandatory first frame from client: `req {type:"req", id, method:"connect", params:{minProtocol,maxProtocol,client:{id,displayName?,version,platform,deviceFamily?,modelIdentifier?,mode,instanceId?}, caps, auth?, locale?, userAgent? } }`.
- Gateway replies `res {type:"res", id, ok:true, payload:hello-ok }` (or `ok:false` with an error, then closes).
- After handshake:
@@ -130,7 +128,7 @@ CLAWDBOT_CONFIG_PATH=~/.clawdbot/b.json CLAWDBOT_STATE_DIR=~/.clawdbot-b clawdbo
- `system-event` — post a presence/system note (structured).
- `send` — send a message via the active channel(s).
- `agent` — run an agent turn (streams events back on same connection).
- `node.list` — list paired + currently-connected bridge nodes (includes `caps`, `deviceFamily`, `modelIdentifier`, `paired`, `connected`, and advertised `commands`).
- `node.list` — list paired + currently-connected nodes (includes `caps`, `deviceFamily`, `modelIdentifier`, `paired`, `connected`, and advertised `commands`).
- `node.describe` — describe a node (capabilities + supported `node.invoke` commands; works for paired nodes and for currently-connected unpaired nodes).
- `node.invoke` — invoke a command on a node (e.g. `canvas.*`, `camera.*`).
- `node.pair.*` — pairing lifecycle (`request`, `list`, `approve`, `reject`, `verify`).

View File

@@ -51,7 +51,7 @@ You can tune console verbosity independently via:
## Tool summary redaction
Verbose tool summaries (e.g. `🛠️ exec: ...`) can mask sensitive tokens before they hit the
Verbose tool summaries (e.g. `🛠️ Exec: ...`) can mask sensitive tokens before they hit the
console stream. This is **tools-only** and does not alter file logs.
- `logging.redactSensitive`: `off` | `tools` (default: `tools`)

View File

@@ -13,7 +13,7 @@ Most setups should use one Gateway because a single Gateway can handle multiple
- `CLAWDBOT_STATE_DIR` — per-instance sessions, creds, caches
- `agents.defaults.workspace` — per-instance workspace root
- `gateway.port` (or `--port`) — unique per instance
- Derived ports (bridge/browser/canvas) must not overlap
- Derived ports (browser/canvas) must not overlap
If these are shared, you will hit config races and port conflicts.
@@ -47,7 +47,7 @@ Run a second Gateway on the same host with its own:
This keeps the rescue bot isolated from the main bot so it can debug or apply config changes if the primary bot is down.
Port spacing: leave at least 20 ports between base ports so the derived bridge/browser/canvas/CDP ports never collide.
Port spacing: leave at least 20 ports between base ports so the derived browser/canvas/CDP ports never collide.
### How to install (rescue bot)
@@ -73,7 +73,6 @@ clawdbot --profile rescue gateway install
Base port = `gateway.port` (or `CLAWDBOT_GATEWAY_PORT` / `--port`).
- `bridge.port = base + 1`
- `browser.controlUrl port = base + 2`
- `canvasHost.port = base + 4`
- Browser profile CDP ports auto-allocate from `browser.controlPort + 9 .. + 108`

View File

@@ -11,16 +11,20 @@ In Gateway-owned pairing, the **Gateway** is the source of truth for which nodes
are allowed to join. UIs (macOS app, future clients) are just frontends that
approve or reject pending requests.
**Important:** WS nodes use **device pairing** (role `node`) during `connect`.
`node.pair.*` is a separate pairing store and does **not** gate the WS handshake.
Only clients that explicitly call `node.pair.*` use this flow.
## Concepts
- **Pending request**: a node asked to join; requires approval.
- **Paired node**: approved node with an issued auth token.
- **Bridge**: transport endpoint only; it forwards requests but does not decide
membership.
- **Transport**: the Gateway WS endpoint forwards requests but does not decide
membership. (Legacy TCP bridge support is deprecated/removed.)
## How pairing works
1. A node connects to the bridge and requests pairing.
1. A node connects to the Gateway WS and requests pairing.
2. The Gateway stores a **pending request** and emits `node.pair.requested`.
3. You approve or reject the request (CLI or UI).
4. On approval, the Gateway issues a **new token** (tokens are rotated on repair).
@@ -81,9 +85,8 @@ Security notes:
- Tokens are secrets; treat `paired.json` as sensitive.
- Rotating a token requires re-approval (or deleting the node entry).
## Bridge behavior
## Transport behavior
- The bridge is **transport only**; it does not store membership.
- The transport is **stateless**; it does not store membership.
- If the Gateway is offline or pairing is disabled, nodes cannot pair.
- If the bridge is running but the Gateway is in remote mode, pairing still
happens against the remote Gateways store.
- If the Gateway is in remote mode, pairing still happens against the remote Gateways store.

View File

@@ -8,7 +8,7 @@ read_when:
This repo supports “remote over SSH” by keeping a single Gateway (the master) running on a dedicated host (desktop/server) and connecting clients to it.
- For **operators (you / the macOS app)**: SSH tunneling is the universal fallback.
- For **nodes (iOS/Android and future devices)**: prefer the Gateway **Bridge** when on the same LAN/tailnet (see [Discovery](/gateway/discovery)).
- For **nodes (iOS/Android and future devices)**: connect to the Gateway **WebSocket** (LAN/tailnet or SSH tunnel as needed).
## The core idea
@@ -55,12 +55,12 @@ One gateway service owns state + channels. Nodes are peripherals.
Flow example (Telegram → node):
- Telegram message arrives at the **Gateway**.
- Gateway runs the **agent** and decides whether to call a node tool.
- Gateway calls the **node** over the Bridge (`node.*` RPC).
- Gateway calls the **node** over the Gateway WebSocket (`node.*` RPC).
- Node returns the result; Gateway replies back out to Telegram.
Notes:
- **Nodes do not run the gateway service.** Only one gateway should run per host unless you intentionally run isolated profiles (see [Multiple gateways](/gateway/multiple-gateways)).
- macOS app “node mode” is just a node client over the Bridge.
- macOS app “node mode” is just a node client over the Gateway WebSocket.
## SSH tunnel (CLI + tools)

View File

@@ -94,8 +94,8 @@ clawdbot gateway --tailscale funnel --auth password
or `tailscale funnel` configuration on shutdown.
- `gateway.bind: "tailnet"` is a direct Tailnet bind (no HTTPS, no Serve/Funnel).
- `gateway.bind: "auto"` prefers loopback; use `tailnet` if you want Tailnet-only.
- Serve/Funnel only expose the **Gateway control UI + WS**. Node **bridge** traffic
uses the separate bridge port (default `18790`) and is **not** proxied by Serve.
- Serve/Funnel only expose the **Gateway control UI + WS**. Nodes connect over
the same Gateway WS endpoint, so Serve can work for node access.
## Browser control server (remote Gateway + local browser)

View File

@@ -43,6 +43,14 @@ Almost always a Node/npm PATH issue. Start here:
- [Gateway troubleshooting](/gateway/troubleshooting)
- [Control UI](/web/control-ui#insecure-http)
### `docs.clawd.bot` shows an SSL error (Comcast/Xfinity)
Some Comcast/Xfinity connections block `docs.clawd.bot` via Xfinity Advanced Security.
Disable Advanced Security or add `docs.clawd.bot` to the allowlist, then retry.
- Xfinity Advanced Security help: https://www.xfinity.com/support/articles/using-xfinity-xfi-advanced-security
- Quick sanity checks: try a mobile hotspot or VPN to confirm its ISP-level filtering
### Service says running, but RPC probe fails
- [Gateway troubleshooting](/gateway/troubleshooting)

View File

@@ -13,6 +13,7 @@ read_when:
<p align="center">
<strong>Any OS + WhatsApp/Telegram/Discord/iMessage gateway for AI agents (Pi).</strong><br />
Plugins add Mattermost and more.
Send a message, get an agent response — from your pocket.
</p>
@@ -23,7 +24,7 @@ read_when:
<a href="/start/clawd">Clawdbot assistant setup</a>
</p>
Clawdbot bridges WhatsApp (via WhatsApp Web / Baileys), Telegram (Bot API / grammY), Discord (Bot API / channels.discord.js), and iMessage (imsg CLI) to coding agents like [Pi](https://github.com/badlogic/pi-mono).
Clawdbot bridges WhatsApp (via WhatsApp Web / Baileys), Telegram (Bot API / grammY), Discord (Bot API / channels.discord.js), and iMessage (imsg CLI) to coding agents like [Pi](https://github.com/badlogic/pi-mono). Plugins add Mattermost (Bot API + WebSocket) and more.
Clawdbot also powers [Clawd](https://clawd.me), the spacelobster assistant.
## Start here
@@ -44,12 +45,12 @@ Remote access: [Web surfaces](/web) and [Tailscale](/gateway/tailscale)
## How it works
```
WhatsApp / Telegram / Discord
WhatsApp / Telegram / Discord / iMessage (+ plugins)
┌───────────────────────────┐
│ Gateway │ ws://127.0.0.1:18789 (loopback-only)
│ (single source) │ tcp://0.0.0.0:18790 (Bridge)
│ (single source) │
│ │ http://<gateway-host>:18793
│ │ /__clawdbot__/canvas/ (Canvas host)
└───────────┬───────────────┘
@@ -58,8 +59,8 @@ WhatsApp / Telegram / Discord
├─ CLI (clawdbot …)
├─ Chat UI (SwiftUI)
├─ macOS app (Clawdbot.app)
├─ iOS node via Bridge + pairing
└─ Android node via Bridge + pairing
├─ iOS node via Gateway WS + pairing
└─ Android node via Gateway WS + pairing
```
Most operations flow through the **Gateway** (`clawdbot gateway`), a single long-running process that owns channel connections and the WebSocket control plane.
@@ -70,7 +71,7 @@ Most operations flow through the **Gateway** (`clawdbot gateway`), a single long
- **Loopback-first**: Gateway WS defaults to `ws://127.0.0.1:18789`.
- The wizard now generates a gateway token by default (even for loopback).
- For Tailnet access, run `clawdbot gateway --bind tailnet --token ...` (token is required for non-loopback binds).
- **Bridge for nodes**: optional LAN/tailnet-facing bridge on `tcp://0.0.0.0:18790` for paired nodes (Bonjour-discoverable).
- **Nodes**: connect to the Gateway WebSocket (LAN/tailnet/SSH as needed); legacy TCP bridge is deprecated/removed.
- **Canvas host**: HTTP file server on `canvasHost.port` (default `18793`), serving `/__clawdbot__/canvas/` for node WebViews; see [Gateway configuration](/gateway/configuration) (`canvasHost`).
- **Remote use**: SSH tunnel or tailnet/VPN; see [Remote access](/gateway/remote) and [Discovery](/gateway/discovery).
@@ -79,6 +80,7 @@ Most operations flow through the **Gateway** (`clawdbot gateway`), a single long
- 📱 **WhatsApp Integration** — Uses Baileys for WhatsApp Web protocol
- ✈️ **Telegram Bot** — DMs + groups via grammY
- 🎮 **Discord Bot** — DMs + guild channels via channels.discord.js
- 🧩 **Mattermost Bot (plugin)** — Bot token + WebSocket events
- 💬 **iMessage** — Local imsg CLI integration (macOS)
- 🤖 **Agent bridge** — Pi (RPC mode) with tool streaming
- ⏱️ **Streaming + chunking** — Block streaming + Telegram draft streaming details ([/concepts/streaming](/concepts/streaming))
@@ -190,6 +192,7 @@ Example:
- [Control UI (browser)](/web/control-ui)
- [Telegram](/channels/telegram)
- [Discord](/channels/discord)
- [Mattermost (plugin)](/channels/mattermost)
- [iMessage](/channels/imessage)
- [Groups](/concepts/groups)
- [WhatsApp group messages](/concepts/group-messages)

View File

@@ -7,7 +7,7 @@ read_when:
# Updating
Clawdbot is moving fast (pre “1.0”). Treat updates like shipping infra: update → run checks → restart → verify.
Clawdbot is moving fast (pre “1.0”). Treat updates like shipping infra: update → run checks → restart (or use `clawdbot update`, which restarts) → verify.
## Recommended: re-run the website installer (upgrade in place)
@@ -81,7 +81,7 @@ Notes:
For **source installs** (git checkout), prefer:
```bash
clawdbot update --restart
clawdbot update
```
It runs a safe-ish update flow:
@@ -89,6 +89,7 @@ It runs a safe-ish update flow:
- Switches to the selected channel (tag or branch).
- Fetches + rebases against the configured upstream (dev channel).
- Installs deps, builds, builds the Control UI, and runs `clawdbot doctor`.
- Restarts the gateway by default (use `--no-restart` to skip).
If you installed via **npm/pnpm** (no git metadata), `clawdbot update` will try to update via your package manager. If it cant detect the install, use “Update (global install)” instead.

View File

@@ -36,7 +36,7 @@ Local trust:
- [Remote access (SSH)](/gateway/remote)
- [Tailscale](/gateway/tailscale)
## Nodes + bridge
## Nodes + transports
- [Nodes overview](/nodes)
- [Bridge protocol (legacy nodes)](/gateway/bridge-protocol)

View File

@@ -6,7 +6,7 @@ read_when:
# Audio / Voice Notes — 2026-01-17
## What works
- **Media understanding (audio)**: If `tools.media.audio` is enabled (or a shared `tools.media.models` entry supports audio), Clawdbot:
- **Media understanding (audio)**: If audio understanding is enabled (or autodetected), Clawdbot:
1) Locates the first audio attachment (local path or URL) and downloads it if needed.
2) Enforces `maxBytes` before sending to each model entry.
3) Runs the first eligible model entry in order (provider or CLI).
@@ -15,6 +15,21 @@ read_when:
- **Command parsing**: When transcription succeeds, `CommandBody`/`RawBody` are set to the transcript so slash commands still work.
- **Verbose logging**: In `--verbose`, we log when transcription runs and when it replaces the body.
## Auto-detection (default)
If you **dont configure models** and `tools.media.audio.enabled` is **not** set to `false`,
Clawdbot auto-detects in this order and stops at the first working option:
1) **Local CLIs** (if installed)
- `sherpa-onnx-offline` (requires `SHERPA_ONNX_MODEL_DIR` with encoder/decoder/joiner/tokens)
- `whisper-cli` (from `whisper-cpp`; uses `WHISPER_CPP_MODEL` or the bundled tiny model)
- `whisper` (Python CLI; downloads models automatically)
2) **Gemini CLI** (`gemini`) using `read_many_files`
3) **Provider keys** (OpenAI → Groq → Deepgram → Google)
To disable auto-detection, set `tools.media.audio.enabled: false`.
To customize, set `tools.media.audio.models`.
Note: Binary detection is best-effort across macOS/Linux/Windows; ensure the CLI is on `PATH` (we expand `~`), or set an explicit CLI model with a full command path.
## Config examples
### Provider + CLI fallback (OpenAI + Whisper CLI)
@@ -26,7 +41,7 @@ read_when:
enabled: true,
maxBytes: 20971520,
models: [
{ provider: "openai", model: "whisper-1" },
{ provider: "openai", model: "gpt-4o-mini-transcribe" },
{
type: "cli",
command: "whisper",
@@ -54,7 +69,7 @@ read_when:
]
},
models: [
{ provider: "openai", model: "whisper-1" }
{ provider: "openai", model: "gpt-4o-mini-transcribe" }
]
}
}
@@ -83,6 +98,7 @@ read_when:
- Audio providers can override `baseUrl`, `headers`, and `providerOptions` via `tools.media.audio`.
- Default size cap is 20MB (`tools.media.audio.maxBytes`). Oversize audio is skipped for that model and the next entry is tried.
- Default `maxChars` for audio is **unset** (full transcript). Set `tools.media.audio.maxChars` or per-entry `maxChars` to trim output.
- OpenAI auto default is `gpt-4o-mini-transcribe`; set `model: "gpt-4o-transcribe"` for higher accuracy.
- Use `tools.media.audio.attachments` to process multiple voice notes (`mode: "all"` + `maxAttachments`).
- Transcript is available to templates as `{{Transcript}}`.
- CLI stdout is capped (5MB); keep CLI output concise.

View File

@@ -138,7 +138,7 @@ Notes:
## Safety + practical limits
- Camera and microphone access trigger the usual OS permission prompts (and require usage strings in Info.plist).
- Video clips are capped (currently `<= 60s`) to avoid oversized bridge payloads (base64 overhead + message limits).
- Video clips are capped (currently `<= 60s`) to avoid oversized node payloads (base64 overhead + message limits).
## macOS screen video (OS-level)

View File

@@ -8,9 +8,11 @@ read_when:
# Nodes
A **node** is a companion device (iOS/Android today) that connects to the Gateway over the **Bridge** and exposes a command surface (e.g. `canvas.*`, `camera.*`, `system.*`) via `node.invoke`. Bridge protocol details: [Bridge protocol](/gateway/bridge-protocol).
A **node** is a companion device (macOS/iOS/Android/headless) that connects to the Gateway **WebSocket** (same port as operators) with `role: "node"` and exposes a command surface (e.g. `canvas.*`, `camera.*`, `system.*`) via `node.invoke`. Protocol details: [Gateway protocol](/gateway/protocol).
macOS can also run in **node mode**: the menubar app connects to the Gateways bridge and exposes its local canvas/camera commands as a node (so `clawdbot nodes …` works against this Mac).
Legacy transport: [Bridge protocol](/gateway/bridge-protocol) (TCP JSONL; deprecated/removed for current nodes).
macOS can also run in **node mode**: the menubar app connects to the Gateways WS server and exposes its local canvas/camera commands as a node (so `clawdbot nodes …` works against this Mac).
Notes:
- Nodes are **peripherals**, not gateways. They dont run the gateway service.
@@ -18,21 +20,23 @@ Notes:
## Pairing + status
Pairing is gateway-owned and approval-based. See [Gateway pairing](/gateway/pairing) for the full flow.
**WS nodes use device pairing.** Nodes present a device identity during `connect`; the Gateway
creates a device pairing request for `role: node`. Approve via the devices CLI (or UI).
Quick CLI:
```bash
clawdbot nodes pending
clawdbot nodes approve <requestId>
clawdbot nodes reject <requestId>
clawdbot devices list
clawdbot devices approve <requestId>
clawdbot devices reject <requestId>
clawdbot nodes status
clawdbot nodes describe --node <idOrNameOrIp>
clawdbot nodes rename --node <idOrNameOrIp> --name "Kitchen iPad"
```
Notes:
- `nodes rename` stores a display name override in the gateway pairing store.
- `nodes status` marks a node as **paired** when its device pairing role includes `node`.
- `node.pair.*` (CLI: `clawdbot nodes pending/approve/reject`) is a separate gateway-owned
node pairing store; it does **not** gate the WS `connect` handshake.
## Remote node host (system.run)
@@ -57,11 +61,9 @@ clawdbot node run --host <gateway-host> --port 18789 --display-name "Build Node"
```bash
clawdbot node install --host <gateway-host> --port 18789 --display-name "Build Node"
clawdbot node status
clawdbot node restart
```
Install starts the service. Use `clawdbot node restart` if you need to re-launch it.
### Pair + name
On the gateway host:
@@ -241,6 +243,7 @@ Notes:
- `system.notify` respects notification permission state on the macOS app.
- `system.run` supports `--cwd`, `--env KEY=VAL`, `--command-timeout`, and `--needs-screen-recording`.
- `system.notify` supports `--priority <passive|active|timeSensitive>` and `--delivery <system|overlay|auto>`.
- macOS nodes drop `PATH` overrides; headless node hosts only accept `PATH` when it prepends the node host PATH.
- On macOS node mode, `system.run` is gated by exec approvals in the macOS app (Settings → Exec approvals).
Ask/allowlist/full behave the same as the headless node host; denied prompts return `SYSTEM_RUN_DENIED`.
- On headless node host, `system.run` is gated by exec approvals (`~/.clawdbot/exec-approvals.json`).
@@ -277,26 +280,26 @@ Nodes may include a `permissions` map in `node.list` / `node.describe`, keyed by
## Headless node host (cross-platform)
Clawdbot can run a **headless node host** (no UI) that connects to the Gateway
bridge and exposes `system.run` / `system.which`. This is useful on Linux/Windows
WebSocket and exposes `system.run` / `system.which`. This is useful on Linux/Windows
or for running a minimal node alongside a server.
Start it:
```bash
clawdbot node run --host <gateway-host> --port 18790
clawdbot node run --host <gateway-host> --port 18789
```
Notes:
- Pairing is still required (the Gateway will show a node approval prompt).
- The node host stores its node id + pairing token in `~/.clawdbot/node.json`.
- The node host stores its node id, token, display name, and gateway connection info in `~/.clawdbot/node.json`.
- Exec approvals are enforced locally via `~/.clawdbot/exec-approvals.json`
(see [Exec approvals](/tools/exec-approvals)).
- On macOS, the headless node host prefers the companion app exec host when reachable and falls
back to local execution if the app is unavailable. Set `CLAWDBOT_NODE_EXEC_HOST=app` to require
the app, or `CLAWDBOT_NODE_EXEC_FALLBACK=0` to disable fallback.
- Add `--tls` / `--tls-fingerprint` when the bridge requires TLS.
- Add `--tls` / `--tls-fingerprint` when the Gateway WS uses TLS.
## Mac node mode
- The macOS menubar app connects to the Gateway bridge as a node (so `clawdbot nodes …` works against this Mac).
- In remote mode, the app opens an SSH tunnel for the bridge port and connects to `localhost`.
- The macOS menubar app connects to the Gateway WS server as a node (so `clawdbot nodes …` works against this Mac).
- In remote mode, the app opens an SSH tunnel for the Gateway port and connects to `localhost`.

View File

@@ -6,7 +6,7 @@ read_when:
---
# Media Understanding (Inbound) — 2026-01-17
Clawdbot can optionally **summarize inbound media** (image/audio/video) before the reply pipeline runs. This is **opt-in** and separate from the base attachment flow—if understanding is off, models still receive the original files/URLs as usual.
Clawdbot can **summarize inbound media** (image/audio/video) before the reply pipeline runs. It autodetects when local tools or provider keys are available, and can be disabled or customized. If understanding is off, models still receive the original files/URLs as usual.
## Goals
- Optional: predigest inbound media into short text for faster routing + better command parsing.
@@ -88,6 +88,11 @@ Each `models[]` entry can be **provider** or **CLI**:
}
```
CLI templates can also use:
- `{{MediaDir}}` (directory containing the media file)
- `{{OutputDir}}` (scratch dir created for this run)
- `{{OutputBase}}` (scratch file base path, no extension)
## Defaults and limits
Recommended defaults:
- `maxChars`: **500** for image/video (short, commandfriendly)
@@ -104,17 +109,22 @@ Rules:
- If `<capability>.enabled: true` but no models are configured, Clawdbot tries the
**active reply model** when its provider supports the capability.
### Auto-enable audio (when keys exist)
If `tools.media.audio.enabled` is **not** set to `false` and you have any supported
audio provider keys configured, Clawdbot will **auto-enable audio transcription**
even when you havent listed models explicitly.
### Auto-detect media understanding (default)
If `tools.media.<capability>.enabled` is **not** set to `false` and you havent
configured models, Clawdbot auto-detects in this order and **stops at the first
working option**:
Providers checked (in order):
1) OpenAI
2) Groq
3) Deepgram
1) **Local CLIs** (audio only; if installed)
- `sherpa-onnx-offline` (requires `SHERPA_ONNX_MODEL_DIR` with encoder/decoder/joiner/tokens)
- `whisper-cli` (`whisper-cpp`; uses `WHISPER_CPP_MODEL` or the bundled tiny model)
- `whisper` (Python CLI; downloads models automatically)
2) **Gemini CLI** (`gemini`) using `read_many_files`
3) **Provider keys**
- Audio: OpenAI → Groq → Deepgram → Google
- Image: OpenAI → Anthropic → Google → MiniMax
- Video: Google
To disable this behavior, set:
To disable auto-detection, set:
```json5
{
tools: {
@@ -126,6 +136,7 @@ To disable this behavior, set:
}
}
```
Note: Binary detection is best-effort across macOS/Linux/Windows; ensure the CLI is on `PATH` (we expand `~`), or set an explicit CLI model with a full command path.
## Capabilities (optional)
If you set `capabilities`, the entry only runs for those media types. For shared
@@ -142,7 +153,7 @@ If you omit `capabilities`, the entry is eligible for the list it appears in.
| Capability | Provider integration | Notes |
|------------|----------------------|-------|
| Image | OpenAI / Anthropic / Google / others via `pi-ai` | Any image-capable model in the registry works. |
| Audio | OpenAI, Groq, Deepgram | Provider transcription (Whisper/Deepgram). |
| Audio | OpenAI, Groq, Deepgram, Google | Provider transcription (Whisper/Deepgram/Gemini). |
| Video | Google (Gemini API) | Provider video understanding. |
## Recommended providers
@@ -151,8 +162,8 @@ If you omit `capabilities`, the entry is eligible for the list it appears in.
- Good defaults: `openai/gpt-5.2`, `anthropic/claude-opus-4-5`, `google/gemini-3-pro-preview`.
**Audio**
- `openai/whisper-1`, `groq/whisper-large-v3-turbo`, or `deepgram/nova-3`.
- CLI fallback: `whisper` binary.
- `openai/gpt-4o-mini-transcribe`, `groq/whisper-large-v3-turbo`, or `deepgram/nova-3`.
- CLI fallback: `whisper-cli` (whisper-cpp) or `whisper`.
- Deepgram setup: [Deepgram (audio transcription)](/providers/deepgram).
**Video**
@@ -209,7 +220,7 @@ When `mode: "all"`, outputs are labeled `[Image 1/2]`, `[Audio 2/2]`, etc.
audio: {
enabled: true,
models: [
{ provider: "openai", model: "whisper-1" },
{ provider: "openai", model: "gpt-4o-mini-transcribe" },
{
type: "cli",
command: "whisper",

View File

@@ -41,7 +41,7 @@ Notes:
Who receives it:
- All WebSocket clients (macOS app, WebChat, etc.)
- All connected bridge nodes (iOS/Android), and also on node connect as an initial “current state” push.
- All connected nodes (iOS/Android), and also on node connect as an initial “current state” push.
## Client behavior
@@ -53,9 +53,9 @@ Who receives it:
### iOS node
- Uses the global list for `VoiceWakeManager` trigger detection.
- Editing Wake Words in Settings calls `voicewake.set` (over the bridge) and also keeps local wake-word detection responsive.
- Editing Wake Words in Settings calls `voicewake.set` (over the Gateway WS) and also keeps local wake-word detection responsive.
### Android node
- Exposes a Wake Words editor in Settings.
- Calls `voicewake.set` over the bridge so edits sync everywhere.
- Calls `voicewake.set` over the Gateway WS so edits sync everywhere.

View File

@@ -129,7 +129,6 @@ CLAWDBOT_IMAGE=clawdbot:latest
CLAWDBOT_GATEWAY_TOKEN=change-me-now
CLAWDBOT_GATEWAY_BIND=lan
CLAWDBOT_GATEWAY_PORT=18789
CLAWDBOT_BRIDGE_PORT=18790
CLAWDBOT_CONFIG_DIR=/root/.clawdbot
CLAWDBOT_WORKSPACE_DIR=/root/clawd
@@ -166,7 +165,6 @@ services:
- TERM=xterm-256color
- CLAWDBOT_GATEWAY_BIND=${CLAWDBOT_GATEWAY_BIND}
- CLAWDBOT_GATEWAY_PORT=${CLAWDBOT_GATEWAY_PORT}
- CLAWDBOT_BRIDGE_PORT=${CLAWDBOT_BRIDGE_PORT}
- CLAWDBOT_GATEWAY_TOKEN=${CLAWDBOT_GATEWAY_TOKEN}
- GOG_KEYRING_PASSWORD=${GOG_KEYRING_PASSWORD}
- XDG_CONFIG_HOME=${XDG_CONFIG_HOME}
@@ -179,9 +177,8 @@ services:
# To expose it publicly, remove the `127.0.0.1:` prefix and firewall accordingly.
- "127.0.0.1:${CLAWDBOT_GATEWAY_PORT}:18789"
# Optional: only if you run iOS/Android nodes against this VPS.
# If you expose these publicly, read /gateway/security and firewall accordingly.
# - "${CLAWDBOT_BRIDGE_PORT}:18790"
# Optional: only if you run iOS/Android nodes against this VPS and need Canvas host.
# If you expose this publicly, read /gateway/security and firewall accordingly.
# - "18793:18793"
command:
[

View File

@@ -40,7 +40,7 @@ node commands return `CANVAS_DISABLED`.
## Agent API surface
Canvas is exposed via the **node bridge**, so the agent can:
Canvas is exposed via the **Gateway WebSocket**, so the agent can:
- show/hide the panel
- navigate to a path or URL

View File

@@ -45,6 +45,13 @@ present. To reset manually:
rm ~/.clawdbot/disable-launchagent
```
## Attach-only mode
To force the macOS app to **never install or manage launchd**, launch it with
`--attach-only` (or `--no-launchd`). This sets `~/.clawdbot/disable-launchagent`,
so the app only attaches to an already running Gateway. You can toggle the same
behavior in Debug Settings.
## Remote mode
Remote mode never starts a local Gateway. The app uses an SSH tunnel to the

View File

@@ -8,7 +8,7 @@ read_when:
## What is shown
- We surface the current agent work state in the menu bar icon and in the first status row of the menu.
- Health status is hidden while work is active; it returns when all sessions are idle.
- The “Nodes” block in the menu lists **devices** only (gateway bridge nodes via `node.list`), not client/presence entries.
- The “Nodes” block in the menu lists **devices** only (paired nodes via `node.list`), not client/presence entries.
- A “Usage” section appears under Context when provider usage snapshots are available.
## State model

View File

@@ -11,7 +11,7 @@ This app now ships Sparkle auto-updates. Release builds must be Developer IDs
## Prereqs
- Developer ID Application cert installed (example: `Developer ID Application: <Developer Name> (<TEAMID>)`).
- Sparkle private key path set in the environment as `SPARKLE_PRIVATE_KEY_FILE` (path to your Sparkle ed25519 private key; public key baked into Info.plist).
- Sparkle private key path set in the environment as `SPARKLE_PRIVATE_KEY_FILE` (path to your Sparkle ed25519 private key; public key baked into Info.plist). If it is missing, check `~/.profile`.
- Notary credentials (keychain profile or API key) for `xcrun notarytool` if you want Gatekeeper-safe DMG/zip distribution.
- We use a Keychain profile named `clawdbot-notary`, created from App Store Connect API key env vars in your shell profile:
- `APP_STORE_CONNECT_API_KEY_P8`, `APP_STORE_CONNECT_KEY_ID`, `APP_STORE_CONNECT_ISSUER_ID`
@@ -30,17 +30,17 @@ Notes:
# From repo root; set release IDs so Sparkle feed is enabled.
# APP_BUILD must be numeric + monotonic for Sparkle compare.
BUNDLE_ID=com.clawdbot.mac \
APP_VERSION=2026.1.21 \
APP_VERSION=2026.1.23 \
APP_BUILD="$(git rev-list --count HEAD)" \
BUILD_CONFIG=release \
SIGN_IDENTITY="Developer ID Application: <Developer Name> (<TEAMID>)" \
scripts/package-mac-app.sh
# Zip for distribution (includes resource forks for Sparkle delta support)
ditto -c -k --sequesterRsrc --keepParent dist/Clawdbot.app dist/Clawdbot-2026.1.21.zip
ditto -c -k --sequesterRsrc --keepParent dist/Clawdbot.app dist/Clawdbot-2026.1.23.zip
# Optional: also build a styled DMG for humans (drag to /Applications)
scripts/create-dmg.sh dist/Clawdbot.app dist/Clawdbot-2026.1.21.dmg
scripts/create-dmg.sh dist/Clawdbot.app dist/Clawdbot-2026.1.23.dmg
# Recommended: build + notarize/staple zip + DMG
# First, create a keychain profile once:
@@ -48,26 +48,26 @@ scripts/create-dmg.sh dist/Clawdbot.app dist/Clawdbot-2026.1.21.dmg
# --apple-id "<apple-id>" --team-id "<team-id>" --password "<app-specific-password>"
NOTARIZE=1 NOTARYTOOL_PROFILE=clawdbot-notary \
BUNDLE_ID=com.clawdbot.mac \
APP_VERSION=2026.1.21 \
APP_VERSION=2026.1.23 \
APP_BUILD="$(git rev-list --count HEAD)" \
BUILD_CONFIG=release \
SIGN_IDENTITY="Developer ID Application: <Developer Name> (<TEAMID>)" \
scripts/package-mac-dist.sh
# Optional: ship dSYM alongside the release
ditto -c -k --keepParent apps/macos/.build/release/Clawdbot.app.dSYM dist/Clawdbot-2026.1.21.dSYM.zip
ditto -c -k --keepParent apps/macos/.build/release/Clawdbot.app.dSYM dist/Clawdbot-2026.1.23.dSYM.zip
```
## Appcast entry
Use the release note generator so Sparkle renders formatted HTML notes:
```bash
SPARKLE_PRIVATE_KEY_FILE=/path/to/ed25519-private-key scripts/make_appcast.sh dist/Clawdbot-2026.1.21.zip https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml
SPARKLE_PRIVATE_KEY_FILE=/path/to/ed25519-private-key scripts/make_appcast.sh dist/Clawdbot-2026.1.23.zip https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml
```
Generates HTML release notes from `CHANGELOG.md` (via [`scripts/changelog-to-html.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/changelog-to-html.sh)) and embeds them in the appcast entry.
Commit the updated `appcast.xml` alongside the release assets (zip + dSYM) when publishing.
## Publish & verify
- Upload `Clawdbot-2026.1.21.zip` (and `Clawdbot-2026.1.21.dSYM.zip`) to the GitHub release for tag `v2026.1.21`.
- Upload `Clawdbot-2026.1.23.zip` (and `Clawdbot-2026.1.23.dSYM.zip`) to the GitHub release for tag `v2026.1.23`.
- Ensure the raw appcast URL matches the baked feed: `https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml`.
- Sanity checks:
- `curl -I https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml` returns 200.

View File

@@ -1,5 +1,5 @@
---
summary: "macOS IPC architecture for Clawdbot app, gateway node bridge, and PeekabooBridge"
summary: "macOS IPC architecture for Clawdbot app, gateway node transport, and PeekabooBridge"
read_when:
- Editing IPC contracts or menu bar app IPC
---
@@ -13,21 +13,21 @@ read_when:
- Predictable permissions: always the same signed bundle ID, launched by launchd, so TCC grants stick.
## How it works
### Gateway + node bridge
### Gateway + node transport
- The app runs the Gateway (local mode) and connects to it as a node.
- Agent actions are performed via `node.invoke` (e.g. `system.run`, `system.notify`, `canvas.*`).
### Node service + app IPC
- A headless node host service connects to the Gateway bridge.
- A headless node host service connects to the Gateway WebSocket.
- `system.run` requests are forwarded to the macOS app over a local Unix socket.
- The app performs the exec in UI context, prompts if needed, and returns output.
Diagram (SCI):
```
Agent -> Gateway -> Bridge -> Node Service (TS)
| IPC (UDS + token + HMAC + TTL)
v
Mac App (UI + TCC + system.run)
Agent -> Gateway -> Node Service (WS)
| IPC (UDS + token + HMAC + TTL)
v
Mac App (UI + TCC + system.run)
```
### PeekabooBridge (UI automation)

View File

@@ -62,7 +62,7 @@ Node service + app IPC:
Diagram (SCI):
```
Gateway -> Bridge -> Node Service (TS)
Gateway -> Node Service (WS)
| IPC (UDS + token + HMAC + TTL)
v
Mac App (UI + TCC + system.run)
@@ -99,7 +99,7 @@ Example:
```
Notes:
- `allowlist` entries are JSON-encoded argv arrays.
- `allowlist` entries are glob patterns for resolved binary paths.
- Choosing “Always Allow” in the prompt adds that command to the allowlist.
- `system.run` environment overrides are filtered (drops `PATH`, `DYLD_*`, `LD_*`, `NODE_OPTIONS`, `PYTHON*`, `PERL*`, `RUBYOPT`) and then merged with the apps environment.

View File

@@ -61,6 +61,7 @@ Plugins can register:
- CLI commands
- Background services
- Optional config validation
- **Skills** (by listing `skills` directories in the plugin manifest)
Plugins run **inprocess** with the Gateway, so treat them as trusted code.
Tool authoring guide: [Plugin agent tools](/plugins/agent-tools).
@@ -144,6 +145,16 @@ Example:
}
```
Clawdbot can also merge **external channel catalogs** (for example, an MPM
registry export). Drop a JSON file at one of:
- `~/.clawdbot/mpm/plugins.json`
- `~/.clawdbot/mpm/catalog.json`
- `~/.clawdbot/plugins/catalog.json`
Or point `CLAWDBOT_PLUGIN_CATALOG_PATHS` (or `CLAWDBOT_MPM_CATALOG_PATHS`) at
one or more JSON files (comma/semicolon/`PATH`-delimited). Each file should
contain `{ "entries": [ { "name": "@scope/pkg", "clawdbot": { "channel": {...}, "install": {...} } } ] }`.
## Plugin IDs
Default plugin ids:

View File

@@ -82,6 +82,8 @@ Enable optional tools in `agents.list[].tools.allow` (or global `tools.allow`):
```
Other config knobs that affect tool availability:
- Allowlists that only name plugin tools are treated as plugin opt-ins; core tools remain
enabled unless you also include core tools or groups in the allowlist.
- `tools.profile` / `agents.list[].tools.profile` (base allowlist)
- `tools.byProvider` / `agents.list[].tools.byProvider` (providerspecific allow/deny)
- `tools.sandbox.tools.*` (sandbox tool policy when sandboxed)

View File

@@ -34,6 +34,7 @@ Optional keys:
- `kind` (string): plugin kind (example: `"memory"`).
- `channels` (array): channel ids registered by this plugin (example: `["matrix"]`).
- `providers` (array): provider ids registered by this plugin.
- `skills` (array): skill directories to load (relative to the plugin root).
- `name` (string): display name for the plugin.
- `description` (string): short plugin summary.
- `uiHints` (object): config field labels/placeholders/sensitive flags for UI rendering.

131
docs/prose.md Normal file
View File

@@ -0,0 +1,131 @@
---
summary: "OpenProse: .prose workflows, slash commands, and state in Clawdbot"
read_when:
- You want to run or write .prose workflows
- You want to enable the OpenProse plugin
- You need to understand state storage
---
# OpenProse
OpenProse is a portable, markdown-first workflow format for orchestrating AI sessions. In Clawdbot it ships as a plugin that installs an OpenProse skill pack plus a `/prose` slash command. Programs live in `.prose` files and can spawn multiple sub-agents with explicit control flow.
Official site: https://www.prose.md
## What it can do
- Multi-agent research + synthesis with explicit parallelism.
- Repeatable approval-safe workflows (code review, incident triage, content pipelines).
- Reusable `.prose` programs you can run across supported agent runtimes.
## Install + enable
Bundled plugins are disabled by default. Enable OpenProse:
```bash
clawdbot plugins enable open-prose
```
Restart the Gateway after enabling the plugin.
Dev/local checkout: `clawdbot plugins install ./extensions/open-prose`
Related docs: [Plugins](/plugin), [Plugin manifest](/plugins/manifest), [Skills](/tools/skills).
## Slash command
OpenProse registers `/prose` as a user-invocable skill command. It routes to the OpenProse VM instructions and uses Clawdbot tools under the hood.
Common commands:
```
/prose help
/prose run <file.prose>
/prose run <handle/slug>
/prose run <https://example.com/file.prose>
/prose compile <file.prose>
/prose examples
/prose update
```
## Example: a simple `.prose` file
```prose
# Research + synthesis with two agents running in parallel.
input topic: "What should we research?"
agent researcher:
model: sonnet
prompt: "You research thoroughly and cite sources."
agent writer:
model: opus
prompt: "You write a concise summary."
parallel:
findings = session: researcher
prompt: "Research {topic}."
draft = session: writer
prompt: "Summarize {topic}."
session "Merge the findings + draft into a final answer."
context: { findings, draft }
```
## File locations
OpenProse keeps state under `.prose/` in your workspace:
```
.prose/
├── .env
├── runs/
│ └── {YYYYMMDD}-{HHMMSS}-{random}/
│ ├── program.prose
│ ├── state.md
│ ├── bindings/
│ └── agents/
└── agents/
```
User-level persistent agents live at:
```
~/.prose/agents/
```
## State modes
OpenProse supports multiple state backends:
- **filesystem** (default): `.prose/runs/...`
- **in-context**: transient, for small programs
- **sqlite** (experimental): requires `sqlite3` binary
- **postgres** (experimental): requires `psql` and a connection string
Notes:
- sqlite/postgres are opt-in and experimental.
- postgres credentials flow into subagent logs; use a dedicated, least-privileged DB.
## Remote programs
`/prose run <handle/slug>` resolves to `https://p.prose.md/<handle>/<slug>`.
Direct URLs are fetched as-is. This uses the `web_fetch` tool (or `exec` for POST).
## Clawdbot runtime mapping
OpenProse programs map to Clawdbot primitives:
| OpenProse concept | Clawdbot tool |
| --- | --- |
| Spawn session / Task tool | `sessions_spawn` |
| File read/write | `read` / `write` |
| Web fetch | `web_fetch` |
If your tool allowlist blocks these tools, OpenProse programs will fail. See [Skills config](/tools/skills-config).
## Security + approvals
Treat `.prose` files like code. Review before running. Use Clawdbot tool allowlists and approval gates to control side effects.
For deterministic, approval-gated workflows, compare with [Lobster](/tools/lobster).

View File

@@ -9,7 +9,7 @@ read_when:
Clawdbot can use many LLM providers. Pick a provider, authenticate, then set the
default model as `provider/model`.
Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/etc.)? See [Channels](/channels).
Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/Mattermost (plugin)/etc.)? See [Channels](/channels).
## Quick start

View File

@@ -13,7 +13,7 @@ Use `pnpm` (Node 22+) from the repo root. Keep the working tree clean before tag
## Operator trigger
When the operator says “release”, immediately do this preflight (no extra questions unless blocked):
- Read this doc and `docs/platforms/mac/release.md`.
- Load env from `~/.profile` and confirm `SPARKLE_PRIVATE_KEY_FILE` + App Store Connect vars are set.
- Load env from `~/.profile` and confirm `SPARKLE_PRIVATE_KEY_FILE` + App Store Connect vars are set (SPARKLE_PRIVATE_KEY_FILE should live in `~/.profile`).
- Use Sparkle keys from `~/Library/CloudStorage/Dropbox/Backup/Sparkle` if needed.
1) **Version & metadata**
@@ -39,8 +39,9 @@ When the operator says “release”, immediately do this preflight (no extra qu
- [ ] `pnpm test` (or `pnpm test:coverage` if you need coverage output)
- [ ] `pnpm run build` (last sanity check after tests)
- [ ] `pnpm release:check` (verifies npm pack contents)
- [ ] `pnpm test:install:smoke` (Docker install smoke test; required before release)
- [ ] `CLAWDBOT_INSTALL_SMOKE_SKIP_NONROOT=1 pnpm test:install:smoke` (Docker install smoke test, fast path; required before release)
- If the immediate previous npm release is known broken, set `CLAWDBOT_INSTALL_SMOKE_PREVIOUS=<last-good-version>` or `CLAWDBOT_INSTALL_SMOKE_SKIP_PREVIOUS=1` for the preinstall step.
- [ ] (Optional) Full installer smoke (adds non-root + CLI coverage): `pnpm test:install:smoke`
- [ ] (Optional) Installer E2E (Docker, runs `curl -fsSL https://clawd.bot/install.sh | bash`, onboards, then runs real tool calls):
- `pnpm test:install:e2e:openai` (requires `OPENAI_API_KEY`)
- `pnpm test:install:e2e:anthropic` (requires `ANTHROPIC_API_KEY`)

View File

@@ -12,6 +12,7 @@ This document explains how Clawdbot manages sessions end-to-end:
- **Session routing** (how inbound messages map to a `sessionKey`)
- **Session store** (`sessions.json`) and what it tracks
- **Transcript persistence** (`*.jsonl`) and its structure
- **Transcript hygiene** (provider-specific fixups before runs)
- **Context limits** (context window vs tracked tokens)
- **Compaction** (manual + auto-compaction) and where to hook pre-compaction work
- **Silent housekeeping** (e.g. memory writes that shouldnt produce user-visible output)
@@ -20,6 +21,7 @@ If you want a higher-level overview first, start with:
- [/concepts/session](/concepts/session)
- [/concepts/compaction](/concepts/compaction)
- [/concepts/session-pruning](/concepts/session-pruning)
- [/reference/transcript-hygiene](/reference/transcript-hygiene)
---

View File

@@ -112,6 +112,23 @@ Default heartbeat prompt:
You are free to edit `HEARTBEAT.md` with a short checklist or reminders. Keep it small to limit token burn.
### Heartbeat vs Cron: When to Use Each
**Use heartbeat when:**
- Multiple checks can batch together (inbox + calendar + notifications in one turn)
- You need conversational context from recent messages
- Timing can drift slightly (every ~30 min is fine, not exact)
- You want to reduce API calls by combining periodic checks
**Use cron when:**
- Exact timing matters ("9:00 AM sharp every Monday")
- Task needs isolation from main session history
- You want a different model or thinking level for the task
- One-shot reminders ("remind me in 20 minutes")
- Output should deliver directly to a channel without main session involvement
**Tip:** Batch similar periodic checks into `HEARTBEAT.md` instead of creating multiple cron jobs. Use cron for precise schedules and standalone tasks.
**Things to check (rotate through these, 2-4 times per day):**
- **Emails** - Any urgent unread messages?
- **Calendar** - Upcoming events in next 24-48h?

View File

@@ -0,0 +1,94 @@
---
summary: "Reference: provider-specific transcript sanitization and repair rules"
read_when:
- You are debugging provider request rejections tied to transcript shape
- You are changing transcript sanitization or tool-call repair logic
- You are investigating tool-call id mismatches across providers
---
# Transcript Hygiene (Provider Fixups)
This document describes **provider-specific fixes** applied to transcripts before a run
(building model context). These are **in-memory** adjustments used to satisfy strict
provider requirements. They do **not** rewrite the stored JSONL transcript on disk.
Scope includes:
- Tool call id sanitization
- Tool result pairing repair
- Turn validation / ordering
- Thought signature cleanup
- Image payload sanitization
If you need transcript storage details, see:
- [/reference/session-management-compaction](/reference/session-management-compaction)
---
## Where this runs
All transcript hygiene is centralized in the embedded runner:
- Policy selection: `src/agents/transcript-policy.ts`
- Sanitization/repair application: `sanitizeSessionHistory` in `src/agents/pi-embedded-runner/google.ts`
The policy uses `provider`, `modelApi`, and `modelId` to decide what to apply.
---
## Global rule: image sanitization
Image payloads are always sanitized to prevent provider-side rejection due to size
limits (downscale/recompress oversized base64 images).
Implementation:
- `sanitizeSessionMessagesImages` in `src/agents/pi-embedded-helpers/images.ts`
- `sanitizeContentBlocksImages` in `src/agents/tool-images.ts`
---
## Provider matrix (current behavior)
**OpenAI / OpenAI Codex**
- Image sanitization only.
- No tool call id sanitization.
- No tool result pairing repair.
- No turn validation or reordering.
- No synthetic tool results.
- No thought signature stripping.
**Google (Generative AI / Gemini CLI / Antigravity)**
- Tool call id sanitization: strict alphanumeric.
- Tool result pairing repair and synthetic tool results.
- Turn validation (Gemini-style turn alternation).
- Google turn ordering fixup (prepend a tiny user bootstrap if history starts with assistant).
- Antigravity Claude: normalize thinking signatures; drop unsigned thinking blocks.
**Anthropic / Minimax (Anthropic-compatible)**
- Tool result pairing repair and synthetic tool results.
- Turn validation (merge consecutive user turns to satisfy strict alternation).
**Mistral (including model-id based detection)**
- Tool call id sanitization: strict9 (alphanumeric length 9).
**OpenRouter Gemini**
- Thought signature cleanup: strip non-base64 `thought_signature` values (keep base64).
**Everything else**
- Image sanitization only.
---
## Historical behavior (pre-2026.1.22)
Before the 2026.1.22 release, Clawdbot applied multiple layers of transcript hygiene:
- A **transcript-sanitize extension** ran on every context build and could:
- Repair tool use/result pairing.
- Sanitize tool call ids (including a non-strict mode that preserved `_`/`-`).
- The runner also performed provider-specific sanitization, which duplicated work.
- Additional mutations occurred outside the provider policy, including:
- Stripping `<final>` tags from assistant text before persistence.
- Dropping empty assistant error turns.
- Trimming assistant content after tool calls.
This complexity caused cross-provider regressions (notably `openai-responses`
`call_id|fc_id` pairing). The 2026.1.22 cleanup removed the extension, centralized
logic in the runner, and made OpenAI **no-touch** beyond image sanitization.

View File

@@ -6,14 +6,14 @@ read_when:
---
# Building a personal assistant with Clawdbot (Clawd-style)
Clawdbot is a WhatsApp + Telegram + Discord gateway for **Pi** agents. This guide is the personal assistant setup: one dedicated WhatsApp number that behaves like your always-on agent.
Clawdbot is a WhatsApp + Telegram + Discord + iMessage gateway for **Pi** agents. Plugins add Mattermost. This guide is the "personal assistant" setup: one dedicated WhatsApp number that behaves like your always-on agent.
## ⚠️ Safety first
Youre putting an agent in a position to:
- run commands on your machine (depending on your Pi tool setup)
- read/write files in your workspace
- send messages back out via WhatsApp/Telegram/Discord
- send messages back out via WhatsApp/Telegram/Discord/Mattermost (plugin)
Start conservative:
- Always set `channels.whatsapp.allowFrom` (never run open-to-the-world on your personal Mac).

View File

@@ -178,7 +178,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
### What is Clawdbot, in one paragraph?
Clawdbot is a personal AI assistant you run on your own devices. It replies on the messaging surfaces you already use (WhatsApp, Telegram, Slack, Discord, Signal, iMessage, WebChat) and can also do voice + a live Canvas on supported platforms. The **Gateway** is the alwayson control plane; the assistant is the product.
Clawdbot is a personal AI assistant you run on your own devices. It replies on the messaging surfaces you already use (WhatsApp, Telegram, Slack, Mattermost (plugin), Discord, Signal, iMessage, WebChat) and can also do voice + a live Canvas on supported platforms. The **Gateway** is the always-on control plane; the assistant is the product.
## Quick start and first-run setup
@@ -235,7 +235,7 @@ Node **>= 22** is required. `pnpm` is recommended. Bun is **not recommended** fo
- **Model/auth setup** (Anthropic **setup-token** recommended for Claude subscriptions, OpenAI Codex OAuth supported, API keys optional, LM Studio local models supported)
- **Workspace** location + bootstrap files
- **Gateway settings** (bind/port/auth/tailscale)
- **Providers** (WhatsApp, Telegram, Discord, Signal, iMessage)
- **Providers** (WhatsApp, Telegram, Discord, Mattermost (plugin), Signal, iMessage)
- **Daemon install** (LaunchAgent on macOS; systemd user unit on Linux/WSL2)
- **Health checks** and **skills** selection
@@ -324,6 +324,7 @@ brew install <formula>
```
If you run Clawdbot via systemd, ensure the service PATH includes `/home/linuxbrew/.linuxbrew/bin` (or your brew prefix) so `brew`-installed tools resolve in nonlogin shells.
Recent builds also prepend common user bin dirs on Linux systemd services (for example `~/.local/bin`, `~/.npm-global/bin`, `~/.local/share/pnpm`, `~/.bun/bin`) and honor `PNPM_HOME`, `NPM_CONFIG_PREFIX`, `BUN_INSTALL`, `VOLTA_HOME`, `ASDF_DATA_DIR`, `NVM_DIR`, and `FNM_DIR` when set.
### Can I switch between npm and git installs later?
@@ -363,7 +364,7 @@ lowest friction and youre okay with sleep/restarts, run it locally.
- **Pros:** alwayson, stable network, no laptop sleep issues, easier to keep running.
- **Cons:** often run headless (use screenshots), remote file access only, you must SSH for updates.
**Clawdbotspecific note:** WhatsApp/Telegram/Slack/Discord all work fine from a VPS. The only real tradeoff is **headless browser** vs a visible window. See [Browser](/tools/browser).
**Clawdbot-specific note:** WhatsApp/Telegram/Slack/Mattermost (plugin)/Discord all work fine from a VPS. The only real trade-off is **headless browser** vs a visible window. See [Browser](/tools/browser).
**Recommended default:** VPS if you had gateway disconnects before. Local is great when youre actively using the Mac and want local file access or UI automation with a visible browser.
@@ -719,43 +720,42 @@ See the full config examples in [Browser](/tools/browser#use-brave-or-another-ch
### How do commands propagate between Telegram, the gateway, and nodes?
Telegram messages are handled by the **gateway**. The gateway runs the agent and
only then calls nodes over the **Bridge** when a node tool is needed:
only then calls nodes over the **Gateway WebSocket** when a node tool is needed:
Telegram → Gateway → Agent → `node.*` → Node → Gateway → Telegram
Nodes dont see inbound provider traffic; they only receive bridge RPC calls.
Nodes dont see inbound provider traffic; they only receive node RPC calls.
### How can my agent access my computer if the Gateway is hosted remotely?
Short answer: **pair your computer as a node**. The Gateway runs elsewhere, but it can
call `node.*` tools (screen, camera, system) on your local machine over the Bridge.
call `node.*` tools (screen, camera, system) on your local machine over the Gateway WebSocket.
Typical setup:
1) Run the Gateway on the alwayson host (VPS/home server).
2) Put the Gateway host + your computer on the same tailnet.
3) Enable the bridge on the Gateway host:
```json5
{ bridge: { enabled: true, bind: "auto" } }
```
4) Open the macOS app locally and connect in **Remote over SSH** mode so it can tunnel
the bridge port and register as a node.
3) Ensure the Gateway WS is reachable (tailnet bind or SSH tunnel).
4) Open the macOS app locally and connect in **Remote over SSH** mode (or direct tailnet)
so it can register as a node.
5) Approve the node on the Gateway:
```bash
clawdbot nodes pending
clawdbot nodes approve <requestId>
```
No separate TCP bridge is required; nodes connect over the Gateway WebSocket.
Security reminder: pairing a macOS node allows `system.run` on that machine. Only
pair devices you trust, and review [Security](/gateway/security).
Docs: [Nodes](/nodes), [Bridge protocol](/gateway/bridge-protocol), [macOS remote mode](/platforms/mac/remote), [Security](/gateway/security).
Docs: [Nodes](/nodes), [Gateway protocol](/gateway/protocol), [macOS remote mode](/platforms/mac/remote), [Security](/gateway/security).
### Do nodes run a gateway service?
No. Only **one gateway** should run per host unless you intentionally run isolated profiles (see [Multiple gateways](/gateway/multiple-gateways)). Nodes are peripherals that connect
to the gateway (iOS/Android nodes, or macOS “node mode” in the menubar app).
A full restart is required for `gateway`, `bridge`, `discovery`, and `canvasHost` changes.
A full restart is required for `gateway`, `discovery`, and `canvasHost` changes.
### Is there an API / RPC way to apply config?
@@ -797,26 +797,19 @@ This keeps the gateway bound to loopback and exposes HTTPS via Tailscale. See [T
### How do I connect a Mac node to a remote Gateway (Tailscale Serve)?
Serve only exposes the **Gateway Control UI**. Nodes use the **bridge port**.
Serve exposes the **Gateway Control UI + WS**. Nodes connect over the same Gateway WS endpoint.
Recommended setup:
1) **Enable the bridge on the gateway host**:
```json5
{
bridge: { enabled: true, bind: "auto" }
}
```
`auto` prefers a tailnet IP when Tailscale is present.
2) **Make sure the VPS + Mac are on the same tailnet**.
3) **Use the macOS app in Remote mode** (SSH target can be the tailnet hostname).
The app will tunnel the bridge port and connect as a node.
4) **Approve the node** on the gateway:
1) **Make sure the VPS + Mac are on the same tailnet**.
2) **Use the macOS app in Remote mode** (SSH target can be the tailnet hostname).
The app will tunnel the Gateway port and connect as a node.
3) **Approve the node** on the gateway:
```bash
clawdbot nodes pending
clawdbot nodes approve <requestId>
```
Docs: [Bridge protocol](/gateway/bridge-protocol), [Discovery](/gateway/discovery), [macOS remote mode](/platforms/mac/remote).
Docs: [Gateway protocol](/gateway/protocol), [Discovery](/gateway/discovery), [macOS remote mode](/platforms/mac/remote).
## Env vars and .env loading

View File

@@ -12,7 +12,7 @@ Goal: go from **zero** → **first working chat** (with sane defaults) as quickl
Recommended path: use the **CLI onboarding wizard** (`clawdbot onboard`). It sets up:
- model/auth (OAuth recommended)
- gateway settings
- channels (WhatsApp/Telegram/Discord/)
- channels (WhatsApp/Telegram/Discord/Mattermost (plugin)/...)
- pairing defaults (secure DMs)
- workspace bootstrap + skills
- optional background service
@@ -80,7 +80,7 @@ clawdbot onboard --install-daemon
What youll choose:
- **Local vs Remote** gateway
- **Auth**: OpenAI Code (Codex) subscription (OAuth) or API keys. For Anthropic we recommend an API key; `claude setup-token` is also supported.
- **Providers**: WhatsApp QR login, Telegram/Discord bot tokens, etc.
- **Providers**: WhatsApp QR login, Telegram/Discord bot tokens, Mattermost plugin tokens, etc.
- **Daemon**: background install (launchd/systemd; WSL2 uses systemd)
- **Runtime**: Node (recommended; required for WhatsApp/Telegram). Bun is **not recommended**.
- **Gateway token**: the wizard generates one by default (even on loopback) and stores it in `gateway.auth.token`.
@@ -140,6 +140,7 @@ WhatsApp doc: [WhatsApp](/channels/whatsapp)
The wizard can write tokens/config for you. If you prefer manual config, start with:
- Telegram: [Telegram](/channels/telegram)
- Discord: [Discord](/channels/discord)
- Mattermost (plugin): [Mattermost](/channels/mattermost)
**Telegram DM tip:** your first DM returns a pairing code. Approve it (see next step) or the bot wont respond.

View File

@@ -67,6 +67,7 @@ Use these hubs to discover every page, including deep dives and reference docs t
- [Telegram (grammY notes)](/channels/grammy)
- [Slack](/channels/slack)
- [Discord](/channels/discord)
- [Mattermost](/channels/mattermost) (plugin)
- [Signal](/channels/signal)
- [iMessage](/channels/imessage)
- [Location parsing](/channels/location)
@@ -96,10 +97,12 @@ Use these hubs to discover every page, including deep dives and reference docs t
## Tools + automation
- [Tools surface](/tools)
- [OpenProse](/prose)
- [CLI reference](/cli)
- [Exec tool](/tools/exec)
- [Elevated mode](/tools/elevated)
- [Cron jobs](/automation/cron-jobs)
- [Cron vs Heartbeat](/automation/cron-vs-heartbeat)
- [Thinking + verbose](/tools/thinking)
- [Models](/concepts/models)
- [Sub-agents](/tools/subagents)

View File

@@ -45,27 +45,29 @@ Stored under `~/.clawdbot/credentials/`:
Treat these as sensitive (they gate access to your assistant).
## 2) Node pairing (iOS/Android nodes joining the gateway)
## 2) Node device pairing (iOS/Android/macOS/headless nodes)
Nodes (iOS/Android, future hardware, etc.) connect to the Gateway and request to join.
The Gateway keeps an authoritative allowlist; new nodes require explicit approve/reject.
Nodes connect to the Gateway as **devices** with `role: node`. The Gateway
creates a device pairing request that must be approved.
### Approve a node
### Approve a node device
```bash
clawdbot nodes pending
clawdbot nodes approve <requestId>
clawdbot devices list
clawdbot devices approve <requestId>
clawdbot devices reject <requestId>
```
### Where the state lives
Stored under `~/.clawdbot/nodes/`:
Stored under `~/.clawdbot/devices/`:
- `pending.json` (short-lived; pending requests expire)
- `paired.json` (paired nodes + tokens)
- `paired.json` (paired devices + tokens)
### Details
### Notes
Full protocol + design notes: [Gateway pairing](/gateway/pairing)
- The legacy `node.pair.*` API (CLI: `clawdbot nodes pending/approve`) is a
separate gateway-owned pairing store. WS nodes still require device pairing.
## Related docs

View File

@@ -327,14 +327,8 @@ Full setup walkthrough (28m) by VelvetShark.
<Card title="OpenRouter Transcription" icon="microphone" href="https://clawdhub.com/obviyus/openrouter-transcribe">
**@obviyus** • `transcription` `multilingual` `skill`
Multi-lingual audio transcription via OpenRouter (Gemini, etc). Available on ClawdHub.
</Card>
<Card title="Google Docs Editor" icon="file-word">
**Community**`docs` `editing` `skill`
Rich-text Google Docs editing skill. Built rapidly with Claude Code.
Multi-lingual audio transcription via OpenRouter (Gemini, etc). Available on ClawdHub.
</Card>
</CardGroup>

View File

@@ -48,7 +48,7 @@ The wizard starts with **QuickStart** (defaults) vs **Advanced** (full control).
- Model/auth (OpenAI Code (Codex) subscription OAuth, Anthropic API key (recommended) or setup-token (paste), plus MiniMax/GLM/Moonshot/AI Gateway options)
- Workspace location + bootstrap files
- Gateway settings (port/bind/auth/tailscale)
- Providers (Telegram, WhatsApp, Discord, Signal)
- Providers (Telegram, WhatsApp, Discord, Mattermost (plugin), Signal)
- Daemon install (LaunchAgent / systemd user unit)
- Health check
- Skills (recommended)
@@ -117,6 +117,7 @@ Tip: `--json` does **not** imply non-interactive mode. Use `--non-interactive` (
- WhatsApp: optional QR login.
- Telegram: bot token.
- Discord: bot token.
- Mattermost (plugin): bot token + base URL.
- Signal: optional `signal-cli` install + account config.
- iMessage: local `imsg` CLI path + DB access.
- DM security: default is pairing. First DM sends a code; approve via `clawdbot pairing approve <channel> <code>` or use allowlists.

View File

@@ -22,7 +22,7 @@ Exec approvals are enforced locally on the execution host:
- **gateway host** → `clawdbot` process on the gateway machine
- **node host** → node runner (macOS companion app or headless node host)
Planned macOS split:
macOS split:
- **node host service** forwards `system.run` to the **macOS app** over local IPC.
- **macOS app** enforces approvals + executes the command in UI context.
@@ -54,6 +54,7 @@ Example schema:
"autoAllowSkills": true,
"allowlist": [
{
"id": "B0C8C0B3-2C2D-4F8A-9A3C-5A4B3C2D1E0F",
"pattern": "~/Projects/**/bin/rg",
"lastUsedAt": 1737150000000,
"lastUsedCommand": "rg -n TODO",
@@ -96,6 +97,7 @@ Examples:
- `/opt/homebrew/bin/rg`
Each allowlist entry tracks:
- **id** stable UUID used for UI identity (optional)
- **last used** timestamp
- **last used command**
- **last resolved path**
@@ -103,8 +105,8 @@ Each allowlist entry tracks:
## Auto-allow skill CLIs
When **Auto-allow skill CLIs** is enabled, executables referenced by known skills
are treated as allowlisted on nodes (macOS node or headless node host). This uses the Bridge RPC to ask the
gateway for the skill bin list. Disable this if you want strict manual allowlists.
are treated as allowlisted on nodes (macOS node or headless node host). This uses
`skills.bins` over the Gateway RPC to fetch the skill bin list. Disable this if you want strict manual allowlists.
## Safe bins (stdin-only)
@@ -113,6 +115,9 @@ that can run in allowlist mode **without** explicit allowlist entries. Safe bins
positional file args and path-like tokens, so they can only operate on the incoming stream.
Shell chaining and redirections are not auto-allowed in allowlist mode.
Shell chaining (`&&`, `||`, `;`) is allowed when every top-level segment satisfies the allowlist
(including safe bins or skill auto-allow). Redirections remain unsupported in allowlist mode.
Default safe bins: `jq`, `grep`, `cut`, `sort`, `uniq`, `head`, `tail`, `tr`, `wc`.
## Control UI editing
@@ -151,12 +156,12 @@ Actions:
- **Always allow** → add to allowlist + run
- **Deny** → block
### macOS IPC flow (planned)
### macOS IPC flow
```
Gateway -> Bridge -> Node Service (TS)
| IPC (UDS + token + HMAC + TTL)
v
Mac App (UI + approvals + system.run)
Gateway -> Node Service (WS)
| IPC (UDS + token + HMAC + TTL)
v
Mac App (UI + approvals + system.run)
```
Security notes:

View File

@@ -66,8 +66,8 @@ Example:
- `host=sandbox`: runs `sh -lc` (login shell) inside the container, so `/etc/profile` may reset `PATH`.
Clawdbot prepends `env.PATH` after profile sourcing; `tools.exec.pathPrepend` applies here too.
- `host=node`: only env overrides you pass are sent to the node. `tools.exec.pathPrepend` only applies
if the exec call already sets `env.PATH`. Node PATH overrides are accepted only when they prepend
the node host PATH (no replacement).
if the exec call already sets `env.PATH`. Headless node hosts accept `PATH` only when it prepends
the node host PATH (no replacement). macOS nodes drop `PATH` overrides entirely.
Per-agent node binding (use the agent list index in config):

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