Compare commits
162
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad6f69f9af | ||
|
|
a4a2c3bc82 | ||
|
|
f69298d7eb | ||
|
|
6280305899 | ||
|
|
543d1ea3c1 | ||
|
|
1569db1754 | ||
|
|
c54c665f97 | ||
|
|
a84000c6d9 | ||
|
|
a979a62f8e | ||
|
|
13e2dd97a7 | ||
|
|
6bba84b043 | ||
|
|
e31251293b | ||
|
|
56ed5cc2d9 | ||
|
|
af31e0d969 | ||
|
|
37fa4f7eef | ||
|
|
9aad6dfe1b | ||
|
|
6b8db36a15 | ||
|
|
171060541a | ||
|
|
003547c818 | ||
|
|
c4e1064066 | ||
|
|
d163dbcfcd | ||
|
|
2acfeb1096 | ||
|
|
b3b6d421cc | ||
|
|
cf72b9db3c | ||
|
|
106e308953 | ||
|
|
bf72a126d1 | ||
|
|
28a5d124c3 | ||
|
|
b4045e6adb | ||
|
|
a7bec3340f | ||
|
|
a4e99ecdaf | ||
|
|
dcd20d564f | ||
|
|
59f6ea9b21 | ||
|
|
e44f28bd4f | ||
|
|
929b86e302 | ||
|
|
76d3d58b5c | ||
|
|
548a32c8d4 | ||
|
|
500c75b4f0 | ||
|
|
3567dc4a47 | ||
|
|
7df37c2dbd | ||
|
|
28a4cbc4ef | ||
|
|
21fe4d9ded | ||
|
|
05d149a49b | ||
|
|
97a41a6509 | ||
|
|
a0be85c34c | ||
|
|
390bd11f33 | ||
|
|
16768a9998 | ||
|
|
e9d6869290 | ||
|
|
9072e35f08 | ||
|
|
d887027e95 | ||
|
|
168a0f4998 | ||
|
|
f3a37664d5 | ||
|
|
8bcbe68637 | ||
|
|
beb9eac5f7 | ||
|
|
0dcffcd5b0 | ||
|
|
56efbce31e | ||
|
|
e7c42884fc | ||
|
|
08c0405f0f | ||
|
|
470add877c | ||
|
|
aaa310c047 | ||
|
|
0cd24137e8 | ||
|
|
8ffb8cc363 | ||
|
|
7a9854cb06 | ||
|
|
5ee4456c6e | ||
|
|
de31583021 | ||
|
|
38b49aa0f6 | ||
|
|
69761e8a51 | ||
|
|
3431d3d115 | ||
|
|
fe9e027d58 | ||
|
|
33d17957e5 | ||
|
|
25ae5f897e | ||
|
|
624ff09314 | ||
|
|
c8003ae472 | ||
|
|
6bf627bce8 | ||
|
|
08525435e0 | ||
|
|
9e39a56033 | ||
|
|
026cf1130e | ||
|
|
bb14b19922 | ||
|
|
78279fb758 | ||
|
|
38cf90f6db | ||
|
|
544ca062a3 | ||
|
|
be9aa5494a | ||
|
|
0d6af15d1c | ||
|
|
eda9410bce | ||
|
|
a51ed8a5dd | ||
|
|
19bcbf85df | ||
|
|
f49d0e5476 | ||
|
|
9c4c9c5edd | ||
|
|
0f34255359 | ||
|
|
de5fb65cb8 | ||
|
|
e773f84e39 | ||
|
|
b969c216fc | ||
|
|
f51a4d2aca | ||
|
|
30b3a9de30 | ||
|
|
0391f6553b | ||
|
|
d0c986c4f0 | ||
|
|
384028e12e | ||
|
|
4c14d6c8db | ||
|
|
6fa437613b | ||
|
|
949fa1051f | ||
|
|
4965727f39 | ||
|
|
7c34883267 | ||
|
|
072c3dc55c | ||
|
|
83b3875131 | ||
|
|
a35083808c | ||
|
|
3d3ec9d972 | ||
|
|
9838a2850f | ||
|
|
6c6bc6ff1c | ||
|
|
1791c1a765 | ||
|
|
6e53c061ff | ||
|
|
1773f8aea2 | ||
|
|
52bdf57743 | ||
|
|
b3ab24eb8e | ||
|
|
6ac1c1d6ea | ||
|
|
7655a501d0 | ||
|
|
3b1b14b0b1 | ||
|
|
bf15c87d2b | ||
|
|
2dae4d382f | ||
|
|
e9a47a02d1 | ||
|
|
929666a8c8 | ||
|
|
cd409e5667 | ||
|
|
8e80823b03 | ||
|
|
319afd192d | ||
|
|
6e0daf0936 | ||
|
|
d0cb4e092f | ||
|
|
f5a881c99d | ||
|
|
66377fc030 | ||
|
|
d8d295b0b3 | ||
|
|
ef36e24522 | ||
|
|
d43d4fcced | ||
|
|
d42b69df74 | ||
|
|
1ec1f6dcbf | ||
|
|
e96b939732 | ||
|
|
e479c870fd | ||
|
|
f2db894685 | ||
|
|
4f03283126 | ||
|
|
ed7dec0975 | ||
|
|
fbb3da506f | ||
|
|
dfa6c5c2b3 | ||
|
|
028eed5fe8 | ||
|
|
2b16a87f04 | ||
|
|
731049936d | ||
|
|
5a5b058ba0 | ||
|
|
72f28be648 | ||
|
|
ff645524d8 | ||
|
|
c534390bc0 | ||
|
|
c5003e5441 | ||
|
|
0b3ebb0c63 | ||
|
|
23981496f9 | ||
|
|
f2e425dc2b | ||
|
|
e48d68bbc7 | ||
|
|
842fc8d08b | ||
|
|
54ec14262b | ||
|
|
d0c70178e0 | ||
|
|
3bc9c330eb | ||
|
|
b7fcc8584f | ||
|
|
fa9aafce83 | ||
|
|
0d5dec4c66 | ||
|
|
b2d5889f6e | ||
|
|
2ee71e4154 | ||
|
|
509215e935 | ||
|
|
f726656d1e | ||
|
|
30d3e1da21 |
No files matched your search
@@ -55,3 +55,5 @@ apps/ios/*.mobileprovision
|
|||||||
# Local untracked files
|
# Local untracked files
|
||||||
.local/
|
.local/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
IDENTITY.md
|
||||||
|
USER.md
|
||||||
@@ -107,6 +107,7 @@
|
|||||||
- Bug investigations: read source code of relevant npm dependencies and all related local code before concluding; aim for high-confidence root cause.
|
- Bug investigations: read source code of relevant npm dependencies and all related local code before concluding; aim for high-confidence root cause.
|
||||||
- Code style: add brief comments for tricky logic; keep files under ~500 LOC when feasible (split/refactor as needed).
|
- Code style: add brief comments for tricky logic; keep files under ~500 LOC when feasible (split/refactor as needed).
|
||||||
- Tool schema guardrails (google-antigravity): avoid `Type.Union` in tool input schemas; no `anyOf`/`oneOf`/`allOf`. Use `stringEnum`/`optionalStringEnum` (Type.Unsafe enum) for string lists, and `Type.Optional(...)` instead of `... | null`. Keep top-level tool schema as `type: "object"` with `properties`.
|
- Tool schema guardrails (google-antigravity): avoid `Type.Union` in tool input schemas; no `anyOf`/`oneOf`/`allOf`. Use `stringEnum`/`optionalStringEnum` (Type.Unsafe enum) for string lists, and `Type.Optional(...)` instead of `... | null`. Keep top-level tool schema as `type: "object"` with `properties`.
|
||||||
|
- Tool schema guardrails: avoid raw `format` property names in tool schemas; some validators treat `format` as a reserved keyword and reject the schema.
|
||||||
- When asked to open a “session” file, open the Pi session logs under `~/.clawdbot/agents/main/sessions/*.jsonl` (newest unless a specific ID is given), not the default `sessions.json`. If logs are needed from another machine, SSH via Tailscale and read the same path there.
|
- When asked to open a “session” file, open the Pi session logs under `~/.clawdbot/agents/main/sessions/*.jsonl` (newest unless a specific ID is given), not the default `sessions.json`. If logs are needed from another machine, SSH via Tailscale and read the same path there.
|
||||||
- Menubar dimming + restart flow mirrors Trimmy: use `scripts/restart-mac.sh` (kills all Clawdbot variants, runs `swift build`, packages, relaunches). Icon dimming depends on MenuBarExtraAccess wiring in AppMain; keep `appearsDisabled` updates intact when touching the status item.
|
- Menubar dimming + restart flow mirrors Trimmy: use `scripts/restart-mac.sh` (kills all Clawdbot variants, runs `swift build`, packages, relaunches). Icon dimming depends on MenuBarExtraAccess wiring in AppMain; keep `appearsDisabled` updates intact when touching the status item.
|
||||||
- Do not rebuild the macOS app over SSH; rebuilds must be run directly on the Mac.
|
- Do not rebuild the macOS app over SSH; rebuilds must be run directly on the Mac.
|
||||||
@@ -116,6 +117,14 @@
|
|||||||
- launchd PATH is minimal; ensure the app’s launch agent PATH includes standard system paths plus your pnpm bin (typically `$HOME/Library/pnpm`) so `pnpm`/`clawdbot` binaries resolve when invoked via `clawdbot-mac`.
|
- launchd PATH is minimal; ensure the app’s launch agent PATH includes standard system paths plus your pnpm bin (typically `$HOME/Library/pnpm`) so `pnpm`/`clawdbot` binaries resolve when invoked via `clawdbot-mac`.
|
||||||
- For manual `clawdbot message send` messages that include `!`, use the heredoc pattern noted below to avoid the Bash tool’s escaping.
|
- For manual `clawdbot message send` messages that include `!`, use the heredoc pattern noted below to avoid the Bash tool’s escaping.
|
||||||
|
|
||||||
|
## NPM + 1Password (publish/verify)
|
||||||
|
- Use the 1password skill; all `op` commands must run inside a fresh tmux session.
|
||||||
|
- Sign in: `eval "$(op signin --account my.1password.com)"` (app unlocked + integration on).
|
||||||
|
- OTP: `op read 'op://Private/Npmjs/one-time password?attribute=otp'`.
|
||||||
|
- Publish: `npm publish --access public --otp="<otp>"` (run from the package dir).
|
||||||
|
- Verify without local npmrc side effects: `npm view <pkg> version --userconfig "$(mktemp)"`.
|
||||||
|
- Kill the tmux session after publish.
|
||||||
|
|
||||||
## Exclamation Mark Escaping Workaround
|
## Exclamation Mark Escaping Workaround
|
||||||
The Claude Code Bash tool escapes `!` to `\\!` in command arguments. When using `clawdbot message send` with messages containing exclamation marks, use heredoc syntax:
|
The Claude Code Bash tool escapes `!` to `\\!` in command arguments. When using `clawdbot message send` with messages containing exclamation marks, use heredoc syntax:
|
||||||
|
|
||||||
|
|||||||
+87
-13
@@ -1,44 +1,117 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 2026.1.15 (unreleased)
|
## 2026.1.16 (unreleased)
|
||||||
|
|
||||||
|
### Highlights
|
||||||
|
- Web search: add `country`/`language` parameters (schema + Brave API) and docs. (#1046) — thanks @YuriNachos.
|
||||||
|
- Plugins: add Zalo Personal plugin (`@clawdbot/zalouser`) and unify channel directory for plugins. (#1032) — thanks @suminhthanh.
|
||||||
|
- Models: add Vercel AI Gateway auth choice + onboarding updates. (#1016) — thanks @timolins.
|
||||||
|
- Sessions: add `session.identityLinks` for cross-platform DM session linking. (#1033) — thanks @thewilloftheshadow.
|
||||||
|
|
||||||
|
### Breaking
|
||||||
|
- **BREAKING:** Discord/Telegram channel tokens now prefer config over env (env is fallback only).
|
||||||
|
- **BREAKING:** Matrix channel credentials now prefer config over env (env is fallback only).
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
- Tools: improve `web_fetch` extraction using Readability (with fallback).
|
||||||
|
- Tools: add Firecrawl fallback for `web_fetch` when configured.
|
||||||
|
- Tools: send Chrome-like headers by default for `web_fetch` to improve extraction on bot-sensitive sites.
|
||||||
|
- Tools: Firecrawl fallback now uses bot-circumvention + cache by default; remove basic HTML fallback when extraction fails.
|
||||||
|
- Directory: unify `clawdbot directory` across channels and plugin channels.
|
||||||
|
- UI: allow deleting sessions from the Control UI.
|
||||||
|
- Skills: add user-invocable skill commands and expanded skill command registration.
|
||||||
|
- Telegram: default reaction level to minimal and enable reaction notifications by default.
|
||||||
|
- Telegram: allow reply-chain messages to bypass mention gating in groups. (#1038) — thanks @adityashaw2.
|
||||||
|
- Cron: isolated cron jobs now start a fresh session id on every run to prevent context buildup.
|
||||||
|
- Docs: add `/help` hub, Node/npm PATH guide, and expand directory CLI docs.
|
||||||
|
- Config: support env var substitution in config values. (#1044) — thanks @sebslight.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- Messages: `/stop` now hard-aborts queued followups and sub-agent runs; suppress zero-count stop notes.
|
||||||
|
- Sessions: reset `compactionCount` on `/new` and `/reset`, and preserve `sessions.json` file mode (0600).
|
||||||
|
- Sessions: repair orphaned user turns before embedded prompts.
|
||||||
|
- Channels: treat replies to the bot as implicit mentions across supported channels.
|
||||||
|
- WhatsApp: scope self-chat response prefix; inject pending-only group history and clear after any processed message.
|
||||||
|
- Agents: drop unsigned Gemini tool calls and avoid JSON Schema `format` keyword collisions.
|
||||||
|
- Auth: inherit/merge sub-agent auth profiles from the main agent.
|
||||||
|
- Gateway: resolve local auth for security probe and validate gateway token/password file modes. (#1011, #1022) — thanks @ivanrvpereira, @kkarimi.
|
||||||
|
- Signal/iMessage: bound transport readiness waits to 30s with periodic logging. (#1014) — thanks @Szpadel.
|
||||||
|
- OpenAI image-gen: remove deprecated `response_format` and use URL downloads.
|
||||||
|
- CLI: auto-update global installs when installed via a package manager.
|
||||||
|
- Discord: truncate skill command descriptions to 100 chars for slash command limits. (#1018) — thanks @evalexpr.
|
||||||
|
- Security: bump `tar` to 7.5.3.
|
||||||
|
- Models: align ZAI thinking toggles.
|
||||||
|
|
||||||
|
## 2026.1.15
|
||||||
|
|
||||||
|
### Highlights
|
||||||
|
- Plugins: add provider auth registry + `clawdbot models auth login` for plugin-driven OAuth/API key flows.
|
||||||
|
- Browser: improve remote CDP/Browserless support (auth passthrough, `wss` upgrade, timeouts, clearer errors).
|
||||||
|
- Heartbeat: per-agent configuration + 24h duplicate suppression. (#980) — thanks @voidserf.
|
||||||
|
- Security: audit warns on weak model tiers; app nodes store auth tokens encrypted (Keychain/SecurePrefs).
|
||||||
|
|
||||||
|
### Breaking
|
||||||
|
- **BREAKING:** iOS minimum version is now 18.0 to support Textual markdown rendering in native chat. (#702)
|
||||||
- **BREAKING:** Microsoft Teams is now a plugin; install `@clawdbot/msteams` via `clawdbot plugins install @clawdbot/msteams`.
|
- **BREAKING:** Microsoft Teams is now a plugin; install `@clawdbot/msteams` via `clawdbot plugins install @clawdbot/msteams`.
|
||||||
|
|
||||||
|
### Changes
|
||||||
- CLI: set process titles to `clawdbot-<command>` for clearer process listings.
|
- CLI: set process titles to `clawdbot-<command>` for clearer process listings.
|
||||||
|
- CLI/macOS: sync remote SSH target/identity to config and let `gateway status` auto-infer SSH targets (ssh-config aware).
|
||||||
|
- Telegram: scope inline buttons with allowlist default + callback gating in DMs/groups.
|
||||||
|
- Telegram: default reaction notifications to own.
|
||||||
|
- Tools: improve `web_fetch` extraction using Readability (with fallback).
|
||||||
- Heartbeat: tighten prompt guidance + suppress duplicate alerts for 24h. (#980) — thanks @voidserf.
|
- Heartbeat: tighten prompt guidance + suppress duplicate alerts for 24h. (#980) — thanks @voidserf.
|
||||||
|
- Repo: ignore local identity files to avoid accidental commits. (#1001) — thanks @gerardward2007.
|
||||||
- Sessions/Security: add `session.dmScope` for multi-user DM isolation and audit warnings. (#948) — thanks @Alphonse-arianee.
|
- Sessions/Security: add `session.dmScope` for multi-user DM isolation and audit warnings. (#948) — thanks @Alphonse-arianee.
|
||||||
- Plugins: add provider auth registry + `clawdbot models auth login` for plugin-driven OAuth/API key flows.
|
- Plugins: add provider auth registry + `clawdbot models auth login` for plugin-driven OAuth/API key flows.
|
||||||
- Onboarding: switch channels setup to a single-select loop with per-channel actions and disabled hints in the picker.
|
- Onboarding: switch channels setup to a single-select loop with per-channel actions and disabled hints in the picker.
|
||||||
- TUI: show provider/model labels for the active session and default model.
|
- TUI: show provider/model labels for the active session and default model.
|
||||||
- Heartbeat: add per-agent heartbeat configuration and multi-agent docs example.
|
- Heartbeat: add per-agent heartbeat configuration and multi-agent docs example.
|
||||||
- Fix: list model picker entries as provider/model pairs for explicit selection. (#970) — thanks @mcinteerj.
|
- UI: show gateway auth guidance + doc link on unauthorized Control UI connections.
|
||||||
- Fix: persist `gateway.mode=local` after selecting Local run mode in `clawdbot configure`, even if no other sections are chosen.
|
- Security: warn on weak model tiers (Haiku, below GPT-5, below Claude 4.5) in `clawdbot security audit`.
|
||||||
- Daemon: fix profile-aware service label resolution (env-driven) and add coverage for launchd/systemd/schtasks. (#969) — thanks @bjesuiter.
|
- Apps: store node auth tokens encrypted (Keychain/SecurePrefs).
|
||||||
- Daemon: share profile/state-dir resolution across service helpers and honor `CLAWDBOT_STATE_DIR` for Windows task scripts.
|
- Daemon: share profile/state-dir resolution across service helpers and honor `CLAWDBOT_STATE_DIR` for Windows task scripts.
|
||||||
- Docs: clarify multi-gateway rescue bot guidance. (#969) — thanks @bjesuiter.
|
- Docs: clarify multi-gateway rescue bot guidance. (#969) — thanks @bjesuiter.
|
||||||
- Agents: add Current Date & Time system prompt section with configurable time format (auto/12/24).
|
- Agents: add Current Date & Time system prompt section with configurable time format (auto/12/24).
|
||||||
- Agents: avoid false positives when logging unsupported Google tool schema keywords.
|
|
||||||
- Status: restore usage summary line for current provider when no OAuth profiles exist.
|
|
||||||
- Tools: normalize Slack/Discord message timestamps with `timestampMs`/`timestampUtc` while keeping raw provider fields.
|
- Tools: normalize Slack/Discord message timestamps with `timestampMs`/`timestampUtc` while keeping raw provider fields.
|
||||||
|
- macOS: add `system.which` for prompt-free remote skill discovery (with gateway fallback to `system.run`).
|
||||||
- Docs: add Date & Time guide and update prompt/timezone configuration docs.
|
- Docs: add Date & Time guide and update prompt/timezone configuration docs.
|
||||||
- Messages: debounce rapid inbound messages across channels with per-connector overrides. (#971) — thanks @juanpablodlc.
|
- Messages: debounce rapid inbound messages across channels with per-connector overrides. (#971) — thanks @juanpablodlc.
|
||||||
- Messages: allow media-only sends (CLI/tool) and show Telegram voice recording status for voice notes. (#957) — thanks @rdev.
|
- Messages: allow media-only sends (CLI/tool) and show Telegram voice recording status for voice notes. (#957) — thanks @rdev.
|
||||||
- Auth/Status: keep auth profiles sticky per session (rotate on compaction/new), surface provider usage headers in `/status` and `clawdbot models status`, and update docs.
|
- Auth/Status: keep auth profiles sticky per session (rotate on compaction/new), surface provider usage headers in `/status` and `clawdbot models status`, and update docs.
|
||||||
- Fix: guard model fallback against undefined provider/model values. (#954) — thanks @roshanasingh4.
|
|
||||||
- Fix: refactor session store updates, add chat.inject, and harden subagent cleanup flow. (#944) — thanks @tyler6204.
|
|
||||||
- Fix: clean up suspended CLI processes across backends. (#978) — thanks @Nachx639.
|
|
||||||
- Fix: parse systemd ExecStart arguments when whitespace is present. (#995) — thanks @roshanasingh4.
|
|
||||||
- CLI: add `--json` output for `clawdbot daemon` lifecycle/install commands.
|
- CLI: add `--json` output for `clawdbot daemon` lifecycle/install commands.
|
||||||
- Memory: make `node-llama-cpp` an optional dependency (avoid Node 25 install failures) and improve local-embeddings fallback/errors.
|
- Memory: make `node-llama-cpp` an optional dependency (avoid Node 25 install failures) and improve local-embeddings fallback/errors.
|
||||||
- Browser: add `snapshot refs=aria` (Playwright aria-ref ids) for self-resolving refs across `snapshot` → `act`.
|
- Browser: add `snapshot refs=aria` (Playwright aria-ref ids) for self-resolving refs across `snapshot` → `act`.
|
||||||
- Browser: `profile="chrome"` now defaults to host control and returns clearer “attach a tab” errors.
|
- Browser: `profile="chrome"` now defaults to host control and returns clearer “attach a tab” errors.
|
||||||
- Browser: extension mode recovers when only one tab is attached (stale targetId fallback).
|
|
||||||
- Browser: prefer stable Chrome for auto-detect, with Brave/Edge fallbacks and updated docs. (#983) — thanks @cpojer.
|
- Browser: prefer stable Chrome for auto-detect, with Brave/Edge fallbacks and updated docs. (#983) — thanks @cpojer.
|
||||||
- Browser: fix `tab not found` for extension relay snapshots/actions when Playwright blocks `newCDPSession` (use the single available Page).
|
- Browser: increase remote CDP reachability timeouts + add `remoteCdpTimeoutMs`/`remoteCdpHandshakeTimeoutMs`.
|
||||||
|
- Browser: preserve auth/query tokens for remote CDP endpoints and pass Basic auth for CDP HTTP/WS. (#895) — thanks @mukhtharcm.
|
||||||
- Telegram: add bidirectional reaction support with configurable notifications and agent guidance. (#964) — thanks @bohdanpodvirnyi.
|
- Telegram: add bidirectional reaction support with configurable notifications and agent guidance. (#964) — thanks @bohdanpodvirnyi.
|
||||||
- Telegram: skip `message_thread_id=1` for General topic sends while keeping typing indicators. (#848) — thanks @azade-c.
|
- Telegram: allow custom commands in the bot menu (merged with native; conflicts ignored). (#860) — thanks @nachoiacovino.
|
||||||
- Discord: allow allowlisted guilds without channel lists to receive messages when `groupPolicy="allowlist"`. — thanks @thewilloftheshadow.
|
- Discord: allow allowlisted guilds without channel lists to receive messages when `groupPolicy="allowlist"`. — thanks @thewilloftheshadow.
|
||||||
|
- Discord: allow emoji/sticker uploads + channel actions in config defaults. (#870) — thanks @JDIVE.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- Fix: list model picker entries as provider/model pairs for explicit selection. (#970) — thanks @mcinteerj.
|
||||||
|
- Fix: align OpenAI image-gen defaults with DALL-E 3 standard quality and document output formats. (#880) — thanks @mkbehr.
|
||||||
|
- Fix: persist `gateway.mode=local` after selecting Local run mode in `clawdbot configure`, even if no other sections are chosen.
|
||||||
|
- Daemon: fix profile-aware service label resolution (env-driven) and add coverage for launchd/systemd/schtasks. (#969) — thanks @bjesuiter.
|
||||||
|
- Agents: avoid false positives when logging unsupported Google tool schema keywords.
|
||||||
|
- Agents: skip Gemini history downgrades for google-antigravity to preserve tool calls. (#894) — thanks @mukhtharcm.
|
||||||
|
- Status: restore usage summary line for current provider when no OAuth profiles exist.
|
||||||
|
- Fix: guard model fallback against undefined provider/model values. (#954) — thanks @roshanasingh4.
|
||||||
|
- Fix: refactor session store updates, add chat.inject, and harden subagent cleanup flow. (#944) — thanks @tyler6204.
|
||||||
|
- Fix: clean up suspended CLI processes across backends. (#978) — thanks @Nachx639.
|
||||||
|
- Fix: support MiniMax coding plan usage responses with `model_remains`/`current_interval_*` payloads.
|
||||||
|
- Fix: suppress WhatsApp pairing replies for historical catch-up DMs on initial link. (#904)
|
||||||
|
- Browser: extension mode recovers when only one tab is attached (stale targetId fallback).
|
||||||
|
- Browser: fix `tab not found` for extension relay snapshots/actions when Playwright blocks `newCDPSession` (use the single available Page).
|
||||||
|
- Browser: upgrade `ws` → `wss` when remote CDP uses `https` (fixes Browserless handshake).
|
||||||
|
- Telegram: skip `message_thread_id=1` for General topic sends while keeping typing indicators. (#848) — thanks @azade-c.
|
||||||
- Fix: sanitize user-facing error text + strip `<final>` tags across reply pipelines. (#975) — thanks @ThomsenDrake.
|
- Fix: sanitize user-facing error text + strip `<final>` tags across reply pipelines. (#975) — thanks @ThomsenDrake.
|
||||||
- Fix: normalize pairing CLI aliases, allow extension channels, and harden Zalo webhook payload parsing. (#991) — thanks @longmaba.
|
- Fix: normalize pairing CLI aliases, allow extension channels, and harden Zalo webhook payload parsing. (#991) — thanks @longmaba.
|
||||||
|
- Fix: allow local Tailscale Serve hostnames without treating tailnet clients as direct. (#885) — thanks @oswalpalash.
|
||||||
|
- Fix: reset sessions after role-ordering conflicts to recover from consecutive user turns. (#998)
|
||||||
|
|
||||||
## 2026.1.14-1
|
## 2026.1.14-1
|
||||||
|
|
||||||
@@ -140,6 +213,7 @@
|
|||||||
### Fixes
|
### Fixes
|
||||||
- Packaging: include `dist/memory/**` in the npm tarball (fixes `ERR_MODULE_NOT_FOUND` for `dist/memory/index.js`).
|
- Packaging: include `dist/memory/**` in the npm tarball (fixes `ERR_MODULE_NOT_FOUND` for `dist/memory/index.js`).
|
||||||
- Agents: persist sub-agent registry across gateway restarts and resume announce flow safely. (#831) — thanks @roshanasingh4.
|
- Agents: persist sub-agent registry across gateway restarts and resume announce flow safely. (#831) — thanks @roshanasingh4.
|
||||||
|
- Agents: strip invalid Gemini thought signatures from OpenRouter history to avoid 400s. (#841, #845) — thanks @MatthieuBizien.
|
||||||
|
|
||||||
## 2026.1.12-1
|
## 2026.1.12-1
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ RUN pnpm install --frozen-lockfile
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN pnpm build
|
RUN pnpm build
|
||||||
|
# Force pnpm for UI build (Bun may fail on ARM/Synology architectures)
|
||||||
|
ENV CLAWDBOT_PREFER_PNPM=1
|
||||||
RUN pnpm ui:install
|
RUN pnpm ui:install
|
||||||
RUN pnpm ui:build
|
RUN pnpm ui:build
|
||||||
|
|
||||||
|
|||||||
@@ -474,23 +474,24 @@ Core contributors:
|
|||||||
Thanks to all clawtributors:
|
Thanks to all clawtributors:
|
||||||
|
|
||||||
<p align="left">
|
<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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></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/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/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/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/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/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/conhecendocontato"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendocontato" title="conhecendocontato"/></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/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/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/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/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/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/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/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/search?q=Marc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc" title="Marc"/></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/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/conhecendocontato"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendocontato" title="conhecendocontato"/></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/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/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/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/search?q=Marc"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Marc" title="Marc"/></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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></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/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=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></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=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a> <a href="https://github.com/Szpadel"><img src="https://avatars.githubusercontent.com/u/1857251?v=4&s=48" width="48" height="48" alt="Szpadel" title="Szpadel"/></a>
|
||||||
</p>
|
</p>
|
||||||
+81
-17
@@ -2,6 +2,87 @@
|
|||||||
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
||||||
<channel>
|
<channel>
|
||||||
<title>Clawdbot</title>
|
<title>Clawdbot</title>
|
||||||
|
<item>
|
||||||
|
<title>2026.1.15</title>
|
||||||
|
<pubDate>Fri, 16 Jan 2026 10:31:53 +0000</pubDate>
|
||||||
|
<link>https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml</link>
|
||||||
|
<sparkle:version>5998</sparkle:version>
|
||||||
|
<sparkle:shortVersionString>2026.1.15</sparkle:shortVersionString>
|
||||||
|
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||||
|
<description><![CDATA[<h2>Clawdbot 2026.1.15</h2>
|
||||||
|
<h3>Highlights</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Plugins: add provider auth registry + <code>clawdbot models auth login</code> for plugin-driven OAuth/API key flows.</li>
|
||||||
|
<li>Browser: improve remote CDP/Browserless support (auth passthrough, <code>wss</code> upgrade, timeouts, clearer errors).</li>
|
||||||
|
<li>Heartbeat: per-agent configuration + 24h duplicate suppression. (#980) — thanks @voidserf.</li>
|
||||||
|
<li>Security: audit warns on weak model tiers; app nodes store auth tokens encrypted (Keychain/SecurePrefs).</li>
|
||||||
|
</ul>
|
||||||
|
<h3>Breaking</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>BREAKING:</strong> iOS minimum version is now 18.0 to support Textual markdown rendering in native chat. (#702)</li>
|
||||||
|
<li><strong>BREAKING:</strong> Microsoft Teams is now a plugin; install <code>@clawdbot/msteams</code> via <code>clawdbot plugins install @clawdbot/msteams</code>.</li>
|
||||||
|
</ul>
|
||||||
|
<h3>Changes</h3>
|
||||||
|
<ul>
|
||||||
|
<li>CLI: set process titles to <code>clawdbot-<command></code> for clearer process listings.</li>
|
||||||
|
<li>CLI/macOS: sync remote SSH target/identity to config and let <code>gateway status</code> auto-infer SSH targets (ssh-config aware).</li>
|
||||||
|
<li>Heartbeat: tighten prompt guidance + suppress duplicate alerts for 24h. (#980) — thanks @voidserf.</li>
|
||||||
|
<li>Sessions/Security: add <code>session.dmScope</code> for multi-user DM isolation and audit warnings. (#948) — thanks @Alphonse-arianee.</li>
|
||||||
|
<li>Plugins: add provider auth registry + <code>clawdbot models auth login</code> for plugin-driven OAuth/API key flows.</li>
|
||||||
|
<li>Onboarding: switch channels setup to a single-select loop with per-channel actions and disabled hints in the picker.</li>
|
||||||
|
<li>TUI: show provider/model labels for the active session and default model.</li>
|
||||||
|
<li>Heartbeat: add per-agent heartbeat configuration and multi-agent docs example.</li>
|
||||||
|
<li>UI: show gateway auth guidance + doc link on unauthorized Control UI connections.</li>
|
||||||
|
<li>Security: warn on weak model tiers (Haiku, below GPT-5, below Claude 4.5) in <code>clawdbot security audit</code>.</li>
|
||||||
|
<li>Apps: store node auth tokens encrypted (Keychain/SecurePrefs).</li>
|
||||||
|
<li>Daemon: share profile/state-dir resolution across service helpers and honor <code>CLAWDBOT_STATE_DIR</code> for Windows task scripts.</li>
|
||||||
|
<li>Docs: clarify multi-gateway rescue bot guidance. (#969) — thanks @bjesuiter.</li>
|
||||||
|
<li>Agents: add Current Date & Time system prompt section with configurable time format (auto/12/24).</li>
|
||||||
|
<li>Tools: normalize Slack/Discord message timestamps with <code>timestampMs</code>/<code>timestampUtc</code> while keeping raw provider fields.</li>
|
||||||
|
<li>macOS: add <code>system.which</code> for prompt-free remote skill discovery (with gateway fallback to <code>system.run</code>).</li>
|
||||||
|
<li>Docs: add Date & Time guide and update prompt/timezone configuration docs.</li>
|
||||||
|
<li>Messages: debounce rapid inbound messages across channels with per-connector overrides. (#971) — thanks @juanpablodlc.</li>
|
||||||
|
<li>Messages: allow media-only sends (CLI/tool) and show Telegram voice recording status for voice notes. (#957) — thanks @rdev.</li>
|
||||||
|
<li>Auth/Status: keep auth profiles sticky per session (rotate on compaction/new), surface provider usage headers in <code>/status</code> and <code>clawdbot models status</code>, and update docs.</li>
|
||||||
|
<li>CLI: add <code>--json</code> output for <code>clawdbot daemon</code> lifecycle/install commands.</li>
|
||||||
|
<li>Memory: make <code>node-llama-cpp</code> an optional dependency (avoid Node 25 install failures) and improve local-embeddings fallback/errors.</li>
|
||||||
|
<li>Browser: add <code>snapshot refs=aria</code> (Playwright aria-ref ids) for self-resolving refs across <code>snapshot</code> → <code>act</code>.</li>
|
||||||
|
<li>Browser: <code>profile="chrome"</code> now defaults to host control and returns clearer “attach a tab” errors.</li>
|
||||||
|
<li>Browser: prefer stable Chrome for auto-detect, with Brave/Edge fallbacks and updated docs. (#983) — thanks @cpojer.</li>
|
||||||
|
<li>Browser: increase remote CDP reachability timeouts + add <code>remoteCdpTimeoutMs</code>/<code>remoteCdpHandshakeTimeoutMs</code>.</li>
|
||||||
|
<li>Browser: preserve auth/query tokens for remote CDP endpoints and pass Basic auth for CDP HTTP/WS. (#895) — thanks @mukhtharcm.</li>
|
||||||
|
<li>Telegram: add bidirectional reaction support with configurable notifications and agent guidance. (#964) — thanks @bohdanpodvirnyi.</li>
|
||||||
|
<li>Telegram: allow custom commands in the bot menu (merged with native; conflicts ignored). (#860) — thanks @nachoiacovino.</li>
|
||||||
|
<li>Discord: allow allowlisted guilds without channel lists to receive messages when <code>groupPolicy="allowlist"</code>. — thanks @thewilloftheshadow.</li>
|
||||||
|
<li>Discord: allow emoji/sticker uploads + channel actions in config defaults. (#870) — thanks @JDIVE.</li>
|
||||||
|
</ul>
|
||||||
|
<h3>Fixes</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Fix: list model picker entries as provider/model pairs for explicit selection. (#970) — thanks @mcinteerj.</li>
|
||||||
|
<li>Fix: align OpenAI image-gen defaults with DALL-E 3 standard quality and document output formats. (#880) — thanks @mkbehr.</li>
|
||||||
|
<li>Fix: persist <code>gateway.mode=local</code> after selecting Local run mode in <code>clawdbot configure</code>, even if no other sections are chosen.</li>
|
||||||
|
<li>Daemon: fix profile-aware service label resolution (env-driven) and add coverage for launchd/systemd/schtasks. (#969) — thanks @bjesuiter.</li>
|
||||||
|
<li>Agents: avoid false positives when logging unsupported Google tool schema keywords.</li>
|
||||||
|
<li>Agents: skip Gemini history downgrades for google-antigravity to preserve tool calls. (#894) — thanks @mukhtharcm.</li>
|
||||||
|
<li>Status: restore usage summary line for current provider when no OAuth profiles exist.</li>
|
||||||
|
<li>Fix: guard model fallback against undefined provider/model values. (#954) — thanks @roshanasingh4.</li>
|
||||||
|
<li>Fix: refactor session store updates, add chat.inject, and harden subagent cleanup flow. (#944) — thanks @tyler6204.</li>
|
||||||
|
<li>Fix: clean up suspended CLI processes across backends. (#978) — thanks @Nachx639.</li>
|
||||||
|
<li>Fix: support MiniMax coding plan usage responses with <code>model_remains</code>/<code>current_interval_*</code> payloads.</li>
|
||||||
|
<li>Fix: suppress WhatsApp pairing replies for historical catch-up DMs on initial link. (#904)</li>
|
||||||
|
<li>Browser: extension mode recovers when only one tab is attached (stale targetId fallback).</li>
|
||||||
|
<li>Browser: fix <code>tab not found</code> for extension relay snapshots/actions when Playwright blocks <code>newCDPSession</code> (use the single available Page).</li>
|
||||||
|
<li>Browser: upgrade <code>ws</code> → <code>wss</code> when remote CDP uses <code>https</code> (fixes Browserless handshake).</li>
|
||||||
|
<li>Telegram: skip <code>message_thread_id=1</code> for General topic sends while keeping typing indicators. (#848) — thanks @azade-c.</li>
|
||||||
|
<li>Fix: sanitize user-facing error text + strip <code><final></code> tags across reply pipelines. (#975) — thanks @ThomsenDrake.</li>
|
||||||
|
<li>Fix: normalize pairing CLI aliases, allow extension channels, and harden Zalo webhook payload parsing. (#991) — thanks @longmaba.</li>
|
||||||
|
<li>Fix: allow local Tailscale Serve hostnames without treating tailnet clients as direct. (#885) — thanks @oswalpalash.</li>
|
||||||
|
<li>Fix: reset sessions after role-ordering conflicts to recover from consecutive user turns. (#998)</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.15/Clawdbot-2026.1.15.zip" length="12127276" type="application/octet-stream" sparkle:edSignature="o79vwTbtW/d91NQFRVfUDhsv6D4zIw7IkhY0N1iLImMu94BURgLcecA6z7Smy3bMobPwOyzN8yfm6mA/Rt8FCA=="/>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<title>2026.1.14-1</title>
|
<title>2026.1.14-1</title>
|
||||||
<pubDate>Thu, 15 Jan 2026 11:14:40 +0000</pubDate>
|
<pubDate>Thu, 15 Jan 2026 11:14:40 +0000</pubDate>
|
||||||
@@ -190,22 +271,5 @@
|
|||||||
]]></description>
|
]]></description>
|
||||||
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.12-2/Clawdbot-2026.1.12-2.zip" length="19854203" type="application/octet-stream" sparkle:edSignature="CVpUofNS+pl6Smk/K0Q8q35saRuuFx90s4sePABORFvGcAF1biajC8zpiImKuXpqD0ENb+VTwDJ1ul1Oxh3wDA=="/>
|
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.12-2/Clawdbot-2026.1.12-2.zip" length="19854203" type="application/octet-stream" sparkle:edSignature="CVpUofNS+pl6Smk/K0Q8q35saRuuFx90s4sePABORFvGcAF1biajC8zpiImKuXpqD0ENb+VTwDJ1ul1Oxh3wDA=="/>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
|
||||||
<title>2026.1.11-3</title>
|
|
||||||
<pubDate>Mon, 12 Jan 2026 10:40:23 +0000</pubDate>
|
|
||||||
<link>https://raw.githubusercontent.com/clawdbot/clawdbot/main/appcast.xml</link>
|
|
||||||
<sparkle:version>5212</sparkle:version>
|
|
||||||
<sparkle:shortVersionString>2026.1.11-3</sparkle:shortVersionString>
|
|
||||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
|
||||||
<description><![CDATA[<h2>Clawdbot 2026.1.11-3</h2>
|
|
||||||
<h3>Fixes</h3>
|
|
||||||
<ul>
|
|
||||||
<li>CLI: avoid top-level await warnings in the entrypoint on fresh installs.</li>
|
|
||||||
<li>CLI: show a commit hash in the banner for npm installs (package.json gitHead fallback).</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.11-3/Clawdbot-2026.1.11-3.zip" length="19860758" type="application/octet-stream" sparkle:edSignature="LbvGUSjc3jGO7aVo2UVA0nEkaJbb3O4iwRBo1TBqoapdTtxnDlS3s6N+Z4vOSLRAoAm22EoZOwbpK9085c7HAQ=="/>
|
|
||||||
</item>
|
|
||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
@@ -119,7 +119,7 @@ dependencies {
|
|||||||
testImplementation("io.kotest:kotest-runner-junit5-jvm:6.0.7")
|
testImplementation("io.kotest:kotest-runner-junit5-jvm:6.0.7")
|
||||||
testImplementation("io.kotest:kotest-assertions-core-jvm:6.0.7")
|
testImplementation("io.kotest:kotest-assertions-core-jvm:6.0.7")
|
||||||
testImplementation("org.robolectric:robolectric:4.16")
|
testImplementation("org.robolectric:robolectric:4.16")
|
||||||
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:6.0.1")
|
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:6.0.2")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<Test>().configureEach {
|
tasks.withType<Test>().configureEach {
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ class BridgeSession(
|
|||||||
connectWithSocket(endpoint, hello, null)
|
connectWithSocket(endpoint, hello, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun connectWithSocket(endpoint: BridgeEndpoint, hello: Hello, tls: BridgeTlsParams?) {
|
private suspend fun connectWithSocket(endpoint: BridgeEndpoint, hello: Hello, tls: BridgeTlsParams?) {
|
||||||
val socket =
|
val socket =
|
||||||
createBridgeSocket(tls) { fingerprint ->
|
createBridgeSocket(tls) { fingerprint ->
|
||||||
onTlsFingerprint?.invoke(tls?.stableId ?: endpoint.stableId, fingerprint)
|
onTlsFingerprint?.invoke(tls?.stableId ?: endpoint.stableId, fingerprint)
|
||||||
@@ -260,15 +260,15 @@ class BridgeSession(
|
|||||||
else -> throw IllegalStateException("unexpected bridge response")
|
else -> throw IllegalStateException("unexpected bridge response")
|
||||||
}
|
}
|
||||||
|
|
||||||
while (scope.isActive) {
|
while (scope.isActive) {
|
||||||
val line = reader.readLine() ?: break
|
val line = reader.readLine() ?: break
|
||||||
val frame = json.parseToJsonElement(line).asObjectOrNull() ?: continue
|
val frame = json.parseToJsonElement(line).asObjectOrNull() ?: continue
|
||||||
when (frame["type"].asStringOrNull()) {
|
when (frame["type"].asStringOrNull()) {
|
||||||
"event" -> {
|
"event" -> {
|
||||||
val event = frame["event"].asStringOrNull() ?: return@withContext
|
val event = frame["event"].asStringOrNull() ?: continue
|
||||||
val payload = frame["payloadJSON"].asStringOrNull()
|
val payload = frame["payloadJSON"].asStringOrNull()
|
||||||
onEvent(event, payload)
|
onEvent(event, payload)
|
||||||
}
|
}
|
||||||
"ping" -> {
|
"ping" -> {
|
||||||
val id = frame["id"].asStringOrNull() ?: ""
|
val id = frame["id"].asStringOrNull() ?: ""
|
||||||
conn.sendJson(buildJsonObject { put("type", JsonPrimitive("pong")); put("id", JsonPrimitive(id)) })
|
conn.sendJson(buildJsonObject { put("type", JsonPrimitive("pong")); put("id", JsonPrimitive(id)) })
|
||||||
@@ -314,20 +314,20 @@ class BridgeSession(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
"invoke-res" -> {
|
"invoke-res" -> {
|
||||||
// gateway->node only (ignore)
|
// gateway->node only (ignore)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
currentConnection = null
|
|
||||||
for ((_, waiter) in pending) {
|
|
||||||
waiter.cancel()
|
|
||||||
}
|
|
||||||
pending.clear()
|
|
||||||
conn.closeQuietly()
|
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
currentConnection = null
|
||||||
|
for ((_, waiter) in pending) {
|
||||||
|
waiter.cancel()
|
||||||
|
}
|
||||||
|
pending.clear()
|
||||||
|
conn.closeQuietly()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildHelloJson(hello: Hello): JsonObject =
|
private fun buildHelloJson(hello: Hello): JsonObject =
|
||||||
buildJsonObject {
|
buildJsonObject {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.clawdbot.android.bridge
|
package com.clawdbot.android.bridge
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
@@ -21,6 +22,7 @@ fun createBridgeSocket(params: BridgeTlsParams?, onStore: ((String) -> Unit)? =
|
|||||||
if (params == null) return Socket()
|
if (params == null) return Socket()
|
||||||
val expected = params.expectedFingerprint?.let(::normalizeFingerprint)
|
val expected = params.expectedFingerprint?.let(::normalizeFingerprint)
|
||||||
val defaultTrust = defaultTrustManager()
|
val defaultTrust = defaultTrustManager()
|
||||||
|
@SuppressLint("CustomX509TrustManager")
|
||||||
val trustManager =
|
val trustManager =
|
||||||
object : X509TrustManager {
|
object : X509TrustManager {
|
||||||
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
|
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
|
||||||
|
|||||||
@@ -32,11 +32,10 @@ func makeBridgeTLSOptions(_ params: BridgeTLSParams?) -> NWProtocolTLS.Options?
|
|||||||
sec_protocol_options_set_verify_block(
|
sec_protocol_options_set_verify_block(
|
||||||
options.securityProtocolOptions,
|
options.securityProtocolOptions,
|
||||||
{ _, trust, complete in
|
{ _, trust, complete in
|
||||||
guard let trust else {
|
let trustRef = sec_trust_copy_ref(trust).takeRetainedValue()
|
||||||
complete(false)
|
if let chain = SecTrustCopyCertificateChain(trustRef) as? [SecCertificate],
|
||||||
return
|
let cert = chain.first
|
||||||
}
|
{
|
||||||
if let cert = SecTrustGetCertificateAtIndex(trust, 0) {
|
|
||||||
let data = SecCertificateCopyData(cert) as Data
|
let data = SecCertificateCopyData(cert) as Data
|
||||||
let fingerprint = sha256Hex(data)
|
let fingerprint = sha256Hex(data)
|
||||||
if let expected {
|
if let expected {
|
||||||
@@ -49,7 +48,7 @@ func makeBridgeTLSOptions(_ params: BridgeTLSParams?) -> NWProtocolTLS.Options?
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let ok = SecTrustEvaluateWithError(trust, nil)
|
let ok = SecTrustEvaluateWithError(trustRef, nil)
|
||||||
complete(ok)
|
complete(ok)
|
||||||
},
|
},
|
||||||
DispatchQueue(label: "com.clawdbot.bridge.tls.verify"))
|
DispatchQueue(label: "com.clawdbot.bridge.tls.verify"))
|
||||||
|
|||||||
@@ -190,14 +190,7 @@ actor CameraController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func listDevices() -> [CameraDeviceInfo] {
|
func listDevices() -> [CameraDeviceInfo] {
|
||||||
let types: [AVCaptureDevice.DeviceType] = [
|
return Self.discoverVideoDevices().map { device in
|
||||||
.builtInWideAngleCamera,
|
|
||||||
]
|
|
||||||
let session = AVCaptureDevice.DiscoverySession(
|
|
||||||
deviceTypes: types,
|
|
||||||
mediaType: .video,
|
|
||||||
position: .unspecified)
|
|
||||||
return session.devices.map { device in
|
|
||||||
CameraDeviceInfo(
|
CameraDeviceInfo(
|
||||||
id: device.uniqueID,
|
id: device.uniqueID,
|
||||||
name: device.localizedName,
|
name: device.localizedName,
|
||||||
@@ -232,7 +225,7 @@ actor CameraController {
|
|||||||
deviceId: String?) -> AVCaptureDevice?
|
deviceId: String?) -> AVCaptureDevice?
|
||||||
{
|
{
|
||||||
if let deviceId, !deviceId.isEmpty {
|
if let deviceId, !deviceId.isEmpty {
|
||||||
if let match = AVCaptureDevice.devices(for: .video).first(where: { $0.uniqueID == deviceId }) {
|
if let match = Self.discoverVideoDevices().first(where: { $0.uniqueID == deviceId }) {
|
||||||
return match
|
return match
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,6 +245,24 @@ actor CameraController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private nonisolated static func discoverVideoDevices() -> [AVCaptureDevice] {
|
||||||
|
let types: [AVCaptureDevice.DeviceType] = [
|
||||||
|
.builtInWideAngleCamera,
|
||||||
|
.builtInUltraWideCamera,
|
||||||
|
.builtInTelephotoCamera,
|
||||||
|
.builtInDualCamera,
|
||||||
|
.builtInDualWideCamera,
|
||||||
|
.builtInTripleCamera,
|
||||||
|
.builtInTrueDepthCamera,
|
||||||
|
.builtInLiDARDepthCamera,
|
||||||
|
]
|
||||||
|
let session = AVCaptureDevice.DiscoverySession(
|
||||||
|
deviceTypes: types,
|
||||||
|
mediaType: .video,
|
||||||
|
position: .unspecified)
|
||||||
|
return session.devices
|
||||||
|
}
|
||||||
|
|
||||||
nonisolated static func clampQuality(_ quality: Double?) -> Double {
|
nonisolated static func clampQuality(_ quality: Double?) -> Double {
|
||||||
let q = quality ?? 0.9
|
let q = quality ?? 0.9
|
||||||
return min(1.0, max(0.05, q))
|
return min(1.0, max(0.05, q))
|
||||||
|
|||||||
@@ -137,9 +137,11 @@ final class ScreenRecordService: @unchecked Sendable {
|
|||||||
recordQueue: DispatchQueue) -> @Sendable (CMSampleBuffer, RPSampleBufferType, Error?) -> Void
|
recordQueue: DispatchQueue) -> @Sendable (CMSampleBuffer, RPSampleBufferType, Error?) -> Void
|
||||||
{
|
{
|
||||||
{ sample, type, error in
|
{ sample, type, error in
|
||||||
|
let sampleBox = UncheckedSendableBox(value: sample)
|
||||||
// ReplayKit can call the capture handler on a background queue.
|
// ReplayKit can call the capture handler on a background queue.
|
||||||
// Serialize writes to avoid queue asserts.
|
// Serialize writes to avoid queue asserts.
|
||||||
recordQueue.async {
|
recordQueue.async {
|
||||||
|
let sample = sampleBox.value
|
||||||
if let error {
|
if let error {
|
||||||
state.withLock { state in
|
state.withLock { state in
|
||||||
if state.handlerError == nil { state.handlerError = error }
|
if state.handlerError == nil { state.handlerError = error }
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ Sources/Voice/VoiceTab.swift
|
|||||||
Sources/Voice/VoiceWakeManager.swift
|
Sources/Voice/VoiceWakeManager.swift
|
||||||
Sources/Voice/VoiceWakePreferences.swift
|
Sources/Voice/VoiceWakePreferences.swift
|
||||||
../shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatComposer.swift
|
../shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatComposer.swift
|
||||||
../shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatMarkdownSplitter.swift
|
../shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatMarkdownRenderer.swift
|
||||||
|
../shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatMarkdownPreprocessor.swift
|
||||||
../shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatMessageViews.swift
|
../shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatMessageViews.swift
|
||||||
../shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatModels.swift
|
../shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatModels.swift
|
||||||
../shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatPayloadDecoding.swift
|
../shared/ClawdbotKit/Sources/ClawdbotChatUI/ChatPayloadDecoding.swift
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: Clawdbot
|
|||||||
options:
|
options:
|
||||||
bundleIdPrefix: com.clawdbot
|
bundleIdPrefix: com.clawdbot
|
||||||
deploymentTarget:
|
deploymentTarget:
|
||||||
iOS: "17.0"
|
iOS: "18.0"
|
||||||
xcodeVersion: "16.0"
|
xcodeVersion: "16.0"
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "9de32b5fc115432dadd84c3ab4d67d2fed22ffaf5675a77033d69ea194ac3862",
|
"originHash" : "7eec77e2b399c480e76fdfc7dc3162652f5c775530e9fc282953de38ef2de79b",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "elevenlabskit",
|
"identity" : "elevenlabskit",
|
||||||
@@ -73,6 +73,15 @@
|
|||||||
"revision" : "8e5e4a8f3617283b556064574651fc0869943c9a"
|
"revision" : "8e5e4a8f3617283b556064574651fc0869943c9a"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-concurrency-extras",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "5a3825302b1a0d744183200915a47b508c828e6f",
|
||||||
|
"version" : "1.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "swift-configuration",
|
"identity" : "swift-configuration",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@@ -144,6 +153,24 @@
|
|||||||
"revision" : "395a77f0aa927f0ff73941d7ac35f2b46d47c9db",
|
"revision" : "395a77f0aa927f0ff73941d7ac35f2b46d47c9db",
|
||||||
"version" : "1.6.3"
|
"version" : "1.6.3"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swiftui-math",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/gonzalezreal/swiftui-math",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "0b5c2cfaaec8d6193db206f675048eeb5ce95f71",
|
||||||
|
"version" : "0.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "textual",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/gonzalezreal/textual",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "a03c1e103d88de4ea0dd8320ea1611ec0d4b29b3",
|
||||||
|
"version" : "0.2.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version" : 3
|
"version" : 3
|
||||||
|
|||||||
@@ -257,30 +257,8 @@ final class AppState {
|
|||||||
|
|
||||||
let configRoot = ClawdbotConfigFile.loadDict()
|
let configRoot = ClawdbotConfigFile.loadDict()
|
||||||
let configGateway = configRoot["gateway"] as? [String: Any]
|
let configGateway = configRoot["gateway"] as? [String: Any]
|
||||||
let configModeRaw = (configGateway?["mode"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
let configMode: ConnectionMode? = switch configModeRaw {
|
|
||||||
case "local":
|
|
||||||
.local
|
|
||||||
case "remote":
|
|
||||||
.remote
|
|
||||||
default:
|
|
||||||
nil
|
|
||||||
}
|
|
||||||
let configRemoteUrl = (configGateway?["remote"] as? [String: Any])?["url"] as? String
|
let configRemoteUrl = (configGateway?["remote"] as? [String: Any])?["url"] as? String
|
||||||
let configHasRemoteUrl = !(configRemoteUrl?
|
let resolvedConnectionMode = ConnectionModeResolver.resolve(root: configRoot).mode
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
.isEmpty ?? true)
|
|
||||||
|
|
||||||
let storedMode = UserDefaults.standard.string(forKey: connectionModeKey)
|
|
||||||
let resolvedConnectionMode: ConnectionMode = if let configMode {
|
|
||||||
configMode
|
|
||||||
} else if configHasRemoteUrl {
|
|
||||||
.remote
|
|
||||||
} else if let storedMode {
|
|
||||||
ConnectionMode(rawValue: storedMode) ?? .local
|
|
||||||
} else {
|
|
||||||
onboardingSeen ? .local : .unconfigured
|
|
||||||
}
|
|
||||||
self.connectionMode = resolvedConnectionMode
|
self.connectionMode = resolvedConnectionMode
|
||||||
|
|
||||||
let storedRemoteTarget = UserDefaults.standard.string(forKey: remoteTargetKey) ?? ""
|
let storedRemoteTarget = UserDefaults.standard.string(forKey: remoteTargetKey) ?? ""
|
||||||
@@ -341,6 +319,15 @@ final class AppState {
|
|||||||
return host
|
return host
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func sanitizeSSHTarget(_ value: String) -> String {
|
||||||
|
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if trimmed.hasPrefix("ssh ") {
|
||||||
|
return trimmed.replacingOccurrences(of: "ssh ", with: "")
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
|
||||||
private func startConfigWatcher() {
|
private func startConfigWatcher() {
|
||||||
let configUrl = ClawdbotConfigFile.url()
|
let configUrl = ClawdbotConfigFile.url()
|
||||||
self.configWatcher = ConfigFileWatcher(url: configUrl) { [weak self] in
|
self.configWatcher = ConfigFileWatcher(url: configUrl) { [weak self] in
|
||||||
@@ -406,6 +393,7 @@ final class AppState {
|
|||||||
|
|
||||||
let connectionMode = self.connectionMode
|
let connectionMode = self.connectionMode
|
||||||
let remoteTarget = self.remoteTarget
|
let remoteTarget = self.remoteTarget
|
||||||
|
let remoteIdentity = self.remoteIdentity
|
||||||
let desiredMode: String? = switch connectionMode {
|
let desiredMode: String? = switch connectionMode {
|
||||||
case .local:
|
case .local:
|
||||||
"local"
|
"local"
|
||||||
@@ -435,15 +423,46 @@ final class AppState {
|
|||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if connectionMode == .remote, let host = remoteHost {
|
if connectionMode == .remote {
|
||||||
var remote = gateway["remote"] as? [String: Any] ?? [:]
|
var remote = gateway["remote"] as? [String: Any] ?? [:]
|
||||||
let existingUrl = (remote["url"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
var remoteChanged = false
|
||||||
let parsedExisting = existingUrl.isEmpty ? nil : URL(string: existingUrl)
|
|
||||||
let scheme = parsedExisting?.scheme?.isEmpty == false ? parsedExisting?.scheme : "ws"
|
if let host = remoteHost {
|
||||||
let port = parsedExisting?.port ?? 18789
|
let existingUrl = (remote["url"] as? String)?
|
||||||
let desiredUrl = "\(scheme ?? "ws")://\(host):\(port)"
|
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
if existingUrl != desiredUrl {
|
let parsedExisting = existingUrl.isEmpty ? nil : URL(string: existingUrl)
|
||||||
remote["url"] = desiredUrl
|
let scheme = parsedExisting?.scheme?.isEmpty == false ? parsedExisting?.scheme : "ws"
|
||||||
|
let port = parsedExisting?.port ?? 18789
|
||||||
|
let desiredUrl = "\(scheme ?? "ws")://\(host):\(port)"
|
||||||
|
if existingUrl != desiredUrl {
|
||||||
|
remote["url"] = desiredUrl
|
||||||
|
remoteChanged = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sanitizedTarget = Self.sanitizeSSHTarget(remoteTarget)
|
||||||
|
if !sanitizedTarget.isEmpty {
|
||||||
|
if (remote["sshTarget"] as? String) != sanitizedTarget {
|
||||||
|
remote["sshTarget"] = sanitizedTarget
|
||||||
|
remoteChanged = true
|
||||||
|
}
|
||||||
|
} else if remote["sshTarget"] != nil {
|
||||||
|
remote.removeValue(forKey: "sshTarget")
|
||||||
|
remoteChanged = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let trimmedIdentity = remoteIdentity.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if !trimmedIdentity.isEmpty {
|
||||||
|
if (remote["sshIdentity"] as? String) != trimmedIdentity {
|
||||||
|
remote["sshIdentity"] = trimmedIdentity
|
||||||
|
remoteChanged = true
|
||||||
|
}
|
||||||
|
} else if remote["sshIdentity"] != nil {
|
||||||
|
remote.removeValue(forKey: "sshIdentity")
|
||||||
|
remoteChanged = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if remoteChanged {
|
||||||
gateway["remote"] = remote
|
gateway["remote"] = remote
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -385,14 +385,8 @@ enum CommandResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func connectionSettings(defaults: UserDefaults = .standard) -> RemoteSettings {
|
static func connectionSettings(defaults: UserDefaults = .standard) -> RemoteSettings {
|
||||||
let modeRaw = defaults.string(forKey: connectionModeKey)
|
let root = ClawdbotConfigFile.loadDict()
|
||||||
let mode: AppState.ConnectionMode
|
let mode = ConnectionModeResolver.resolve(root: root, defaults: defaults).mode
|
||||||
if let modeRaw {
|
|
||||||
mode = AppState.ConnectionMode(rawValue: modeRaw) ?? .local
|
|
||||||
} else {
|
|
||||||
let seen = defaults.bool(forKey: "clawdbot.onboardingSeen")
|
|
||||||
mode = seen ? .local : .unconfigured
|
|
||||||
}
|
|
||||||
let target = defaults.string(forKey: remoteTargetKey) ?? ""
|
let target = defaults.string(forKey: remoteTargetKey) ?? ""
|
||||||
let identity = defaults.string(forKey: remoteIdentityKey) ?? ""
|
let identity = defaults.string(forKey: remoteIdentityKey) ?? ""
|
||||||
let projectRoot = defaults.string(forKey: remoteProjectRootKey) ?? ""
|
let projectRoot = defaults.string(forKey: remoteProjectRootKey) ?? ""
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum EffectiveConnectionModeSource: Sendable, Equatable {
|
||||||
|
case configMode
|
||||||
|
case configRemoteURL
|
||||||
|
case userDefaults
|
||||||
|
case onboarding
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EffectiveConnectionMode: Sendable, Equatable {
|
||||||
|
let mode: AppState.ConnectionMode
|
||||||
|
let source: EffectiveConnectionModeSource
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ConnectionModeResolver {
|
||||||
|
static func resolve(
|
||||||
|
root: [String: Any],
|
||||||
|
defaults: UserDefaults = .standard) -> EffectiveConnectionMode
|
||||||
|
{
|
||||||
|
let gateway = root["gateway"] as? [String: Any]
|
||||||
|
let configModeRaw = (gateway?["mode"] as? String) ?? ""
|
||||||
|
let configMode = configModeRaw
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
.lowercased()
|
||||||
|
|
||||||
|
switch configMode {
|
||||||
|
case "local":
|
||||||
|
return EffectiveConnectionMode(mode: .local, source: .configMode)
|
||||||
|
case "remote":
|
||||||
|
return EffectiveConnectionMode(mode: .remote, source: .configMode)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
let remoteURLRaw = ((gateway?["remote"] as? [String: Any])?["url"] as? String) ?? ""
|
||||||
|
let remoteURL = remoteURLRaw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if !remoteURL.isEmpty {
|
||||||
|
return EffectiveConnectionMode(mode: .remote, source: .configRemoteURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let storedModeRaw = defaults.string(forKey: connectionModeKey) {
|
||||||
|
let storedMode = AppState.ConnectionMode(rawValue: storedModeRaw) ?? .local
|
||||||
|
return EffectiveConnectionMode(mode: storedMode, source: .userDefaults)
|
||||||
|
}
|
||||||
|
|
||||||
|
let seen = defaults.bool(forKey: "clawdbot.onboardingSeen")
|
||||||
|
return EffectiveConnectionMode(mode: seen ? .local : .unconfigured, source: .onboarding)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -188,9 +188,15 @@ extension CronJobEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyDeleteAfterRun(to root: inout [String: Any]) {
|
func applyDeleteAfterRun(
|
||||||
if self.scheduleKind == .at {
|
to root: inout [String: Any],
|
||||||
root["deleteAfterRun"] = self.deleteAfterRun
|
scheduleKind: ScheduleKind? = nil,
|
||||||
|
deleteAfterRun: Bool? = nil)
|
||||||
|
{
|
||||||
|
let resolvedSchedule = scheduleKind ?? self.scheduleKind
|
||||||
|
let resolvedDelete = deleteAfterRun ?? self.deleteAfterRun
|
||||||
|
if resolvedSchedule == .at {
|
||||||
|
root["deleteAfterRun"] = resolvedDelete
|
||||||
} else if self.job?.deleteAfterRun != nil {
|
} else if self.job?.deleteAfterRun != nil {
|
||||||
root["deleteAfterRun"] = false
|
root["deleteAfterRun"] = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import ConcurrencyExtras
|
||||||
import Foundation
|
import Foundation
|
||||||
import OSLog
|
import OSLog
|
||||||
|
|
||||||
@@ -16,6 +17,13 @@ actor GatewayEndpointStore {
|
|||||||
static let shared = GatewayEndpointStore()
|
static let shared = GatewayEndpointStore()
|
||||||
private static let supportedBindModes: Set<String> = ["loopback", "tailnet", "lan", "auto"]
|
private static let supportedBindModes: Set<String> = ["loopback", "tailnet", "lan", "auto"]
|
||||||
private static let remoteConnectingDetail = "Connecting to remote gateway…"
|
private static let remoteConnectingDetail = "Connecting to remote gateway…"
|
||||||
|
private static let staticLogger = Logger(subsystem: "com.clawdbot", category: "gateway-endpoint")
|
||||||
|
private enum EnvOverrideWarningKind: Sendable {
|
||||||
|
case token
|
||||||
|
case password
|
||||||
|
}
|
||||||
|
|
||||||
|
private static let envOverrideWarnings = LockIsolated((token: false, password: false))
|
||||||
|
|
||||||
struct Deps: Sendable {
|
struct Deps: Sendable {
|
||||||
let mode: @Sendable () async -> AppState.ConnectionMode
|
let mode: @Sendable () async -> AppState.ConnectionMode
|
||||||
@@ -30,16 +38,18 @@ actor GatewayEndpointStore {
|
|||||||
mode: { await MainActor.run { AppStateStore.shared.connectionMode } },
|
mode: { await MainActor.run { AppStateStore.shared.connectionMode } },
|
||||||
token: {
|
token: {
|
||||||
let root = ClawdbotConfigFile.loadDict()
|
let root = ClawdbotConfigFile.loadDict()
|
||||||
|
let isRemote = ConnectionModeResolver.resolve(root: root).mode == .remote
|
||||||
return GatewayEndpointStore.resolveGatewayToken(
|
return GatewayEndpointStore.resolveGatewayToken(
|
||||||
isRemote: CommandResolver.connectionModeIsRemote(),
|
isRemote: isRemote,
|
||||||
root: root,
|
root: root,
|
||||||
env: ProcessInfo.processInfo.environment,
|
env: ProcessInfo.processInfo.environment,
|
||||||
launchdSnapshot: GatewayLaunchAgentManager.launchdConfigSnapshot())
|
launchdSnapshot: GatewayLaunchAgentManager.launchdConfigSnapshot())
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
let root = ClawdbotConfigFile.loadDict()
|
let root = ClawdbotConfigFile.loadDict()
|
||||||
|
let isRemote = ConnectionModeResolver.resolve(root: root).mode == .remote
|
||||||
return GatewayEndpointStore.resolveGatewayPassword(
|
return GatewayEndpointStore.resolveGatewayPassword(
|
||||||
isRemote: CommandResolver.connectionModeIsRemote(),
|
isRemote: isRemote,
|
||||||
root: root,
|
root: root,
|
||||||
env: ProcessInfo.processInfo.environment,
|
env: ProcessInfo.processInfo.environment,
|
||||||
launchdSnapshot: GatewayLaunchAgentManager.launchdConfigSnapshot())
|
launchdSnapshot: GatewayLaunchAgentManager.launchdConfigSnapshot())
|
||||||
@@ -68,6 +78,14 @@ actor GatewayEndpointStore {
|
|||||||
let raw = env["CLAWDBOT_GATEWAY_PASSWORD"] ?? ""
|
let raw = env["CLAWDBOT_GATEWAY_PASSWORD"] ?? ""
|
||||||
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
if !trimmed.isEmpty {
|
if !trimmed.isEmpty {
|
||||||
|
if let configPassword = self.resolveConfigPassword(isRemote: isRemote, root: root),
|
||||||
|
!configPassword.isEmpty
|
||||||
|
{
|
||||||
|
self.warnEnvOverrideOnce(
|
||||||
|
kind: .password,
|
||||||
|
envVar: "CLAWDBOT_GATEWAY_PASSWORD",
|
||||||
|
configKey: isRemote ? "gateway.remote.password" : "gateway.auth.password")
|
||||||
|
}
|
||||||
return trimmed
|
return trimmed
|
||||||
}
|
}
|
||||||
if isRemote {
|
if isRemote {
|
||||||
@@ -99,6 +117,26 @@ actor GatewayEndpointStore {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func resolveConfigPassword(isRemote: Bool, root: [String: Any]) -> String? {
|
||||||
|
if isRemote {
|
||||||
|
if let gateway = root["gateway"] as? [String: Any],
|
||||||
|
let remote = gateway["remote"] as? [String: Any],
|
||||||
|
let password = remote["password"] as? String
|
||||||
|
{
|
||||||
|
return password.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let gateway = root["gateway"] as? [String: Any],
|
||||||
|
let auth = gateway["auth"] as? [String: Any],
|
||||||
|
let password = auth["password"] as? String
|
||||||
|
{
|
||||||
|
return password.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
private static func resolveGatewayToken(
|
private static func resolveGatewayToken(
|
||||||
isRemote: Bool,
|
isRemote: Bool,
|
||||||
root: [String: Any],
|
root: [String: Any],
|
||||||
@@ -108,6 +146,14 @@ actor GatewayEndpointStore {
|
|||||||
let raw = env["CLAWDBOT_GATEWAY_TOKEN"] ?? ""
|
let raw = env["CLAWDBOT_GATEWAY_TOKEN"] ?? ""
|
||||||
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
if !trimmed.isEmpty {
|
if !trimmed.isEmpty {
|
||||||
|
if let configToken = self.resolveConfigToken(isRemote: isRemote, root: root),
|
||||||
|
!configToken.isEmpty
|
||||||
|
{
|
||||||
|
self.warnEnvOverrideOnce(
|
||||||
|
kind: .token,
|
||||||
|
envVar: "CLAWDBOT_GATEWAY_TOKEN",
|
||||||
|
configKey: isRemote ? "gateway.remote.token" : "gateway.auth.token")
|
||||||
|
}
|
||||||
return trimmed
|
return trimmed
|
||||||
}
|
}
|
||||||
if isRemote {
|
if isRemote {
|
||||||
@@ -139,6 +185,49 @@ actor GatewayEndpointStore {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func resolveConfigToken(isRemote: Bool, root: [String: Any]) -> String? {
|
||||||
|
if isRemote {
|
||||||
|
if let gateway = root["gateway"] as? [String: Any],
|
||||||
|
let remote = gateway["remote"] as? [String: Any],
|
||||||
|
let token = remote["token"] as? String
|
||||||
|
{
|
||||||
|
return token.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let gateway = root["gateway"] as? [String: Any],
|
||||||
|
let auth = gateway["auth"] as? [String: Any],
|
||||||
|
let token = auth["token"] as? String
|
||||||
|
{
|
||||||
|
return token.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func warnEnvOverrideOnce(
|
||||||
|
kind: EnvOverrideWarningKind,
|
||||||
|
envVar: String,
|
||||||
|
configKey: String)
|
||||||
|
{
|
||||||
|
let shouldWarn = Self.envOverrideWarnings.withValue { state in
|
||||||
|
switch kind {
|
||||||
|
case .token:
|
||||||
|
guard !state.token else { return false }
|
||||||
|
state.token = true
|
||||||
|
return true
|
||||||
|
case .password:
|
||||||
|
guard !state.password else { return false }
|
||||||
|
state.password = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
guard shouldWarn else { return }
|
||||||
|
Self.staticLogger.warning(
|
||||||
|
"\(envVar, privacy: .public) is set and overrides \(configKey, privacy: .public). " +
|
||||||
|
"If this is unintentional, clear it with: launchctl unsetenv \(envVar, privacy: .public)")
|
||||||
|
}
|
||||||
|
|
||||||
private let deps: Deps
|
private let deps: Deps
|
||||||
private let logger = Logger(subsystem: "com.clawdbot", category: "gateway-endpoint")
|
private let logger = Logger(subsystem: "com.clawdbot", category: "gateway-endpoint")
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,10 @@ extension GatewayLaunchAgentManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static func readDaemonLoaded() async -> Bool? {
|
private static func readDaemonLoaded() async -> Bool? {
|
||||||
let result = await self.runDaemonCommand(["status", "--json", "--no-probe"], timeout: 15, quiet: true)
|
let result = await self.runDaemonCommandResult(
|
||||||
|
["status", "--json", "--no-probe"],
|
||||||
|
timeout: 15,
|
||||||
|
quiet: true)
|
||||||
guard result.success, let payload = result.payload else { return nil }
|
guard result.success, let payload = result.payload else { return nil }
|
||||||
guard
|
guard
|
||||||
let json = try? JSONSerialization.jsonObject(with: payload) as? [String: Any],
|
let json = try? JSONSerialization.jsonObject(with: payload) as? [String: Any],
|
||||||
|
|||||||
@@ -87,6 +87,14 @@ final class GatewayProcessManager {
|
|||||||
self.status = .stopped
|
self.status = .stopped
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Many surfaces can call `setActive(true)` in quick succession (startup, Canvas, health checks).
|
||||||
|
// Avoid spawning multiple concurrent "start" tasks that can thrash launchd and flap the port.
|
||||||
|
switch self.status {
|
||||||
|
case .starting, .running, .attachedExisting:
|
||||||
|
return
|
||||||
|
case .stopped, .failed:
|
||||||
|
break
|
||||||
|
}
|
||||||
self.status = .starting
|
self.status = .starting
|
||||||
self.logger.debug("gateway start requested")
|
self.logger.debug("gateway start requested")
|
||||||
|
|
||||||
|
|||||||
@@ -92,10 +92,13 @@ struct GeneralSettings: View {
|
|||||||
Text(policy.title).tag(policy)
|
Text(policy.title).tag(policy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.pickerStyle(.segmented)
|
.labelsHidden()
|
||||||
|
.pickerStyle(.menu)
|
||||||
|
|
||||||
Text("""
|
Text("""
|
||||||
Controls remote command execution on this Mac when it is paired as a node. "Always Ask" prompts on each command; "Always Allow" runs without prompts; "Never" disables `system.run`.
|
Controls remote command execution on this Mac when it is paired as a node. \
|
||||||
|
"Always Ask" prompts on each command; "Always Allow" runs without prompts; \
|
||||||
|
"Never" disables `system.run`.
|
||||||
""")
|
""")
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundStyle(.tertiary)
|
.foregroundStyle(.tertiary)
|
||||||
@@ -111,7 +114,8 @@ struct GeneralSettings: View {
|
|||||||
Text("While Using").tag(ClawdbotLocationMode.whileUsing.rawValue)
|
Text("While Using").tag(ClawdbotLocationMode.whileUsing.rawValue)
|
||||||
Text("Always").tag(ClawdbotLocationMode.always.rawValue)
|
Text("Always").tag(ClawdbotLocationMode.always.rawValue)
|
||||||
}
|
}
|
||||||
.pickerStyle(.segmented)
|
.labelsHidden()
|
||||||
|
.pickerStyle(.menu)
|
||||||
|
|
||||||
Toggle("Precise Location", isOn: self.$locationPreciseEnabled)
|
Toggle("Precise Location", isOn: self.$locationPreciseEnabled)
|
||||||
.disabled(self.locationMode == .off)
|
.disabled(self.locationMode == .off)
|
||||||
|
|||||||
@@ -395,7 +395,7 @@ extension InstancesSettings {
|
|||||||
host: "phone",
|
host: "phone",
|
||||||
ip: "10.0.0.3",
|
ip: "10.0.0.3",
|
||||||
version: "2.0.0",
|
version: "2.0.0",
|
||||||
platform: "iOS 17.2",
|
platform: "iOS 18.0",
|
||||||
deviceFamily: "iPhone",
|
deviceFamily: "iPhone",
|
||||||
modelIdentifier: nil,
|
modelIdentifier: nil,
|
||||||
lastInputSeconds: 35,
|
lastInputSeconds: 35,
|
||||||
@@ -446,7 +446,7 @@ extension InstancesSettings {
|
|||||||
_ = view.platformIcon("watchOS 10")
|
_ = view.platformIcon("watchOS 10")
|
||||||
_ = view.platformIcon("unknown 1.0")
|
_ = view.platformIcon("unknown 1.0")
|
||||||
_ = view.prettyPlatform("macOS 14.2")
|
_ = view.prettyPlatform("macOS 14.2")
|
||||||
_ = view.prettyPlatform("iOS 17")
|
_ = view.prettyPlatform("iOS 18")
|
||||||
_ = view.prettyPlatform("ipados 17.1")
|
_ = view.prettyPlatform("ipados 17.1")
|
||||||
_ = view.prettyPlatform("linux")
|
_ = view.prettyPlatform("linux")
|
||||||
_ = view.prettyPlatform(" ")
|
_ = view.prettyPlatform(" ")
|
||||||
|
|||||||
@@ -460,7 +460,7 @@ actor MacNodeBridgeSession {
|
|||||||
do {
|
do {
|
||||||
try await self.send(response)
|
try await self.send(response)
|
||||||
} catch {
|
} catch {
|
||||||
await self.logInvokeSendFailure(error)
|
self.logInvokeSendFailure(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,14 +19,14 @@ enum MacNodeBridgeTLSStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func loadFingerprint(stableID: String) -> String? {
|
static func loadFingerprint(stableID: String) -> String? {
|
||||||
let key = keyPrefix + stableID
|
let key = self.keyPrefix + stableID
|
||||||
let raw = defaults.string(forKey: key)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
let raw = self.defaults.string(forKey: key)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
return raw?.isEmpty == false ? raw : nil
|
return raw?.isEmpty == false ? raw : nil
|
||||||
}
|
}
|
||||||
|
|
||||||
static func saveFingerprint(_ value: String, stableID: String) {
|
static func saveFingerprint(_ value: String, stableID: String) {
|
||||||
let key = keyPrefix + stableID
|
let key = self.keyPrefix + stableID
|
||||||
defaults.set(value, forKey: key)
|
self.defaults.set(value, forKey: key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,11 +40,10 @@ func makeMacNodeTLSOptions(_ params: MacNodeBridgeTLSParams?) -> NWProtocolTLS.O
|
|||||||
sec_protocol_options_set_verify_block(
|
sec_protocol_options_set_verify_block(
|
||||||
options.securityProtocolOptions,
|
options.securityProtocolOptions,
|
||||||
{ _, trust, complete in
|
{ _, trust, complete in
|
||||||
guard let trust else {
|
let trustRef = sec_trust_copy_ref(trust).takeRetainedValue()
|
||||||
complete(false)
|
if let chain = SecTrustCopyCertificateChain(trustRef) as? [SecCertificate],
|
||||||
return
|
let cert = chain.first
|
||||||
}
|
{
|
||||||
if let cert = SecTrustGetCertificateAtIndex(trust, 0) {
|
|
||||||
let data = SecCertificateCopyData(cert) as Data
|
let data = SecCertificateCopyData(cert) as Data
|
||||||
let fingerprint = sha256Hex(data)
|
let fingerprint = sha256Hex(data)
|
||||||
if let expected {
|
if let expected {
|
||||||
@@ -57,7 +56,7 @@ func makeMacNodeTLSOptions(_ params: MacNodeBridgeTLSParams?) -> NWProtocolTLS.O
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let ok = SecTrustEvaluateWithError(trust, nil)
|
let ok = SecTrustEvaluateWithError(trustRef, nil)
|
||||||
complete(ok)
|
complete(ok)
|
||||||
},
|
},
|
||||||
DispatchQueue(label: "com.clawdbot.macos.bridge.tls.verify"))
|
DispatchQueue(label: "com.clawdbot.macos.bridge.tls.verify"))
|
||||||
@@ -71,5 +70,5 @@ private func sha256Hex(_ data: Data) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func normalizeMacNodeFingerprint(_ raw: String) -> String {
|
private func normalizeMacNodeFingerprint(_ raw: String) -> String {
|
||||||
raw.lowercased().filter { $0.isHexDigit }
|
raw.lowercased().filter(\.isHexDigit)
|
||||||
}
|
}
|
||||||
@@ -164,6 +164,7 @@ final class MacNodeModeCoordinator {
|
|||||||
]
|
]
|
||||||
|
|
||||||
if SystemRunPolicy.load() != .never {
|
if SystemRunPolicy.load() != .never {
|
||||||
|
commands.append(ClawdbotSystemCommand.which.rawValue)
|
||||||
commands.append(ClawdbotSystemCommand.run.rawValue)
|
commands.append(ClawdbotSystemCommand.run.rawValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,7 +464,7 @@ final class MacNodeModeCoordinator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func targetFromResult(_ result: NWBrowser.Result) -> BridgeTarget? {
|
private nonisolated static func targetFromResult(_ result: NWBrowser.Result) -> BridgeTarget? {
|
||||||
let endpoint = result.endpoint
|
let endpoint = result.endpoint
|
||||||
guard case .service = endpoint else { return nil }
|
guard case .service = endpoint else { return nil }
|
||||||
let stableID = BridgeEndpointID.stableID(endpoint)
|
let stableID = BridgeEndpointID.stableID(endpoint)
|
||||||
@@ -477,7 +478,7 @@ final class MacNodeModeCoordinator {
|
|||||||
return BridgeTarget(endpoint: endpoint, stableID: stableID, tls: tlsParams)
|
return BridgeTarget(endpoint: endpoint, stableID: stableID, tls: tlsParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func resolveDiscoveredTLSParams(
|
private nonisolated static func resolveDiscoveredTLSParams(
|
||||||
stableID: String,
|
stableID: String,
|
||||||
tlsEnabled: Bool,
|
tlsEnabled: Bool,
|
||||||
tlsFingerprintSha256: String?) -> MacNodeBridgeTLSParams?
|
tlsFingerprintSha256: String?) -> MacNodeBridgeTLSParams?
|
||||||
@@ -503,7 +504,7 @@ final class MacNodeModeCoordinator {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func resolveManualTLSParams(stableID: String) -> MacNodeBridgeTLSParams? {
|
private nonisolated static func resolveManualTLSParams(stableID: String) -> MacNodeBridgeTLSParams? {
|
||||||
if let stored = MacNodeBridgeTLSStore.loadFingerprint(stableID: stableID) {
|
if let stored = MacNodeBridgeTLSStore.loadFingerprint(stableID: stableID) {
|
||||||
return MacNodeBridgeTLSParams(
|
return MacNodeBridgeTLSParams(
|
||||||
required: true,
|
required: true,
|
||||||
@@ -519,12 +520,12 @@ final class MacNodeModeCoordinator {
|
|||||||
storeKey: stableID)
|
storeKey: stableID)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func txtValue(_ dict: [String: String], key: String) -> String? {
|
private nonisolated static func txtValue(_ dict: [String: String], key: String) -> String? {
|
||||||
let raw = dict[key]?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
let raw = dict[key]?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
return raw.isEmpty ? nil : raw
|
return raw.isEmpty ? nil : raw
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func txtBoolValue(_ dict: [String: String], key: String) -> Bool {
|
private nonisolated static func txtBoolValue(_ dict: [String: String], key: String) -> Bool {
|
||||||
guard let raw = self.txtValue(dict, key: key)?.lowercased() else { return false }
|
guard let raw = self.txtValue(dict, key: key)?.lowercased() else { return false }
|
||||||
return raw == "1" || raw == "true" || raw == "yes"
|
return raw == "1" || raw == "true" || raw == "yes"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ actor MacNodeRuntime {
|
|||||||
return try await self.handleScreenRecordInvoke(req)
|
return try await self.handleScreenRecordInvoke(req)
|
||||||
case ClawdbotSystemCommand.run.rawValue:
|
case ClawdbotSystemCommand.run.rawValue:
|
||||||
return try await self.handleSystemRun(req)
|
return try await self.handleSystemRun(req)
|
||||||
|
case ClawdbotSystemCommand.which.rawValue:
|
||||||
|
return try await self.handleSystemWhich(req)
|
||||||
case ClawdbotSystemCommand.notify.rawValue:
|
case ClawdbotSystemCommand.notify.rawValue:
|
||||||
return try await self.handleSystemNotify(req)
|
return try await self.handleSystemNotify(req)
|
||||||
default:
|
default:
|
||||||
@@ -494,6 +496,33 @@ actor MacNodeRuntime {
|
|||||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
|
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func handleSystemWhich(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
|
||||||
|
let params = try Self.decodeParams(ClawdbotSystemWhichParams.self, from: req.paramsJSON)
|
||||||
|
let bins = params.bins
|
||||||
|
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||||
|
.filter { !$0.isEmpty }
|
||||||
|
guard !bins.isEmpty else {
|
||||||
|
return Self.errorResponse(req, code: .invalidRequest, message: "INVALID_REQUEST: bins required")
|
||||||
|
}
|
||||||
|
|
||||||
|
let searchPaths = CommandResolver.preferredPaths()
|
||||||
|
var matches: [String] = []
|
||||||
|
var paths: [String: String] = [:]
|
||||||
|
for bin in bins {
|
||||||
|
if let path = CommandResolver.findExecutable(named: bin, searchPaths: searchPaths) {
|
||||||
|
matches.append(bin)
|
||||||
|
paths[bin] = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WhichPayload: Encodable {
|
||||||
|
let bins: [String]
|
||||||
|
let paths: [String: String]
|
||||||
|
}
|
||||||
|
let payload = try Self.encodePayload(WhichPayload(bins: matches, paths: paths))
|
||||||
|
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload)
|
||||||
|
}
|
||||||
|
|
||||||
private func handleSystemNotify(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
|
private func handleSystemNotify(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
|
||||||
let params = try Self.decodeParams(ClawdbotSystemNotifyParams.self, from: req.paramsJSON)
|
let params = try Self.decodeParams(ClawdbotSystemNotifyParams.self, from: req.paramsJSON)
|
||||||
let title = params.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
let title = params.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
@@ -586,8 +615,8 @@ actor MacNodeRuntime {
|
|||||||
let key = rawKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
let key = rawKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
guard !key.isEmpty else { continue }
|
guard !key.isEmpty else { continue }
|
||||||
let upper = key.uppercased()
|
let upper = key.uppercased()
|
||||||
if blockedEnvKeys.contains(upper) { continue }
|
if self.blockedEnvKeys.contains(upper) { continue }
|
||||||
if blockedEnvPrefixes.contains(where: { upper.hasPrefix($0) }) { continue }
|
if self.blockedEnvPrefixes.contains(where: { upper.hasPrefix($0) }) { continue }
|
||||||
merged[key] = value
|
merged[key] = value
|
||||||
}
|
}
|
||||||
return merged
|
return merged
|
||||||
|
|||||||
@@ -351,10 +351,11 @@ actor PortGuardian {
|
|||||||
if port == GatewayEnvironment.gatewayPort() { return cmd.contains("ssh") }
|
if port == GatewayEnvironment.gatewayPort() { return cmd.contains("ssh") }
|
||||||
return false
|
return false
|
||||||
case .local:
|
case .local:
|
||||||
if !cmd.contains("clawdbot") { return false }
|
// The gateway daemon may listen as `clawdbot` or as its runtime (`node`, `bun`, etc).
|
||||||
if full.contains("gateway-daemon") { return true }
|
if full.contains("gateway-daemon") { return true }
|
||||||
// If args are unavailable, treat a clawdbot listener as expected.
|
// If args are unavailable, treat a clawdbot listener as expected.
|
||||||
return full == cmd
|
if cmd.contains("clawdbot"), full == cmd { return true }
|
||||||
|
return false
|
||||||
case .unconfigured:
|
case .unconfigured:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ enum SystemRunPolicy: String, CaseIterable, Identifiable {
|
|||||||
var title: String {
|
var title: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .never:
|
case .never:
|
||||||
return "Never"
|
"Never"
|
||||||
case .ask:
|
case .ask:
|
||||||
return "Always Ask"
|
"Always Ask"
|
||||||
case .always:
|
case .always:
|
||||||
return "Always Allow"
|
"Always Allow"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,13 +75,13 @@ enum SystemRunAllowlist {
|
|||||||
|
|
||||||
static func contains(_ argv: [String], defaults: UserDefaults = .standard) -> Bool {
|
static func contains(_ argv: [String], defaults: UserDefaults = .standard) -> Bool {
|
||||||
let key = key(for: argv)
|
let key = key(for: argv)
|
||||||
return load(from: defaults).contains(key)
|
return self.load(from: defaults).contains(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func add(_ argv: [String], defaults: UserDefaults = .standard) {
|
static func add(_ argv: [String], defaults: UserDefaults = .standard) {
|
||||||
let key = key(for: argv)
|
let key = key(for: argv)
|
||||||
guard !key.isEmpty else { return }
|
guard !key.isEmpty else { return }
|
||||||
var allowlist = load(from: defaults)
|
var allowlist = self.load(from: defaults)
|
||||||
if allowlist.insert(key).inserted {
|
if allowlist.insert(key).inserted {
|
||||||
MacNodeConfigFile.setSystemRunAllowlist(Array(allowlist).sorted())
|
MacNodeConfigFile.setSystemRunAllowlist(Array(allowlist).sorted())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import ClawdbotProtocol
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Testing
|
import Testing
|
||||||
@testable import Clawdbot
|
@testable import Clawdbot
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ struct CronJobEditorSmokeTests {
|
|||||||
thinking: "low",
|
thinking: "low",
|
||||||
timeoutSeconds: 120,
|
timeoutSeconds: 120,
|
||||||
deliver: true,
|
deliver: true,
|
||||||
provider: "whatsapp",
|
channel: "whatsapp",
|
||||||
to: "+15551234567",
|
to: "+15551234567",
|
||||||
bestEffortDeliver: true),
|
bestEffortDeliver: true),
|
||||||
isolation: CronIsolation(postToMainPrefix: "Cron"),
|
isolation: CronIsolation(postToMainPrefix: "Cron"),
|
||||||
@@ -70,22 +70,16 @@ struct CronJobEditorSmokeTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test func cronJobEditorIncludesDeleteAfterRunForAtSchedule() throws {
|
@Test func cronJobEditorIncludesDeleteAfterRunForAtSchedule() throws {
|
||||||
var view = CronJobEditor(
|
let view = CronJobEditor(
|
||||||
job: nil,
|
job: nil,
|
||||||
isSaving: .constant(false),
|
isSaving: .constant(false),
|
||||||
error: .constant(nil),
|
error: .constant(nil),
|
||||||
onCancel: {},
|
onCancel: {},
|
||||||
onSave: { _ in })
|
onSave: { _ in })
|
||||||
view.name = "One-shot"
|
|
||||||
view.sessionTarget = .main
|
|
||||||
view.payloadKind = .systemEvent
|
|
||||||
view.systemEventText = "hello"
|
|
||||||
view.scheduleKind = .at
|
|
||||||
view.atDate = Date(timeIntervalSince1970: 1_700_000_000)
|
|
||||||
view.deleteAfterRun = true
|
|
||||||
|
|
||||||
let payload = try view.buildPayload()
|
var root: [String: Any] = [:]
|
||||||
let raw = payload["deleteAfterRun"]?.value as? Bool
|
view.applyDeleteAfterRun(to: &root, scheduleKind: .at, deleteAfterRun: true)
|
||||||
|
let raw = root["deleteAfterRun"] as? Bool
|
||||||
#expect(raw == true)
|
#expect(raw == true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ struct CronModelsTests {
|
|||||||
thinking: "low",
|
thinking: "low",
|
||||||
timeoutSeconds: 15,
|
timeoutSeconds: 15,
|
||||||
deliver: true,
|
deliver: true,
|
||||||
provider: "whatsapp",
|
channel: "whatsapp",
|
||||||
to: "+15551234567",
|
to: "+15551234567",
|
||||||
bestEffortDeliver: false)
|
bestEffortDeliver: false)
|
||||||
let data = try JSONEncoder().encode(payload)
|
let data = try JSONEncoder().encode(payload)
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ import Testing
|
|||||||
@testable import Clawdbot
|
@testable import Clawdbot
|
||||||
|
|
||||||
@Suite struct GatewayEndpointStoreTests {
|
@Suite struct GatewayEndpointStoreTests {
|
||||||
|
private func makeDefaults() -> UserDefaults {
|
||||||
|
let suiteName = "GatewayEndpointStoreTests.\(UUID().uuidString)"
|
||||||
|
let defaults = UserDefaults(suiteName: suiteName)!
|
||||||
|
defaults.removePersistentDomain(forName: suiteName)
|
||||||
|
return defaults
|
||||||
|
}
|
||||||
|
|
||||||
@Test func resolveGatewayTokenPrefersEnvAndFallsBackToLaunchd() {
|
@Test func resolveGatewayTokenPrefersEnvAndFallsBackToLaunchd() {
|
||||||
let snapshot = LaunchAgentPlistSnapshot(
|
let snapshot = LaunchAgentPlistSnapshot(
|
||||||
programArguments: [],
|
programArguments: [],
|
||||||
@@ -66,4 +73,70 @@ import Testing
|
|||||||
launchdSnapshot: snapshot)
|
launchdSnapshot: snapshot)
|
||||||
#expect(password == "launchd-pass")
|
#expect(password == "launchd-pass")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test func connectionModeResolverPrefersConfigModeOverDefaults() {
|
||||||
|
let defaults = self.makeDefaults()
|
||||||
|
defaults.set("remote", forKey: connectionModeKey)
|
||||||
|
|
||||||
|
let root: [String: Any] = [
|
||||||
|
"gateway": [
|
||||||
|
"mode": " local ",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
let resolved = ConnectionModeResolver.resolve(root: root, defaults: defaults)
|
||||||
|
#expect(resolved.mode == .local)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func connectionModeResolverTrimsConfigMode() {
|
||||||
|
let defaults = self.makeDefaults()
|
||||||
|
defaults.set("local", forKey: connectionModeKey)
|
||||||
|
|
||||||
|
let root: [String: Any] = [
|
||||||
|
"gateway": [
|
||||||
|
"mode": " remote ",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
let resolved = ConnectionModeResolver.resolve(root: root, defaults: defaults)
|
||||||
|
#expect(resolved.mode == .remote)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func connectionModeResolverFallsBackToDefaultsWhenMissingConfig() {
|
||||||
|
let defaults = self.makeDefaults()
|
||||||
|
defaults.set("remote", forKey: connectionModeKey)
|
||||||
|
|
||||||
|
let resolved = ConnectionModeResolver.resolve(root: [:], defaults: defaults)
|
||||||
|
#expect(resolved.mode == .remote)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func connectionModeResolverFallsBackToDefaultsOnUnknownConfig() {
|
||||||
|
let defaults = self.makeDefaults()
|
||||||
|
defaults.set("local", forKey: connectionModeKey)
|
||||||
|
|
||||||
|
let root: [String: Any] = [
|
||||||
|
"gateway": [
|
||||||
|
"mode": "staging",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
let resolved = ConnectionModeResolver.resolve(root: root, defaults: defaults)
|
||||||
|
#expect(resolved.mode == .local)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func connectionModeResolverPrefersRemoteURLWhenModeMissing() {
|
||||||
|
let defaults = self.makeDefaults()
|
||||||
|
defaults.set("local", forKey: connectionModeKey)
|
||||||
|
|
||||||
|
let root: [String: Any] = [
|
||||||
|
"gateway": [
|
||||||
|
"remote": [
|
||||||
|
"url": " ws://umbrel:18789 ",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
let resolved = ConnectionModeResolver.resolve(root: root, defaults: defaults)
|
||||||
|
#expect(resolved.mode == .remote)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ struct InstancesSettingsSmokeTests {
|
|||||||
host: "gateway",
|
host: "gateway",
|
||||||
ip: "10.0.0.4",
|
ip: "10.0.0.4",
|
||||||
version: "3.0.0",
|
version: "3.0.0",
|
||||||
platform: "iOS 17",
|
platform: "iOS 18",
|
||||||
deviceFamily: nil,
|
deviceFamily: nil,
|
||||||
modelIdentifier: nil,
|
modelIdentifier: nil,
|
||||||
lastInputSeconds: nil,
|
lastInputSeconds: nil,
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import Testing
|
|||||||
stateversion: StateVersion(presence: 1, health: 1),
|
stateversion: StateVersion(presence: 1, health: 1),
|
||||||
uptimems: 123,
|
uptimems: 123,
|
||||||
configpath: nil,
|
configpath: nil,
|
||||||
statedir: nil)
|
statedir: nil,
|
||||||
|
sessiondefaults: nil)
|
||||||
|
|
||||||
let hello = HelloOk(
|
let hello = HelloOk(
|
||||||
type: "hello",
|
type: "hello",
|
||||||
|
|||||||
@@ -21,6 +21,15 @@ struct MacNodeRuntimeTests {
|
|||||||
#expect(response.ok == false)
|
#expect(response.ok == false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test func handleInvokeRejectsEmptySystemWhich() async throws {
|
||||||
|
let runtime = MacNodeRuntime()
|
||||||
|
let params = ClawdbotSystemWhichParams(bins: [])
|
||||||
|
let json = try String(data: JSONEncoder().encode(params), encoding: .utf8)
|
||||||
|
let response = await runtime.handleInvoke(
|
||||||
|
BridgeInvokeRequest(id: "req-2b", command: ClawdbotSystemCommand.which.rawValue, paramsJSON: json))
|
||||||
|
#expect(response.ok == false)
|
||||||
|
}
|
||||||
|
|
||||||
@Test func handleInvokeRejectsEmptyNotification() async throws {
|
@Test func handleInvokeRejectsEmptyNotification() async throws {
|
||||||
let runtime = MacNodeRuntime()
|
let runtime = MacNodeRuntime()
|
||||||
let params = ClawdbotSystemNotifyParams(title: "", body: "")
|
let params = ClawdbotSystemNotifyParams(title: "", body: "")
|
||||||
@@ -65,6 +74,10 @@ struct MacNodeRuntimeTests {
|
|||||||
{
|
{
|
||||||
CLLocation(latitude: 0, longitude: 0)
|
CLLocation(latitude: 0, longitude: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func confirmSystemRun(command: String, cwd: String?) async -> SystemRunDecision {
|
||||||
|
.allowOnce
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let services = await MainActor.run { FakeMainActorServices() }
|
let services = await MainActor.run { FakeMainActorServices() }
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ struct OnboardingViewSmokeTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test func pageOrderOmitsWorkspaceAndIdentitySteps() {
|
@Test func pageOrderOmitsWorkspaceAndIdentitySteps() {
|
||||||
let order = OnboardingView.pageOrder(for: .local, needsBootstrap: false)
|
let order = OnboardingView.pageOrder(for: .local, showOnboardingChat: false)
|
||||||
#expect(!order.contains(7))
|
#expect(!order.contains(7))
|
||||||
#expect(order.contains(3))
|
#expect(order.contains(3))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func pageOrderOmitsOnboardingChatWhenIdentityKnown() {
|
@Test func pageOrderOmitsOnboardingChatWhenIdentityKnown() {
|
||||||
let order = OnboardingView.pageOrder(for: .local, needsBootstrap: false)
|
let order = OnboardingView.pageOrder(for: .local, showOnboardingChat: false)
|
||||||
#expect(!order.contains(8))
|
#expect(!order.contains(8))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,7 @@ struct SettingsViewSmokeTests {
|
|||||||
thinking: "low",
|
thinking: "low",
|
||||||
timeoutSeconds: 30,
|
timeoutSeconds: 30,
|
||||||
deliver: true,
|
deliver: true,
|
||||||
provider: "sms",
|
channel: "sms",
|
||||||
to: "+15551234567",
|
to: "+15551234567",
|
||||||
bestEffortDeliver: true),
|
bestEffortDeliver: true),
|
||||||
isolation: CronIsolation(postToMainPrefix: "[cron] "),
|
isolation: CronIsolation(postToMainPrefix: "[cron] "),
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import Testing
|
|||||||
let wav = makeWav16Mono(sampleRate: 8000, samples: 80)
|
let wav = makeWav16Mono(sampleRate: 8000, samples: 80)
|
||||||
defer { _ = TalkAudioPlayer.shared.stop() }
|
defer { _ = TalkAudioPlayer.shared.stop() }
|
||||||
|
|
||||||
_ = try await withTimeout(seconds: 2.0) {
|
_ = try await withTimeout(seconds: 4.0) {
|
||||||
await TalkAudioPlayer.shared.play(data: wav)
|
await TalkAudioPlayer.shared.play(data: wav)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ import Testing
|
|||||||
await Task.yield()
|
await Task.yield()
|
||||||
_ = await TalkAudioPlayer.shared.play(data: wav)
|
_ = await TalkAudioPlayer.shared.play(data: wav)
|
||||||
|
|
||||||
_ = try await withTimeout(seconds: 2.0) {
|
_ = try await withTimeout(seconds: 4.0) {
|
||||||
await first.value
|
await first.value
|
||||||
}
|
}
|
||||||
#expect(true)
|
#expect(true)
|
||||||
|
|||||||
@@ -17,6 +17,6 @@ import Testing
|
|||||||
#expect(opts.thinking == "low")
|
#expect(opts.thinking == "low")
|
||||||
#expect(opts.deliver == true)
|
#expect(opts.deliver == true)
|
||||||
#expect(opts.to == nil)
|
#expect(opts.to == nil)
|
||||||
#expect(opts.provider == .last)
|
#expect(opts.channel == .last)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,7 @@ import PackageDescription
|
|||||||
let package = Package(
|
let package = Package(
|
||||||
name: "ClawdbotKit",
|
name: "ClawdbotKit",
|
||||||
platforms: [
|
platforms: [
|
||||||
.iOS(.v17),
|
.iOS(.v18),
|
||||||
.macOS(.v15),
|
.macOS(.v15),
|
||||||
],
|
],
|
||||||
products: [
|
products: [
|
||||||
@@ -14,6 +14,7 @@ let package = Package(
|
|||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/steipete/ElevenLabsKit", exact: "0.1.0"),
|
.package(url: "https://github.com/steipete/ElevenLabsKit", exact: "0.1.0"),
|
||||||
|
.package(url: "https://github.com/gonzalezreal/textual", exact: "0.2.0"),
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
@@ -29,7 +30,13 @@ let package = Package(
|
|||||||
]),
|
]),
|
||||||
.target(
|
.target(
|
||||||
name: "ClawdbotChatUI",
|
name: "ClawdbotChatUI",
|
||||||
dependencies: ["ClawdbotKit"],
|
dependencies: [
|
||||||
|
"ClawdbotKit",
|
||||||
|
.product(
|
||||||
|
name: "Textual",
|
||||||
|
package: "textual",
|
||||||
|
condition: .when(platforms: [.macOS, .iOS])),
|
||||||
|
],
|
||||||
swiftSettings: [
|
swiftSettings: [
|
||||||
.enableUpcomingFeature("StrictConcurrency"),
|
.enableUpcomingFeature("StrictConcurrency"),
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum ChatMarkdownPreprocessor {
|
||||||
|
struct InlineImage: Identifiable {
|
||||||
|
let id = UUID()
|
||||||
|
let label: String
|
||||||
|
let image: ClawdbotPlatformImage?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Result {
|
||||||
|
let cleaned: String
|
||||||
|
let images: [InlineImage]
|
||||||
|
}
|
||||||
|
|
||||||
|
static func preprocess(markdown raw: String) -> Result {
|
||||||
|
let pattern = #"!\[([^\]]*)\]\((data:image\/[^;]+;base64,[^)]+)\)"#
|
||||||
|
guard let re = try? NSRegularExpression(pattern: pattern) else {
|
||||||
|
return Result(cleaned: raw, images: [])
|
||||||
|
}
|
||||||
|
|
||||||
|
let ns = raw as NSString
|
||||||
|
let matches = re.matches(in: raw, range: NSRange(location: 0, length: ns.length))
|
||||||
|
if matches.isEmpty { return Result(cleaned: raw, images: []) }
|
||||||
|
|
||||||
|
var images: [InlineImage] = []
|
||||||
|
var cleaned = raw
|
||||||
|
|
||||||
|
for match in matches.reversed() {
|
||||||
|
guard match.numberOfRanges >= 3 else { continue }
|
||||||
|
let label = ns.substring(with: match.range(at: 1))
|
||||||
|
let dataURL = ns.substring(with: match.range(at: 2))
|
||||||
|
|
||||||
|
let image: ClawdbotPlatformImage? = {
|
||||||
|
guard let comma = dataURL.firstIndex(of: ",") else { return nil }
|
||||||
|
let b64 = String(dataURL[dataURL.index(after: comma)...])
|
||||||
|
guard let data = Data(base64Encoded: b64) else { return nil }
|
||||||
|
return ClawdbotPlatformImage(data: data)
|
||||||
|
}()
|
||||||
|
images.append(InlineImage(label: label, image: image))
|
||||||
|
|
||||||
|
let start = cleaned.index(cleaned.startIndex, offsetBy: match.range.location)
|
||||||
|
let end = cleaned.index(start, offsetBy: match.range.length)
|
||||||
|
cleaned.replaceSubrange(start..<end, with: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
let normalized = cleaned
|
||||||
|
.replacingOccurrences(of: "\n\n\n", with: "\n\n")
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
return Result(cleaned: normalized, images: images.reversed())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import Textual
|
||||||
|
|
||||||
|
public enum ChatMarkdownVariant: String, CaseIterable, Sendable {
|
||||||
|
case standard
|
||||||
|
case compact
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
struct ChatMarkdownRenderer: View {
|
||||||
|
enum Context {
|
||||||
|
case user
|
||||||
|
case assistant
|
||||||
|
}
|
||||||
|
|
||||||
|
let text: String
|
||||||
|
let context: Context
|
||||||
|
let variant: ChatMarkdownVariant
|
||||||
|
let font: Font
|
||||||
|
let textColor: Color
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let processed = ChatMarkdownPreprocessor.preprocess(markdown: self.text)
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
StructuredText(markdown: processed.cleaned)
|
||||||
|
.modifier(ChatMarkdownStyle(
|
||||||
|
variant: self.variant,
|
||||||
|
context: self.context,
|
||||||
|
font: self.font,
|
||||||
|
textColor: self.textColor))
|
||||||
|
|
||||||
|
if !processed.images.isEmpty {
|
||||||
|
InlineImageList(images: processed.images)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ChatMarkdownStyle: ViewModifier {
|
||||||
|
let variant: ChatMarkdownVariant
|
||||||
|
let context: ChatMarkdownRenderer.Context
|
||||||
|
let font: Font
|
||||||
|
let textColor: Color
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
Group {
|
||||||
|
if self.variant == .compact {
|
||||||
|
content.textual.structuredTextStyle(.default)
|
||||||
|
} else {
|
||||||
|
content.textual.structuredTextStyle(.gitHub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(self.font)
|
||||||
|
.foregroundStyle(self.textColor)
|
||||||
|
.textual.inlineStyle(self.inlineStyle)
|
||||||
|
.textual.textSelection(.enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var inlineStyle: InlineStyle {
|
||||||
|
let linkColor: Color = self.context == .user ? self.textColor : .accentColor
|
||||||
|
let codeScale: CGFloat = self.variant == .compact ? 0.85 : 0.9
|
||||||
|
return InlineStyle()
|
||||||
|
.code(.monospaced, .fontScale(codeScale))
|
||||||
|
.link(.foregroundColor(linkColor))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private struct InlineImageList: View {
|
||||||
|
let images: [ChatMarkdownPreprocessor.InlineImage]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ForEach(images, id: \.id) { item in
|
||||||
|
if let img = item.image {
|
||||||
|
ClawdbotPlatformImageFactory.image(img)
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.frame(maxHeight: 260)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||||
|
.strokeBorder(Color.white.opacity(0.12), lineWidth: 1))
|
||||||
|
} else {
|
||||||
|
Text(item.label.isEmpty ? "Image" : item.label)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
enum ChatMarkdownSplitter {
|
|
||||||
struct InlineImage: Identifiable {
|
|
||||||
let id = UUID()
|
|
||||||
let label: String
|
|
||||||
let image: ClawdbotPlatformImage?
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Block: Identifiable {
|
|
||||||
enum Kind: Equatable {
|
|
||||||
case text
|
|
||||||
case code(language: String?)
|
|
||||||
}
|
|
||||||
|
|
||||||
let id = UUID()
|
|
||||||
let kind: Kind
|
|
||||||
let text: String
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SplitResult {
|
|
||||||
let blocks: [Block]
|
|
||||||
let images: [InlineImage]
|
|
||||||
}
|
|
||||||
|
|
||||||
static func split(markdown raw: String) -> SplitResult {
|
|
||||||
let extracted = self.extractInlineImages(from: raw)
|
|
||||||
let blocks = self.splitCodeBlocks(from: extracted.cleaned)
|
|
||||||
return SplitResult(blocks: blocks, images: extracted.images)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func splitCodeBlocks(from raw: String) -> [Block] {
|
|
||||||
var blocks: [Block] = []
|
|
||||||
var buffer: [String] = []
|
|
||||||
var inCode = false
|
|
||||||
var codeLang: String?
|
|
||||||
var codeLines: [String] = []
|
|
||||||
|
|
||||||
for line in raw.split(separator: "\n", omittingEmptySubsequences: false).map(String.init) {
|
|
||||||
if line.hasPrefix("```") {
|
|
||||||
if inCode {
|
|
||||||
blocks.append(Block(kind: .code(language: codeLang), text: codeLines.joined(separator: "\n")))
|
|
||||||
codeLines.removeAll(keepingCapacity: true)
|
|
||||||
inCode = false
|
|
||||||
codeLang = nil
|
|
||||||
} else {
|
|
||||||
let text = buffer.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
if !text.isEmpty {
|
|
||||||
blocks.append(Block(kind: .text, text: text))
|
|
||||||
}
|
|
||||||
buffer.removeAll(keepingCapacity: true)
|
|
||||||
inCode = true
|
|
||||||
codeLang = line.dropFirst(3).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
if codeLang?.isEmpty == true { codeLang = nil }
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if inCode {
|
|
||||||
codeLines.append(line)
|
|
||||||
} else {
|
|
||||||
buffer.append(line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if inCode {
|
|
||||||
blocks.append(Block(kind: .code(language: codeLang), text: codeLines.joined(separator: "\n")))
|
|
||||||
} else {
|
|
||||||
let text = buffer.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
if !text.isEmpty {
|
|
||||||
blocks.append(Block(kind: .text, text: text))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return blocks.isEmpty ? [Block(kind: .text, text: raw)] : blocks
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func extractInlineImages(from raw: String) -> (cleaned: String, images: [InlineImage]) {
|
|
||||||
let pattern = #"!\[([^\]]*)\]\((data:image\/[^;]+;base64,[^)]+)\)"#
|
|
||||||
guard let re = try? NSRegularExpression(pattern: pattern) else {
|
|
||||||
return (raw, [])
|
|
||||||
}
|
|
||||||
|
|
||||||
let ns = raw as NSString
|
|
||||||
let matches = re.matches(in: raw, range: NSRange(location: 0, length: ns.length))
|
|
||||||
if matches.isEmpty { return (raw, []) }
|
|
||||||
|
|
||||||
var images: [InlineImage] = []
|
|
||||||
var cleaned = raw
|
|
||||||
|
|
||||||
for match in matches.reversed() {
|
|
||||||
guard match.numberOfRanges >= 3 else { continue }
|
|
||||||
let label = ns.substring(with: match.range(at: 1))
|
|
||||||
let dataURL = ns.substring(with: match.range(at: 2))
|
|
||||||
|
|
||||||
let image: ClawdbotPlatformImage? = {
|
|
||||||
guard let comma = dataURL.firstIndex(of: ",") else { return nil }
|
|
||||||
let b64 = String(dataURL[dataURL.index(after: comma)...])
|
|
||||||
guard let data = Data(base64Encoded: b64) else { return nil }
|
|
||||||
return ClawdbotPlatformImage(data: data)
|
|
||||||
}()
|
|
||||||
images.append(InlineImage(label: label, image: image))
|
|
||||||
|
|
||||||
let start = cleaned.index(cleaned.startIndex, offsetBy: match.range.location)
|
|
||||||
let end = cleaned.index(start, offsetBy: match.range.length)
|
|
||||||
cleaned.replaceSubrange(start..<end, with: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
let normalized = cleaned
|
|
||||||
.replacingOccurrences(of: "\n\n\n", with: "\n\n")
|
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
return (normalized, images.reversed())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -137,10 +137,16 @@ private struct ChatBubbleShape: InsettableShape {
|
|||||||
struct ChatMessageBubble: View {
|
struct ChatMessageBubble: View {
|
||||||
let message: ClawdbotChatMessage
|
let message: ClawdbotChatMessage
|
||||||
let style: ClawdbotChatView.Style
|
let style: ClawdbotChatView.Style
|
||||||
|
let markdownVariant: ChatMarkdownVariant
|
||||||
let userAccent: Color?
|
let userAccent: Color?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ChatMessageBody(message: self.message, isUser: self.isUser, style: self.style, userAccent: self.userAccent)
|
ChatMessageBody(
|
||||||
|
message: self.message,
|
||||||
|
isUser: self.isUser,
|
||||||
|
style: self.style,
|
||||||
|
markdownVariant: self.markdownVariant,
|
||||||
|
userAccent: self.userAccent)
|
||||||
.frame(maxWidth: ChatUIConstants.bubbleMaxWidth, alignment: self.isUser ? .trailing : .leading)
|
.frame(maxWidth: ChatUIConstants.bubbleMaxWidth, alignment: self.isUser ? .trailing : .leading)
|
||||||
.frame(maxWidth: .infinity, alignment: self.isUser ? .trailing : .leading)
|
.frame(maxWidth: .infinity, alignment: self.isUser ? .trailing : .leading)
|
||||||
.padding(.horizontal, 2)
|
.padding(.horizontal, 2)
|
||||||
@@ -154,6 +160,7 @@ private struct ChatMessageBody: View {
|
|||||||
let message: ClawdbotChatMessage
|
let message: ClawdbotChatMessage
|
||||||
let isUser: Bool
|
let isUser: Bool
|
||||||
let style: ClawdbotChatView.Style
|
let style: ClawdbotChatView.Style
|
||||||
|
let markdownVariant: ChatMarkdownVariant
|
||||||
let userAccent: Color?
|
let userAccent: Color?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -169,39 +176,14 @@ private struct ChatMessageBody: View {
|
|||||||
isUser: self.isUser)
|
isUser: self.isUser)
|
||||||
}
|
}
|
||||||
} else if self.isUser {
|
} else if self.isUser {
|
||||||
let split = ChatMarkdownSplitter.split(markdown: text)
|
ChatMarkdownRenderer(
|
||||||
ForEach(split.blocks) { block in
|
text: text,
|
||||||
switch block.kind {
|
context: .user,
|
||||||
case .text:
|
variant: self.markdownVariant,
|
||||||
MarkdownTextView(text: block.text, textColor: textColor, font: .system(size: 14))
|
font: .system(size: 14),
|
||||||
case let .code(language):
|
textColor: textColor)
|
||||||
CodeBlockView(code: block.text, language: language, isUser: self.isUser)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !split.images.isEmpty {
|
|
||||||
ForEach(
|
|
||||||
split.images,
|
|
||||||
id: \ChatMarkdownSplitter.InlineImage.id)
|
|
||||||
{ (item: ChatMarkdownSplitter.InlineImage) in
|
|
||||||
if let img = item.image {
|
|
||||||
ClawdbotPlatformImageFactory.image(img)
|
|
||||||
.resizable()
|
|
||||||
.scaledToFit()
|
|
||||||
.frame(maxHeight: 260)
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
||||||
.strokeBorder(Color.white.opacity(0.12), lineWidth: 1))
|
|
||||||
} else {
|
|
||||||
Text(item.label.isEmpty ? "Image" : item.label)
|
|
||||||
.font(.footnote)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
ChatAssistantTextBody(text: text)
|
ChatAssistantTextBody(text: text, markdownVariant: self.markdownVariant)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.inlineAttachments.isEmpty {
|
if !self.inlineAttachments.isEmpty {
|
||||||
@@ -505,10 +487,11 @@ extension ChatTypingIndicatorBubble: @MainActor Equatable {
|
|||||||
@MainActor
|
@MainActor
|
||||||
struct ChatStreamingAssistantBubble: View {
|
struct ChatStreamingAssistantBubble: View {
|
||||||
let text: String
|
let text: String
|
||||||
|
let markdownVariant: ChatMarkdownVariant
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
ChatAssistantTextBody(text: self.text)
|
ChatAssistantTextBody(text: self.text, markdownVariant: self.markdownVariant)
|
||||||
}
|
}
|
||||||
.padding(12)
|
.padding(12)
|
||||||
.background(
|
.background(
|
||||||
@@ -612,114 +595,22 @@ private struct TypingDots: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private struct MarkdownTextView: View {
|
|
||||||
let text: String
|
|
||||||
let textColor: Color
|
|
||||||
let font: Font
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
let normalized = self.text.replacingOccurrences(
|
|
||||||
of: "(?<!\\n)\\n(?!\\n)",
|
|
||||||
with: " ",
|
|
||||||
options: .regularExpression)
|
|
||||||
let options = AttributedString.MarkdownParsingOptions(
|
|
||||||
interpretedSyntax: .inlineOnlyPreservingWhitespace)
|
|
||||||
if let attributed = try? AttributedString(markdown: normalized, options: options) {
|
|
||||||
Text(attributed)
|
|
||||||
.font(self.font)
|
|
||||||
.foregroundStyle(self.textColor)
|
|
||||||
} else {
|
|
||||||
Text(normalized)
|
|
||||||
.font(self.font)
|
|
||||||
.foregroundStyle(self.textColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private struct ChatAssistantTextBody: View {
|
private struct ChatAssistantTextBody: View {
|
||||||
let text: String
|
let text: String
|
||||||
|
let markdownVariant: ChatMarkdownVariant
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let segments = AssistantTextParser.segments(from: self.text)
|
let segments = AssistantTextParser.segments(from: self.text)
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
ForEach(segments) { segment in
|
ForEach(segments) { segment in
|
||||||
let font = segment.kind == .thinking ? Font.system(size: 14).italic() : Font.system(size: 14)
|
let font = segment.kind == .thinking ? Font.system(size: 14).italic() : Font.system(size: 14)
|
||||||
ChatMarkdownBody(text: segment.text, textColor: ClawdbotChatTheme.assistantText, font: font)
|
ChatMarkdownRenderer(
|
||||||
|
text: segment.text,
|
||||||
|
context: .assistant,
|
||||||
|
variant: self.markdownVariant,
|
||||||
|
font: font,
|
||||||
|
textColor: ClawdbotChatTheme.assistantText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private struct ChatMarkdownBody: View {
|
|
||||||
let text: String
|
|
||||||
let textColor: Color
|
|
||||||
let font: Font
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
let split = ChatMarkdownSplitter.split(markdown: self.text)
|
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
|
||||||
ForEach(split.blocks) { block in
|
|
||||||
switch block.kind {
|
|
||||||
case .text:
|
|
||||||
MarkdownTextView(text: block.text, textColor: self.textColor, font: self.font)
|
|
||||||
case let .code(language):
|
|
||||||
CodeBlockView(code: block.text, language: language, isUser: false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !split.images.isEmpty {
|
|
||||||
ForEach(
|
|
||||||
split.images,
|
|
||||||
id: \ChatMarkdownSplitter.InlineImage.id)
|
|
||||||
{ (item: ChatMarkdownSplitter.InlineImage) in
|
|
||||||
if let img = item.image {
|
|
||||||
ClawdbotPlatformImageFactory.image(img)
|
|
||||||
.resizable()
|
|
||||||
.scaledToFit()
|
|
||||||
.frame(maxHeight: 260)
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
||||||
.strokeBorder(Color.white.opacity(0.12), lineWidth: 1))
|
|
||||||
} else {
|
|
||||||
Text(item.label.isEmpty ? "Image" : item.label)
|
|
||||||
.font(.footnote)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.textSelection(.enabled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
private struct CodeBlockView: View {
|
|
||||||
let code: String
|
|
||||||
let language: String?
|
|
||||||
let isUser: Bool
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
if let language, !language.isEmpty {
|
|
||||||
Text(language)
|
|
||||||
.font(.caption2.monospaced())
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
Text(self.code)
|
|
||||||
.font(.system(size: 13, weight: .regular, design: .monospaced))
|
|
||||||
.foregroundStyle(self.isUser ? .white : .primary)
|
|
||||||
.textSelection(.enabled)
|
|
||||||
}
|
|
||||||
.padding(12)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.background(self.isUser ? Color.white.opacity(0.16) : Color.black.opacity(0.06))
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
||||||
.strokeBorder(Color.black.opacity(0.08), lineWidth: 1))
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,6 +14,7 @@ public struct ClawdbotChatView: View {
|
|||||||
@State private var hasPerformedInitialScroll = false
|
@State private var hasPerformedInitialScroll = false
|
||||||
private let showsSessionSwitcher: Bool
|
private let showsSessionSwitcher: Bool
|
||||||
private let style: Style
|
private let style: Style
|
||||||
|
private let markdownVariant: ChatMarkdownVariant
|
||||||
private let userAccent: Color?
|
private let userAccent: Color?
|
||||||
|
|
||||||
private enum Layout {
|
private enum Layout {
|
||||||
@@ -42,11 +43,13 @@ public struct ClawdbotChatView: View {
|
|||||||
viewModel: ClawdbotChatViewModel,
|
viewModel: ClawdbotChatViewModel,
|
||||||
showsSessionSwitcher: Bool = false,
|
showsSessionSwitcher: Bool = false,
|
||||||
style: Style = .standard,
|
style: Style = .standard,
|
||||||
|
markdownVariant: ChatMarkdownVariant = .standard,
|
||||||
userAccent: Color? = nil)
|
userAccent: Color? = nil)
|
||||||
{
|
{
|
||||||
self._viewModel = State(initialValue: viewModel)
|
self._viewModel = State(initialValue: viewModel)
|
||||||
self.showsSessionSwitcher = showsSessionSwitcher
|
self.showsSessionSwitcher = showsSessionSwitcher
|
||||||
self.style = style
|
self.style = style
|
||||||
|
self.markdownVariant = markdownVariant
|
||||||
self.userAccent = userAccent
|
self.userAccent = userAccent
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +154,11 @@ public struct ClawdbotChatView: View {
|
|||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var messageListRows: some View {
|
private var messageListRows: some View {
|
||||||
ForEach(self.visibleMessages) { msg in
|
ForEach(self.visibleMessages) { msg in
|
||||||
ChatMessageBubble(message: msg, style: self.style, userAccent: self.userAccent)
|
ChatMessageBubble(
|
||||||
|
message: msg,
|
||||||
|
style: self.style,
|
||||||
|
markdownVariant: self.markdownVariant,
|
||||||
|
userAccent: self.userAccent)
|
||||||
.frame(
|
.frame(
|
||||||
maxWidth: .infinity,
|
maxWidth: .infinity,
|
||||||
alignment: msg.role.lowercased() == "user" ? .trailing : .leading)
|
alignment: msg.role.lowercased() == "user" ? .trailing : .leading)
|
||||||
@@ -172,7 +179,7 @@ public struct ClawdbotChatView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let text = self.viewModel.streamingAssistantText, AssistantTextParser.hasVisibleContent(in: text) {
|
if let text = self.viewModel.streamingAssistantText, AssistantTextParser.hasVisibleContent(in: text) {
|
||||||
ChatStreamingAssistantBubble(text: text)
|
ChatStreamingAssistantBubble(text: text, markdownVariant: self.markdownVariant)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Foundation
|
|||||||
|
|
||||||
public enum ClawdbotSystemCommand: String, Codable, Sendable {
|
public enum ClawdbotSystemCommand: String, Codable, Sendable {
|
||||||
case run = "system.run"
|
case run = "system.run"
|
||||||
|
case which = "system.which"
|
||||||
case notify = "system.notify"
|
case notify = "system.notify"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +40,14 @@ public struct ClawdbotSystemRunParams: Codable, Sendable, Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct ClawdbotSystemWhichParams: Codable, Sendable, Equatable {
|
||||||
|
public var bins: [String]
|
||||||
|
|
||||||
|
public init(bins: [String]) {
|
||||||
|
self.bins = bins
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct ClawdbotSystemNotifyParams: Codable, Sendable, Equatable {
|
public struct ClawdbotSystemNotifyParams: Codable, Sendable, Equatable {
|
||||||
public var title: String
|
public var title: String
|
||||||
public var body: String
|
public var body: String
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import Testing
|
||||||
|
@testable import ClawdbotChatUI
|
||||||
|
|
||||||
|
@Suite("ChatMarkdownPreprocessor")
|
||||||
|
struct ChatMarkdownPreprocessorTests {
|
||||||
|
@Test func extractsDataURLImages() {
|
||||||
|
let base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIHWP4////GQAJ+wP/2hN8NwAAAABJRU5ErkJggg=="
|
||||||
|
let markdown = """
|
||||||
|
Hello
|
||||||
|
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
let result = ChatMarkdownPreprocessor.preprocess(markdown: markdown)
|
||||||
|
|
||||||
|
#expect(result.cleaned == "Hello")
|
||||||
|
#expect(result.images.count == 1)
|
||||||
|
#expect(result.images.first?.image != nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,6 +78,7 @@ Isolated jobs run a dedicated agent turn in session `cron:<jobId>`.
|
|||||||
|
|
||||||
Key behaviors:
|
Key behaviors:
|
||||||
- Prompt is prefixed with `[cron:<jobId> <job name>]` for traceability.
|
- Prompt is prefixed with `[cron:<jobId> <job name>]` for traceability.
|
||||||
|
- Each run starts a **fresh session id** (no prior conversation carry-over).
|
||||||
- A summary is posted to the main session (prefix `Cron`, configurable).
|
- A summary is posted to the main session (prefix `Cron`, configurable).
|
||||||
- `wakeMode: "now"` triggers an immediate heartbeat after posting the summary.
|
- `wakeMode: "now"` triggers an immediate heartbeat after posting the summary.
|
||||||
- If `payload.deliver: true`, output is delivered to a channel; otherwise it stays internal.
|
- If `payload.deliver: true`, output is delivered to a channel; otherwise it stays internal.
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ Status: ready for DM and guild text channels via the official Discord bot gatewa
|
|||||||
2) Set the token for Clawdbot:
|
2) Set the token for Clawdbot:
|
||||||
- Env: `DISCORD_BOT_TOKEN=...`
|
- Env: `DISCORD_BOT_TOKEN=...`
|
||||||
- Or config: `channels.discord.token: "..."`.
|
- Or config: `channels.discord.token: "..."`.
|
||||||
|
- If both are set, config wins; env is fallback.
|
||||||
3) Invite the bot to your server with message permissions.
|
3) Invite the bot to your server with message permissions.
|
||||||
4) Start the gateway.
|
4) Start the gateway.
|
||||||
5) DM access is pairing by default; approve the pairing code on first contact.
|
5) DM access is pairing by default; approve the pairing code on first contact.
|
||||||
@@ -39,8 +40,8 @@ Minimal config:
|
|||||||
1. Create a Discord application → Bot, enable the intents you need (DMs + guild messages + message content), and grab the bot token.
|
1. Create a Discord application → Bot, enable the intents you need (DMs + guild messages + message content), and grab the bot token.
|
||||||
2. Invite the bot to your server with the permissions required to read/send messages where you want to use it.
|
2. Invite the bot to your server with the permissions required to read/send messages where you want to use it.
|
||||||
3. Configure Clawdbot with `DISCORD_BOT_TOKEN` (or `channels.discord.token` in `~/.clawdbot/clawdbot.json`).
|
3. Configure Clawdbot with `DISCORD_BOT_TOKEN` (or `channels.discord.token` in `~/.clawdbot/clawdbot.json`).
|
||||||
4. Run the gateway; it auto-starts the Discord channel when a token is available (env or config) and `channels.discord.enabled` is not `false`.
|
4. Run the gateway; it auto-starts the Discord channel when a token is available (config first, env fallback) and `channels.discord.enabled` is not `false`.
|
||||||
- If you prefer env vars, set `DISCORD_BOT_TOKEN` (a config block is optional).
|
- If you prefer env vars, set `DISCORD_BOT_TOKEN` (and omit config).
|
||||||
5. Direct chats: use `user:<id>` (or a `<@id>` mention) when delivering; all turns land in the shared `main` session. Bare numeric IDs are ambiguous and rejected.
|
5. Direct chats: use `user:<id>` (or a `<@id>` mention) when delivering; all turns land in the shared `main` session. Bare numeric IDs are ambiguous and rejected.
|
||||||
6. Guild channels: use `channel:<channelId>` for delivery. Mentions are required by default and can be set per guild or per channel.
|
6. Guild channels: use `channel:<channelId>` for delivery. Mentions are required by default and can be set per guild or per channel.
|
||||||
7. Direct chats: secure by default via `channels.discord.dm.policy` (default: `"pairing"`). Unknown senders get a pairing code (expires after 1 hour); approve via `clawdbot pairing approve discord <code>`.
|
7. Direct chats: secure by default via `channels.discord.dm.policy` (default: `"pairing"`). Unknown senders get a pairing code (expires after 1 hour); approve via `clawdbot pairing approve discord <code>`.
|
||||||
@@ -227,6 +228,8 @@ Outbound Discord API calls retry on rate limits (429) using Discord `retry_after
|
|||||||
actions: {
|
actions: {
|
||||||
reactions: true,
|
reactions: true,
|
||||||
stickers: true,
|
stickers: true,
|
||||||
|
emojiUploads: true,
|
||||||
|
stickerUploads: true,
|
||||||
polls: true,
|
polls: true,
|
||||||
permissions: true,
|
permissions: true,
|
||||||
messages: true,
|
messages: true,
|
||||||
@@ -237,6 +240,7 @@ Outbound Discord API calls retry on rate limits (429) using Discord `retry_after
|
|||||||
roleInfo: true,
|
roleInfo: true,
|
||||||
roles: false,
|
roles: false,
|
||||||
channelInfo: true,
|
channelInfo: true,
|
||||||
|
channels: true,
|
||||||
voiceStatus: true,
|
voiceStatus: true,
|
||||||
events: true,
|
events: true,
|
||||||
moderation: false
|
moderation: false
|
||||||
@@ -304,8 +308,9 @@ ack reaction after the bot replies.
|
|||||||
- `retry`: retry policy for outbound Discord API calls (attempts, minDelayMs, maxDelayMs, jitter).
|
- `retry`: retry policy for outbound Discord API calls (attempts, minDelayMs, maxDelayMs, jitter).
|
||||||
- `actions`: per-action tool gates; omit to allow all (set `false` to disable).
|
- `actions`: per-action tool gates; omit to allow all (set `false` to disable).
|
||||||
- `reactions` (covers react + read reactions)
|
- `reactions` (covers react + read reactions)
|
||||||
- `stickers`, `polls`, `permissions`, `messages`, `threads`, `pins`, `search`
|
- `stickers`, `emojiUploads`, `stickerUploads`, `polls`, `permissions`, `messages`, `threads`, `pins`, `search`
|
||||||
- `memberInfo`, `roleInfo`, `channelInfo`, `voiceStatus`, `events`
|
- `memberInfo`, `roleInfo`, `channelInfo`, `voiceStatus`, `events`
|
||||||
|
- `channels` (create/edit/delete channels + categories + permissions)
|
||||||
- `roles` (role add/remove, default `false`)
|
- `roles` (role add/remove, default `false`)
|
||||||
- `moderation` (timeout/kick/ban, default `false`)
|
- `moderation` (timeout/kick/ban, default `false`)
|
||||||
|
|
||||||
@@ -321,6 +326,8 @@ Reaction notifications use `guilds.<id>.reactionNotifications`:
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| reactions | enabled | React + list reactions + emojiList |
|
| reactions | enabled | React + list reactions + emojiList |
|
||||||
| stickers | enabled | Send stickers |
|
| stickers | enabled | Send stickers |
|
||||||
|
| emojiUploads | enabled | Upload emojis |
|
||||||
|
| stickerUploads | enabled | Upload stickers |
|
||||||
| polls | enabled | Create polls |
|
| polls | enabled | Create polls |
|
||||||
| permissions | enabled | Channel permission snapshot |
|
| permissions | enabled | Channel permission snapshot |
|
||||||
| messages | enabled | Read/send/edit/delete |
|
| messages | enabled | Read/send/edit/delete |
|
||||||
@@ -330,6 +337,7 @@ Reaction notifications use `guilds.<id>.reactionNotifications`:
|
|||||||
| memberInfo | enabled | Member info |
|
| memberInfo | enabled | Member info |
|
||||||
| roleInfo | enabled | Role list |
|
| roleInfo | enabled | Role list |
|
||||||
| channelInfo | enabled | Channel info + list |
|
| channelInfo | enabled | Channel info + list |
|
||||||
|
| channels | enabled | Channel/category management |
|
||||||
| voiceStatus | enabled | Voice state lookup |
|
| voiceStatus | enabled | Voice state lookup |
|
||||||
| events | enabled | List/create scheduled events |
|
| events | enabled | List/create scheduled events |
|
||||||
| roles | disabled | Role add/remove |
|
| roles | disabled | Role add/remove |
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ Text is supported everywhere; media and reactions vary by channel.
|
|||||||
- [Microsoft Teams](/channels/msteams) — Bot Framework; enterprise support (plugin, installed separately).
|
- [Microsoft Teams](/channels/msteams) — Bot Framework; enterprise support (plugin, installed separately).
|
||||||
- [Matrix](/channels/matrix) — Matrix protocol (plugin, installed separately).
|
- [Matrix](/channels/matrix) — Matrix protocol (plugin, installed separately).
|
||||||
- [Zalo](/channels/zalo) — Zalo Bot API; Vietnam's popular 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.
|
- [WebChat](/web/webchat) — Gateway WebChat UI over WebSocket.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ Details: [Plugins](/plugin)
|
|||||||
2) Configure credentials:
|
2) Configure credentials:
|
||||||
- Env: `MATRIX_HOMESERVER`, `MATRIX_USER_ID`, `MATRIX_ACCESS_TOKEN` (or `MATRIX_PASSWORD`)
|
- Env: `MATRIX_HOMESERVER`, `MATRIX_USER_ID`, `MATRIX_ACCESS_TOKEN` (or `MATRIX_PASSWORD`)
|
||||||
- Or config: `channels.matrix.*`
|
- Or config: `channels.matrix.*`
|
||||||
|
- Config takes precedence over env; env is fallback.
|
||||||
3) Restart the gateway (or finish onboarding).
|
3) Restart the gateway (or finish onboarding).
|
||||||
4) DM access defaults to pairing; approve the pairing code on first contact.
|
4) DM access defaults to pairing; approve the pairing code on first contact.
|
||||||
|
|
||||||
|
|||||||
+53
-15
@@ -13,6 +13,7 @@ Status: production-ready for bot DMs + groups via grammY. Long-polling by defaul
|
|||||||
2) Set the token:
|
2) Set the token:
|
||||||
- Env: `TELEGRAM_BOT_TOKEN=...`
|
- Env: `TELEGRAM_BOT_TOKEN=...`
|
||||||
- Or config: `channels.telegram.botToken: "..."`.
|
- Or config: `channels.telegram.botToken: "..."`.
|
||||||
|
- If both are set, config wins; env is fallback.
|
||||||
3) Start the gateway.
|
3) Start the gateway.
|
||||||
4) DM access is pairing by default; approve the pairing code on first contact.
|
4) DM access is pairing by default; approve the pairing code on first contact.
|
||||||
|
|
||||||
@@ -60,11 +61,11 @@ Example:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Env option: `TELEGRAM_BOT_TOKEN=...` (works for the default account).
|
Env option: `TELEGRAM_BOT_TOKEN=...` (works for the default account; used only when config is missing).
|
||||||
|
|
||||||
Multi-account support: use `channels.telegram.accounts` with per-account tokens and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern.
|
Multi-account support: use `channels.telegram.accounts` with per-account tokens and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern.
|
||||||
|
|
||||||
3) Start the gateway. Telegram starts when a token is resolved (env or config).
|
3) Start the gateway. Telegram starts when a token is resolved (config first, env fallback).
|
||||||
4) DM access defaults to pairing. Approve the code when the bot is first contacted.
|
4) DM access defaults to pairing. Approve the code when the bot is first contacted.
|
||||||
5) For groups: add the bot, decide privacy/admin behavior (below), then set `channels.telegram.groups` to control mention gating + allowlists.
|
5) For groups: add the bot, decide privacy/admin behavior (below), then set `channels.telegram.groups` to control mention gating + allowlists.
|
||||||
|
|
||||||
@@ -101,6 +102,29 @@ group messages, so use admin if you need full visibility.
|
|||||||
- Raw HTML from models is escaped to avoid Telegram parse errors.
|
- Raw HTML from models is escaped to avoid Telegram parse errors.
|
||||||
- If Telegram rejects the HTML payload, Clawdbot retries the same message as plain text.
|
- If Telegram rejects the HTML payload, Clawdbot retries the same message as plain text.
|
||||||
|
|
||||||
|
## Commands (native + custom)
|
||||||
|
Clawdbot registers native commands (like `/status`, `/reset`, `/model`) with Telegram’s bot menu on startup.
|
||||||
|
You can add custom commands to the menu via config:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
telegram: {
|
||||||
|
customCommands: [
|
||||||
|
{ command: "backup", description: "Git backup" },
|
||||||
|
{ command: "generate", description: "Create an image" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Custom commands are **menu entries only**; Clawdbot does not implement them unless you handle them elsewhere.
|
||||||
|
- Command names are normalized (leading `/` stripped, lowercased) and must match `a-z`, `0-9`, `_` (1–32 chars).
|
||||||
|
- Custom commands **cannot override native commands**. Conflicts are ignored and logged.
|
||||||
|
- If `commands.native` is disabled, only custom commands are registered (or cleared if none).
|
||||||
|
|
||||||
## Limits
|
## Limits
|
||||||
- Outbound text is chunked to `channels.telegram.textChunkLimit` (default 4000).
|
- Outbound text is chunked to `channels.telegram.textChunkLimit` (default 4000).
|
||||||
- Media downloads/uploads are capped by `channels.telegram.mediaMaxMb` (default 5).
|
- Media downloads/uploads are capped by `channels.telegram.mediaMaxMb` (default 5).
|
||||||
@@ -196,13 +220,15 @@ Private chats can include `message_thread_id` in some edge cases. Clawdbot keeps
|
|||||||
|
|
||||||
## Inline Buttons
|
## Inline Buttons
|
||||||
|
|
||||||
Telegram supports inline keyboards with callback buttons. Enable this feature via capabilities:
|
Telegram supports inline keyboards with callback buttons.
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
"channels": {
|
"channels": {
|
||||||
"telegram": {
|
"telegram": {
|
||||||
"capabilities": ["inlineButtons"]
|
"capabilities": {
|
||||||
|
"inlineButtons": "allowlist"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -215,7 +241,9 @@ For per-account configuration:
|
|||||||
"telegram": {
|
"telegram": {
|
||||||
"accounts": {
|
"accounts": {
|
||||||
"main": {
|
"main": {
|
||||||
"capabilities": ["inlineButtons"]
|
"capabilities": {
|
||||||
|
"inlineButtons": "allowlist"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,6 +251,16 @@ For per-account configuration:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Scopes:
|
||||||
|
- `off` — inline buttons disabled
|
||||||
|
- `dm` — only DMs (group targets blocked)
|
||||||
|
- `group` — only groups (DM targets blocked)
|
||||||
|
- `all` — DMs + groups
|
||||||
|
- `allowlist` — DMs + groups, but only senders allowed by `allowFrom`/`groupAllowFrom` (same rules as control commands)
|
||||||
|
|
||||||
|
Default: `allowlist`.
|
||||||
|
Legacy: `capabilities: ["inlineButtons"]` = `inlineButtons: "all"`.
|
||||||
|
|
||||||
### Sending buttons
|
### Sending buttons
|
||||||
|
|
||||||
Use the message tool with the `buttons` parameter:
|
Use the message tool with the `buttons` parameter:
|
||||||
@@ -250,12 +288,12 @@ When a user clicks a button, the callback data is sent back to the agent as a me
|
|||||||
|
|
||||||
### Configuration options
|
### Configuration options
|
||||||
|
|
||||||
Telegram capabilities can be configured at two levels:
|
Telegram capabilities can be configured at two levels (object form shown above; legacy string arrays still supported):
|
||||||
|
|
||||||
- `channels.telegram.capabilities`: Global default capability list applied to all Telegram accounts unless overridden.
|
- `channels.telegram.capabilities`: Global default capability config applied to all Telegram accounts unless overridden.
|
||||||
- `channels.telegram.accounts.<account>.capabilities`: Per-account capabilities that override or extend the global defaults for that specific account.
|
- `channels.telegram.accounts.<account>.capabilities`: Per-account capabilities that override the global defaults for that specific account.
|
||||||
|
|
||||||
Use the global setting when all Telegram bots/accounts should behave the same. Use per-account configuration when different bots need different behaviors (for example, one account only handles DMs while another is allowed in groups or has extra capabilities).
|
Use the global setting when all Telegram bots/accounts should behave the same. Use per-account configuration when different bots need different behaviors (for example, one account only handles DMs while another is allowed in groups).
|
||||||
## Access control (DMs + groups)
|
## Access control (DMs + groups)
|
||||||
|
|
||||||
### DM access
|
### DM access
|
||||||
@@ -374,8 +412,8 @@ The agent sees reactions as **system notifications** in the conversation history
|
|||||||
|
|
||||||
**Configuration:**
|
**Configuration:**
|
||||||
- `channels.telegram.reactionNotifications`: Controls which reactions trigger notifications
|
- `channels.telegram.reactionNotifications`: Controls which reactions trigger notifications
|
||||||
- `"off"` — ignore all reactions (default when not set)
|
- `"off"` — ignore all reactions
|
||||||
- `"own"` — notify when users react to bot messages (best-effort; in-memory)
|
- `"own"` — notify when users react to bot messages (best-effort; in-memory) (default)
|
||||||
- `"all"` — notify for all reactions
|
- `"all"` — notify for all reactions
|
||||||
|
|
||||||
- `channels.telegram.reactionLevel`: Controls agent's reaction capability
|
- `channels.telegram.reactionLevel`: Controls agent's reaction capability
|
||||||
@@ -454,8 +492,8 @@ Provider options:
|
|||||||
- `channels.telegram.groups.<id>.enabled`: disable the group when `false`.
|
- `channels.telegram.groups.<id>.enabled`: disable the group when `false`.
|
||||||
- `channels.telegram.groups.<id>.topics.<threadId>.*`: per-topic overrides (same fields as group).
|
- `channels.telegram.groups.<id>.topics.<threadId>.*`: per-topic overrides (same fields as group).
|
||||||
- `channels.telegram.groups.<id>.topics.<threadId>.requireMention`: per-topic mention gating override.
|
- `channels.telegram.groups.<id>.topics.<threadId>.requireMention`: per-topic mention gating override.
|
||||||
- `channels.telegram.capabilities`: Enable channel features (e.g., "inlineButtons").
|
- `channels.telegram.capabilities.inlineButtons`: `off | dm | group | all | allowlist` (default: allowlist).
|
||||||
- `channels.telegram.accounts.<account>.capabilities`: Per-account capabilities.
|
- `channels.telegram.accounts.<account>.capabilities.inlineButtons`: per-account override.
|
||||||
- `channels.telegram.replyToMode`: `off | first | all` (default: `first`).
|
- `channels.telegram.replyToMode`: `off | first | all` (default: `first`).
|
||||||
- `channels.telegram.textChunkLimit`: outbound chunk size (chars).
|
- `channels.telegram.textChunkLimit`: outbound chunk size (chars).
|
||||||
- `channels.telegram.streamMode`: `off | partial | block` (draft streaming).
|
- `channels.telegram.streamMode`: `off | partial | block` (draft streaming).
|
||||||
@@ -468,8 +506,8 @@ Provider options:
|
|||||||
- `channels.telegram.actions.reactions`: gate Telegram tool reactions.
|
- `channels.telegram.actions.reactions`: gate Telegram tool reactions.
|
||||||
- `channels.telegram.actions.sendMessage`: gate Telegram tool message sends.
|
- `channels.telegram.actions.sendMessage`: gate Telegram tool message sends.
|
||||||
- `channels.telegram.actions.deleteMessage`: gate Telegram tool message deletes.
|
- `channels.telegram.actions.deleteMessage`: gate Telegram tool message deletes.
|
||||||
- `channels.telegram.reactionNotifications`: `off | own | all` — control which reactions trigger system events (default: `off` when not set).
|
- `channels.telegram.reactionNotifications`: `off | own | all` — control which reactions trigger system events (default: `own` when not set).
|
||||||
- `channels.telegram.reactionLevel`: `off | ack | minimal | extensive` — control agent's reaction capability (default: `ack` when not set).
|
- `channels.telegram.reactionLevel`: `off | ack | minimal | extensive` — control agent's reaction capability (default: `minimal` when not set).
|
||||||
|
|
||||||
Related global options:
|
Related global options:
|
||||||
- `agents.list[].groupChat.mentionPatterns` (mention gating patterns).
|
- `agents.list[].groupChat.mentionPatterns` (mention gating patterns).
|
||||||
|
|||||||
@@ -82,15 +82,13 @@ When the wizard asks for your personal WhatsApp number, enter the phone you will
|
|||||||
"selfChatMode": true,
|
"selfChatMode": true,
|
||||||
"dmPolicy": "allowlist",
|
"dmPolicy": "allowlist",
|
||||||
"allowFrom": ["+15551234567"]
|
"allowFrom": ["+15551234567"]
|
||||||
},
|
|
||||||
"messages": {
|
|
||||||
"responsePrefix": "[clawdbot]"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Tip: set `messages.responsePrefix` explicitly if you want a consistent bot prefix
|
Self-chat replies default to `[{identity.name}]` when set (otherwise `[clawdbot]`)
|
||||||
on outbound replies.
|
if `messages.responsePrefix` is unset. Set it explicitly to customize or disable
|
||||||
|
the prefix (use `""` to remove it).
|
||||||
|
|
||||||
### Number sourcing tips
|
### Number sourcing tips
|
||||||
- **Local eSIM** from your country's mobile carrier (most reliable)
|
- **Local eSIM** from your country's mobile carrier (most reliable)
|
||||||
@@ -204,9 +202,9 @@ The wizard uses it to set your **allowlist/owner** so your own DMs are permitted
|
|||||||
- `always`: always triggers.
|
- `always`: always triggers.
|
||||||
- `/activation mention|always` is owner-only and must be sent as a standalone message.
|
- `/activation mention|always` is owner-only and must be sent as a standalone message.
|
||||||
- Owner = `channels.whatsapp.allowFrom` (or self E.164 if unset).
|
- Owner = `channels.whatsapp.allowFrom` (or self E.164 if unset).
|
||||||
- **History injection**:
|
- **History injection** (pending-only):
|
||||||
- Recent messages (default 50) inserted under:
|
- Recent *unprocessed* messages (default 50) inserted under:
|
||||||
`[Chat messages since your last reply - for context]`
|
`[Chat messages since your last reply - for context]` (messages already in the session are not re-injected)
|
||||||
- Current message under:
|
- Current message under:
|
||||||
`[Current message - respond to this]`
|
`[Current message - respond to this]`
|
||||||
- Sender suffix appended: `[from: Name (+E164)]`
|
- Sender suffix appended: `[from: Name (+E164)]`
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
---
|
||||||
|
summary: "Zalo personal account support via zca-cli (QR login), capabilities, and configuration"
|
||||||
|
read_when:
|
||||||
|
- Setting up Zalo Personal for Clawdbot
|
||||||
|
- Debugging Zalo Personal login or message flow
|
||||||
|
---
|
||||||
|
# Zalo Personal (unofficial)
|
||||||
|
|
||||||
|
Status: experimental. This integration automates a **personal Zalo account** via `zca-cli`.
|
||||||
|
|
||||||
|
> **Warning:** This is an unofficial integration and may result in account suspension/ban. Use at your own risk.
|
||||||
|
|
||||||
|
## Plugin required
|
||||||
|
Zalo Personal ships as a plugin and is not bundled with the core install.
|
||||||
|
- Install via CLI: `clawdbot plugins install @clawdbot/zalouser`
|
||||||
|
- Or from a source checkout: `clawdbot plugins install ./extensions/zalouser`
|
||||||
|
- Details: [Plugins](/plugin)
|
||||||
|
|
||||||
|
## Prerequisite: zca-cli
|
||||||
|
The Gateway machine must have the `zca` binary available in `PATH`.
|
||||||
|
|
||||||
|
- Verify: `zca --version`
|
||||||
|
- If missing, install zca-cli (see `extensions/zalouser/README.md` or the upstream zca-cli docs).
|
||||||
|
|
||||||
|
## Quick setup (beginner)
|
||||||
|
1) Install the plugin (see above).
|
||||||
|
2) Login (QR, on the Gateway machine):
|
||||||
|
- `clawdbot channels login --channel zalouser`
|
||||||
|
- Scan the QR code in the terminal with the Zalo mobile app.
|
||||||
|
3) Enable the channel:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
zalouser: {
|
||||||
|
enabled: true,
|
||||||
|
dmPolicy: "pairing"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4) Restart the Gateway (or finish onboarding).
|
||||||
|
5) DM access defaults to pairing; approve the pairing code on first contact.
|
||||||
|
|
||||||
|
## What it is
|
||||||
|
- Uses `zca listen` to receive inbound messages.
|
||||||
|
- Uses `zca msg ...` to send replies (text/media/link).
|
||||||
|
- Designed for “personal account” use cases where Zalo Bot API is not available.
|
||||||
|
|
||||||
|
## Naming
|
||||||
|
Channel id is `zalouser` to make it explicit this automates a **personal Zalo user account** (unofficial). We keep `zalo` reserved for a potential future official Zalo API integration.
|
||||||
|
|
||||||
|
## Finding IDs (directory)
|
||||||
|
Use the directory CLI to discover peers/groups and their IDs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot directory self --channel zalouser
|
||||||
|
clawdbot directory peers list --channel zalouser --query "name"
|
||||||
|
clawdbot directory groups list --channel zalouser --query "work"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Limits
|
||||||
|
- Outbound text is chunked to ~2000 characters (Zalo client limits).
|
||||||
|
- Streaming is blocked by default.
|
||||||
|
|
||||||
|
## Access control (DMs)
|
||||||
|
`channels.zalouser.dmPolicy` supports: `pairing | allowlist | open | disabled` (default: `pairing`).
|
||||||
|
|
||||||
|
Approve via:
|
||||||
|
- `clawdbot pairing list zalouser`
|
||||||
|
- `clawdbot pairing approve zalouser <code>`
|
||||||
|
|
||||||
|
## Multi-account
|
||||||
|
Accounts map to zca profiles. Example:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
zalouser: {
|
||||||
|
enabled: true,
|
||||||
|
defaultAccount: "default",
|
||||||
|
accounts: {
|
||||||
|
work: { enabled: true, profile: "work" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**`zca` not found:**
|
||||||
|
- Install zca-cli and ensure it’s on `PATH` for the Gateway process.
|
||||||
|
|
||||||
|
**Login doesn’t stick:**
|
||||||
|
- `clawdbot channels status --probe`
|
||||||
|
- Re-login: `clawdbot channels logout --channel zalouser && clawdbot channels login --channel zalouser`
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
summary: "CLI reference for `clawdbot config` (get/set/unset config values)"
|
||||||
|
read_when:
|
||||||
|
- You want to read or edit config non-interactively
|
||||||
|
---
|
||||||
|
|
||||||
|
# `clawdbot config`
|
||||||
|
|
||||||
|
Config helpers: get/set/unset values by path. Run without a subcommand to open
|
||||||
|
the configure wizard (same as `clawdbot configure`).
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot config get browser.executablePath
|
||||||
|
clawdbot config set browser.executablePath "/usr/bin/google-chrome"
|
||||||
|
clawdbot config set agents.defaults.heartbeat.every "2h"
|
||||||
|
clawdbot config unset tools.web.search.apiKey
|
||||||
|
```
|
||||||
|
|
||||||
|
## Paths
|
||||||
|
|
||||||
|
Paths use dot or bracket notation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot config get agents.defaults.workspace
|
||||||
|
clawdbot config get agents.list[0].id
|
||||||
|
```
|
||||||
|
|
||||||
|
## Values
|
||||||
|
|
||||||
|
Values are parsed as JSON5 when possible; otherwise they are treated as strings.
|
||||||
|
Use `--json` to require JSON5 parsing.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot config set agents.defaults.heartbeat.every "0m"
|
||||||
|
clawdbot config set gateway.port 19001 --json
|
||||||
|
clawdbot config set channels.whatsapp.groups '["*"]' --json
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart the gateway after edits.
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
---
|
---
|
||||||
summary: "CLI reference for `clawdbot configure` / `clawdbot config` (interactive configuration prompts)"
|
summary: "CLI reference for `clawdbot configure` (interactive configuration prompts)"
|
||||||
read_when:
|
read_when:
|
||||||
- You want to tweak credentials, devices, or agent defaults interactively
|
- You want to tweak credentials, devices, or agent defaults interactively
|
||||||
---
|
---
|
||||||
|
|
||||||
# `clawdbot configure` (alias: `config`)
|
# `clawdbot configure`
|
||||||
|
|
||||||
Interactive prompt to set up credentials, devices, and agent defaults.
|
Interactive prompt to set up credentials, devices, and agent defaults.
|
||||||
|
|
||||||
|
Tip: `clawdbot config` without a subcommand opens the same wizard. Use
|
||||||
|
`clawdbot config get|set|unset` for non-interactive edits.
|
||||||
|
|
||||||
Related:
|
Related:
|
||||||
- Gateway configuration reference: [Configuration](/gateway/configuration)
|
- Gateway configuration reference: [Configuration](/gateway/configuration)
|
||||||
|
- Config CLI: [Config](/cli/config)
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Choosing where the Gateway runs always updates `gateway.mode`. You can select "Continue" without other sections if that is all you need.
|
- Choosing where the Gateway runs always updates `gateway.mode`. You can select "Continue" without other sections if that is all you need.
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
summary: "CLI reference for `clawdbot directory` (self, peers, groups)"
|
||||||
|
read_when:
|
||||||
|
- You want to look up contacts/groups/self ids for a channel
|
||||||
|
- You are developing a channel directory adapter
|
||||||
|
---
|
||||||
|
|
||||||
|
# `clawdbot directory`
|
||||||
|
|
||||||
|
Directory lookups for channels that support it (contacts/peers, groups, and “me”).
|
||||||
|
|
||||||
|
## Common flags
|
||||||
|
- `--channel <name>`: channel id/alias (required when multiple channels are configured; auto when only one is configured)
|
||||||
|
- `--account <id>`: account id (default: channel default)
|
||||||
|
- `--json`: output JSON
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `directory` is meant to help you find IDs you can paste into other commands (especially `clawdbot message send --to ...`).
|
||||||
|
- For many channels, results are config-backed (allowlists / configured groups) rather than a live provider directory.
|
||||||
|
- Default output is `id` (and sometimes `name`) separated by a tab; use `--json` for scripting.
|
||||||
|
|
||||||
|
## Using results with `message send`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot directory peers list --channel slack --query "U0"
|
||||||
|
clawdbot message send --channel slack --to user:U012ABCDEF --message "hello"
|
||||||
|
```
|
||||||
|
|
||||||
|
## ID formats (by channel)
|
||||||
|
|
||||||
|
- WhatsApp: `+15551234567` (DM), `1234567890-1234567890@g.us` (group)
|
||||||
|
- Telegram: `@username` or numeric chat id; groups are numeric ids
|
||||||
|
- Slack: `user:U…` and `channel:C…`
|
||||||
|
- Discord: `user:<id>` and `channel:<id>`
|
||||||
|
- Matrix (plugin): `user:@user:server`, `room:!roomId:server`, or `#alias:server`
|
||||||
|
- Microsoft Teams (plugin): `user:<id>` and `conversation:<id>`
|
||||||
|
- Zalo (plugin): user id (Bot API)
|
||||||
|
- Zalo Personal / `zalouser` (plugin): thread id (DM/group) from `zca` (`me`, `friend list`, `group list`)
|
||||||
|
|
||||||
|
## Self (“me”)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot directory self --channel zalouser
|
||||||
|
```
|
||||||
|
|
||||||
|
## Peers (contacts/users)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot directory peers list --channel zalouser
|
||||||
|
clawdbot directory peers list --channel zalouser --query "name"
|
||||||
|
clawdbot directory peers list --channel zalouser --limit 50
|
||||||
|
```
|
||||||
|
|
||||||
|
## Groups
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot directory groups list --channel zalouser
|
||||||
|
clawdbot directory groups list --channel zalouser --query "work"
|
||||||
|
clawdbot directory groups members --channel zalouser --group-id <id>
|
||||||
|
```
|
||||||
@@ -21,3 +21,14 @@ clawdbot doctor --repair
|
|||||||
clawdbot doctor --deep
|
clawdbot doctor --deep
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## macOS: `launchctl` env overrides
|
||||||
|
|
||||||
|
If you previously ran `launchctl setenv CLAWDBOT_GATEWAY_TOKEN ...` (or `...PASSWORD`), that value overrides your config file and can cause persistent “unauthorized” errors.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
launchctl getenv CLAWDBOT_GATEWAY_TOKEN
|
||||||
|
launchctl getenv CLAWDBOT_GATEWAY_PASSWORD
|
||||||
|
|
||||||
|
launchctl unsetenv CLAWDBOT_GATEWAY_TOKEN
|
||||||
|
launchctl unsetenv CLAWDBOT_GATEWAY_PASSWORD
|
||||||
|
```
|
||||||
+19
-4
@@ -13,7 +13,8 @@ This page describes the current CLI behavior. If commands change, update this do
|
|||||||
|
|
||||||
- [`setup`](/cli/setup)
|
- [`setup`](/cli/setup)
|
||||||
- [`onboard`](/cli/onboard)
|
- [`onboard`](/cli/onboard)
|
||||||
- [`configure`](/cli/configure) (alias: `config`)
|
- [`configure`](/cli/configure)
|
||||||
|
- [`config`](/cli/config)
|
||||||
- [`doctor`](/cli/doctor)
|
- [`doctor`](/cli/doctor)
|
||||||
- [`dashboard`](/cli/dashboard)
|
- [`dashboard`](/cli/dashboard)
|
||||||
- [`reset`](/cli/reset)
|
- [`reset`](/cli/reset)
|
||||||
@@ -83,7 +84,11 @@ Palette source of truth: `src/terminal/palette.ts` (aka “lobster seam”).
|
|||||||
clawdbot [--dev] [--profile <name>] <command>
|
clawdbot [--dev] [--profile <name>] <command>
|
||||||
setup
|
setup
|
||||||
onboard
|
onboard
|
||||||
configure (alias: config)
|
configure
|
||||||
|
config
|
||||||
|
get
|
||||||
|
set
|
||||||
|
unset
|
||||||
doctor
|
doctor
|
||||||
security
|
security
|
||||||
audit
|
audit
|
||||||
@@ -278,7 +283,7 @@ Options:
|
|||||||
- `--non-interactive`
|
- `--non-interactive`
|
||||||
- `--mode <local|remote>`
|
- `--mode <local|remote>`
|
||||||
- `--flow <quickstart|advanced>`
|
- `--flow <quickstart|advanced>`
|
||||||
- `--auth-choice <setup-token|claude-cli|token|openai-codex|openai-api-key|openrouter-api-key|moonshot-api-key|codex-cli|antigravity|gemini-api-key|zai-api-key|apiKey|minimax-api|opencode-zen|skip>`
|
- `--auth-choice <setup-token|claude-cli|token|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|codex-cli|antigravity|gemini-api-key|zai-api-key|apiKey|minimax-api|opencode-zen|skip>`
|
||||||
- `--token-provider <id>` (non-interactive; used with `--auth-choice token`)
|
- `--token-provider <id>` (non-interactive; used with `--auth-choice token`)
|
||||||
- `--token <token>` (non-interactive; used with `--auth-choice token`)
|
- `--token <token>` (non-interactive; used with `--auth-choice token`)
|
||||||
- `--token-profile-id <id>` (non-interactive; default: `<provider>:manual`)
|
- `--token-profile-id <id>` (non-interactive; default: `<provider>:manual`)
|
||||||
@@ -286,6 +291,7 @@ Options:
|
|||||||
- `--anthropic-api-key <key>`
|
- `--anthropic-api-key <key>`
|
||||||
- `--openai-api-key <key>`
|
- `--openai-api-key <key>`
|
||||||
- `--openrouter-api-key <key>`
|
- `--openrouter-api-key <key>`
|
||||||
|
- `--ai-gateway-api-key <key>`
|
||||||
- `--moonshot-api-key <key>`
|
- `--moonshot-api-key <key>`
|
||||||
- `--gemini-api-key <key>`
|
- `--gemini-api-key <key>`
|
||||||
- `--zai-api-key <key>`
|
- `--zai-api-key <key>`
|
||||||
@@ -310,9 +316,18 @@ Options:
|
|||||||
- `--node-manager <npm|pnpm|bun>` (pnpm recommended; bun not recommended for Gateway runtime)
|
- `--node-manager <npm|pnpm|bun>` (pnpm recommended; bun not recommended for Gateway runtime)
|
||||||
- `--json`
|
- `--json`
|
||||||
|
|
||||||
### `configure` / `config`
|
### `configure`
|
||||||
Interactive configuration wizard (models, channels, skills, gateway).
|
Interactive configuration wizard (models, channels, skills, gateway).
|
||||||
|
|
||||||
|
### `config`
|
||||||
|
Non-interactive config helpers (get/set/unset). Running `clawdbot config` with no
|
||||||
|
subcommand launches the wizard.
|
||||||
|
|
||||||
|
Subcommands:
|
||||||
|
- `config get <path>`: print a config value (dot/bracket path).
|
||||||
|
- `config set <path> <value>`: set a value (JSON5 or raw string).
|
||||||
|
- `config unset <path>`: remove a value.
|
||||||
|
|
||||||
### `doctor`
|
### `doctor`
|
||||||
Health checks + quick fixes (config + gateway + legacy services).
|
Health checks + quick fixes (config + gateway + legacy services).
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -46,7 +46,7 @@ Target formats (`--to`):
|
|||||||
- Channels: WhatsApp/Telegram/Discord/Slack/Signal/iMessage/MS Teams
|
- Channels: WhatsApp/Telegram/Discord/Slack/Signal/iMessage/MS Teams
|
||||||
- Required: `--to`, plus `--message` or `--media`
|
- Required: `--to`, plus `--message` or `--media`
|
||||||
- Optional: `--media`, `--reply-to`, `--thread-id`, `--gif-playback`
|
- Optional: `--media`, `--reply-to`, `--thread-id`, `--gif-playback`
|
||||||
- Telegram only: `--buttons` (requires `"inlineButtons"` in `channels.telegram.capabilities` or `channels.telegram.accounts.<id>.capabilities`)
|
- Telegram only: `--buttons` (requires `channels.telegram.capabilities.inlineButtons` to allow it)
|
||||||
- Telegram only: `--thread-id` (forum topic id)
|
- Telegram only: `--thread-id` (forum topic id)
|
||||||
- Slack only: `--thread-id` (thread timestamp; `--reply-to` uses the same field)
|
- Slack only: `--thread-id` (thread timestamp; `--reply-to` uses the same field)
|
||||||
- WhatsApp only: `--gif-playback`
|
- WhatsApp only: `--gif-playback`
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ clawdbot plugins info <id>
|
|||||||
clawdbot plugins enable <id>
|
clawdbot plugins enable <id>
|
||||||
clawdbot plugins disable <id>
|
clawdbot plugins disable <id>
|
||||||
clawdbot plugins doctor
|
clawdbot plugins doctor
|
||||||
|
clawdbot plugins update <id>
|
||||||
|
clawdbot plugins update --all
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install
|
### Install
|
||||||
@@ -31,3 +33,12 @@ clawdbot plugins install <npm-spec>
|
|||||||
|
|
||||||
Security note: treat plugin installs like running code. Prefer pinned versions.
|
Security note: treat plugin installs like running code. Prefer pinned versions.
|
||||||
|
|
||||||
|
### Update
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot plugins update <id>
|
||||||
|
clawdbot plugins update --all
|
||||||
|
clawdbot plugins update <id> --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
Updates only apply to plugins installed from npm (tracked in `plugins.installs`).
|
||||||
@@ -13,7 +13,7 @@ Note: `agents.list[].groupChat.mentionPatterns` is now used by Telegram/Discord/
|
|||||||
- Activation modes: `mention` (default) or `always`. `mention` requires a ping (real WhatsApp @-mentions via `mentionedJids`, regex patterns, or the bot’s E.164 anywhere in the text). `always` wakes the agent on every message but it should reply only when it can add meaningful value; otherwise it returns the silent token `NO_REPLY`. Defaults can be set in config (`channels.whatsapp.groups`) and overridden per group via `/activation`. When `channels.whatsapp.groups` is set, it also acts as a group allowlist (include `"*"` to allow all).
|
- Activation modes: `mention` (default) or `always`. `mention` requires a ping (real WhatsApp @-mentions via `mentionedJids`, regex patterns, or the bot’s E.164 anywhere in the text). `always` wakes the agent on every message but it should reply only when it can add meaningful value; otherwise it returns the silent token `NO_REPLY`. Defaults can be set in config (`channels.whatsapp.groups`) and overridden per group via `/activation`. When `channels.whatsapp.groups` is set, it also acts as a group allowlist (include `"*"` to allow all).
|
||||||
- Group policy: `channels.whatsapp.groupPolicy` controls whether group messages are accepted (`open|disabled|allowlist`). `allowlist` uses `channels.whatsapp.groupAllowFrom` (fallback: explicit `channels.whatsapp.allowFrom`). Default is `allowlist` (blocked until you add senders).
|
- Group policy: `channels.whatsapp.groupPolicy` controls whether group messages are accepted (`open|disabled|allowlist`). `allowlist` uses `channels.whatsapp.groupAllowFrom` (fallback: explicit `channels.whatsapp.allowFrom`). Default is `allowlist` (blocked until you add senders).
|
||||||
- Per-group sessions: session keys look like `agent:<agentId>:whatsapp:group:<jid>` so commands such as `/verbose on` or `/think high` (sent as standalone messages) are scoped to that group; personal DM state is untouched. Heartbeats are skipped for group threads.
|
- Per-group sessions: session keys look like `agent:<agentId>:whatsapp:group:<jid>` so commands such as `/verbose on` or `/think high` (sent as standalone messages) are scoped to that group; personal DM state is untouched. Heartbeats are skipped for group threads.
|
||||||
- Context injection: last N (default 50) group messages are prefixed under `[Chat messages since your last reply - for context]`, with the triggering line under `[Current message - respond to this]`.
|
- Context injection: **pending-only** group messages (default 50) that *did not* trigger a run are prefixed under `[Chat messages since your last reply - for context]`, with the triggering line under `[Current message - respond to this]`. Messages already in the session are not re-injected.
|
||||||
- Sender surfacing: every group batch now ends with `[from: Sender Name (+E164)]` so Pi knows who is speaking.
|
- Sender surfacing: every group batch now ends with `[from: Sender Name (+E164)]` so Pi knows who is speaking.
|
||||||
- Ephemeral/view-once: we unwrap those before extracting text/mentions, so pings inside them still trigger.
|
- Ephemeral/view-once: we unwrap those before extracting text/mentions, so pings inside them still trigger.
|
||||||
- Group system prompt: on the first turn of a group session (and whenever `/activation` changes the mode) we inject a short blurb into the system prompt like `You are replying inside the WhatsApp group "<subject>". Group members: Alice (+44...), Bob (+43...), … Activation: trigger-only … Address the specific sender noted in the message context.` If metadata isn’t available we still tell the agent it’s a group chat.
|
- Group system prompt: on the first turn of a group session (and whenever `/activation` changes the mode) we inject a short blurb into the system prompt like `You are replying inside the WhatsApp group "<subject>". Group members: Alice (+44...), Bob (+43...), … Activation: trigger-only … Address the specific sender noted in the message context.` If metadata isn’t available we still tell the agent it’s a group chat.
|
||||||
|
|||||||
@@ -177,6 +177,8 @@ Quick mental model (evaluation order for group messages):
|
|||||||
## Mention gating (default)
|
## Mention gating (default)
|
||||||
Group messages require a mention unless overridden per group. Defaults live per subsystem under `*.groups."*"`.
|
Group messages require a mention unless overridden per group. Defaults live per subsystem under `*.groups."*"`.
|
||||||
|
|
||||||
|
Replying to a bot message counts as an implicit mention (when the channel supports reply metadata). This applies to Telegram, WhatsApp, Slack, Discord, and Microsoft Teams.
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
channels: {
|
channels: {
|
||||||
@@ -219,7 +221,7 @@ Notes:
|
|||||||
- Per-agent override: `agents.list[].groupChat.mentionPatterns` (useful when multiple agents share a group).
|
- Per-agent override: `agents.list[].groupChat.mentionPatterns` (useful when multiple agents share a group).
|
||||||
- Mention gating is only enforced when mention detection is possible (native mentions or `mentionPatterns` are configured).
|
- Mention gating is only enforced when mention detection is possible (native mentions or `mentionPatterns` are configured).
|
||||||
- Discord defaults live in `channels.discord.guilds."*"` (overridable per guild/channel).
|
- Discord defaults live in `channels.discord.guilds."*"` (overridable per guild/channel).
|
||||||
- Group history context is wrapped uniformly across channels; use `messages.groupChat.historyLimit` for the global default and `channels.<channel>.historyLimit` (or `channels.<channel>.accounts.*.historyLimit`) for overrides. Set `0` to disable.
|
- Group history context is wrapped uniformly across channels and is **pending-only** (messages skipped due to mention gating); use `messages.groupChat.historyLimit` for the global default and `channels.<channel>.historyLimit` (or `channels.<channel>.accounts.*.historyLimit`) for overrides. Set `0` to disable.
|
||||||
|
|
||||||
## Group allowlists
|
## Group allowlists
|
||||||
When `channels.whatsapp.groups`, `channels.telegram.groups`, or `channels.imessage.groups` is configured, the keys act as a group allowlist. Use `"*"` to allow all groups while still setting default mention behavior.
|
When `channels.whatsapp.groups`, `channels.telegram.groups`, or `channels.imessage.groups` is configured, the keys act as a group allowlist. Use `"*"` to allow all groups while still setting default mention behavior.
|
||||||
|
|||||||
@@ -85,6 +85,10 @@ When a channel supplies history, it uses a shared wrapper:
|
|||||||
- `[Chat messages since your last reply - for context]`
|
- `[Chat messages since your last reply - for context]`
|
||||||
- `[Current message - respond to this]`
|
- `[Current message - respond to this]`
|
||||||
|
|
||||||
|
History buffers are **pending-only**: they include group messages that did *not*
|
||||||
|
trigger a run (for example, mention-gated messages) and **exclude** messages
|
||||||
|
already in the session transcript.
|
||||||
|
|
||||||
Directive stripping only applies to the **current message** section so history
|
Directive stripping only applies to the **current message** section so history
|
||||||
remains intact. Channels that wrap history should set `CommandBody` (or
|
remains intact. Channels that wrap history should set `CommandBody` (or
|
||||||
`RawBody`) to the original message text and keep `Body` as the combined prompt.
|
`RawBody`) to the original message text and keep `Body` as the combined prompt.
|
||||||
|
|||||||
@@ -93,6 +93,13 @@ Clawdbot ships with the pi‑ai catalog. These providers require **no**
|
|||||||
- CLI: `clawdbot onboard --auth-choice zai-api-key`
|
- CLI: `clawdbot onboard --auth-choice zai-api-key`
|
||||||
- Aliases: `z.ai/*` and `z-ai/*` normalize to `zai/*`
|
- Aliases: `z.ai/*` and `z-ai/*` normalize to `zai/*`
|
||||||
|
|
||||||
|
### Vercel AI Gateway
|
||||||
|
|
||||||
|
- Provider: `vercel-ai-gateway`
|
||||||
|
- Auth: `AI_GATEWAY_API_KEY`
|
||||||
|
- Example model: `vercel-ai-gateway/anthropic/claude-opus-4.5`
|
||||||
|
- CLI: `clawdbot onboard --auth-choice ai-gateway-api-key`
|
||||||
|
|
||||||
### Other built-in providers
|
### Other built-in providers
|
||||||
|
|
||||||
- OpenRouter: `openrouter` (`OPENROUTER_API_KEY`)
|
- OpenRouter: `openrouter` (`OPENROUTER_API_KEY`)
|
||||||
|
|||||||
+11
-11
@@ -1,28 +1,28 @@
|
|||||||
---
|
---
|
||||||
summary: "Command queue design that serializes auto-reply command execution"
|
summary: "Command queue design that serializes inbound auto-reply runs"
|
||||||
read_when:
|
read_when:
|
||||||
- Changing auto-reply execution or concurrency
|
- Changing auto-reply execution or concurrency
|
||||||
---
|
---
|
||||||
# Command Queue (2026-01-03)
|
# Command Queue (2026-01-16)
|
||||||
|
|
||||||
We now serialize command-based auto-replies (WhatsApp Web listener) through a tiny in-process queue to prevent multiple commands from running at once, while allowing safe parallelism across sessions.
|
We serialize inbound auto-reply runs (all channels) through a tiny in-process queue to prevent multiple agent runs from colliding, while still allowing safe parallelism across sessions.
|
||||||
|
|
||||||
## Why
|
## Why
|
||||||
- Some auto-reply commands are expensive (LLM calls) and can collide when multiple inbound messages arrive close together.
|
- Auto-reply runs can be expensive (LLM calls) and can collide when multiple inbound messages arrive close together.
|
||||||
- Serializing avoids competing for terminal/stdin, keeps logs readable, and reduces the chance of rate limits from upstream tools.
|
- Serializing avoids competing for shared resources (session files, logs, CLI stdin) and reduces the chance of upstream rate limits.
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
- A lane-aware FIFO queue drains each lane synchronously.
|
- A lane-aware FIFO queue drains each lane with a configurable concurrency cap (default 1).
|
||||||
- `runEmbeddedPiAgent` enqueues by **session key** (lane `session:<key>`) to guarantee only one active run per session.
|
- `runEmbeddedPiAgent` enqueues by **session key** (lane `session:<key>`) to guarantee only one active run per session.
|
||||||
- Each session run is then queued into a **global lane** (`main` by default) so overall parallelism is capped by `agents.defaults.maxConcurrent`.
|
- Each session run is then queued into a **global lane** (`main` by default) so overall parallelism is capped by `agents.defaults.maxConcurrent`.
|
||||||
- When verbose logging is enabled, queued commands emit a short notice if they waited more than ~2s before starting.
|
- When verbose logging is enabled, queued runs emit a short notice if they waited more than ~2s before starting.
|
||||||
- Typing indicators (`onReplyStart`) still fire immediately on enqueue so user experience is unchanged while we wait our turn.
|
- Typing indicators still fire immediately on enqueue (when supported by the channel) so user experience is unchanged while we wait our turn.
|
||||||
|
|
||||||
## Queue modes (per channel)
|
## Queue modes (per channel)
|
||||||
Inbound messages can steer the current run, wait for a followup turn, or do both:
|
Inbound messages can steer the current run, wait for a followup turn, or do both:
|
||||||
- `steer`: inject immediately into the current run (cancels pending tool calls after the next tool boundary). If not streaming, falls back to followup.
|
- `steer`: inject immediately into the current run (cancels pending tool calls after the next tool boundary). If not streaming, falls back to followup.
|
||||||
- `followup`: enqueue for the next agent turn after the current run ends.
|
- `followup`: enqueue for the next agent turn after the current run ends.
|
||||||
- `collect`: coalesce all queued messages into a **single** followup turn (default).
|
- `collect`: coalesce all queued messages into a **single** followup turn (default). If messages target different channels/threads, they drain individually to preserve routing.
|
||||||
- `steer-backlog` (aka `steer+backlog`): steer now **and** preserve the message for a followup turn.
|
- `steer-backlog` (aka `steer+backlog`): steer now **and** preserve the message for a followup turn.
|
||||||
- `interrupt` (legacy): abort the active run for that session, then run the newest message.
|
- `interrupt` (legacy): abort the active run for that session, then run the newest message.
|
||||||
- `queue` (legacy alias): same as `steer`.
|
- `queue` (legacy alias): same as `steer`.
|
||||||
@@ -66,9 +66,9 @@ Defaults: `debounceMs: 1000`, `cap: 20`, `drop: summarize`.
|
|||||||
- `/queue default` or `/queue reset` clears the session override.
|
- `/queue default` or `/queue reset` clears the session override.
|
||||||
|
|
||||||
## Scope and guarantees
|
## Scope and guarantees
|
||||||
- Applies only to config-driven command replies; plain text replies are unaffected.
|
- Applies to auto-reply agent runs across all inbound channels that use the gateway reply pipeline (WhatsApp web, Telegram, Slack, Discord, Signal, iMessage, webchat, etc.).
|
||||||
- Default lane (`main`) is process-wide for inbound + main heartbeats; set `agents.defaults.maxConcurrent` to allow multiple sessions in parallel.
|
- Default lane (`main`) is process-wide for inbound + main heartbeats; set `agents.defaults.maxConcurrent` to allow multiple sessions in parallel.
|
||||||
- Additional lanes may exist (e.g. `cron`) so background jobs can run in parallel without blocking inbound replies.
|
- Additional lanes may exist (e.g. `cron`, `subagent`) so background jobs can run in parallel without blocking inbound replies.
|
||||||
- Per-session lanes guarantee that only one agent run touches a given session at a time.
|
- Per-session lanes guarantee that only one agent run touches a given session at a time.
|
||||||
- No external dependencies or background worker threads; pure TypeScript + promises.
|
- No external dependencies or background worker threads; pure TypeScript + promises.
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ Use `session.dmScope` to control how **direct messages** are grouped:
|
|||||||
- `main` (default): all DMs share the main session for continuity.
|
- `main` (default): all DMs share the main session for continuity.
|
||||||
- `per-peer`: isolate by sender id across channels.
|
- `per-peer`: isolate by sender id across channels.
|
||||||
- `per-channel-peer`: isolate by channel + sender (recommended for multi-user inboxes).
|
- `per-channel-peer`: isolate by channel + sender (recommended for multi-user inboxes).
|
||||||
|
Use `session.identityLinks` to map provider-prefixed peer ids to a canonical identity so the same person shares a DM session across channels when using `per-peer` or `per-channel-peer`.
|
||||||
|
|
||||||
## Gateway is the source of truth
|
## Gateway is the source of truth
|
||||||
All session state is **owned by the gateway** (the “master” Clawdbot). UI clients (macOS app, WebChat, etc.) must query the gateway for session lists and token counts instead of reading local files.
|
All session state is **owned by the gateway** (the “master” Clawdbot). UI clients (macOS app, WebChat, etc.) must query the gateway for session lists and token counts instead of reading local files.
|
||||||
@@ -42,6 +43,7 @@ the workspace is writable. See [Memory](/concepts/memory) and
|
|||||||
- Multiple phone numbers and channels can map to the same agent main key; they act as transports into one conversation.
|
- Multiple phone numbers and channels can map to the same agent main key; they act as transports into one conversation.
|
||||||
- `per-peer`: `agent:<agentId>:dm:<peerId>`.
|
- `per-peer`: `agent:<agentId>:dm:<peerId>`.
|
||||||
- `per-channel-peer`: `agent:<agentId>:<channel>:dm:<peerId>`.
|
- `per-channel-peer`: `agent:<agentId>:<channel>:dm:<peerId>`.
|
||||||
|
- If `session.identityLinks` matches a provider-prefixed peer id (for example `telegram:123`), the canonical key replaces `<peerId>` so the same person shares a session across channels.
|
||||||
- Group chats isolate state: `agent:<agentId>:<channel>:group:<id>` (rooms/channels use `agent:<agentId>:<channel>:channel:<id>`).
|
- Group chats isolate state: `agent:<agentId>:<channel>:group:<id>` (rooms/channels use `agent:<agentId>:<channel>:channel:<id>`).
|
||||||
- Telegram forum topics append `:topic:<threadId>` to the group id for isolation.
|
- Telegram forum topics append `:topic:<threadId>` to the group id for isolation.
|
||||||
- Legacy `group:<id>` keys are still recognized for migration.
|
- Legacy `group:<id>` keys are still recognized for migration.
|
||||||
@@ -55,6 +57,7 @@ the workspace is writable. See [Memory](/concepts/memory) and
|
|||||||
- Idle expiry: `session.idleMinutes` (default 60). After the timeout a new `sessionId` is minted on the next message.
|
- Idle expiry: `session.idleMinutes` (default 60). After the timeout a new `sessionId` is minted on the next message.
|
||||||
- Reset triggers: exact `/new` or `/reset` (plus any extras in `resetTriggers`) start a fresh session id and pass the remainder of the message through. If `/new` or `/reset` is sent alone, Clawdbot runs a short “hello” greeting turn to confirm the reset.
|
- Reset triggers: exact `/new` or `/reset` (plus any extras in `resetTriggers`) start a fresh session id and pass the remainder of the message through. If `/new` or `/reset` is sent alone, Clawdbot runs a short “hello” greeting turn to confirm the reset.
|
||||||
- Manual reset: delete specific keys from the store or remove the JSONL transcript; the next message recreates them.
|
- Manual reset: delete specific keys from the store or remove the JSONL transcript; the next message recreates them.
|
||||||
|
- Isolated cron jobs always mint a fresh `sessionId` per run (no idle reuse).
|
||||||
|
|
||||||
## Send policy (optional)
|
## Send policy (optional)
|
||||||
Block delivery for specific session types without listing individual ids.
|
Block delivery for specific session types without listing individual ids.
|
||||||
@@ -86,6 +89,9 @@ Send these as standalone messages so they register.
|
|||||||
session: {
|
session: {
|
||||||
scope: "per-sender", // keep group keys separate
|
scope: "per-sender", // keep group keys separate
|
||||||
dmScope: "main", // DM continuity (set per-channel-peer for shared inboxes)
|
dmScope: "main", // DM continuity (set per-channel-peer for shared inboxes)
|
||||||
|
identityLinks: {
|
||||||
|
alice: ["telegram:123456789", "discord:987654321012345678"]
|
||||||
|
},
|
||||||
idleMinutes: 120,
|
idleMinutes: 120,
|
||||||
resetTriggers: ["/new", "/reset"],
|
resetTriggers: ["/new", "/reset"],
|
||||||
store: "~/.clawdbot/agents/{agentId}/sessions/sessions.json",
|
store: "~/.clawdbot/agents/{agentId}/sessions/sessions.json",
|
||||||
@@ -100,7 +106,7 @@ Send these as standalone messages so they register.
|
|||||||
- `clawdbot gateway call sessions.list --params '{}'` — fetch sessions from the running gateway (use `--url`/`--token` for remote gateway access).
|
- `clawdbot gateway call sessions.list --params '{}'` — fetch sessions from the running gateway (use `--url`/`--token` for remote gateway access).
|
||||||
- Send `/status` as a standalone message in chat to see whether the agent is reachable, how much of the session context is used, current thinking/verbose toggles, and when your WhatsApp web creds were last refreshed (helps spot relink needs).
|
- Send `/status` as a standalone message in chat to see whether the agent is reachable, how much of the session context is used, current thinking/verbose toggles, and when your WhatsApp web creds were last refreshed (helps spot relink needs).
|
||||||
- Send `/context list` or `/context detail` to see what’s in the system prompt and injected workspace files (and the biggest context contributors).
|
- Send `/context list` or `/context detail` to see what’s in the system prompt and injected workspace files (and the biggest context contributors).
|
||||||
- Send `/stop` as a standalone message to abort the current run.
|
- Send `/stop` as a standalone message to abort the current run, clear queued followups for that session, and stop any sub-agent runs spawned from it (the reply includes the stopped count).
|
||||||
- Send `/compact` (optional instructions) as a standalone message to summarize older context and free up window space. See [/concepts/compaction](/concepts/compaction).
|
- Send `/compact` (optional instructions) as a standalone message to summarize older context and free up window space. See [/concepts/compaction](/concepts/compaction).
|
||||||
- JSONL transcripts can be opened directly to review full turns.
|
- JSONL transcripts can be opened directly to review full turns.
|
||||||
|
|
||||||
|
|||||||
+27
-1
@@ -733,6 +733,22 @@
|
|||||||
"source": "/wizard",
|
"source": "/wizard",
|
||||||
"destination": "/start/wizard"
|
"destination": "/start/wizard"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"source": "/install/node",
|
||||||
|
"destination": "/install#nodejs--npm-path-sanity"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/install/node/",
|
||||||
|
"destination": "/install#nodejs--npm-path-sanity"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/start/faq",
|
||||||
|
"destination": "/help"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/start/faq/",
|
||||||
|
"destination": "/help"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"source": "/oauth",
|
"source": "/oauth",
|
||||||
"destination": "/concepts/oauth"
|
"destination": "/concepts/oauth"
|
||||||
@@ -752,7 +768,6 @@
|
|||||||
"start/wizard",
|
"start/wizard",
|
||||||
"start/setup",
|
"start/setup",
|
||||||
"start/pairing",
|
"start/pairing",
|
||||||
"start/faq",
|
|
||||||
"start/clawd",
|
"start/clawd",
|
||||||
"start/showcase",
|
"start/showcase",
|
||||||
"start/hubs",
|
"start/hubs",
|
||||||
@@ -760,6 +775,14 @@
|
|||||||
"start/lore"
|
"start/lore"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"group": "Help",
|
||||||
|
"pages": [
|
||||||
|
"help/index",
|
||||||
|
"help/troubleshooting",
|
||||||
|
"help/faq"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"group": "Install & Updates",
|
"group": "Install & Updates",
|
||||||
"pages": [
|
"pages": [
|
||||||
@@ -792,6 +815,7 @@
|
|||||||
"cli/health",
|
"cli/health",
|
||||||
"cli/sessions",
|
"cli/sessions",
|
||||||
"cli/channels",
|
"cli/channels",
|
||||||
|
"cli/directory",
|
||||||
"cli/skills",
|
"cli/skills",
|
||||||
"cli/plugins",
|
"cli/plugins",
|
||||||
"cli/memory",
|
"cli/memory",
|
||||||
@@ -907,6 +931,7 @@
|
|||||||
"channels/msteams",
|
"channels/msteams",
|
||||||
"channels/matrix",
|
"channels/matrix",
|
||||||
"channels/zalo",
|
"channels/zalo",
|
||||||
|
"channels/zalouser",
|
||||||
"broadcast-groups",
|
"broadcast-groups",
|
||||||
"channels/troubleshooting",
|
"channels/troubleshooting",
|
||||||
"channels/location"
|
"channels/location"
|
||||||
@@ -945,6 +970,7 @@
|
|||||||
"tools",
|
"tools",
|
||||||
"plugin",
|
"plugin",
|
||||||
"plugins/voice-call",
|
"plugins/voice-call",
|
||||||
|
"plugins/zalouser",
|
||||||
"tools/exec",
|
"tools/exec",
|
||||||
"tools/web",
|
"tools/web",
|
||||||
"tools/apply-patch",
|
"tools/apply-patch",
|
||||||
|
|||||||
@@ -53,6 +53,24 @@ Env var equivalents:
|
|||||||
- `CLAWDBOT_LOAD_SHELL_ENV=1`
|
- `CLAWDBOT_LOAD_SHELL_ENV=1`
|
||||||
- `CLAWDBOT_SHELL_ENV_TIMEOUT_MS=15000`
|
- `CLAWDBOT_SHELL_ENV_TIMEOUT_MS=15000`
|
||||||
|
|
||||||
|
## Env var substitution in config
|
||||||
|
|
||||||
|
You can reference env vars directly in config string values using `${VAR_NAME}` syntax:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
models: {
|
||||||
|
providers: {
|
||||||
|
"vercel-gateway": {
|
||||||
|
apiKey: "${VERCEL_GATEWAY_API_KEY}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Configuration: Env var substitution](/gateway/configuration#env-var-substitution-in-config) for full details.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [Gateway configuration](/gateway/configuration)
|
- [Gateway configuration](/gateway/configuration)
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ clawdbot doctor
|
|||||||
If you’d rather not manage env vars yourself, the onboarding wizard can store
|
If you’d rather not manage env vars yourself, the onboarding wizard can store
|
||||||
API keys for daemon use: `clawdbot onboard`.
|
API keys for daemon use: `clawdbot onboard`.
|
||||||
|
|
||||||
See [/start/faq](/start/faq) for details on env inheritance (`env.shellEnv`,
|
See [Help](/help) for details on env inheritance (`env.shellEnv`,
|
||||||
`~/.clawdbot/.env`, systemd/launchd).
|
`~/.clawdbot/.env`, systemd/launchd).
|
||||||
|
|
||||||
## Anthropic: Claude Code CLI setup-token (supported)
|
## Anthropic: Claude Code CLI setup-token (supported)
|
||||||
|
|||||||
@@ -283,6 +283,48 @@ Env var equivalent:
|
|||||||
- `CLAWDBOT_LOAD_SHELL_ENV=1`
|
- `CLAWDBOT_LOAD_SHELL_ENV=1`
|
||||||
- `CLAWDBOT_SHELL_ENV_TIMEOUT_MS=15000`
|
- `CLAWDBOT_SHELL_ENV_TIMEOUT_MS=15000`
|
||||||
|
|
||||||
|
### Env var substitution in config
|
||||||
|
|
||||||
|
You can reference environment variables directly in any config string value using
|
||||||
|
`${VAR_NAME}` syntax. Variables are substituted at config load time, before validation.
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
models: {
|
||||||
|
providers: {
|
||||||
|
"vercel-gateway": {
|
||||||
|
apiKey: "${VERCEL_GATEWAY_API_KEY}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
gateway: {
|
||||||
|
auth: {
|
||||||
|
token: "${CLAWDBOT_GATEWAY_TOKEN}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rules:**
|
||||||
|
- Only uppercase env var names are matched: `[A-Z_][A-Z0-9_]*`
|
||||||
|
- Missing or empty env vars throw an error at config load
|
||||||
|
- Escape with `$${VAR}` to output a literal `${VAR}`
|
||||||
|
- Works with `$include` (included files also get substitution)
|
||||||
|
|
||||||
|
**Inline substitution:**
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
models: {
|
||||||
|
providers: {
|
||||||
|
custom: {
|
||||||
|
baseUrl: "${CUSTOM_API_BASE}/v1" // → "https://api.example.com/v1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Auth storage (OAuth + API keys)
|
### Auth storage (OAuth + API keys)
|
||||||
|
|
||||||
Clawdbot stores **per-agent** auth profiles (OAuth + API keys) in:
|
Clawdbot stores **per-agent** auth profiles (OAuth + API keys) in:
|
||||||
@@ -871,6 +913,7 @@ Notes:
|
|||||||
- `commands.text: false` disables parsing chat messages for commands.
|
- `commands.text: false` disables parsing chat messages for commands.
|
||||||
- `commands.native: "auto"` (default) turns on native commands for Discord/Telegram and leaves Slack off; unsupported channels stay text-only.
|
- `commands.native: "auto"` (default) turns on native commands for Discord/Telegram and leaves Slack off; unsupported channels stay text-only.
|
||||||
- Set `commands.native: true|false` to force all, or override per channel with `channels.discord.commands.native`, `channels.telegram.commands.native`, `channels.slack.commands.native` (bool or `"auto"`). `false` clears previously registered commands on Discord/Telegram at startup; Slack commands are managed in the Slack app.
|
- Set `commands.native: true|false` to force all, or override per channel with `channels.discord.commands.native`, `channels.telegram.commands.native`, `channels.slack.commands.native` (bool or `"auto"`). `false` clears previously registered commands on Discord/Telegram at startup; Slack commands are managed in the Slack app.
|
||||||
|
- `channels.telegram.customCommands` adds extra Telegram bot menu entries. Names are normalized; conflicts with native commands are ignored.
|
||||||
- `commands.bash: true` enables `! <cmd>` to run host shell commands (`/bash <cmd>` also works as an alias). Requires `tools.elevated.enabled` and allowlisting the sender in `tools.elevated.allowFrom.<channel>`.
|
- `commands.bash: true` enables `! <cmd>` to run host shell commands (`/bash <cmd>` also works as an alias). Requires `tools.elevated.enabled` and allowlisting the sender in `tools.elevated.allowFrom.<channel>`.
|
||||||
- `commands.bashForegroundMs` controls how long bash waits before backgrounding. While a bash job is running, new `! <cmd>` requests are rejected (one at a time).
|
- `commands.bashForegroundMs` controls how long bash waits before backgrounding. While a bash job is running, new `! <cmd>` requests are rejected (one at a time).
|
||||||
- `commands.config: true` enables `/config` (reads/writes `clawdbot.json`).
|
- `commands.config: true` enables `/config` (reads/writes `clawdbot.json`).
|
||||||
@@ -929,6 +972,10 @@ Set `channels.telegram.configWrites: false` to block Telegram-initiated config w
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
customCommands: [
|
||||||
|
{ command: "backup", description: "Git backup" },
|
||||||
|
{ command: "generate", description: "Create an image" }
|
||||||
|
],
|
||||||
historyLimit: 50, // include last N group messages as context (0 disables)
|
historyLimit: 50, // include last N group messages as context (0 disables)
|
||||||
replyToMode: "first", // off | first | all
|
replyToMode: "first", // off | first | all
|
||||||
streamMode: "partial", // off | partial | block (draft streaming; separate from block streaming)
|
streamMode: "partial", // off | partial | block (draft streaming; separate from block streaming)
|
||||||
@@ -938,6 +985,7 @@ Set `channels.telegram.configWrites: false` to block Telegram-initiated config w
|
|||||||
breakPreference: "paragraph" // paragraph | newline | sentence
|
breakPreference: "paragraph" // paragraph | newline | sentence
|
||||||
},
|
},
|
||||||
actions: { reactions: true, sendMessage: true }, // tool action gates (false disables)
|
actions: { reactions: true, sendMessage: true }, // tool action gates (false disables)
|
||||||
|
reactionNotifications: "own", // off | own | all
|
||||||
mediaMaxMb: 5,
|
mediaMaxMb: 5,
|
||||||
retry: { // outbound retry policy
|
retry: { // outbound retry policy
|
||||||
attempts: 3,
|
attempts: 3,
|
||||||
@@ -1266,7 +1314,9 @@ See [Messages](/concepts/messages) for queueing, sessions, and streaming context
|
|||||||
`responsePrefix` is applied to **all outbound replies** (tool summaries, block
|
`responsePrefix` is applied to **all outbound replies** (tool summaries, block
|
||||||
streaming, final replies) across channels unless already present.
|
streaming, final replies) across channels unless already present.
|
||||||
|
|
||||||
If `messages.responsePrefix` is unset, no prefix is applied by default.
|
If `messages.responsePrefix` is unset, no prefix is applied by default. WhatsApp self-chat
|
||||||
|
replies are the exception: they default to `[{identity.name}]` when set, otherwise
|
||||||
|
`[clawdbot]`, so same-phone conversations stay legible.
|
||||||
Set it to `"auto"` to derive `[{identity.name}]` for the routed agent (when set).
|
Set it to `"auto"` to derive `[{identity.name}]` for the routed agent (when set).
|
||||||
|
|
||||||
#### Template variables
|
#### Template variables
|
||||||
@@ -1701,11 +1751,18 @@ Legacy: `tools.bash` is still accepted as an alias.
|
|||||||
- `tools.web.search.maxResults` (1–10, default 5)
|
- `tools.web.search.maxResults` (1–10, default 5)
|
||||||
- `tools.web.search.timeoutSeconds` (default 30)
|
- `tools.web.search.timeoutSeconds` (default 30)
|
||||||
- `tools.web.search.cacheTtlMinutes` (default 15)
|
- `tools.web.search.cacheTtlMinutes` (default 15)
|
||||||
- `tools.web.fetch.enabled` (default false; sandboxed sessions auto-enable unless set to false)
|
- `tools.web.fetch.enabled` (default true)
|
||||||
- `tools.web.fetch.maxChars` (default 50000)
|
- `tools.web.fetch.maxChars` (default 50000)
|
||||||
- `tools.web.fetch.timeoutSeconds` (default 30)
|
- `tools.web.fetch.timeoutSeconds` (default 30)
|
||||||
- `tools.web.fetch.cacheTtlMinutes` (default 15)
|
- `tools.web.fetch.cacheTtlMinutes` (default 15)
|
||||||
- `tools.web.fetch.userAgent` (optional override)
|
- `tools.web.fetch.userAgent` (optional override)
|
||||||
|
- `tools.web.fetch.readability` (default true; disable to use basic HTML cleanup only)
|
||||||
|
- `tools.web.fetch.firecrawl.enabled` (default true when an API key is set)
|
||||||
|
- `tools.web.fetch.firecrawl.apiKey` (optional; defaults to `FIRECRAWL_API_KEY`)
|
||||||
|
- `tools.web.fetch.firecrawl.baseUrl` (default https://api.firecrawl.dev)
|
||||||
|
- `tools.web.fetch.firecrawl.onlyMainContent` (default true)
|
||||||
|
- `tools.web.fetch.firecrawl.maxAgeMs` (optional)
|
||||||
|
- `tools.web.fetch.firecrawl.timeoutSeconds` (optional)
|
||||||
|
|
||||||
`agents.defaults.subagents` configures sub-agent defaults:
|
`agents.defaults.subagents` configures sub-agent defaults:
|
||||||
- `model`: default model for spawned sub-agents (string or `{ primary, fallbacks }`). If omitted, sub-agents inherit the caller’s model unless overridden per agent or per call.
|
- `model`: default model for spawned sub-agents (string or `{ primary, fallbacks }`). If omitted, sub-agents inherit the caller’s model unless overridden per agent or per call.
|
||||||
@@ -2262,6 +2319,9 @@ Controls session scoping, idle expiry, reset triggers, and where the session sto
|
|||||||
session: {
|
session: {
|
||||||
scope: "per-sender",
|
scope: "per-sender",
|
||||||
dmScope: "main",
|
dmScope: "main",
|
||||||
|
identityLinks: {
|
||||||
|
alice: ["telegram:123456789", "discord:987654321012345678"]
|
||||||
|
},
|
||||||
idleMinutes: 60,
|
idleMinutes: 60,
|
||||||
resetTriggers: ["/new", "/reset"],
|
resetTriggers: ["/new", "/reset"],
|
||||||
// Default is already per-agent under ~/.clawdbot/agents/<agentId>/sessions/sessions.json
|
// Default is already per-agent under ~/.clawdbot/agents/<agentId>/sessions/sessions.json
|
||||||
@@ -2290,6 +2350,8 @@ Fields:
|
|||||||
- `main`: all DMs share the main session for continuity.
|
- `main`: all DMs share the main session for continuity.
|
||||||
- `per-peer`: isolate DMs by sender id across channels.
|
- `per-peer`: isolate DMs by sender id across channels.
|
||||||
- `per-channel-peer`: isolate DMs per channel + sender (recommended for multi-user inboxes).
|
- `per-channel-peer`: isolate DMs per channel + sender (recommended for multi-user inboxes).
|
||||||
|
- `identityLinks`: map canonical ids to provider-prefixed peers so the same person shares a DM session across channels when using `per-peer` or `per-channel-peer`.
|
||||||
|
- Example: `alice: ["telegram:123456789", "discord:987654321012345678"]`.
|
||||||
- `agentToAgent.maxPingPongTurns`: max reply-back turns between requester/target (0–5, default 5).
|
- `agentToAgent.maxPingPongTurns`: max reply-back turns between requester/target (0–5, default 5).
|
||||||
- `sendPolicy.default`: `allow` or `deny` fallback when no rule matches.
|
- `sendPolicy.default`: `allow` or `deny` fallback when no rule matches.
|
||||||
- `sendPolicy.rules[]`: match by `channel`, `chatType` (`direct|group|room`), or `keyPrefix` (e.g. `cron:`). First deny wins; otherwise allow.
|
- `sendPolicy.rules[]`: match by `channel`, `chatType` (`direct|group|room`), or `keyPrefix` (e.g. `cron:`). First deny wins; otherwise allow.
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ By default, Clawdbot routes **all DMs into the main session** so your assistant
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
This prevents cross-user context leakage while keeping group chats isolated. See [Session Management](/concepts/session) and [Configuration](/gateway/configuration).
|
This prevents cross-user context leakage while keeping group chats isolated. If the same person contacts you on multiple channels, use `session.identityLinks` to collapse those DM sessions into one canonical identity. See [Session Management](/concepts/session) and [Configuration](/gateway/configuration).
|
||||||
|
|
||||||
## Allowlists (DM + groups) — terminology
|
## Allowlists (DM + groups) — terminology
|
||||||
|
|
||||||
@@ -161,6 +161,15 @@ Even with strong system prompts, **prompt injection is not solved**. What helps
|
|||||||
- Run sensitive tool execution in a sandbox; keep secrets out of the agent’s reachable filesystem.
|
- Run sensitive tool execution in a sandbox; keep secrets out of the agent’s reachable filesystem.
|
||||||
- **Model choice matters:** older/legacy models can be less robust against prompt injection and tool misuse. Prefer modern, instruction-hardened models for any bot with tools. We recommend Anthropic Opus 4.5 because it’s quite good at recognizing prompt injections (see [“A step forward on safety”](https://www.anthropic.com/news/claude-opus-4-5)).
|
- **Model choice matters:** older/legacy models can be less robust against prompt injection and tool misuse. Prefer modern, instruction-hardened models for any bot with tools. We recommend Anthropic Opus 4.5 because it’s quite good at recognizing prompt injections (see [“A step forward on safety”](https://www.anthropic.com/news/claude-opus-4-5)).
|
||||||
|
|
||||||
|
### Model strength (security note)
|
||||||
|
|
||||||
|
Prompt injection resistance is **not** uniform across model tiers. Smaller/cheaper models are generally more susceptible to tool misuse and instruction hijacking, especially under adversarial prompts.
|
||||||
|
|
||||||
|
Recommendations:
|
||||||
|
- **Use the latest generation, best-tier model** for any bot that can run tools or touch files/networks.
|
||||||
|
- **Avoid weaker tiers** (for example, Sonnet or Haiku) for tool-enabled agents or untrusted inboxes.
|
||||||
|
- If you must use a smaller model, **reduce blast radius** (read-only tools, strong sandboxing, minimal filesystem access, strict allowlists).
|
||||||
|
|
||||||
## Reasoning & verbose output in groups
|
## Reasoning & verbose output in groups
|
||||||
|
|
||||||
`/reasoning` and `/verbose` can expose internal reasoning or tool output that
|
`/reasoning` and `/verbose` can expose internal reasoning or tool output that
|
||||||
|
|||||||
@@ -169,6 +169,20 @@ The agent was interrupted mid-response.
|
|||||||
|
|
||||||
**Fix:** Just send another message. The session continues.
|
**Fix:** Just send another message. The session continues.
|
||||||
|
|
||||||
|
### "Agent failed before reply: Unknown model: anthropic/claude-haiku-3-5"
|
||||||
|
|
||||||
|
Clawdbot intentionally rejects **older/insecure models** (especially those more
|
||||||
|
vulnerable to prompt injection). If you see this error, the model name is no
|
||||||
|
longer supported.
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
- Pick a **latest** model for the provider and update your config or model alias.
|
||||||
|
- If you’re unsure which models are available, run `clawdbot models list` or
|
||||||
|
`clawdbot models scan` and choose a supported one.
|
||||||
|
- Check gateway logs for the detailed failure reason.
|
||||||
|
|
||||||
|
See also: [Models CLI](/cli/models) and [Model providers](/concepts/model-providers).
|
||||||
|
|
||||||
### Messages Not Triggering
|
### Messages Not Triggering
|
||||||
|
|
||||||
**Check 1:** Is the sender allowlisted?
|
**Check 1:** Is the sender allowlisted?
|
||||||
@@ -306,6 +320,138 @@ Clawdbot keeps conversation history in memory.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Common troubleshooting
|
||||||
|
|
||||||
|
### “All models failed” — what should I check first?
|
||||||
|
|
||||||
|
- **Credentials** present for the provider(s) being tried (auth profiles + env vars).
|
||||||
|
- **Model routing**: confirm `agents.defaults.model.primary` and fallbacks are models you can access.
|
||||||
|
- **Gateway logs** in `/tmp/clawdbot/…` for the exact provider error.
|
||||||
|
- **Model status**: use `/model status` (chat) or `clawdbot models status` (CLI).
|
||||||
|
|
||||||
|
### I’m running on my personal WhatsApp number — why is self-chat weird?
|
||||||
|
|
||||||
|
Enable self-chat mode and allowlist your own number:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
whatsapp: {
|
||||||
|
selfChatMode: true,
|
||||||
|
dmPolicy: "allowlist",
|
||||||
|
allowFrom: ["+15555550123"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
See [WhatsApp setup](/channels/whatsapp).
|
||||||
|
|
||||||
|
### WhatsApp logged me out. How do I re‑auth?
|
||||||
|
|
||||||
|
Run the login command again and scan the QR code:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot channels login
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build errors on `main` — what’s the standard fix path?
|
||||||
|
|
||||||
|
1) `git pull origin main && pnpm install`
|
||||||
|
2) `pnpm clawdbot doctor`
|
||||||
|
3) Check GitHub issues or Discord
|
||||||
|
4) Temporary workaround: check out an older commit
|
||||||
|
|
||||||
|
### npm install fails (allow-build-scripts / missing tar or yargs). What now?
|
||||||
|
|
||||||
|
If you’re running from source, use the repo’s package manager: **pnpm** (preferred).
|
||||||
|
The repo declares `packageManager: "pnpm@…"`.
|
||||||
|
|
||||||
|
Typical recovery:
|
||||||
|
```bash
|
||||||
|
git status # ensure you’re in the repo root
|
||||||
|
pnpm install
|
||||||
|
pnpm build
|
||||||
|
pnpm clawdbot doctor
|
||||||
|
clawdbot daemon restart
|
||||||
|
```
|
||||||
|
|
||||||
|
Why: pnpm is the configured package manager for this repo.
|
||||||
|
|
||||||
|
### How do I switch between git installs and npm installs?
|
||||||
|
|
||||||
|
Use the **website installer** and select the install method with a flag. It
|
||||||
|
upgrades in place and rewrites the gateway service to point at the new install.
|
||||||
|
|
||||||
|
Switch **to git install**:
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://clawd.bot/install.sh | bash -s -- --install-method git --no-onboard
|
||||||
|
```
|
||||||
|
|
||||||
|
Switch **to npm global**:
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://clawd.bot/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- The git flow only rebases if the repo is clean. Commit or stash changes first.
|
||||||
|
- After switching, run:
|
||||||
|
```bash
|
||||||
|
clawdbot doctor
|
||||||
|
clawdbot daemon restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### Telegram block streaming isn’t splitting text between tool calls. Why?
|
||||||
|
|
||||||
|
Block streaming only sends **completed text blocks**. Common reasons you see a single message:
|
||||||
|
- `agents.defaults.blockStreamingDefault` is still `"off"`.
|
||||||
|
- `channels.telegram.blockStreaming` is set to `false`.
|
||||||
|
- `channels.telegram.streamMode` is `partial` or `block` **and draft streaming is active**
|
||||||
|
(private chat + topics). Draft streaming disables block streaming in that case.
|
||||||
|
- Your `minChars` / coalesce settings are too high, so chunks get merged.
|
||||||
|
- The model emits one large text block (no mid‑reply flush points).
|
||||||
|
|
||||||
|
Fix checklist:
|
||||||
|
1) Put block streaming settings under `agents.defaults`, not the root.
|
||||||
|
2) Set `channels.telegram.streamMode: "off"` if you want real multi‑message block replies.
|
||||||
|
3) Use smaller chunk/coalesce thresholds while debugging.
|
||||||
|
|
||||||
|
See [Streaming](/concepts/streaming).
|
||||||
|
|
||||||
|
### Discord doesn’t reply in my server even with `requireMention: false`. Why?
|
||||||
|
|
||||||
|
`requireMention` only controls mention‑gating **after** the channel passes allowlists.
|
||||||
|
By default `channels.discord.groupPolicy` is **allowlist**, so guilds must be explicitly enabled.
|
||||||
|
If you set `channels.discord.guilds.<guildId>.channels`, only the listed channels are allowed; omit it to allow all channels in the guild.
|
||||||
|
|
||||||
|
Fix checklist:
|
||||||
|
1) Set `channels.discord.groupPolicy: "open"` **or** add a guild allowlist entry (and optionally a channel allowlist).
|
||||||
|
2) Use **numeric channel IDs** in `channels.discord.guilds.<guildId>.channels`.
|
||||||
|
3) Put `requireMention: false` **under** `channels.discord.guilds` (global or per‑channel).
|
||||||
|
Top‑level `channels.discord.requireMention` is not a supported key.
|
||||||
|
4) Ensure the bot has **Message Content Intent** and channel permissions.
|
||||||
|
5) Run `clawdbot channels status --probe` for audit hints.
|
||||||
|
|
||||||
|
Docs: [Discord](/channels/discord), [Channels troubleshooting](/channels/troubleshooting).
|
||||||
|
|
||||||
|
### Cloud Code Assist API error: invalid tool schema (400). What now?
|
||||||
|
|
||||||
|
This is almost always a **tool schema compatibility** issue. The Cloud Code Assist
|
||||||
|
endpoint accepts a strict subset of JSON Schema. Clawdbot scrubs/normalizes tool
|
||||||
|
schemas in current `main`, but the fix is not in the last release yet (as of
|
||||||
|
January 13, 2026).
|
||||||
|
|
||||||
|
Fix checklist:
|
||||||
|
1) **Update Clawdbot**:
|
||||||
|
- If you can run from source, pull `main` and restart the gateway.
|
||||||
|
- Otherwise, wait for the next release that includes the schema scrubber.
|
||||||
|
2) Avoid unsupported keywords like `anyOf/oneOf/allOf`, `patternProperties`,
|
||||||
|
`additionalProperties`, `minLength`, `maxLength`, `format`, etc.
|
||||||
|
3) If you define custom tools, keep the top‑level schema as `type: "object"` with
|
||||||
|
`properties` and simple enums.
|
||||||
|
|
||||||
|
See [Tools](/tools) and [TypeBox schemas](/concepts/typebox).
|
||||||
|
|
||||||
## macOS Specific Issues
|
## macOS Specific Issues
|
||||||
|
|
||||||
### App Crashes when Granting Permissions (Speech/Mic)
|
### App Crashes when Granting Permissions (Speech/Mic)
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
summary: "FAQ (concepts): what Clawdbot is and how it fits together"
|
||||||
|
read_when:
|
||||||
|
- You’re new and want the mental model
|
||||||
|
- You’re not debugging a specific error
|
||||||
|
---
|
||||||
|
|
||||||
|
# FAQ (concepts)
|
||||||
|
|
||||||
|
If you’re here because something’s broken, start with: [Troubleshooting](/help/troubleshooting).
|
||||||
|
|
||||||
|
## What is Clawdbot?
|
||||||
|
|
||||||
|
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 always‑on control plane; the assistant is the product.
|
||||||
|
|
||||||
|
## What runtime do I need?
|
||||||
|
|
||||||
|
Node **>= 22** is required. `pnpm` is recommended. Bun is **not recommended** for the Gateway.
|
||||||
|
|
||||||
|
## What’s the recommended setup flow?
|
||||||
|
|
||||||
|
Use the onboarding wizard:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot onboard --install-daemon
|
||||||
|
```
|
||||||
|
|
||||||
|
Then use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot dashboard
|
||||||
|
```
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
summary: "Help hub: common fixes, install sanity, and where to look when something breaks"
|
||||||
|
read_when:
|
||||||
|
- You’re new and want the “what do I click/run” guide
|
||||||
|
- Something broke and you want the fastest path to a fix
|
||||||
|
---
|
||||||
|
|
||||||
|
# Help
|
||||||
|
|
||||||
|
If you want a quick “get unstuck” flow, start here:
|
||||||
|
|
||||||
|
- **Troubleshooting:** [Start here](/help/troubleshooting)
|
||||||
|
- **Install sanity (Node/npm/PATH):** [Install](/install#nodejs--npm-path-sanity)
|
||||||
|
- **Gateway issues:** [Gateway troubleshooting](/gateway/troubleshooting)
|
||||||
|
- **Logs:** [Logging](/logging) and [Gateway logging](/gateway/logging)
|
||||||
|
- **Repairs:** [Doctor](/gateway/doctor)
|
||||||
|
|
||||||
|
If you’re looking for conceptual questions (not “something broke”):
|
||||||
|
|
||||||
|
- [FAQ (concepts)](/help/faq)
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
summary: "Troubleshooting hub: symptoms → checks → fixes"
|
||||||
|
read_when:
|
||||||
|
- You see an error and want the fix path
|
||||||
|
- The installer says “success” but the CLI doesn’t work
|
||||||
|
---
|
||||||
|
|
||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
## First 60 seconds
|
||||||
|
|
||||||
|
Run these in order:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot status
|
||||||
|
clawdbot status --all
|
||||||
|
clawdbot daemon status
|
||||||
|
clawdbot logs --follow
|
||||||
|
clawdbot doctor
|
||||||
|
```
|
||||||
|
|
||||||
|
If the gateway is reachable, deep probes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot status --deep
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common “it broke” cases
|
||||||
|
|
||||||
|
### `clawdbot: command not found`
|
||||||
|
|
||||||
|
Almost always a Node/npm PATH issue. Start here:
|
||||||
|
|
||||||
|
- [Install (Node/npm PATH sanity)](/install#nodejs--npm-path-sanity)
|
||||||
|
|
||||||
|
### Gateway “unauthorized”, can’t connect, or keeps reconnecting
|
||||||
|
|
||||||
|
- [Gateway troubleshooting](/gateway/troubleshooting)
|
||||||
|
- [Gateway authentication](/gateway/authentication)
|
||||||
|
|
||||||
|
### Daemon says running, but RPC probe fails
|
||||||
|
|
||||||
|
- [Gateway troubleshooting](/gateway/troubleshooting)
|
||||||
|
- [Background process / daemon](/gateway/background-process)
|
||||||
|
|
||||||
|
### Model/auth failures (rate limit, billing, “all models failed”)
|
||||||
|
|
||||||
|
- [Models](/cli/models)
|
||||||
|
- [OAuth / auth concepts](/concepts/oauth)
|
||||||
|
|
||||||
|
### When filing an issue
|
||||||
|
|
||||||
|
Paste a safe report:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot status --all
|
||||||
|
```
|
||||||
|
|
||||||
|
If you can, include the relevant log tail from `clawdbot logs --follow`.
|
||||||
+1
-1
@@ -165,7 +165,7 @@ Example:
|
|||||||
|
|
||||||
- Start here:
|
- Start here:
|
||||||
- [Docs hubs (all pages linked)](/start/hubs)
|
- [Docs hubs (all pages linked)](/start/hubs)
|
||||||
- [FAQ](/start/faq) ← *common questions answered*
|
- [Help](/help) ← *common fixes + troubleshooting*
|
||||||
- [Configuration](/gateway/configuration)
|
- [Configuration](/gateway/configuration)
|
||||||
- [Configuration examples](/gateway/configuration-examples)
|
- [Configuration examples](/gateway/configuration-examples)
|
||||||
- [Slash commands](/tools/slash-commands)
|
- [Slash commands](/tools/slash-commands)
|
||||||
|
|||||||
+2
-16
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
summary: "Bun workflow (experimental): installs, patches, and gotchas vs pnpm"
|
summary: "Bun workflow (experimental): installs and gotchas vs pnpm"
|
||||||
read_when:
|
read_when:
|
||||||
- You want the fastest local dev loop (bun + watch)
|
- You want the fastest local dev loop (bun + watch)
|
||||||
- You hit Bun install/patch/lifecycle script issues
|
- You hit Bun install/patch/lifecycle script issues
|
||||||
@@ -8,7 +8,7 @@ read_when:
|
|||||||
# Bun (experimental)
|
# Bun (experimental)
|
||||||
|
|
||||||
Goal: run this repo with **Bun** (optional, not recommended for WhatsApp/Telegram)
|
Goal: run this repo with **Bun** (optional, not recommended for WhatsApp/Telegram)
|
||||||
without losing pnpm patch behavior.
|
without diverging from pnpm workflows.
|
||||||
|
|
||||||
⚠️ **Not recommended for Gateway runtime** (WhatsApp/Telegram bugs). Use Node for production.
|
⚠️ **Not recommended for Gateway runtime** (WhatsApp/Telegram bugs). Use Node for production.
|
||||||
|
|
||||||
@@ -39,20 +39,6 @@ bun run build
|
|||||||
bun run vitest run
|
bun run vitest run
|
||||||
```
|
```
|
||||||
|
|
||||||
## pnpm patchedDependencies under Bun
|
|
||||||
|
|
||||||
pnpm supports `package.json#pnpm.patchedDependencies` and records it in `pnpm-lock.yaml`.
|
|
||||||
Bun (and npm/yarn) do not support pnpm patches, so we apply them in `postinstall` when pnpm is **not** the installer:
|
|
||||||
|
|
||||||
- [`scripts/postinstall.js`](https://github.com/clawdbot/clawdbot/blob/main/scripts/postinstall.js) detects the package manager via `npm_config_user_agent` and applies every entry from `package.json#pnpm.patchedDependencies` into `node_modules/...` using a built-in JS patcher (no `git`/system `patch` dependency).
|
|
||||||
- Under pnpm, this fallback is skipped because pnpm already applies `patchedDependencies` itself.
|
|
||||||
|
|
||||||
To add a new patch that works in both pnpm + Bun:
|
|
||||||
|
|
||||||
1. Add an entry to `package.json#pnpm.patchedDependencies`
|
|
||||||
2. Add the patch file under `patches/`
|
|
||||||
3. Run `pnpm install` (updates `pnpm-lock.yaml` patch hash)
|
|
||||||
|
|
||||||
## Bun lifecycle scripts (blocked by default)
|
## Bun lifecycle scripts (blocked by default)
|
||||||
|
|
||||||
Bun may block dependency lifecycle scripts unless explicitly trusted (`bun pm untrusted` / `bun pm trust`).
|
Bun may block dependency lifecycle scripts unless explicitly trusted (`bun pm untrusted` / `bun pm trust`).
|
||||||
|
|||||||
@@ -152,7 +152,6 @@ WORKDIR /app
|
|||||||
# Cache dependencies unless package metadata changes
|
# Cache dependencies unless package metadata changes
|
||||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
|
||||||
COPY ui/package.json ./ui/package.json
|
COPY ui/package.json ./ui/package.json
|
||||||
COPY patches ./patches
|
|
||||||
COPY scripts ./scripts
|
COPY scripts ./scripts
|
||||||
|
|
||||||
RUN pnpm install --frozen-lockfile
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|||||||
@@ -9,6 +9,29 @@ read_when:
|
|||||||
|
|
||||||
Runtime baseline: **Node >=22**.
|
Runtime baseline: **Node >=22**.
|
||||||
|
|
||||||
|
If the installer says it succeeded but you later see `clawdbot: command not found`, it’s usually a Node/npm PATH issue (global npm bin dir not on PATH). See the section below.
|
||||||
|
|
||||||
|
## Node.js + npm (PATH sanity)
|
||||||
|
|
||||||
|
Quick diagnosis:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node -v
|
||||||
|
npm -v
|
||||||
|
npm bin -g
|
||||||
|
echo "$PATH"
|
||||||
|
```
|
||||||
|
|
||||||
|
If the output of `npm bin -g` is **not** present inside `echo "$PATH"`, your shell can’t find global npm binaries (including `clawdbot`).
|
||||||
|
|
||||||
|
Fix: add it to your shell startup file (zsh: `~/.zshrc`, bash: `~/.bashrc`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PATH="/path/from/npm/bin/-g:$PATH"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open a new terminal (or `rehash` in zsh / `hash -r` in bash).
|
||||||
|
|
||||||
## Recommended (installer script)
|
## Recommended (installer script)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ To see the current flags/behavior, run:
|
|||||||
curl -fsSL https://clawd.bot/install.sh | bash -s -- --help
|
curl -fsSL https://clawd.bot/install.sh | bash -s -- --help
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If the installer completes but `clawdbot` is not found in a new terminal, it’s usually a Node/npm PATH issue. See: [Install](/install#nodejs--npm-path-sanity).
|
||||||
|
|
||||||
## install.sh (recommended)
|
## install.sh (recommended)
|
||||||
|
|
||||||
What it does (high level):
|
What it does (high level):
|
||||||
@@ -71,9 +73,3 @@ Help:
|
|||||||
```bash
|
```bash
|
||||||
curl -fsSL https://clawd.bot/install-cli.sh | bash -s -- --help
|
curl -fsSL https://clawd.bot/install-cli.sh | bash -s -- --help
|
||||||
```
|
```
|
||||||
|
|
||||||
## Patches (npm / pnpm / bun)
|
|
||||||
|
|
||||||
Clawdbot’s `postinstall` script includes a builtin JS patcher that can apply `pnpm.patchedDependencies` patches even when the package manager doesn’t support them (notably Bun). pnpm itself already applies `pnpm.patchedDependencies`, so the fallback skips pnpm installs to avoid double-applying.
|
|
||||||
|
|
||||||
See: [Bun notes](/install/bun).
|
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
summary: "Node.js + npm install sanity: versions, PATH, and global installs"
|
||||||
|
read_when:
|
||||||
|
- You installed Clawdbot but `clawdbot` is “command not found”
|
||||||
|
- You’re setting up Node.js/npm on a new machine
|
||||||
|
- `npm install -g ...` fails with permissions or PATH issues
|
||||||
|
---
|
||||||
|
|
||||||
|
# Node.js + npm (PATH sanity)
|
||||||
|
|
||||||
|
Clawdbot’s runtime baseline is **Node 22+**.
|
||||||
|
|
||||||
|
If you can run `npm install -g clawdbot@latest` but later see `clawdbot: command not found`, it’s almost always a **PATH** issue: the directory where npm puts global binaries isn’t on your shell’s PATH.
|
||||||
|
|
||||||
|
## Quick diagnosis
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node -v
|
||||||
|
npm -v
|
||||||
|
npm bin -g
|
||||||
|
echo "$PATH"
|
||||||
|
```
|
||||||
|
|
||||||
|
If the output of `npm bin -g` is **not** present inside `echo "$PATH"`, your shell can’t find global npm binaries (including `clawdbot`).
|
||||||
|
|
||||||
|
## Fix: put npm’s global bin dir on PATH
|
||||||
|
|
||||||
|
1) Find your global bin directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm bin -g
|
||||||
|
```
|
||||||
|
|
||||||
|
2) Add it to your shell startup file:
|
||||||
|
|
||||||
|
- zsh: `~/.zshrc`
|
||||||
|
- bash: `~/.bashrc`
|
||||||
|
|
||||||
|
Example (replace the path with your `npm bin -g` output):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PATH="/path/from/npm/bin/-g:$PATH"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open a **new terminal** (or run `rehash` in zsh / `hash -r` in bash).
|
||||||
|
|
||||||
|
## Fix: avoid `sudo npm install -g` / permission errors (Linux)
|
||||||
|
|
||||||
|
If `npm install -g ...` fails with `EACCES`, switch npm’s global prefix to a user-writable directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p "$HOME/.npm-global"
|
||||||
|
npm config set prefix "$HOME/.npm-global"
|
||||||
|
export PATH="$HOME/.npm-global/bin:$PATH"
|
||||||
|
```
|
||||||
|
|
||||||
|
Persist the `export PATH=...` line in your shell startup file.
|
||||||
|
|
||||||
|
## Recommended Node install options
|
||||||
|
|
||||||
|
You’ll have the fewest surprises if Node/npm are installed in a way that:
|
||||||
|
|
||||||
|
- keeps Node updated (22+)
|
||||||
|
- makes `npm bin -g` stable and on PATH in new shells
|
||||||
|
|
||||||
|
Common choices:
|
||||||
|
|
||||||
|
- macOS: Homebrew (`brew install node`) or a version manager
|
||||||
|
- Linux: your preferred version manager, or a distro-supported install that provides Node 22+
|
||||||
|
- Windows: official Node installer, `winget`, or a Windows Node version manager
|
||||||
|
|
||||||
|
If you use a version manager (nvm/fnm/asdf/etc), ensure it’s initialized in the shell you use day-to-day (zsh vs bash) so the PATH it sets is present when you run installers.
|
||||||
@@ -99,7 +99,7 @@ It can set up:
|
|||||||
- model provider config/login
|
- model provider config/login
|
||||||
- Linux systemd **user** service (daemon)
|
- Linux systemd **user** service (daemon)
|
||||||
|
|
||||||
If you’re doing OAuth on a headless VM: do OAuth on a normal machine first, then copy the auth profile to the VM (see [FAQ](/start/faq)).
|
If you’re doing OAuth on a headless VM: do OAuth on a normal machine first, then copy the auth profile to the VM (see [Help](/help)).
|
||||||
|
|
||||||
## 5) Remote access options
|
## 5) Remote access options
|
||||||
|
|
||||||
|
|||||||
@@ -241,7 +241,6 @@ RUN curl -L https://github.com/steipete/wacli/releases/latest/download/wacli_Lin
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
|
||||||
COPY ui/package.json ./ui/package.json
|
COPY ui/package.json ./ui/package.json
|
||||||
COPY patches ./patches
|
|
||||||
COPY scripts ./scripts
|
COPY scripts ./scripts
|
||||||
|
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ See [Voice Call](/plugins/voice-call) for a concrete example plugin.
|
|||||||
|
|
||||||
- Microsoft Teams is plugin-only as of 2026.1.15; install `@clawdbot/msteams` if you use Teams.
|
- Microsoft Teams is plugin-only as of 2026.1.15; install `@clawdbot/msteams` if you use Teams.
|
||||||
- [Voice Call](/plugins/voice-call) — `@clawdbot/voice-call`
|
- [Voice Call](/plugins/voice-call) — `@clawdbot/voice-call`
|
||||||
|
- [Zalo Personal](/plugins/zalouser) — `@clawdbot/zalouser`
|
||||||
- [Matrix](/channels/matrix) — `@clawdbot/matrix`
|
- [Matrix](/channels/matrix) — `@clawdbot/matrix`
|
||||||
- [Zalo](/channels/zalo) — `@clawdbot/zalo`
|
- [Zalo](/channels/zalo) — `@clawdbot/zalo`
|
||||||
- [Microsoft Teams](/channels/msteams) — `@clawdbot/msteams`
|
- [Microsoft Teams](/channels/msteams) — `@clawdbot/msteams`
|
||||||
@@ -160,11 +161,15 @@ clawdbot plugins install <path> # add a local file/dir to plugins.l
|
|||||||
clawdbot plugins install ./extensions/voice-call # relative path ok
|
clawdbot plugins install ./extensions/voice-call # relative path ok
|
||||||
clawdbot plugins install ./plugin.tgz # install from a local tarball
|
clawdbot plugins install ./plugin.tgz # install from a local tarball
|
||||||
clawdbot plugins install @clawdbot/voice-call # install from npm
|
clawdbot plugins install @clawdbot/voice-call # install from npm
|
||||||
|
clawdbot plugins update <id>
|
||||||
|
clawdbot plugins update --all
|
||||||
clawdbot plugins enable <id>
|
clawdbot plugins enable <id>
|
||||||
clawdbot plugins disable <id>
|
clawdbot plugins disable <id>
|
||||||
clawdbot plugins doctor
|
clawdbot plugins doctor
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`plugins update` only works for npm installs tracked under `plugins.installs`.
|
||||||
|
|
||||||
Plugins may also register their own top‑level commands (example: `clawdbot voicecall`).
|
Plugins may also register their own top‑level commands (example: `clawdbot voicecall`).
|
||||||
|
|
||||||
## Plugin API (overview)
|
## Plugin API (overview)
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
---
|
||||||
|
summary: "Zalo Personal plugin: QR login + messaging via zca-cli (plugin install + channel config + CLI + tool)"
|
||||||
|
read_when:
|
||||||
|
- You want Zalo Personal (unofficial) support in Clawdbot
|
||||||
|
- You are configuring or developing the zalouser plugin
|
||||||
|
---
|
||||||
|
|
||||||
|
# Zalo Personal (plugin)
|
||||||
|
|
||||||
|
Zalo Personal support for Clawdbot via a plugin, using `zca-cli` to automate a normal Zalo user account.
|
||||||
|
|
||||||
|
> **Warning:** Unofficial automation may lead to account suspension/ban. Use at your own risk.
|
||||||
|
|
||||||
|
## Naming
|
||||||
|
Channel id is `zalouser` to make it explicit this automates a **personal Zalo user account** (unofficial). We keep `zalo` reserved for a potential future official Zalo API integration.
|
||||||
|
|
||||||
|
## Where it runs
|
||||||
|
This plugin runs **inside the Gateway process**.
|
||||||
|
|
||||||
|
If you use a remote Gateway, install/configure it on the **machine running the Gateway**, then restart the Gateway.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
### Option A: install from npm
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot plugins install @clawdbot/zalouser
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart the Gateway afterwards.
|
||||||
|
|
||||||
|
### Option B: install from a local folder (dev)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot plugins install ./extensions/zalouser
|
||||||
|
cd ./extensions/zalouser && pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart the Gateway afterwards.
|
||||||
|
|
||||||
|
## Prerequisite: zca-cli
|
||||||
|
The Gateway machine must have `zca` on `PATH`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
zca --version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Config
|
||||||
|
Channel config lives under `channels.zalouser` (not `plugins.entries.*`):
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
channels: {
|
||||||
|
zalouser: {
|
||||||
|
enabled: true,
|
||||||
|
dmPolicy: "pairing"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot channels login --channel zalouser
|
||||||
|
clawdbot channels logout --channel zalouser
|
||||||
|
clawdbot channels status --probe
|
||||||
|
clawdbot message send --channel zalouser --to <threadId> --message "Hello from Clawdbot"
|
||||||
|
clawdbot directory peers list --channel zalouser --query "name"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Agent tool
|
||||||
|
Tool name: `zalouser`
|
||||||
|
|
||||||
|
Actions: `send`, `image`, `link`, `friends`, `groups`, `me`, `status`
|
||||||
@@ -27,6 +27,7 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/etc.)? See [Chann
|
|||||||
- [OpenAI (API + Codex)](/providers/openai)
|
- [OpenAI (API + Codex)](/providers/openai)
|
||||||
- [Anthropic (API + Claude Code CLI)](/providers/anthropic)
|
- [Anthropic (API + Claude Code CLI)](/providers/anthropic)
|
||||||
- [OpenRouter](/providers/openrouter)
|
- [OpenRouter](/providers/openrouter)
|
||||||
|
- [Vercel AI Gateway](/providers/vercel-ai-gateway)
|
||||||
- [Moonshot AI (Kimi)](/providers/moonshot)
|
- [Moonshot AI (Kimi)](/providers/moonshot)
|
||||||
- [OpenCode Zen](/providers/opencode)
|
- [OpenCode Zen](/providers/opencode)
|
||||||
- [Z.AI](/providers/zai)
|
- [Z.AI](/providers/zai)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ model as `provider/model`.
|
|||||||
- [OpenAI (API + Codex)](/providers/openai)
|
- [OpenAI (API + Codex)](/providers/openai)
|
||||||
- [Anthropic (API + Claude Code CLI)](/providers/anthropic)
|
- [Anthropic (API + Claude Code CLI)](/providers/anthropic)
|
||||||
- [OpenRouter](/providers/openrouter)
|
- [OpenRouter](/providers/openrouter)
|
||||||
|
- [Vercel AI Gateway](/providers/vercel-ai-gateway)
|
||||||
- [Moonshot AI (Kimi)](/providers/moonshot)
|
- [Moonshot AI (Kimi)](/providers/moonshot)
|
||||||
- [Synthetic](/providers/synthetic)
|
- [Synthetic](/providers/synthetic)
|
||||||
- [OpenCode Zen](/providers/opencode)
|
- [OpenCode Zen](/providers/opencode)
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
summary: "Vercel AI Gateway setup (auth + model selection)"
|
||||||
|
read_when:
|
||||||
|
- You want to use Vercel AI Gateway with Clawdbot
|
||||||
|
- You need the API key env var or CLI auth choice
|
||||||
|
---
|
||||||
|
# Vercel AI Gateway
|
||||||
|
|
||||||
|
|
||||||
|
The [Vercel AI Gateway](https://vercel.com/ai-gateway) provides a unified API to access hundreds of models through a single endpoint.
|
||||||
|
|
||||||
|
- Provider: `vercel-ai-gateway`
|
||||||
|
- Auth: `AI_GATEWAY_API_KEY`
|
||||||
|
- API: Anthropic Messages compatible
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
1) Set the API key (recommended: store it for the Gateway):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot onboard --auth-choice ai-gateway-api-key
|
||||||
|
```
|
||||||
|
|
||||||
|
2) Set a default model:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: { primary: "vercel-ai-gateway/anthropic/claude-opus-4.5" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Non-interactive example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot onboard --non-interactive \
|
||||||
|
--mode local \
|
||||||
|
--auth-choice ai-gateway-api-key \
|
||||||
|
--ai-gateway-api-key "$AI_GATEWAY_API_KEY"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment note
|
||||||
|
|
||||||
|
If the Gateway runs as a daemon (launchd/systemd), make sure `AI_GATEWAY_API_KEY`
|
||||||
|
is available to that process (for example, in `~/.clawdbot/.env` or via
|
||||||
|
`env.shellEnv`).
|
||||||
+80
-146
@@ -70,11 +70,14 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
|
|||||||
- [I set `COPILOT_GITHUB_TOKEN`, but models status shows “Shell env: off.” Why?](#i-set-copilot_github_token-but-models-status-shows-shell-env-off-why)
|
- [I set `COPILOT_GITHUB_TOKEN`, but models status shows “Shell env: off.” Why?](#i-set-copilot_github_token-but-models-status-shows-shell-env-off-why)
|
||||||
- [Sessions & multiple chats](#sessions-multiple-chats)
|
- [Sessions & multiple chats](#sessions-multiple-chats)
|
||||||
- [How do I start a fresh conversation?](#how-do-i-start-a-fresh-conversation)
|
- [How do I start a fresh conversation?](#how-do-i-start-a-fresh-conversation)
|
||||||
- [How do I completely reset Clawdbot (but keep it installed)?](#how-do-i-completely-reset-clawdbot-but-keep-it-installed)
|
- [Do sessions reset automatically if I never send `/new`?](#do-sessions-reset-automatically-if-i-never-send-new)
|
||||||
|
- [How do I completely reset Clawdbot but keep it installed?](#how-do-i-completely-reset-clawdbot-but-keep-it-installed)
|
||||||
- [I’m getting “context too large” errors — how do I reset or compact?](#im-getting-context-too-large-errors-how-do-i-reset-or-compact)
|
- [I’m getting “context too large” errors — how do I reset or compact?](#im-getting-context-too-large-errors-how-do-i-reset-or-compact)
|
||||||
|
- [Why am I getting heartbeat messages every 30 minutes?](#why-am-i-getting-heartbeat-messages-every-30-minutes)
|
||||||
- [Do I need to add a “bot account” to a WhatsApp group?](#do-i-need-to-add-a-bot-account-to-a-whatsapp-group)
|
- [Do I need to add a “bot account” to a WhatsApp group?](#do-i-need-to-add-a-bot-account-to-a-whatsapp-group)
|
||||||
- [Why doesn’t Clawdbot reply in a group?](#why-doesnt-clawdbot-reply-in-a-group)
|
- [Why doesn’t Clawdbot reply in a group?](#why-doesnt-clawdbot-reply-in-a-group)
|
||||||
- [Do groups/threads share context with DMs?](#do-groupsthreads-share-context-with-dms)
|
- [Do groups/threads share context with DMs?](#do-groupsthreads-share-context-with-dms)
|
||||||
|
- [How many workspaces and agents can I create?](#how-many-workspaces-and-agents-can-i-create)
|
||||||
- [Models: defaults, selection, aliases, switching](#models-defaults-selection-aliases-switching)
|
- [Models: defaults, selection, aliases, switching](#models-defaults-selection-aliases-switching)
|
||||||
- [What is the “default model”?](#what-is-the-default-model)
|
- [What is the “default model”?](#what-is-the-default-model)
|
||||||
- [How do I switch models on the fly (without restarting)?](#how-do-i-switch-models-on-the-fly-without-restarting)
|
- [How do I switch models on the fly (without restarting)?](#how-do-i-switch-models-on-the-fly-without-restarting)
|
||||||
@@ -107,25 +110,17 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
|
|||||||
- [Logging and debugging](#logging-and-debugging)
|
- [Logging and debugging](#logging-and-debugging)
|
||||||
- [Where are logs?](#where-are-logs)
|
- [Where are logs?](#where-are-logs)
|
||||||
- [How do I start/stop/restart the Gateway daemon?](#how-do-i-startstoprestart-the-gateway-daemon)
|
- [How do I start/stop/restart the Gateway daemon?](#how-do-i-startstoprestart-the-gateway-daemon)
|
||||||
|
- [ELI5: `clawdbot daemon restart` vs `clawdbot gateway`](#eli5-clawdbot-daemon-restart-vs-clawdbot-gateway)
|
||||||
- [What’s the fastest way to get more details when something fails?](#whats-the-fastest-way-to-get-more-details-when-something-fails)
|
- [What’s the fastest way to get more details when something fails?](#whats-the-fastest-way-to-get-more-details-when-something-fails)
|
||||||
- [Media & attachments](#media-attachments)
|
- [Media & attachments](#media-attachments)
|
||||||
- [My skill generated an image/PDF, but nothing was sent](#my-skill-generated-an-imagepdf-but-nothing-was-sent)
|
- [My skill generated an image/PDF, but nothing was sent](#my-skill-generated-an-imagepdf-but-nothing-was-sent)
|
||||||
- [Security and access control](#security-and-access-control)
|
- [Security and access control](#security-and-access-control)
|
||||||
- [Is it safe to expose Clawdbot to inbound DMs?](#is-it-safe-to-expose-clawdbot-to-inbound-dms)
|
- [Is it safe to expose Clawdbot to inbound DMs?](#is-it-safe-to-expose-clawdbot-to-inbound-dms)
|
||||||
|
- [I ran `/start` in Telegram but didn’t get a pairing code](#i-ran-start-in-telegram-but-didnt-get-a-pairing-code)
|
||||||
- [WhatsApp: will it message my contacts? How does pairing work?](#whatsapp-will-it-message-my-contacts-how-does-pairing-work)
|
- [WhatsApp: will it message my contacts? How does pairing work?](#whatsapp-will-it-message-my-contacts-how-does-pairing-work)
|
||||||
- [Chat commands, aborting tasks, and “it won’t stop”](#chat-commands-aborting-tasks-and-it-wont-stop)
|
- [Chat commands, aborting tasks, and “it won’t stop”](#chat-commands-aborting-tasks-and-it-wont-stop)
|
||||||
- [How do I stop/cancel a running task?](#how-do-i-stopcancel-a-running-task)
|
- [How do I stop/cancel a running task?](#how-do-i-stopcancel-a-running-task)
|
||||||
- [Why does it feel like the bot “ignores” rapid‑fire messages?](#why-does-it-feel-like-the-bot-ignores-rapidfire-messages)
|
- [Why does it feel like the bot “ignores” rapid‑fire messages?](#why-does-it-feel-like-the-bot-ignores-rapidfire-messages)
|
||||||
- [Common troubleshooting](#common-troubleshooting)
|
|
||||||
- [“All models failed” — what should I check first?](#all-models-failed-what-should-i-check-first)
|
|
||||||
- [I’m running on my personal WhatsApp number — why is self-chat weird?](#im-running-on-my-personal-whatsapp-number-why-is-self-chat-weird)
|
|
||||||
- [WhatsApp logged me out. How do I re‑auth?](#whatsapp-logged-me-out-how-do-i-reauth)
|
|
||||||
- [Build errors on `main` — what’s the standard fix path?](#build-errors-on-main-whats-the-standard-fix-path)
|
|
||||||
- [npm install fails (allow-build-scripts / missing tar or yargs). What now?](#npm-install-fails-allow-build-scripts--missing-tar-or-yargs-what-now)
|
|
||||||
- [How do I switch between git installs and npm installs?](#how-do-i-switch-between-git-installs-and-npm-installs)
|
|
||||||
- [Telegram block streaming isn’t splitting text between tool calls. Why?](#telegram-block-streaming-isnt-splitting-text-between-tool-calls-why)
|
|
||||||
- [Discord doesn’t reply in my server even with `requireMention: false`. Why?](#discord-doesnt-reply-in-my-server-even-with-requiremention-false-why)
|
|
||||||
- [Cloud Code Assist API error: invalid tool schema (400). What now?](#cloud-code-assist-api-error-invalid-tool-schema-400-what-now)
|
|
||||||
|
|
||||||
## First 60 seconds if something's broken
|
## First 60 seconds if something's broken
|
||||||
|
|
||||||
@@ -858,7 +853,21 @@ See [/concepts/model-providers](/concepts/model-providers) and [/environment](/e
|
|||||||
|
|
||||||
Send `/new` or `/reset` as a standalone message. See [Session management](/concepts/session).
|
Send `/new` or `/reset` as a standalone message. See [Session management](/concepts/session).
|
||||||
|
|
||||||
### How do I completely reset Clawdbot (but keep it installed)?
|
### Do sessions reset automatically if I never send `/new`?
|
||||||
|
|
||||||
|
Yes. Sessions expire after `session.idleMinutes` (default **60**). The **next**
|
||||||
|
message starts a fresh session id for that chat key. This does not delete
|
||||||
|
transcripts — it just starts a new session.
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
session: {
|
||||||
|
idleMinutes: 240
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### How do I completely reset Clawdbot but keep it installed?
|
||||||
|
|
||||||
Use the reset command:
|
Use the reset command:
|
||||||
|
|
||||||
@@ -905,6 +914,24 @@ If it keeps happening:
|
|||||||
|
|
||||||
Docs: [Compaction](/concepts/compaction), [Session pruning](/concepts/session-pruning), [Session management](/concepts/session).
|
Docs: [Compaction](/concepts/compaction), [Session pruning](/concepts/session-pruning), [Session management](/concepts/session).
|
||||||
|
|
||||||
|
### Why am I getting heartbeat messages every 30 minutes?
|
||||||
|
|
||||||
|
Heartbeats run every **30m** by default. Tune or disable them:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
heartbeat: {
|
||||||
|
every: "2h" // or "0m" to disable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Per-agent overrides use `agents.list[].heartbeat`. Docs: [Heartbeat](/gateway/heartbeat).
|
||||||
|
|
||||||
### Do I need to add a “bot account” to a WhatsApp group?
|
### Do I need to add a “bot account” to a WhatsApp group?
|
||||||
|
|
||||||
No. Clawdbot runs on **your own account**, so if you’re in the group, Clawdbot can see it.
|
No. Clawdbot runs on **your own account**, so if you’re in the group, Clawdbot can see it.
|
||||||
@@ -935,6 +962,19 @@ See [Groups](/concepts/groups) and [Group messages](/concepts/group-messages).
|
|||||||
|
|
||||||
Direct chats collapse to the main session by default. Groups/channels have their own session keys, and Telegram topics / Discord threads are separate sessions. See [Groups](/concepts/groups) and [Group messages](/concepts/group-messages).
|
Direct chats collapse to the main session by default. Groups/channels have their own session keys, and Telegram topics / Discord threads are separate sessions. See [Groups](/concepts/groups) and [Group messages](/concepts/group-messages).
|
||||||
|
|
||||||
|
### How many workspaces and agents can I create?
|
||||||
|
|
||||||
|
No hard limits. Dozens (even hundreds) are fine, but watch for:
|
||||||
|
|
||||||
|
- **Disk growth:** sessions + transcripts live under `~/.clawdbot/agents/<agentId>/sessions/`.
|
||||||
|
- **Token cost:** more agents means more concurrent model usage.
|
||||||
|
- **Ops overhead:** per-agent auth profiles, workspaces, and channel routing.
|
||||||
|
|
||||||
|
Tips:
|
||||||
|
- Keep one **active** workspace per agent (`agents.defaults.workspace`).
|
||||||
|
- Prune old sessions (delete JSONL or store entries) if disk grows.
|
||||||
|
- Use `clawdbot doctor` to spot stray workspaces and profile mismatches.
|
||||||
|
|
||||||
## Models: defaults, selection, aliases, switching
|
## Models: defaults, selection, aliases, switching
|
||||||
|
|
||||||
### What is the “default model”?
|
### What is the “default model”?
|
||||||
@@ -1315,9 +1355,11 @@ Facts (from code):
|
|||||||
|
|
||||||
Fix:
|
Fix:
|
||||||
- Fastest: `clawdbot dashboard` (prints + copies tokenized link, tries to open; shows SSH hint if headless).
|
- Fastest: `clawdbot dashboard` (prints + copies tokenized link, tries to open; shows SSH hint if headless).
|
||||||
|
- If you don’t have a token yet: `clawdbot doctor --generate-gateway-token`.
|
||||||
- If remote, tunnel first: `ssh -N -L 18789:127.0.0.1:18789 user@host` then open `http://127.0.0.1:18789/?token=...`.
|
- If remote, tunnel first: `ssh -N -L 18789:127.0.0.1:18789 user@host` then open `http://127.0.0.1:18789/?token=...`.
|
||||||
- Set `gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`) on the gateway host.
|
- Set `gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`) on the gateway host.
|
||||||
- In the Control UI settings, paste the same token (or refresh with a one-time `?token=...` link).
|
- In the Control UI settings, paste the same token (or refresh with a one-time `?token=...` link).
|
||||||
|
- Still stuck? Run `clawdbot status --all` and follow [Troubleshooting](/gateway/troubleshooting). See [Dashboard](/web/dashboard) for auth details.
|
||||||
|
|
||||||
### I set `gateway.bind: "tailnet"` but it can’t bind / nothing listens
|
### I set `gateway.bind: "tailnet"` but it can’t bind / nothing listens
|
||||||
|
|
||||||
@@ -1407,6 +1449,14 @@ clawdbot daemon restart
|
|||||||
|
|
||||||
If you run the gateway manually, `clawdbot gateway --force` can reclaim the port. See [Gateway](/gateway).
|
If you run the gateway manually, `clawdbot gateway --force` can reclaim the port. See [Gateway](/gateway).
|
||||||
|
|
||||||
|
### ELI5: `clawdbot daemon restart` vs `clawdbot gateway`
|
||||||
|
|
||||||
|
- `clawdbot daemon restart`: restarts the **background service** (launchd/systemd).
|
||||||
|
- `clawdbot gateway`: runs the gateway **in the foreground** for this terminal session.
|
||||||
|
|
||||||
|
If you installed the daemon, use the daemon commands. Use `clawdbot gateway` when
|
||||||
|
you want a one-off, foreground run.
|
||||||
|
|
||||||
### What’s the fastest way to get more details when something fails?
|
### What’s the fastest way to get more details when something fails?
|
||||||
|
|
||||||
Start the Gateway with `--verbose` to get more console detail. Then inspect the log file for channel auth, model routing, and RPC errors.
|
Start the Gateway with `--verbose` to get more console detail. Then inspect the log file for channel auth, model routing, and RPC errors.
|
||||||
@@ -1423,7 +1473,11 @@ CLI sending:
|
|||||||
clawdbot message send --to +15555550123 --message "Here you go" --media /path/to/file.png
|
clawdbot message send --to +15555550123 --message "Here you go" --media /path/to/file.png
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: images are resized/recompressed (max side 2048px) to hit size limits. See [Images](/nodes/images).
|
Also check:
|
||||||
|
- The target channel supports outbound media and isn’t blocked by allowlists.
|
||||||
|
- The file is within the provider’s size limits (images are resized to max 2048px).
|
||||||
|
|
||||||
|
See [Images](/nodes/images).
|
||||||
|
|
||||||
## Security and access control
|
## Security and access control
|
||||||
|
|
||||||
@@ -1439,6 +1493,19 @@ Treat inbound DMs as untrusted input. Defaults are designed to reduce risk:
|
|||||||
|
|
||||||
Run `clawdbot doctor` to surface risky DM policies.
|
Run `clawdbot doctor` to surface risky DM policies.
|
||||||
|
|
||||||
|
### I ran `/start` in Telegram but didn’t get a pairing code
|
||||||
|
|
||||||
|
Pairing codes are sent **only** when an unknown sender messages the bot and
|
||||||
|
`dmPolicy: "pairing"` is enabled. `/start` by itself doesn’t generate a code.
|
||||||
|
|
||||||
|
Check pending requests:
|
||||||
|
```bash
|
||||||
|
clawdbot pairing list telegram
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want immediate access, allowlist your sender id or set `dmPolicy: "open"`
|
||||||
|
for that account.
|
||||||
|
|
||||||
### WhatsApp: will it message my contacts? How does pairing work?
|
### WhatsApp: will it message my contacts? How does pairing work?
|
||||||
|
|
||||||
No. Default WhatsApp DM policy is **pairing**. Unknown senders only get a pairing code and their message is **not processed**. Clawdbot only replies to chats it receives or to explicit sends you trigger.
|
No. Default WhatsApp DM policy is **pairing**. Unknown senders only get a pairing code and their message is **not processed**. Clawdbot only replies to chats it receives or to explicit sends you trigger.
|
||||||
@@ -1496,139 +1563,6 @@ Queue mode controls how new messages interact with an in‑flight run. Use `/que
|
|||||||
|
|
||||||
You can add options like `debounce:2s cap:25 drop:summarize` for followup modes.
|
You can add options like `debounce:2s cap:25 drop:summarize` for followup modes.
|
||||||
|
|
||||||
## Common troubleshooting
|
|
||||||
|
|
||||||
### “All models failed” — what should I check first?
|
|
||||||
|
|
||||||
- **Credentials** present for the provider(s) being tried (auth profiles + env vars).
|
|
||||||
- **Model routing**: confirm `agents.defaults.model.primary` and fallbacks are models you can access.
|
|
||||||
- **Gateway logs** in `/tmp/clawdbot/…` for the exact provider error.
|
|
||||||
- **`/model status`** to see current configured models + shorthands.
|
|
||||||
|
|
||||||
### I’m running on my personal WhatsApp number — why is self-chat weird?
|
|
||||||
|
|
||||||
Enable self-chat mode and allowlist your own number:
|
|
||||||
|
|
||||||
```json5
|
|
||||||
{
|
|
||||||
channels: {
|
|
||||||
whatsapp: {
|
|
||||||
selfChatMode: true,
|
|
||||||
dmPolicy: "allowlist",
|
|
||||||
allowFrom: ["+15555550123"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
See [WhatsApp setup](/channels/whatsapp).
|
|
||||||
|
|
||||||
### WhatsApp logged me out. How do I re‑auth?
|
|
||||||
|
|
||||||
Run the login command again and scan the QR code:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
clawdbot channels login
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build errors on `main` — what’s the standard fix path?
|
|
||||||
|
|
||||||
1) `git pull origin main && pnpm install`
|
|
||||||
2) `pnpm clawdbot doctor`
|
|
||||||
3) Check GitHub issues or Discord
|
|
||||||
4) Temporary workaround: check out an older commit
|
|
||||||
|
|
||||||
### npm install fails (allow-build-scripts / missing tar or yargs). What now?
|
|
||||||
|
|
||||||
If you’re running from source, use the repo’s package manager: **pnpm** (preferred).
|
|
||||||
The repo declares `packageManager: "pnpm@…"`, and pnpm patches are tracked in `pnpm.patchedDependencies`.
|
|
||||||
|
|
||||||
Typical recovery:
|
|
||||||
```bash
|
|
||||||
git status # ensure you’re in the repo root
|
|
||||||
pnpm install
|
|
||||||
pnpm build
|
|
||||||
pnpm clawdbot doctor
|
|
||||||
clawdbot daemon restart
|
|
||||||
```
|
|
||||||
|
|
||||||
Why: pnpm is the configured package manager for this repo, and the dependency
|
|
||||||
patching workflow relies on it.
|
|
||||||
|
|
||||||
### How do I switch between git installs and npm installs?
|
|
||||||
|
|
||||||
Use the **website installer** and select the install method with a flag. It
|
|
||||||
upgrades in place and rewrites the gateway service to point at the new install.
|
|
||||||
|
|
||||||
Switch **to git install**:
|
|
||||||
```bash
|
|
||||||
curl -fsSL https://clawd.bot/install.sh | bash -s -- --install-method git --no-onboard
|
|
||||||
```
|
|
||||||
|
|
||||||
Switch **to npm global**:
|
|
||||||
```bash
|
|
||||||
curl -fsSL https://clawd.bot/install.sh | bash
|
|
||||||
```
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- The git flow only rebases if the repo is clean. Commit or stash changes first.
|
|
||||||
- After switching, run:
|
|
||||||
```bash
|
|
||||||
clawdbot doctor
|
|
||||||
clawdbot daemon restart
|
|
||||||
```
|
|
||||||
|
|
||||||
### Telegram block streaming isn’t splitting text between tool calls. Why?
|
|
||||||
|
|
||||||
Block streaming only sends **completed text blocks**. Common reasons you see a single message:
|
|
||||||
- `agents.defaults.blockStreamingDefault` is still `"off"`.
|
|
||||||
- `channels.telegram.blockStreaming` is set to `false`.
|
|
||||||
- `channels.telegram.streamMode` is `partial` or `block` **and draft streaming is active**
|
|
||||||
(private chat + topics). Draft streaming disables block streaming in that case.
|
|
||||||
- Your `minChars` / coalesce settings are too high, so chunks get merged.
|
|
||||||
- The model emits one large text block (no mid‑reply flush points).
|
|
||||||
|
|
||||||
Fix checklist:
|
|
||||||
1) Put block streaming settings under `agents.defaults`, not the root.
|
|
||||||
2) Set `channels.telegram.streamMode: "off"` if you want real multi‑message block replies.
|
|
||||||
3) Use smaller chunk/coalesce thresholds while debugging.
|
|
||||||
|
|
||||||
See [Streaming](/concepts/streaming).
|
|
||||||
|
|
||||||
### Discord doesn’t reply in my server even with `requireMention: false`. Why?
|
|
||||||
|
|
||||||
`requireMention` only controls mention‑gating **after** the channel passes allowlists.
|
|
||||||
By default `channels.discord.groupPolicy` is **allowlist**, so guilds must be explicitly enabled.
|
|
||||||
If you set `channels.discord.guilds.<guildId>.channels`, only the listed channels are allowed; omit it to allow all channels in the guild.
|
|
||||||
|
|
||||||
Fix checklist:
|
|
||||||
1) Set `channels.discord.groupPolicy: "open"` **or** add a guild allowlist entry (and optionally a channel allowlist).
|
|
||||||
2) Use **numeric channel IDs** in `channels.discord.guilds.<guildId>.channels`.
|
|
||||||
3) Put `requireMention: false` **under** `channels.discord.guilds` (global or per‑channel).
|
|
||||||
Top‑level `channels.discord.requireMention` is not a supported key.
|
|
||||||
4) Ensure the bot has **Message Content Intent** and channel permissions.
|
|
||||||
5) Run `clawdbot channels status --probe` for audit hints.
|
|
||||||
|
|
||||||
Docs: [Discord](/channels/discord), [Channels troubleshooting](/channels/troubleshooting).
|
|
||||||
|
|
||||||
### Cloud Code Assist API error: invalid tool schema (400). What now?
|
|
||||||
|
|
||||||
This is almost always a **tool schema compatibility** issue. The Cloud Code Assist
|
|
||||||
endpoint accepts a strict subset of JSON Schema. Clawdbot scrubs/normalizes tool
|
|
||||||
schemas in current `main`, but the fix is not in the last release yet (as of
|
|
||||||
January 13, 2026).
|
|
||||||
|
|
||||||
Fix checklist:
|
|
||||||
1) **Update Clawdbot**:
|
|
||||||
- If you can run from source, pull `main` and restart the gateway.
|
|
||||||
- Otherwise, wait for the next release that includes the schema scrubber.
|
|
||||||
2) Avoid unsupported keywords like `anyOf/oneOf/allOf`, `patternProperties`,
|
|
||||||
`additionalProperties`, `minLength`, `maxLength`, `format`, etc.
|
|
||||||
3) If you define custom tools, keep the top‑level schema as `type: "object"` with
|
|
||||||
`properties` and simple enums.
|
|
||||||
|
|
||||||
See [Tools](/tools) and [TypeBox schemas](/concepts/typebox).
|
|
||||||
|
|
||||||
## Answer the exact question from the screenshot/chat log
|
## Answer the exact question from the screenshot/chat log
|
||||||
|
|
||||||
**Q: “What’s the default model for Anthropic with an API key?”**
|
**Q: “What’s the default model for Anthropic with an API key?”**
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ Use these hubs to discover every page, including deep dives and reference docs t
|
|||||||
- [Wizard](/start/wizard)
|
- [Wizard](/start/wizard)
|
||||||
- [Setup](/start/setup)
|
- [Setup](/start/setup)
|
||||||
- [Dashboard (local Gateway)](http://127.0.0.1:18789/)
|
- [Dashboard (local Gateway)](http://127.0.0.1:18789/)
|
||||||
- [FAQ](/start/faq)
|
- [Help](/help)
|
||||||
- [Configuration](/gateway/configuration)
|
- [Configuration](/gateway/configuration)
|
||||||
- [Configuration examples](/gateway/configuration-examples)
|
- [Configuration examples](/gateway/configuration-examples)
|
||||||
- [Clawdbot assistant (Clawd)](/start/clawd)
|
- [Clawdbot assistant (Clawd)](/start/clawd)
|
||||||
|
|||||||
@@ -146,6 +146,19 @@ Full setup walkthrough (28m) by VelvetShark.
|
|||||||
|
|
||||||
14+ agents under one gateway with Opus 4.5 orchestrator delegating to Codex workers. Comprehensive [technical write-up](https://github.com/adam91holt/orchestrated-ai-articles) covering the Dream Team roster, model selection, sandboxing, webhooks, heartbeats, and delegation flows. [Clawdspace](https://github.com/adam91holt/clawdspace) for agent sandboxing. [Blog post](https://adams-ai-journey.ghost.io/2026-the-year-of-the-orchestrator/).
|
14+ agents under one gateway with Opus 4.5 orchestrator delegating to Codex workers. Comprehensive [technical write-up](https://github.com/adam91holt/orchestrated-ai-articles) covering the Dream Team roster, model selection, sandboxing, webhooks, heartbeats, and delegation flows. [Clawdspace](https://github.com/adam91holt/clawdspace) for agent sandboxing. [Blog post](https://adams-ai-journey.ghost.io/2026-the-year-of-the-orchestrator/).
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card title="Linear CLI" icon="terminal" href="https://github.com/Finesssee/linear-cli">
|
||||||
|
**@NessZerra** • `devtools` `linear` `cli` `issues`
|
||||||
|
|
||||||
|
CLI for Linear that integrates with agentic workflows (Claude Code, Clawdbot). Manage issues, projects, and workflows from the terminal. First external PR merged!
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title="Beeper CLI" icon="message" href="https://github.com/blqke/beepcli">
|
||||||
|
**@jules** • `messaging` `beeper` `cli` `automation`
|
||||||
|
|
||||||
|
Read, send, and archive messages via Beeper Desktop. Uses Beeper local MCP API so agents can manage all your chats (iMessage, WhatsApp, etc.) in one place.
|
||||||
|
</Card>
|
||||||
|
|
||||||
</CardGroup>
|
</CardGroup>
|
||||||
|
|
||||||
## 🤖 Automation & Workflows
|
## 🤖 Automation & Workflows
|
||||||
|
|||||||
+16
-1
@@ -45,7 +45,7 @@ The wizard starts with **QuickStart** (defaults) vs **Advanced** (full control).
|
|||||||
## What the wizard does
|
## What the wizard does
|
||||||
|
|
||||||
**Local mode (default)** walks you through:
|
**Local mode (default)** walks you through:
|
||||||
- Model/auth (OpenAI Code (Codex) subscription OAuth, Anthropic API key (recommended) or `claude setup-token`, plus MiniMax/GLM/Moonshot options)
|
- Model/auth (OpenAI Code (Codex) subscription OAuth, Anthropic API key (recommended) or `claude setup-token`, plus MiniMax/GLM/Moonshot/AI Gateway options)
|
||||||
- Workspace location + bootstrap files
|
- Workspace location + bootstrap files
|
||||||
- Gateway settings (port/bind/auth/tailscale)
|
- Gateway settings (port/bind/auth/tailscale)
|
||||||
- Providers (Telegram, WhatsApp, Discord, Signal)
|
- Providers (Telegram, WhatsApp, Discord, Signal)
|
||||||
@@ -68,6 +68,8 @@ Tip: `--json` does **not** imply non-interactive mode. Use `--non-interactive` (
|
|||||||
|
|
||||||
1) **Existing config detection**
|
1) **Existing config detection**
|
||||||
- If `~/.clawdbot/clawdbot.json` exists, choose **Keep / Modify / Reset**.
|
- If `~/.clawdbot/clawdbot.json` exists, choose **Keep / Modify / Reset**.
|
||||||
|
- Re-running the wizard does **not** wipe anything unless you explicitly choose **Reset**
|
||||||
|
(or pass `--reset`).
|
||||||
- If the config is invalid or contains legacy keys, the wizard stops and asks
|
- If the config is invalid or contains legacy keys, the wizard stops and asks
|
||||||
you to run `clawdbot doctor` before continuing.
|
you to run `clawdbot doctor` before continuing.
|
||||||
- Reset uses `trash` (never `rm`) and offers scopes:
|
- Reset uses `trash` (never `rm`) and offers scopes:
|
||||||
@@ -86,6 +88,8 @@ Tip: `--json` does **not** imply non-interactive mode. Use `--non-interactive` (
|
|||||||
- **OpenAI API key**: uses `OPENAI_API_KEY` if present or prompts for a key, then saves it to `~/.clawdbot/.env` so launchd can read it.
|
- **OpenAI API key**: uses `OPENAI_API_KEY` if present or prompts for a key, then saves it to `~/.clawdbot/.env` so launchd can read it.
|
||||||
- **OpenCode Zen (multi-model proxy)**: prompts for `OPENCODE_API_KEY` (or `OPENCODE_ZEN_API_KEY`, get it at https://opencode.ai/auth).
|
- **OpenCode Zen (multi-model proxy)**: prompts for `OPENCODE_API_KEY` (or `OPENCODE_ZEN_API_KEY`, get it at https://opencode.ai/auth).
|
||||||
- **API key**: stores the key for you.
|
- **API key**: stores the key for you.
|
||||||
|
- **Vercel AI Gateway (multi-model proxy)**: prompts for `AI_GATEWAY_API_KEY`.
|
||||||
|
- More detail: [Vercel AI Gateway](/providers/vercel-ai-gateway)
|
||||||
- **MiniMax M2.1**: config is auto-written.
|
- **MiniMax M2.1**: config is auto-written.
|
||||||
- More detail: [MiniMax](/providers/minimax)
|
- More detail: [MiniMax](/providers/minimax)
|
||||||
- **Synthetic (Anthropic-compatible)**: prompts for `SYNTHETIC_API_KEY`.
|
- **Synthetic (Anthropic-compatible)**: prompts for `SYNTHETIC_API_KEY`.
|
||||||
@@ -209,6 +213,17 @@ clawdbot onboard --non-interactive \
|
|||||||
--gateway-bind loopback
|
--gateway-bind loopback
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Vercel AI Gateway example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot onboard --non-interactive \
|
||||||
|
--mode local \
|
||||||
|
--auth-choice ai-gateway-api-key \
|
||||||
|
--ai-gateway-api-key "$AI_GATEWAY_API_KEY" \
|
||||||
|
--gateway-port 18789 \
|
||||||
|
--gateway-bind loopback
|
||||||
|
```
|
||||||
|
|
||||||
Moonshot example:
|
Moonshot example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -112,3 +112,18 @@ curl -s http://127.0.0.1:18791/tabs
|
|||||||
| `browser.noSandbox` | Add `--no-sandbox` flag (needed for some Linux setups) | `false` |
|
| `browser.noSandbox` | Add `--no-sandbox` flag (needed for some Linux setups) | `false` |
|
||||||
| `browser.attachOnly` | Don't launch browser, only attach to existing | `false` |
|
| `browser.attachOnly` | Don't launch browser, only attach to existing | `false` |
|
||||||
| `browser.cdpPort` | Chrome DevTools Protocol port | `18800` |
|
| `browser.cdpPort` | Chrome DevTools Protocol port | `18800` |
|
||||||
|
|
||||||
|
### Problem: "Chrome extension relay is running, but no tab is connected"
|
||||||
|
|
||||||
|
You’re using the `chrome` profile (extension relay). It expects the Clawdbot
|
||||||
|
browser extension to be attached to a live tab.
|
||||||
|
|
||||||
|
Fix options:
|
||||||
|
1. **Use the managed browser:** `clawdbot browser start --browser-profile clawd`
|
||||||
|
(or set `browser.defaultProfile: "clawd"`).
|
||||||
|
2. **Use the extension relay:** install the extension, open a tab, and click the
|
||||||
|
Clawdbot extension icon to attach it.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- The `chrome` profile uses your **system default Chromium browser** when possible.
|
||||||
|
- Local `clawd` profiles auto-assign `cdpPort`/`cdpUrl`; only set those for remote CDP.
|
||||||
+67
-3
@@ -14,8 +14,10 @@ control server.
|
|||||||
|
|
||||||
Beginner view:
|
Beginner view:
|
||||||
- Think of it as a **separate, agent-only browser**.
|
- Think of it as a **separate, agent-only browser**.
|
||||||
- It does **not** touch your personal browser profile.
|
- The `clawd` profile does **not** touch your personal browser profile.
|
||||||
- The agent can **open tabs, read pages, click, and type** in a safe lane.
|
- The agent can **open tabs, read pages, click, and type** in a safe lane.
|
||||||
|
- The default `chrome` profile uses the **system default Chromium browser** via the
|
||||||
|
extension relay; switch to `clawd` for the isolated managed browser.
|
||||||
|
|
||||||
## What you get
|
## What you get
|
||||||
|
|
||||||
@@ -39,6 +41,14 @@ clawdbot browser --browser-profile clawd snapshot
|
|||||||
If you get “Browser disabled”, enable it in config (see below) and restart the
|
If you get “Browser disabled”, enable it in config (see below) and restart the
|
||||||
Gateway.
|
Gateway.
|
||||||
|
|
||||||
|
## Profiles: `clawd` vs `chrome`
|
||||||
|
|
||||||
|
- `clawd`: managed, isolated browser (no extension required).
|
||||||
|
- `chrome`: extension relay to your **system browser** (requires the Clawdbot
|
||||||
|
extension to be attached to a tab).
|
||||||
|
|
||||||
|
Set `browser.defaultProfile: "clawd"` if you want managed mode by default.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Browser settings live in `~/.clawdbot/clawdbot.json`.
|
Browser settings live in `~/.clawdbot/clawdbot.json`.
|
||||||
@@ -49,6 +59,8 @@ Browser settings live in `~/.clawdbot/clawdbot.json`.
|
|||||||
enabled: true, // default: true
|
enabled: true, // default: true
|
||||||
controlUrl: "http://127.0.0.1:18791",
|
controlUrl: "http://127.0.0.1:18791",
|
||||||
cdpUrl: "http://127.0.0.1:18792", // defaults to controlUrl + 1
|
cdpUrl: "http://127.0.0.1:18792", // defaults to controlUrl + 1
|
||||||
|
remoteCdpTimeoutMs: 1500, // remote CDP HTTP timeout (ms)
|
||||||
|
remoteCdpHandshakeTimeoutMs: 3000, // remote CDP WebSocket handshake timeout (ms)
|
||||||
defaultProfile: "chrome",
|
defaultProfile: "chrome",
|
||||||
color: "#FF4500",
|
color: "#FF4500",
|
||||||
headless: false,
|
headless: false,
|
||||||
@@ -69,13 +81,25 @@ Notes:
|
|||||||
- If you override the Gateway port (`gateway.port` or `CLAWDBOT_GATEWAY_PORT`),
|
- If you override the Gateway port (`gateway.port` or `CLAWDBOT_GATEWAY_PORT`),
|
||||||
the default browser ports shift to stay in the same “family” (control = gateway + 2).
|
the default browser ports shift to stay in the same “family” (control = gateway + 2).
|
||||||
- `cdpUrl` defaults to `controlUrl + 1` when unset.
|
- `cdpUrl` defaults to `controlUrl + 1` when unset.
|
||||||
|
- `remoteCdpTimeoutMs` applies to remote (non-loopback) CDP reachability checks.
|
||||||
|
- `remoteCdpHandshakeTimeoutMs` applies to remote CDP WebSocket reachability checks.
|
||||||
- `attachOnly: true` means “never launch a local browser; only attach if it is already running.”
|
- `attachOnly: true` means “never launch a local browser; only attach if it is already running.”
|
||||||
- `color` + per-profile `color` tint the browser UI so you can see which profile is active.
|
- `color` + per-profile `color` tint the browser UI so you can see which profile is active.
|
||||||
- Auto-detect order: default browser if Chromium-based; otherwise Chrome → Brave → Edge → Chromium → Chrome Canary.
|
- Default profile is `chrome` (extension relay). Use `defaultProfile: "clawd"` for the managed browser.
|
||||||
|
- Auto-detect order: system default browser if Chromium-based; otherwise Chrome → Brave → Edge → Chromium → Chrome Canary.
|
||||||
|
- Local `clawd` profiles auto-assign `cdpPort`/`cdpUrl` — set those only for remote CDP.
|
||||||
|
|
||||||
## Use Brave (or another Chromium-based browser)
|
## Use Brave (or another Chromium-based browser)
|
||||||
|
|
||||||
Set `browser.executablePath` to override auto-detection:
|
If your **system default** browser is Chromium-based (Chrome/Brave/Edge/etc),
|
||||||
|
Clawdbot uses it automatically. Set `browser.executablePath` to override
|
||||||
|
auto-detection:
|
||||||
|
|
||||||
|
CLI example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot config set browser.executablePath "/usr/bin/google-chrome"
|
||||||
|
```
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
// macOS
|
// macOS
|
||||||
@@ -134,6 +158,42 @@ Example:
|
|||||||
Use `profiles.<name>.cdpUrl` for **remote CDP** if you want the Gateway to talk
|
Use `profiles.<name>.cdpUrl` for **remote CDP** if you want the Gateway to talk
|
||||||
directly to a Chromium-based browser instance without a remote control server.
|
directly to a Chromium-based browser instance without a remote control server.
|
||||||
|
|
||||||
|
Remote CDP URLs can include auth:
|
||||||
|
- Query tokens (e.g., `https://provider.example?token=<token>`)
|
||||||
|
- HTTP Basic auth (e.g., `https://user:pass@provider.example`)
|
||||||
|
|
||||||
|
Clawdbot preserves the auth when calling `/json/*` endpoints and when connecting
|
||||||
|
to the CDP WebSocket. Prefer environment variables or secrets managers for
|
||||||
|
tokens instead of committing them to config files.
|
||||||
|
|
||||||
|
### Browserless (hosted remote CDP)
|
||||||
|
|
||||||
|
[Browserless](https://browserless.io) is a hosted Chromium service that exposes
|
||||||
|
CDP endpoints over HTTPS. You can point a Clawdbot browser profile at a
|
||||||
|
Browserless region endpoint and authenticate with your API key.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
browser: {
|
||||||
|
enabled: true,
|
||||||
|
defaultProfile: "browserless",
|
||||||
|
remoteCdpTimeoutMs: 2000,
|
||||||
|
remoteCdpHandshakeTimeoutMs: 4000,
|
||||||
|
profiles: {
|
||||||
|
browserless: {
|
||||||
|
cdpUrl: "https://production-sfo.browserless.io?token=<BROWSERLESS_API_KEY>",
|
||||||
|
color: "#00AA00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Replace `<BROWSERLESS_API_KEY>` with your real Browserless token.
|
||||||
|
- Choose the region endpoint that matches your Browserless account (see their docs).
|
||||||
|
|
||||||
### Running the control server on the browser machine
|
### Running the control server on the browser machine
|
||||||
|
|
||||||
Run a standalone browser control server (recommended when your Gateway is remote):
|
Run a standalone browser control server (recommended when your Gateway is remote):
|
||||||
@@ -339,6 +399,10 @@ Playwright. If Playwright isn’t installed, those endpoints return a clear 501
|
|||||||
error. ARIA snapshots and basic screenshots still work for clawd-managed Chrome.
|
error. ARIA snapshots and basic screenshots still work for clawd-managed Chrome.
|
||||||
For the Chrome extension relay driver, ARIA snapshots and screenshots require Playwright.
|
For the Chrome extension relay driver, ARIA snapshots and screenshots require Playwright.
|
||||||
|
|
||||||
|
If you see `Playwright is not available in this gateway build`, install the full
|
||||||
|
Playwright package (not `playwright-core`) and restart the gateway, or reinstall
|
||||||
|
Clawdbot with browser support.
|
||||||
|
|
||||||
## How it works (internal)
|
## How it works (internal)
|
||||||
|
|
||||||
High-level flow:
|
High-level flow:
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
summary: "Firecrawl fallback for web_fetch (anti-bot + cached extraction)"
|
||||||
|
read_when:
|
||||||
|
- You want Firecrawl-backed web extraction
|
||||||
|
- You need a Firecrawl API key
|
||||||
|
- You want anti-bot extraction for web_fetch
|
||||||
|
---
|
||||||
|
|
||||||
|
# Firecrawl
|
||||||
|
|
||||||
|
Clawdbot can use **Firecrawl** as a fallback extractor for `web_fetch`. It is a hosted
|
||||||
|
content extraction service that supports bot circumvention and caching, which helps
|
||||||
|
with JS-heavy sites or pages that block plain HTTP fetches.
|
||||||
|
|
||||||
|
## Get an API key
|
||||||
|
|
||||||
|
1) Create a Firecrawl account and generate an API key.
|
||||||
|
2) Store it in config or set `FIRECRAWL_API_KEY` in the gateway environment.
|
||||||
|
|
||||||
|
## Configure Firecrawl
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
tools: {
|
||||||
|
web: {
|
||||||
|
fetch: {
|
||||||
|
firecrawl: {
|
||||||
|
apiKey: "FIRECRAWL_API_KEY_HERE",
|
||||||
|
baseUrl: "https://api.firecrawl.dev",
|
||||||
|
onlyMainContent: true,
|
||||||
|
maxAgeMs: 172800000,
|
||||||
|
timeoutSeconds: 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- `firecrawl.enabled` defaults to true when an API key is present.
|
||||||
|
- `maxAgeMs` controls how old cached results can be (ms). Default is 2 days.
|
||||||
|
|
||||||
|
## Stealth / bot circumvention
|
||||||
|
|
||||||
|
Firecrawl exposes a **proxy mode** parameter for bot circumvention (`basic`, `stealth`, or `auto`).
|
||||||
|
Clawdbot always uses `proxy: "auto"` plus `storeInCache: true` for Firecrawl requests.
|
||||||
|
If proxy is omitted, Firecrawl defaults to `auto`. `auto` retries with stealth proxies if a basic attempt fails, which may use more credits
|
||||||
|
than basic-only scraping.
|
||||||
|
|
||||||
|
## How `web_fetch` uses Firecrawl
|
||||||
|
|
||||||
|
`web_fetch` extraction order:
|
||||||
|
1) Readability (local)
|
||||||
|
2) Firecrawl (if configured)
|
||||||
|
3) Basic HTML cleanup (last fallback)
|
||||||
|
|
||||||
|
See [Web tools](/tools/web) for the full web tool setup.
|
||||||
@@ -215,6 +215,7 @@ Notes:
|
|||||||
- Responses are cached (default 15 min).
|
- Responses are cached (default 15 min).
|
||||||
- For JS-heavy sites, prefer the browser tool.
|
- For JS-heavy sites, prefer the browser tool.
|
||||||
- See [Web tools](/tools/web) for setup.
|
- See [Web tools](/tools/web) for setup.
|
||||||
|
- See [Firecrawl](/tools/firecrawl) for the optional anti-bot fallback.
|
||||||
|
|
||||||
### `browser`
|
### `browser`
|
||||||
Control the dedicated clawd browser.
|
Control the dedicated clawd browser.
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ Notes:
|
|||||||
- Use `{baseDir}` in instructions to reference the skill folder path.
|
- Use `{baseDir}` in instructions to reference the skill folder path.
|
||||||
- Optional frontmatter keys:
|
- Optional frontmatter keys:
|
||||||
- `homepage` — URL surfaced as “Website” in the macOS Skills UI (also supported via `metadata.clawdbot.homepage`).
|
- `homepage` — URL surfaced as “Website” in the macOS Skills UI (also supported via `metadata.clawdbot.homepage`).
|
||||||
|
- `user-invocable` — `true|false` (default: `true`). When `true`, the skill is exposed as a user slash command.
|
||||||
|
- `disable-model-invocation` — `true|false` (default: `false`). When `true`, the skill is excluded from the model prompt (still available via user invocation).
|
||||||
|
|
||||||
## Gating (load-time filters)
|
## Gating (load-time filters)
|
||||||
|
|
||||||
|
|||||||
Loaded 100 of 409 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user