Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
728cd5e974 | ||
|
|
0fab14ad9d | ||
|
|
d23febcd6a | ||
|
|
d763926364 | ||
|
|
7a683a4b62 | ||
|
|
44a237b637 | ||
|
|
375304bf13 | ||
|
|
d59aab7fd3 | ||
|
|
60748b1370 | ||
|
|
624cb33534 | ||
|
|
9603eff79a | ||
|
|
e9d6dec2f4 | ||
|
|
0cbfea79fa | ||
|
|
aaae327563 | ||
|
|
5b23f847d6 | ||
|
|
5a55789bc1 | ||
|
|
9c396f6331 | ||
|
|
51e871f9e5 | ||
|
|
47634c294d | ||
|
|
9c1122def0 | ||
|
|
2bd9e84851 | ||
|
|
5c2eedc340 | ||
|
|
3b7d103758 | ||
|
|
8146c43aa3 | ||
|
|
df386927ce | ||
|
|
0c18b2c442 | ||
|
|
1a7b7eba59 | ||
|
|
54fb59b8f3 | ||
|
|
a2f0d335f4 | ||
|
|
2e70c3ceab | ||
|
|
ca1902fb4e | ||
|
|
f11a89031b | ||
|
|
3c22fab679 | ||
|
|
ad46e95df9 | ||
|
|
7d4f2d9aed | ||
|
|
738b3592cd | ||
|
|
cd2af64860 | ||
|
|
04f1e767b2 | ||
|
|
1c7ac2a6ab | ||
|
|
77cf40da87 | ||
|
|
6ef3837e73 | ||
|
|
e7167e35ed | ||
|
|
56b3b44342 | ||
|
|
113eea5047 | ||
|
|
7b04e6ac42 | ||
|
|
d0a4cce41e | ||
|
|
5283872e00 |
No files matched your search
@@ -70,7 +70,7 @@
|
||||
- Never commit or publish real phone numbers, videos, or live configuration values. Use obviously fake placeholders in docs, tests, and examples.
|
||||
|
||||
## Troubleshooting
|
||||
- Rebrand/migration issues (Clawdis → Clawdbot) or legacy config/service warnings: run `clawdbot doctor` (see `docs/gateway/doctor.md`).
|
||||
- Rebrand/migration issues or legacy config/service warnings: run `clawdbot doctor` (see `docs/gateway/doctor.md`).
|
||||
|
||||
## Agent-Specific Notes
|
||||
- Vocabulary: "makeup" = "mac app".
|
||||
|
||||
+14
-2
@@ -4,10 +4,11 @@
|
||||
|
||||
### Changes
|
||||
- Docs: clarify per-agent auth stores, sandboxed skill binaries, and elevated semantics.
|
||||
- Daemon: support profile-aware service names for multi-gateway setups. (#671) — thanks @bjesuiter.
|
||||
- Docs: add FAQ entries for missing provider auth after adding agents and Gemini thinking signature errors.
|
||||
- Agents: add optional auth-profile copy prompt on `agents add` and improve auth error messaging.
|
||||
- Security: add `clawdbot security audit` (`--deep`, `--fix`) and surface it in `status --all` and `doctor`.
|
||||
- Security: add `clawdbot security audit` (`--deep`, `--fix`) and surface it in `status --all` and `doctor` (includes browser control exposure checks).
|
||||
- Security: expand `clawdbot security audit` checks (model hygiene, config includes, plugin allowlists, exposure matrix) and extend `--fix` to tighten more sensitive state paths.
|
||||
- Security: add `SECURITY.md` reporting policy.
|
||||
- Plugins: add Zalo channel plugin with gateway HTTP hooks and onboarding install prompt. (#854) — thanks @longmaba.
|
||||
- Onboarding: add a security checkpoint prompt (docs link + sandboxing hint); require `--accept-risk` for `--non-interactive`.
|
||||
- Docs: expand gateway security hardening guidance and incident response checklist.
|
||||
@@ -15,11 +16,22 @@
|
||||
- Security: add detect-secrets CI scan and baseline guidance. (#227) — thanks @Hyaxia.
|
||||
- Tools: add `web_search`/`web_fetch` (Brave API), auto-enable `web_fetch` for sandboxed sessions, and remove the `brave-search` skill.
|
||||
- CLI/Docs: add a web tools configure section for storing Brave API keys and update onboarding tips.
|
||||
- Tools: return a setup hint (docs link) when web_search runs without a Brave API key.
|
||||
- Config/Doctor: remove legacy Clawdis env fallbacks and config/service migrations (Clawdbot-only).
|
||||
- Browser: add Chrome extension relay takeover mode (toolbar button), plus `clawdbot browser extension install/path` and remote browser control via `clawdbot browser serve` + `browser.controlToken`.
|
||||
- CLI/Docs: add per-command CLI doc pages and link them from `clawdbot <command> --help`.
|
||||
- Browser: copy the installed Chrome extension path to clipboard after `clawdbot browser extension install/path`.
|
||||
- WhatsApp: add `channels.whatsapp.sendReadReceipts` to disable auto read receipts. (#882) — thanks @chrisrodz.
|
||||
|
||||
### Fixes
|
||||
- Browser: add tests for snapshot labels/efficient query params and labeled image responses.
|
||||
- macOS: ensure launchd log directory exists with a test-only override. (#909) — thanks @roshanasingh4.
|
||||
- Packaging: run `pnpm build` on `prepack` so npm publishes include fresh `dist/` output.
|
||||
- Telegram: register dock native commands with underscores to avoid `BOT_COMMAND_INVALID` (#929, fixes #901) — thanks @grp06.
|
||||
- Google: downgrade unsigned thinking blocks before send to avoid missing signature errors.
|
||||
- Agents: make user time zone and 24-hour time explicit in the system prompt. (#859) — thanks @CashWilliams.
|
||||
- Agents: strip downgraded tool call text without eating adjacent replies and filter thinking-tag leaks. (#905) — thanks @erikpr1994.
|
||||
- Agents: cap tool call IDs for OpenAI/OpenRouter to avoid request rejections. (#875) — thanks @j1philli.
|
||||
- Doctor: avoid re-adding WhatsApp config when only legacy ack reactions are set. (#927, fixes #900) — thanks @grp06.
|
||||
- Agents: scrub tuple `items` schemas for Gemini tool calls. (#926, fixes #746) — thanks @grp06.
|
||||
- Agents: stabilize sub-agent announce status from runtime outcomes and normalize Result/Notes. (#835) — thanks @roshanasingh4.
|
||||
|
||||
@@ -468,25 +468,28 @@ AI/vibe-coded PRs welcome! 🤖
|
||||
|
||||
Special thanks to @andrewting19 for the Anthropic OAuth tool-name fix.
|
||||
|
||||
Core contributors:
|
||||
- @cpojer — Telegram onboarding UX + docs
|
||||
|
||||
Thanks to all clawtributors:
|
||||
|
||||
<p align="left">
|
||||
<a href="https://github.com/steipete"><img src="https://avatars.githubusercontent.com/u/58493?v=4&s=48" width="48" height="48" alt="steipete" title="steipete"/></a> <a href="https://github.com/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/joshp123"><img src="https://avatars.githubusercontent.com/u/1497361?v=4&s=48" width="48" height="48" alt="joshp123" title="joshp123"/></a> <a href="https://github.com/mukhtharcm"><img src="https://avatars.githubusercontent.com/u/56378562?v=4&s=48" width="48" height="48" alt="mukhtharcm" title="mukhtharcm"/></a> <a href="https://github.com/maxsumrall"><img src="https://avatars.githubusercontent.com/u/628843?v=4&s=48" width="48" height="48" alt="maxsumrall" title="maxsumrall"/></a> <a href="https://github.com/xadenryan"><img src="https://avatars.githubusercontent.com/u/165437834?v=4&s=48" width="48" height="48" alt="xadenryan" title="xadenryan"/></a> <a href="https://github.com/tobiasbischoff"><img src="https://avatars.githubusercontent.com/u/711564?v=4&s=48" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></a> <a href="https://github.com/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/vrknetha"><img src="https://avatars.githubusercontent.com/u/20596261?v=4&s=48" width="48" height="48" alt="vrknetha" title="vrknetha"/></a> <a href="https://github.com/radek-paclt"><img src="https://avatars.githubusercontent.com/u/50451445?v=4&s=48" width="48" height="48" alt="radek-paclt" title="radek-paclt"/></a>
|
||||
<a href="https://github.com/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/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/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/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/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/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/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="Sebastian Barrios" title="Sebastian Barrios"/></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/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/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/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/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/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/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/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/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/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/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/jonasjancarik"><img src="https://avatars.githubusercontent.com/u/2459191?v=4&s=48" width="48" height="48" alt="jonasjancarik" title="jonasjancarik"/></a> <a href="https://github.com/search?q=Keith%20the%20Silly%20Goose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Keith the Silly Goose" title="Keith the Silly Goose"/></a> <a href="https://github.com/search?q=L36%20Server"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="L36 Server" title="L36 Server"/></a> <a href="https://github.com/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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=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/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/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/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/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=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/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/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/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/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a>
|
||||
<a href="https://github.com/carlulsoe"><img src="https://avatars.githubusercontent.com/u/34673973?v=4&s=48" width="48" height="48" alt="carlulsoe" title="carlulsoe"/></a> <a href="https://github.com/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a> <a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></a> <a href="https://github.com/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></a> <a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/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/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/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/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/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/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/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/vrknetha"><img src="https://avatars.githubusercontent.com/u/20596261?v=4&s=48" width="48" height="48" alt="vrknetha" title="vrknetha"/></a>
|
||||
<a href="https://github.com/radek-paclt"><img src="https://avatars.githubusercontent.com/u/50451445?v=4&s=48" width="48" height="48" alt="radek-paclt" title="radek-paclt"/></a> <a href="https://github.com/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/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/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/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/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/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/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/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/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/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/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/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/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/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/jonasjancarik"><img src="https://avatars.githubusercontent.com/u/2459191?v=4&s=48" width="48" height="48" alt="jonasjancarik" title="jonasjancarik"/></a> <a href="https://github.com/search?q=Keith%20the%20Silly%20Goose"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Keith the Silly Goose" title="Keith the Silly Goose"/></a> <a href="https://github.com/search?q=L36%20Server"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="L36 Server" title="L36 Server"/></a> <a href="https://github.com/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/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/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/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/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/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/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/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/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/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/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/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=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/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/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/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/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/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/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/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>
|
||||
</p>
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
# Security Policy
|
||||
|
||||
If you believe you’ve found a security issue in Clawdbot, please report it privately.
|
||||
|
||||
## Reporting
|
||||
|
||||
- Email: `steipete@gmail.com`
|
||||
- What to include: reproduction steps, impact assessment, and (if possible) a minimal PoC.
|
||||
|
||||
## Operational Guidance
|
||||
|
||||
For threat model + hardening guidance (including `clawdbot security audit --deep` and `--fix`), see:
|
||||
|
||||
- `https://docs.clawd.bot/gateway/security`
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
import Foundation
|
||||
|
||||
enum LogLocator {
|
||||
private static let logDir = URL(fileURLWithPath: "/tmp/clawdbot")
|
||||
private static let stdoutLog = logDir.appendingPathComponent("clawdbot-stdout.log")
|
||||
private static let gatewayLog = logDir.appendingPathComponent("clawdbot-gateway.log")
|
||||
private static var logDir: URL {
|
||||
if let override = ProcessInfo.processInfo.environment["CLAWDBOT_LOG_DIR"], !override.isEmpty {
|
||||
return URL(fileURLWithPath: override)
|
||||
}
|
||||
|
||||
return URL(fileURLWithPath: "/tmp/clawdbot")
|
||||
}
|
||||
|
||||
private static var stdoutLog: URL {
|
||||
logDir.appendingPathComponent("clawdbot-stdout.log")
|
||||
}
|
||||
|
||||
private static var gatewayLog: URL {
|
||||
logDir.appendingPathComponent("clawdbot-gateway.log")
|
||||
}
|
||||
|
||||
private static func ensureLogDirExists() {
|
||||
try? FileManager.default.createDirectory(at: self.logDir, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
private static func modificationDate(for url: URL) -> Date {
|
||||
(try? url.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? .distantPast
|
||||
@@ -11,6 +27,7 @@ enum LogLocator {
|
||||
|
||||
/// Returns the newest log file under /tmp/clawdbot/ (rolling or stdout), or nil if none exist.
|
||||
static func bestLogFile() -> URL? {
|
||||
self.ensureLogDirExists()
|
||||
let fm = FileManager.default
|
||||
let files = (try? fm.contentsOfDirectory(
|
||||
at: self.logDir,
|
||||
@@ -26,11 +43,13 @@ enum LogLocator {
|
||||
|
||||
/// Path to use for launchd stdout/err.
|
||||
static var launchdLogPath: String {
|
||||
stdoutLog.path
|
||||
self.ensureLogDirExists()
|
||||
return stdoutLog.path
|
||||
}
|
||||
|
||||
/// Path to use for the Gateway launchd job stdout/err.
|
||||
static var launchdGatewayLogPath: String {
|
||||
gatewayLog.path
|
||||
self.ensureLogDirExists()
|
||||
return gatewayLog.path
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import Darwin
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import Clawdbot
|
||||
|
||||
@Suite struct LogLocatorTests {
|
||||
@Test func launchdGatewayLogPathEnsuresTmpDirExists() throws {
|
||||
let fm = FileManager.default
|
||||
let baseDir = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
||||
let logDir = baseDir.appendingPathComponent("clawdbot-tests-\(UUID().uuidString)")
|
||||
|
||||
setenv("CLAWDBOT_LOG_DIR", logDir.path, 1)
|
||||
defer {
|
||||
unsetenv("CLAWDBOT_LOG_DIR")
|
||||
try? fm.removeItem(at: logDir)
|
||||
}
|
||||
|
||||
_ = LogLocator.launchdGatewayLogPath
|
||||
|
||||
var isDir: ObjCBool = false
|
||||
#expect(fm.fileExists(atPath: logDir.path, isDirectory: &isDir))
|
||||
#expect(isDir.boolValue == true)
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ Text is supported everywhere; media and reactions vary by channel.
|
||||
- [Signal](/channels/signal) — signal-cli; privacy-focused.
|
||||
- [iMessage](/channels/imessage) — macOS only; native integration.
|
||||
- [Microsoft Teams](/channels/msteams) — Bot Framework; enterprise support.
|
||||
- [Zalo](/channels/zalo) — Zalo Bot API; Vietnam's popular messenger (plugin).
|
||||
- [Zalo](/channels/zalo) — Zalo Bot API; Vietnam's popular messenger (plugin, installed separately).
|
||||
- [WebChat](/web/webchat) — Gateway WebChat UI over WebSocket.
|
||||
|
||||
## Notes
|
||||
|
||||
@@ -93,6 +93,7 @@ group messages, so use admin if you need full visibility.
|
||||
- Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`.
|
||||
- Replies always route back to the same Telegram chat.
|
||||
- Long-polling uses grammY runner with per-chat sequencing; overall concurrency is capped by `agents.defaults.maxConcurrent`.
|
||||
- Telegram Bot API does not support read receipts; there is no `sendReadReceipts` option.
|
||||
|
||||
## Formatting (Telegram HTML)
|
||||
- Outbound Telegram text uses `parse_mode: "HTML"` (Telegram’s supported tag subset).
|
||||
@@ -167,7 +168,7 @@ Forward any message from the group to `@userinfobot` or `@getidsbot` on Telegram
|
||||
|
||||
**Tip:** For your own user ID, DM the bot and it will reply with your user ID (pairing message), or use `/whoami` once commands are enabled.
|
||||
|
||||
**Privacy note:** `@userinfobot` is a third-party bot. If you prefer, use gateway logs (`clawdbot logs`) or Telegram developer tools to find user/chat IDs.
|
||||
**Privacy note:** `@userinfobot` is a third-party bot. If you prefer, add the bot to the group, send a message, and use `clawdbot logs --follow` to read `chat.id`, or use the Bot API `getUpdates`.
|
||||
|
||||
## Config writes
|
||||
By default, Telegram is allowed to write config updates triggered by channel events or `/config set|unset`.
|
||||
@@ -200,7 +201,22 @@ Private chats can include `message_thread_id` in some edge cases. Clawdbot keeps
|
||||
- `clawdbot pairing list telegram`
|
||||
- `clawdbot pairing approve telegram <CODE>`
|
||||
- Pairing is the default token exchange used for Telegram DMs. Details: [Pairing](/start/pairing)
|
||||
- `channels.telegram.allowFrom` accepts numeric user IDs (recommended) or `@username` entries. It is **not** the bot username; use the human sender’s ID (get it from `@userinfobot` or the `from.id` field in the gateway log).
|
||||
- `channels.telegram.allowFrom` accepts numeric user IDs (recommended) or `@username` entries. It is **not** the bot username; use the human sender’s ID.
|
||||
|
||||
#### Finding your Telegram user ID
|
||||
Safer (no third-party bot):
|
||||
1) Start the gateway and DM your bot.
|
||||
2) Run `clawdbot logs --follow` and look for `from.id`.
|
||||
|
||||
Alternate (official Bot API):
|
||||
1) DM your bot.
|
||||
2) Fetch updates with your bot token and read `message.from.id`:
|
||||
```bash
|
||||
curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
```
|
||||
|
||||
Third-party (less private):
|
||||
- DM `@userinfobot` or `@getidsbot` and use the returned user id.
|
||||
|
||||
### Group access
|
||||
|
||||
|
||||
@@ -138,6 +138,32 @@ Behavior:
|
||||
- Self-chat mode (allowFrom includes your number) avoids auto read receipts and ignores mention JIDs.
|
||||
- Read receipts sent for non-self-chat DMs.
|
||||
|
||||
## Read receipts
|
||||
By default, the gateway marks inbound WhatsApp messages as read (blue ticks) once they are accepted.
|
||||
|
||||
Disable globally:
|
||||
```json5
|
||||
{
|
||||
channels: { whatsapp: { sendReadReceipts: false } }
|
||||
}
|
||||
```
|
||||
|
||||
Disable per account:
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
whatsapp: {
|
||||
accounts: {
|
||||
personal: { sendReadReceipts: false }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Self-chat mode always skips read receipts.
|
||||
|
||||
## WhatsApp FAQ: sending messages + pairing
|
||||
|
||||
**Will Clawdbot message random contacts when I link WhatsApp?**
|
||||
|
||||
@@ -7,6 +7,12 @@ read_when:
|
||||
|
||||
Status: experimental. Direct messages only; groups coming soon per Zalo docs.
|
||||
|
||||
## Plugin required
|
||||
Zalo ships as a plugin and is not bundled with the core install.
|
||||
- Install via CLI: `clawdbot plugins install @clawdbot/zalo`
|
||||
- Or select **Zalo** during onboarding and confirm the install prompt
|
||||
- Details: [Plugins](/plugin)
|
||||
|
||||
## Quick setup (beginner)
|
||||
1) Install the Zalo plugin:
|
||||
- From a source checkout: `clawdbot plugins install ./extensions/zalo`
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot agent` (send one agent turn via the Gateway)"
|
||||
read_when:
|
||||
- You want to run one agent turn from scripts (optionally deliver reply)
|
||||
---
|
||||
|
||||
# `clawdbot agent`
|
||||
|
||||
Run an agent turn via the Gateway (use `--local` for embedded).
|
||||
|
||||
Related:
|
||||
- Agent send tool: [Agent send](/tools/agent-send)
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
clawdbot agent --to +15555550123 --message "status update" --deliver
|
||||
clawdbot agent --session-id 1234 --message "Summarize inbox" --thinking medium
|
||||
```
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot agents` (list/add/delete isolated agents)"
|
||||
read_when:
|
||||
- You want multiple isolated agents (workspaces + routing + auth)
|
||||
---
|
||||
|
||||
# `clawdbot agents`
|
||||
|
||||
Manage isolated agents (workspaces + auth + routing).
|
||||
|
||||
Related:
|
||||
- Multi-agent routing: [Multi-Agent Routing](/concepts/multi-agent)
|
||||
- Agent workspace: [Agent workspace](/concepts/agent-workspace)
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
clawdbot agents list
|
||||
clawdbot agents add work --workspace ~/clawd-work
|
||||
clawdbot agents delete work
|
||||
```
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot browser` (profiles, tabs, actions, extension relay, remote serve)"
|
||||
read_when:
|
||||
- You use `clawdbot browser` and want examples for common tasks
|
||||
- You want to control a remote browser via `browser.controlUrl`
|
||||
- You want to use the Chrome extension relay (attach/detach via toolbar button)
|
||||
---
|
||||
|
||||
# `clawdbot browser`
|
||||
|
||||
Manage Clawdbot’s browser control server and run browser actions (tabs, snapshots, screenshots, navigation, clicks, typing).
|
||||
|
||||
Related:
|
||||
- Browser tool + API: [Browser tool](/tools/browser)
|
||||
- Chrome extension relay: [Chrome extension](/tools/chrome-extension)
|
||||
|
||||
## Common flags
|
||||
|
||||
- `--url <controlUrl>`: override `browser.controlUrl` for this command invocation.
|
||||
- `--browser-profile <name>`: choose a browser profile (default comes from config).
|
||||
- `--json`: machine-readable output (where supported).
|
||||
|
||||
## Quick start (local)
|
||||
|
||||
```bash
|
||||
clawdbot browser status
|
||||
clawdbot browser start
|
||||
clawdbot browser tabs
|
||||
clawdbot browser open https://example.com
|
||||
clawdbot browser snapshot
|
||||
```
|
||||
|
||||
## Profiles
|
||||
|
||||
Profiles are named browser instances with their own data directory and CDP settings.
|
||||
|
||||
```bash
|
||||
clawdbot browser profiles
|
||||
clawdbot browser create-profile --name work --color "#FF5A36"
|
||||
clawdbot browser delete-profile --name work
|
||||
```
|
||||
|
||||
Use a specific profile:
|
||||
|
||||
```bash
|
||||
clawdbot browser --browser-profile work tabs
|
||||
```
|
||||
|
||||
## Tabs
|
||||
|
||||
```bash
|
||||
clawdbot browser tabs
|
||||
clawdbot browser open https://docs.clawd.bot
|
||||
clawdbot browser focus <targetId>
|
||||
clawdbot browser close <targetId>
|
||||
```
|
||||
|
||||
## Snapshot / screenshot / actions
|
||||
|
||||
Snapshot:
|
||||
|
||||
```bash
|
||||
clawdbot browser snapshot
|
||||
```
|
||||
|
||||
Screenshot:
|
||||
|
||||
```bash
|
||||
clawdbot browser screenshot
|
||||
```
|
||||
|
||||
Navigate/click/type (ref-based UI automation):
|
||||
|
||||
```bash
|
||||
clawdbot browser navigate https://example.com
|
||||
clawdbot browser click <ref>
|
||||
clawdbot browser type <ref> "hello"
|
||||
```
|
||||
|
||||
## Chrome extension relay (attach via toolbar button)
|
||||
|
||||
This mode lets the agent control an existing Chrome tab that you attach manually (it does not auto-attach).
|
||||
|
||||
Install the unpacked extension to a stable path:
|
||||
|
||||
```bash
|
||||
clawdbot browser extension install
|
||||
clawdbot browser extension path
|
||||
```
|
||||
|
||||
Then Chrome → `chrome://extensions` → enable “Developer mode” → “Load unpacked” → select the printed folder.
|
||||
|
||||
Full guide: [Chrome extension](/tools/chrome-extension)
|
||||
|
||||
## Remote browser control (`clawdbot browser serve`)
|
||||
|
||||
If the Gateway runs on a different machine than the browser, run a standalone browser control server on the machine that runs Chrome:
|
||||
|
||||
```bash
|
||||
clawdbot browser serve --bind 127.0.0.1 --port 18791 --token <token>
|
||||
```
|
||||
|
||||
Then point the Gateway at it using `browser.controlUrl` + `browser.controlToken` (or `CLAWDBOT_BROWSER_CONTROL_TOKEN`).
|
||||
|
||||
Security + TLS best-practices: [Browser tool](/tools/browser), [Tailscale](/gateway/tailscale), [Security](/gateway/security)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot channels` (accounts, status, login/logout, logs)"
|
||||
read_when:
|
||||
- You want to add/remove channel accounts (WhatsApp/Telegram/Discord/Slack/Signal/iMessage)
|
||||
- You want to check channel status or tail channel logs
|
||||
---
|
||||
|
||||
# `clawdbot channels`
|
||||
|
||||
Manage chat channel accounts and their runtime status on the Gateway.
|
||||
|
||||
Related docs:
|
||||
- Channel guides: [Channels](/channels/index)
|
||||
- Gateway configuration: [Configuration](/gateway/configuration)
|
||||
|
||||
## Common commands
|
||||
|
||||
```bash
|
||||
clawdbot channels list
|
||||
clawdbot channels status
|
||||
clawdbot channels logs --channel all
|
||||
```
|
||||
|
||||
## Add / remove accounts
|
||||
|
||||
```bash
|
||||
clawdbot channels add --channel telegram --token <bot-token>
|
||||
clawdbot channels remove --channel telegram --delete
|
||||
```
|
||||
|
||||
Tip: `clawdbot channels add --help` shows per-channel flags (token, app token, signal-cli paths, etc).
|
||||
|
||||
## Login / logout (interactive)
|
||||
|
||||
```bash
|
||||
clawdbot channels login --channel whatsapp
|
||||
clawdbot channels logout --channel whatsapp
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Run `clawdbot status --deep` for a broad probe.
|
||||
- Use `clawdbot doctor` for guided fixes.
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot configure` / `clawdbot config` (interactive configuration prompts)"
|
||||
read_when:
|
||||
- You want to tweak credentials, devices, or agent defaults interactively
|
||||
---
|
||||
|
||||
# `clawdbot configure` (alias: `config`)
|
||||
|
||||
Interactive prompt to set up credentials, devices, and agent defaults.
|
||||
|
||||
Related:
|
||||
- Gateway configuration reference: [Configuration](/gateway/configuration)
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
clawdbot configure
|
||||
clawdbot configure --section models --section channels
|
||||
```
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot cron` (schedule and run background jobs)"
|
||||
read_when:
|
||||
- You want scheduled jobs and wakeups
|
||||
- You’re debugging cron execution and logs
|
||||
---
|
||||
|
||||
# `clawdbot cron`
|
||||
|
||||
Manage cron jobs for the Gateway scheduler.
|
||||
|
||||
Related:
|
||||
- Cron jobs: [Cron jobs](/automation/cron-jobs)
|
||||
|
||||
Tip: run `clawdbot cron --help` for the full command surface.
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot daemon` (install/uninstall/status for the Gateway service)"
|
||||
read_when:
|
||||
- You want to run the Gateway as a background service
|
||||
- You’re debugging daemon install, status, or logs
|
||||
---
|
||||
|
||||
# `clawdbot daemon`
|
||||
|
||||
Manage the Gateway daemon (background service).
|
||||
|
||||
Related:
|
||||
- Gateway CLI: [Gateway](/cli/gateway)
|
||||
- macOS platform notes: [macOS](/platforms/macos)
|
||||
|
||||
Tip: run `clawdbot daemon --help` for platform-specific flags.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot dashboard` (open the Control UI)"
|
||||
read_when:
|
||||
- You want to open the Control UI with your current token
|
||||
- You want to print the URL without launching a browser
|
||||
---
|
||||
|
||||
# `clawdbot dashboard`
|
||||
|
||||
Open the Control UI using your current auth.
|
||||
|
||||
```bash
|
||||
clawdbot dashboard
|
||||
clawdbot dashboard --no-open
|
||||
```
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot dns` (wide-area discovery helpers)"
|
||||
read_when:
|
||||
- You want wide-area discovery (DNS-SD) via Tailscale + CoreDNS
|
||||
- You’re setting up split DNS for clawdbot.internal
|
||||
---
|
||||
|
||||
# `clawdbot dns`
|
||||
|
||||
DNS helpers for wide-area discovery (Tailscale + CoreDNS). Currently focused on macOS + Homebrew CoreDNS.
|
||||
|
||||
Related:
|
||||
- Gateway discovery: [Discovery](/gateway/discovery)
|
||||
- Wide-area discovery config: [Configuration](/gateway/configuration)
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
clawdbot dns setup
|
||||
clawdbot dns setup --apply
|
||||
```
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot docs` (search the live docs index)"
|
||||
read_when:
|
||||
- You want to search the live Clawdbot docs from the terminal
|
||||
---
|
||||
|
||||
# `clawdbot docs`
|
||||
|
||||
Search the live docs index.
|
||||
|
||||
```bash
|
||||
clawdbot docs browser extension
|
||||
clawdbot docs sandbox allowHostControl
|
||||
```
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot doctor` (health checks + guided repairs)"
|
||||
read_when:
|
||||
- You have connectivity/auth issues and want guided fixes
|
||||
- You updated and want a sanity check
|
||||
---
|
||||
|
||||
# `clawdbot doctor`
|
||||
|
||||
Health checks + quick fixes for the gateway and channels.
|
||||
|
||||
Related:
|
||||
- Troubleshooting: [Troubleshooting](/gateway/troubleshooting)
|
||||
- Security audit: [Security](/gateway/security)
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
clawdbot doctor
|
||||
clawdbot doctor --repair
|
||||
clawdbot doctor --deep
|
||||
```
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot health` (gateway health endpoint via RPC)"
|
||||
read_when:
|
||||
- You want to quickly check the running Gateway’s health
|
||||
---
|
||||
|
||||
# `clawdbot health`
|
||||
|
||||
Fetch health from the running Gateway.
|
||||
|
||||
```bash
|
||||
clawdbot health
|
||||
clawdbot health --json
|
||||
```
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot hooks` (Gmail Pub/Sub + webhook helpers)"
|
||||
read_when:
|
||||
- You want to wire Gmail Pub/Sub events into Clawdbot hooks
|
||||
- You want to run the gog watch service and renew loop
|
||||
---
|
||||
|
||||
# `clawdbot hooks`
|
||||
|
||||
Webhook helpers and hook-based integrations.
|
||||
|
||||
Related:
|
||||
- Webhooks: [Webhook](/automation/webhook)
|
||||
- Gmail Pub/Sub: [Gmail Pub/Sub](/automation/gmail-pubsub)
|
||||
|
||||
## Gmail
|
||||
|
||||
```bash
|
||||
clawdbot hooks gmail setup --account you@example.com
|
||||
clawdbot hooks gmail run
|
||||
```
|
||||
|
||||
+38
-1
@@ -9,6 +9,43 @@ read_when:
|
||||
|
||||
This page describes the current CLI behavior. If commands change, update this doc.
|
||||
|
||||
## Command pages
|
||||
|
||||
- [`setup`](/cli/setup)
|
||||
- [`onboard`](/cli/onboard)
|
||||
- [`configure`](/cli/configure) (alias: `config`)
|
||||
- [`doctor`](/cli/doctor)
|
||||
- [`dashboard`](/cli/dashboard)
|
||||
- [`reset`](/cli/reset)
|
||||
- [`uninstall`](/cli/uninstall)
|
||||
- [`update`](/cli/update)
|
||||
- [`message`](/cli/message)
|
||||
- [`agent`](/cli/agent)
|
||||
- [`agents`](/cli/agents)
|
||||
- [`status`](/cli/status)
|
||||
- [`health`](/cli/health)
|
||||
- [`sessions`](/cli/sessions)
|
||||
- [`gateway`](/cli/gateway)
|
||||
- [`daemon`](/cli/daemon)
|
||||
- [`logs`](/cli/logs)
|
||||
- [`models`](/cli/models)
|
||||
- [`memory`](/cli/memory)
|
||||
- [`nodes`](/cli/nodes)
|
||||
- [`sandbox`](/cli/sandbox)
|
||||
- [`tui`](/cli/tui)
|
||||
- [`browser`](/cli/browser)
|
||||
- [`wake`](/cli/wake)
|
||||
- [`cron`](/cli/cron)
|
||||
- [`dns`](/cli/dns)
|
||||
- [`docs`](/cli/docs)
|
||||
- [`hooks`](/cli/hooks)
|
||||
- [`pairing`](/cli/pairing)
|
||||
- [`plugins`](/cli/plugins) (plugin commands)
|
||||
- [`channels`](/cli/channels)
|
||||
- [`security`](/cli/security)
|
||||
- [`skills`](/cli/skills)
|
||||
- [`voicecall`](/cli/voicecall) (plugin; if installed)
|
||||
|
||||
## Global flags
|
||||
|
||||
- `--dev`: isolate state under `~/.clawdbot-dev` and shift default ports.
|
||||
@@ -748,7 +785,7 @@ Location:
|
||||
|
||||
## Browser
|
||||
|
||||
Browser control CLI (dedicated Chrome/Chromium). See [/tools/browser](/tools/browser).
|
||||
Browser control CLI (dedicated Chrome/Chromium). See [`clawdbot browser`](/cli/browser) and the [Browser tool](/tools/browser).
|
||||
|
||||
Common options:
|
||||
- `--url <controlUrl>`
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot logs` (tail gateway logs via RPC)"
|
||||
read_when:
|
||||
- You need to tail Gateway logs remotely (without SSH)
|
||||
- You want JSON log lines for tooling
|
||||
---
|
||||
|
||||
# `clawdbot logs`
|
||||
|
||||
Tail Gateway file logs over RPC (works in remote mode).
|
||||
|
||||
Related:
|
||||
- Logging overview: [Logging](/logging)
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
clawdbot logs
|
||||
clawdbot logs --follow
|
||||
clawdbot logs --json
|
||||
clawdbot logs --limit 500
|
||||
```
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot memory` (status/index/search)"
|
||||
read_when:
|
||||
- You want to index or search semantic memory
|
||||
- You’re debugging memory availability or indexing
|
||||
---
|
||||
|
||||
# `clawdbot memory`
|
||||
|
||||
Memory search tools (semantic memory status/index/search).
|
||||
|
||||
Related:
|
||||
- Memory concept: [Memory](/concepts/memory)
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
clawdbot memory status
|
||||
clawdbot memory index
|
||||
clawdbot memory search "release checklist"
|
||||
```
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot models` (status/list/set/scan, aliases, fallbacks, auth)"
|
||||
read_when:
|
||||
- You want to change default models or view provider auth status
|
||||
- You want to scan available models/providers and debug auth profiles
|
||||
---
|
||||
|
||||
# `clawdbot models`
|
||||
|
||||
Model discovery, scanning, and configuration (default model, fallbacks, auth profiles).
|
||||
|
||||
Related:
|
||||
- Providers + models: [Models](/providers/models)
|
||||
- Provider auth setup: [Getting started](/start/getting-started)
|
||||
|
||||
## Common commands
|
||||
|
||||
```bash
|
||||
clawdbot models status
|
||||
clawdbot models list
|
||||
clawdbot models set <model-or-alias>
|
||||
clawdbot models scan
|
||||
```
|
||||
|
||||
## Aliases + fallbacks
|
||||
|
||||
```bash
|
||||
clawdbot models aliases list
|
||||
clawdbot models fallbacks list
|
||||
```
|
||||
|
||||
## Auth profiles
|
||||
|
||||
```bash
|
||||
clawdbot models auth add
|
||||
clawdbot models auth setup-token
|
||||
clawdbot models auth paste-token
|
||||
```
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot nodes` (list/status/approve/invoke, camera/canvas/screen)"
|
||||
read_when:
|
||||
- You’re managing paired nodes (cameras, screen, canvas)
|
||||
- You need to approve requests or invoke node commands
|
||||
---
|
||||
|
||||
# `clawdbot nodes`
|
||||
|
||||
Manage paired nodes (devices) and invoke node capabilities.
|
||||
|
||||
Related:
|
||||
- Nodes overview: [Nodes](/nodes)
|
||||
- Camera: [Camera nodes](/nodes/camera)
|
||||
- Images: [Image nodes](/nodes/images)
|
||||
|
||||
## Common commands
|
||||
|
||||
```bash
|
||||
clawdbot nodes list
|
||||
clawdbot nodes pending
|
||||
clawdbot nodes approve <requestId>
|
||||
clawdbot nodes status
|
||||
```
|
||||
|
||||
## Invoke / run
|
||||
|
||||
```bash
|
||||
clawdbot nodes invoke --node <id|name|ip> --command <command> --params <json>
|
||||
clawdbot nodes run --node <id|name|ip> <command...>
|
||||
```
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot onboard` (interactive onboarding wizard)"
|
||||
read_when:
|
||||
- You want guided setup for gateway, workspace, auth, channels, and skills
|
||||
---
|
||||
|
||||
# `clawdbot onboard`
|
||||
|
||||
Interactive onboarding wizard (local or remote Gateway setup).
|
||||
|
||||
Related:
|
||||
- Wizard guide: [Onboarding](/start/onboarding)
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
clawdbot onboard
|
||||
clawdbot onboard --flow quickstart
|
||||
clawdbot onboard --mode remote --remote-url ws://gateway-host:18789
|
||||
```
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot pairing` (approve/list pairing requests)"
|
||||
read_when:
|
||||
- You’re using pairing-mode DMs and need to approve senders
|
||||
---
|
||||
|
||||
# `clawdbot pairing`
|
||||
|
||||
Approve or inspect DM pairing requests (for channels that support pairing).
|
||||
|
||||
Related:
|
||||
- Pairing flow: [Pairing](/start/pairing)
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
clawdbot pairing list whatsapp
|
||||
clawdbot pairing approve whatsapp <code> --notify
|
||||
```
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot plugins` (list, install, enable/disable, doctor)"
|
||||
read_when:
|
||||
- You want to install or manage in-process Gateway plugins
|
||||
- You want to debug plugin load failures
|
||||
---
|
||||
|
||||
# `clawdbot plugins`
|
||||
|
||||
Manage Gateway plugins/extensions (loaded in-process).
|
||||
|
||||
Related:
|
||||
- Plugin system: [Plugins](/plugin)
|
||||
- Security hardening: [Security](/gateway/security)
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
clawdbot plugins list
|
||||
clawdbot plugins info <id>
|
||||
clawdbot plugins enable <id>
|
||||
clawdbot plugins disable <id>
|
||||
clawdbot plugins doctor
|
||||
```
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
clawdbot plugins install <npm-spec>
|
||||
```
|
||||
|
||||
Security note: treat plugin installs like running code. Prefer pinned versions.
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot reset` (reset local state/config)"
|
||||
read_when:
|
||||
- You want to wipe local state while keeping the CLI installed
|
||||
- You want a dry-run of what would be removed
|
||||
---
|
||||
|
||||
# `clawdbot reset`
|
||||
|
||||
Reset local config/state (keeps the CLI installed).
|
||||
|
||||
```bash
|
||||
clawdbot reset
|
||||
clawdbot reset --dry-run
|
||||
clawdbot reset --scope config+creds+sessions --yes --non-interactive
|
||||
```
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot security` (audit and fix common security footguns)"
|
||||
read_when:
|
||||
- You want to run a quick security audit on config/state
|
||||
- You want to apply safe “fix” suggestions (chmod, tighten defaults)
|
||||
---
|
||||
|
||||
# `clawdbot security`
|
||||
|
||||
Security tools (audit + optional fixes).
|
||||
|
||||
Related:
|
||||
- Security guide: [Security](/gateway/security)
|
||||
|
||||
## Audit
|
||||
|
||||
```bash
|
||||
clawdbot security audit
|
||||
clawdbot security audit --deep
|
||||
clawdbot security audit --fix
|
||||
```
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot sessions` (list stored sessions + usage)"
|
||||
read_when:
|
||||
- You want to list stored sessions and see recent activity
|
||||
---
|
||||
|
||||
# `clawdbot sessions`
|
||||
|
||||
List stored conversation sessions.
|
||||
|
||||
```bash
|
||||
clawdbot sessions
|
||||
clawdbot sessions --active 120
|
||||
clawdbot sessions --json
|
||||
```
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot setup` (initialize config + workspace)"
|
||||
read_when:
|
||||
- You’re doing first-run setup without the full onboarding wizard
|
||||
- You want to set the default workspace path
|
||||
---
|
||||
|
||||
# `clawdbot setup`
|
||||
|
||||
Initialize `~/.clawdbot/clawdbot.json` and the agent workspace.
|
||||
|
||||
Related:
|
||||
- Getting started: [Getting started](/start/getting-started)
|
||||
- Wizard: [Onboarding](/start/onboarding)
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
clawdbot setup
|
||||
clawdbot setup --workspace ~/clawd
|
||||
```
|
||||
|
||||
To run the wizard via setup:
|
||||
|
||||
```bash
|
||||
clawdbot setup --wizard
|
||||
```
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot skills` (list/info/check) and skill eligibility"
|
||||
read_when:
|
||||
- You want to see which skills are available and ready to run
|
||||
- You want to debug missing binaries/env/config for skills
|
||||
---
|
||||
|
||||
# `clawdbot skills`
|
||||
|
||||
Inspect skills (bundled + workspace + managed overrides) and see what’s eligible vs missing requirements.
|
||||
|
||||
Related:
|
||||
- Skills system: [Skills](/tools/skills)
|
||||
- Skills config: [Skills config](/tools/skills-config)
|
||||
- ClawdHub installs: [ClawdHub](/tools/clawdhub)
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
clawdbot skills list
|
||||
clawdbot skills list --eligible
|
||||
clawdbot skills info <name>
|
||||
clawdbot skills check
|
||||
```
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot status` (diagnostics, probes, usage snapshots)"
|
||||
read_when:
|
||||
- You want a quick diagnosis of channel health + recent session recipients
|
||||
- You want a pasteable “all” status for debugging
|
||||
---
|
||||
|
||||
# `clawdbot status`
|
||||
|
||||
Diagnostics for channels + sessions.
|
||||
|
||||
```bash
|
||||
clawdbot status
|
||||
clawdbot status --all
|
||||
clawdbot status --deep
|
||||
clawdbot status --usage
|
||||
```
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot tui` (terminal UI connected to the Gateway)"
|
||||
read_when:
|
||||
- You want a terminal UI for the Gateway (remote-friendly)
|
||||
- You want to pass url/token/session from scripts
|
||||
---
|
||||
|
||||
# `clawdbot tui`
|
||||
|
||||
Open the terminal UI connected to the Gateway.
|
||||
|
||||
Related:
|
||||
- TUI guide: [TUI](/tui)
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
clawdbot tui
|
||||
clawdbot tui --url ws://127.0.0.1:18789 --token <token>
|
||||
clawdbot tui --session main --deliver
|
||||
```
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot uninstall` (remove gateway service + local data)"
|
||||
read_when:
|
||||
- You want to remove the gateway service and/or local state
|
||||
- You want a dry-run first
|
||||
---
|
||||
|
||||
# `clawdbot uninstall`
|
||||
|
||||
Uninstall the gateway service + local data (CLI remains).
|
||||
|
||||
```bash
|
||||
clawdbot uninstall
|
||||
clawdbot uninstall --all --yes
|
||||
clawdbot uninstall --dry-run
|
||||
```
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot voicecall` (voice-call plugin command surface)"
|
||||
read_when:
|
||||
- You use the voice-call plugin and want the CLI entry points
|
||||
- You want quick examples for `voicecall call|continue|status|tail|expose`
|
||||
---
|
||||
|
||||
# `clawdbot voicecall`
|
||||
|
||||
`voicecall` is a plugin-provided command. It only appears if the voice-call plugin is installed and enabled.
|
||||
|
||||
Primary doc:
|
||||
- Voice-call plugin: [Voice Call](/plugins/voice-call)
|
||||
|
||||
## Common commands
|
||||
|
||||
```bash
|
||||
clawdbot voicecall status --call-id <id>
|
||||
clawdbot voicecall call --to "+15555550123" --message "Hello" --mode notify
|
||||
clawdbot voicecall continue --call-id <id> --message "Any questions?"
|
||||
clawdbot voicecall end --call-id <id>
|
||||
```
|
||||
|
||||
## Exposing webhooks (Tailscale)
|
||||
|
||||
```bash
|
||||
clawdbot voicecall expose --mode serve
|
||||
clawdbot voicecall expose --mode funnel
|
||||
clawdbot voicecall unexpose
|
||||
```
|
||||
|
||||
Security note: only expose the webhook endpoint to networks you trust. Prefer Tailscale Serve over Funnel when possible.
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
summary: "CLI reference for `clawdbot wake` (enqueue a system event and optionally trigger an immediate heartbeat)"
|
||||
read_when:
|
||||
- You want to “poke” a running Gateway to process a system event
|
||||
- You use `wake` with cron jobs or remote nodes
|
||||
---
|
||||
|
||||
# `clawdbot wake`
|
||||
|
||||
Enqueue a system event on the Gateway and optionally trigger an immediate heartbeat.
|
||||
|
||||
This is a lightweight “poke” for automation flows where you don’t want to run a full command, but you do want the Gateway to react quickly.
|
||||
|
||||
Related:
|
||||
- Cron jobs: [Cron](/cli/cron)
|
||||
- Gateway heartbeat: [Heartbeat](/gateway/heartbeat)
|
||||
|
||||
## Common commands
|
||||
|
||||
```bash
|
||||
clawdbot wake --text "sync"
|
||||
clawdbot wake --text "sync" --mode now
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
- `--text <text>`: system event text.
|
||||
- `--mode <mode>`: `now` or `next-heartbeat` (default).
|
||||
- `--json`: machine-readable output.
|
||||
|
||||
## Notes
|
||||
|
||||
- Requires a running Gateway reachable by your current config (local or remote).
|
||||
- If you’re using sandboxing, `wake` still targets the Gateway; sandboxing does not block the command itself.
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
summary: "Compaction modes and configuration"
|
||||
read_when:
|
||||
- You want to configure compaction summarization behavior
|
||||
- You are tuning compaction settings in clawdbot.json
|
||||
---
|
||||
# Compaction
|
||||
|
||||
Compaction summarizes older session history so the conversation stays within the model context window. The summary is stored in the session JSONL history and combined with the most recent messages.
|
||||
|
||||
## Modes
|
||||
|
||||
`agents.defaults.compaction.mode` controls how summaries are generated.
|
||||
|
||||
- `default` (default): use the built-in compaction summarizer.
|
||||
- `safeguard`: uses a chunked summarization pass to avoid context overflow for very long histories. If chunked summarization fails, Clawdbot falls back to a minimal summary plus file-operation metadata.
|
||||
|
||||
## Configuration
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
compaction: {
|
||||
mode: "safeguard",
|
||||
reserveTokensFloor: 20000,
|
||||
memoryFlush: {
|
||||
enabled: true,
|
||||
softThresholdTokens: 4000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Related docs
|
||||
|
||||
- [Context window + compaction behavior](/concepts/compaction)
|
||||
- [Gateway configuration reference](/gateway/configuration)
|
||||
@@ -44,18 +44,18 @@ file creation:
|
||||
{ agent: { skipBootstrap: true } }
|
||||
```
|
||||
|
||||
## Legacy workspace folders
|
||||
## Extra workspace folders
|
||||
|
||||
Older installs may have created `~/clawdis` or `~/clawdbot`. Keeping multiple
|
||||
workspace directories around can cause confusing auth or state drift, because
|
||||
only one workspace is active at a time.
|
||||
Older installs may have created `~/clawdbot`. Keeping multiple workspace
|
||||
directories around can cause confusing auth or state drift, because only one
|
||||
workspace is active at a time.
|
||||
|
||||
**Recommendation:** keep a single active workspace. If you no longer use the
|
||||
legacy folders, archive or move them to Trash (for example `trash ~/clawdis`).
|
||||
extra folders, archive or move them to Trash (for example `trash ~/clawdbot`).
|
||||
If you intentionally keep multiple workspaces, make sure
|
||||
`agents.defaults.workspace` points to the active one.
|
||||
|
||||
`clawdbot doctor` warns when it detects legacy workspace directories.
|
||||
`clawdbot doctor` warns when it detects extra workspace directories.
|
||||
|
||||
## Workspace file map (what each file means)
|
||||
|
||||
|
||||
+56
-8
@@ -53,6 +53,14 @@
|
||||
"source": "/context/",
|
||||
"destination": "/concepts/context"
|
||||
},
|
||||
{
|
||||
"source": "/compaction",
|
||||
"destination": "/concepts/compaction"
|
||||
},
|
||||
{
|
||||
"source": "/compaction/",
|
||||
"destination": "/concepts/compaction"
|
||||
},
|
||||
{
|
||||
"source": "/minimax",
|
||||
"destination": "/providers/minimax"
|
||||
@@ -197,6 +205,14 @@
|
||||
"source": "/providers/troubleshooting/",
|
||||
"destination": "/channels/troubleshooting"
|
||||
},
|
||||
{
|
||||
"source": "/providers/zalo",
|
||||
"destination": "/channels/zalo"
|
||||
},
|
||||
{
|
||||
"source": "/providers/zalo/",
|
||||
"destination": "/channels/zalo"
|
||||
},
|
||||
{
|
||||
"source": "/providers/whatsapp",
|
||||
"destination": "/channels/whatsapp"
|
||||
@@ -549,14 +565,6 @@
|
||||
"source": "/concepts/provider-routing/",
|
||||
"destination": "/concepts/channel-routing"
|
||||
},
|
||||
{
|
||||
"source": "/refactor/provider-plugin",
|
||||
"destination": "/refactor/channel-plugin"
|
||||
},
|
||||
{
|
||||
"source": "/refactor/provider-plugin/",
|
||||
"destination": "/refactor/channel-plugin"
|
||||
},
|
||||
{
|
||||
"source": "/queue",
|
||||
"destination": "/concepts/queue"
|
||||
@@ -750,6 +758,7 @@
|
||||
"install/index",
|
||||
"install/installer",
|
||||
"install/updating",
|
||||
"install/uninstall",
|
||||
"install/ansible",
|
||||
"install/nix",
|
||||
"install/docker",
|
||||
@@ -760,8 +769,38 @@
|
||||
"group": "CLI",
|
||||
"pages": [
|
||||
"cli/index",
|
||||
"cli/setup",
|
||||
"cli/onboard",
|
||||
"cli/configure",
|
||||
"cli/doctor",
|
||||
"cli/dashboard",
|
||||
"cli/reset",
|
||||
"cli/uninstall",
|
||||
"cli/browser",
|
||||
"cli/message",
|
||||
"cli/agent",
|
||||
"cli/agents",
|
||||
"cli/status",
|
||||
"cli/health",
|
||||
"cli/sessions",
|
||||
"cli/channels",
|
||||
"cli/skills",
|
||||
"cli/plugins",
|
||||
"cli/memory",
|
||||
"cli/models",
|
||||
"cli/logs",
|
||||
"cli/nodes",
|
||||
"cli/gateway",
|
||||
"cli/daemon",
|
||||
"cli/tui",
|
||||
"cli/voicecall",
|
||||
"cli/wake",
|
||||
"cli/cron",
|
||||
"cli/dns",
|
||||
"cli/docs",
|
||||
"cli/hooks",
|
||||
"cli/pairing",
|
||||
"cli/security",
|
||||
"cli/update",
|
||||
"cli/sandbox"
|
||||
]
|
||||
@@ -788,6 +827,7 @@
|
||||
"concepts/channel-routing",
|
||||
"concepts/messages",
|
||||
"concepts/streaming",
|
||||
"concepts/markdown-formatting",
|
||||
"concepts/groups",
|
||||
"concepts/group-messages",
|
||||
"concepts/typing-indicators",
|
||||
@@ -814,11 +854,14 @@
|
||||
"gateway/configuration-examples",
|
||||
"gateway/authentication",
|
||||
"gateway/openai-http-api",
|
||||
"gateway/cli-backends",
|
||||
"gateway/local-models",
|
||||
"gateway/background-process",
|
||||
"gateway/health",
|
||||
"gateway/heartbeat",
|
||||
"gateway/doctor",
|
||||
"gateway/logging",
|
||||
"logging",
|
||||
"gateway/security",
|
||||
"gateway/sandbox-vs-tool-policy-vs-elevated",
|
||||
"gateway/sandboxing",
|
||||
@@ -853,6 +896,7 @@
|
||||
"channels/signal",
|
||||
"channels/imessage",
|
||||
"channels/msteams",
|
||||
"channels/zalo",
|
||||
"broadcast-groups",
|
||||
"channels/troubleshooting",
|
||||
"channels/location"
|
||||
@@ -865,9 +909,11 @@
|
||||
"providers/models",
|
||||
"providers/openai",
|
||||
"providers/anthropic",
|
||||
"bedrock",
|
||||
"providers/moonshot",
|
||||
"providers/minimax",
|
||||
"providers/openrouter",
|
||||
"providers/synthetic",
|
||||
"providers/opencode",
|
||||
"providers/glm",
|
||||
"providers/zai"
|
||||
@@ -888,11 +934,13 @@
|
||||
"pages": [
|
||||
"tools",
|
||||
"plugin",
|
||||
"plugins/voice-call",
|
||||
"tools/exec",
|
||||
"tools/web",
|
||||
"tools/apply-patch",
|
||||
"tools/elevated",
|
||||
"tools/browser",
|
||||
"tools/browser-login",
|
||||
"tools/chrome-extension",
|
||||
"tools/browser-linux-troubleshooting",
|
||||
"tools/slash-commands",
|
||||
|
||||
@@ -1,306 +0,0 @@
|
||||
---
|
||||
summary: "Spec for hooks.gmail.model - cheaper model for Gmail PubSub processing"
|
||||
read_when:
|
||||
- Implementing hooks.gmail.model feature
|
||||
- Modifying Gmail hook processing
|
||||
- Working on hook model selection
|
||||
---
|
||||
# hooks.gmail.model: Cheaper Model for Gmail PubSub Processing
|
||||
|
||||
## Problem
|
||||
|
||||
Gmail PubSub hook processing (`/gmail-pubsub`) currently uses the session's primary model (`agents.defaults.model.primary`), which may be an expensive model like `claude-opus-4-5`. For automated email processing that doesn't require the most capable model, this wastes tokens/cost.
|
||||
|
||||
## Solution
|
||||
|
||||
Add `hooks.gmail.model` config option to specify an optional cheaper model for Gmail PubSub processing, with intelligent fallback to the primary model on auth/rate-limit/timeout failures.
|
||||
|
||||
## Config Structure
|
||||
|
||||
```json5
|
||||
{
|
||||
hooks: {
|
||||
gmail: {
|
||||
account: "user@gmail.com",
|
||||
// ... existing gmail config ...
|
||||
|
||||
// NEW: Optional model override for Gmail hook processing
|
||||
model: "openrouter/meta-llama/llama-3.3-70b-instruct:free",
|
||||
|
||||
// NEW: Optional thinking level override
|
||||
thinking: "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `hooks.gmail.model` | `string` | (none) | Model to use for Gmail hook processing. Accepts `provider/model` refs or aliases from `agents.defaults.models`. |
|
||||
| `hooks.gmail.thinking` | `string` | (inherited) | Thinking level override (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`; GPT-5.2 + Codex models only). If unset, inherits from `agents.defaults.thinkingDefault` or model's default. |
|
||||
|
||||
### Alias Support
|
||||
|
||||
`hooks.gmail.model` accepts:
|
||||
- Full refs: `"openrouter/meta-llama/llama-3.3-70b-instruct:free"`
|
||||
- Aliases from `agents.defaults.models`: `"Opus"`, `"Sonnet"`, `"GLM"`
|
||||
|
||||
Resolution uses `buildModelAliasIndex()` from `model-selection.ts`.
|
||||
|
||||
## Fallback Behavior
|
||||
|
||||
### Fallback Triggers
|
||||
|
||||
Auth, rate-limit, and timeout errors trigger fallback:
|
||||
- `401 Unauthorized`
|
||||
- `403 Forbidden`
|
||||
- `429 Too Many Requests`
|
||||
- Timeouts (provider hangs / network timeouts)
|
||||
|
||||
Other errors (500s, content errors) fail in place.
|
||||
|
||||
### Fallback Chain
|
||||
|
||||
```
|
||||
hooks.gmail.model (if set)
|
||||
↓ (on auth/rate-limit/timeout)
|
||||
agents.defaults.model.fallbacks[0..n]
|
||||
↓ (exhausted)
|
||||
agents.defaults.model.primary
|
||||
```
|
||||
|
||||
### Uncatalogued Model
|
||||
|
||||
If `hooks.gmail.model` is set but not found in the model catalog or allowlist:
|
||||
- **Config load**: Log warning (surfaced in `clawdbot doctor`)
|
||||
- **Allowlist**: If `agents.defaults.models` is set and the model isn't listed, the hook falls back to primary.
|
||||
|
||||
### Cooldown Integration
|
||||
|
||||
Uses existing model-failover cooldown from `model-failover.ts`:
|
||||
- After auth/rate-limit failure, model enters cooldown
|
||||
- Next hook invocation respects cooldown before retrying
|
||||
- Integrates with auth profile rotation
|
||||
|
||||
## Implementation
|
||||
|
||||
### Type Changes
|
||||
|
||||
```typescript
|
||||
// src/config/types.ts
|
||||
export type HooksGmailConfig = {
|
||||
account?: string;
|
||||
label?: string;
|
||||
// ... existing fields ...
|
||||
|
||||
/** Optional model override for Gmail hook processing (provider/model or alias). */
|
||||
model?: string;
|
||||
/** Optional thinking level override for Gmail hook processing. */
|
||||
thinking?: "off" | "minimal" | "low" | "medium" | "high";
|
||||
};
|
||||
```
|
||||
|
||||
### Model Resolution
|
||||
|
||||
New function in `src/cron/isolated-agent.ts` or `src/agents/model-selection.ts`:
|
||||
|
||||
```typescript
|
||||
export function resolveHooksGmailModel(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
defaultProvider: string;
|
||||
defaultModel: string;
|
||||
}): { provider: string; model: string; isHooksOverride: boolean } | null {
|
||||
const hooksModel = params.cfg.hooks?.gmail?.model;
|
||||
if (!hooksModel) return null;
|
||||
|
||||
const aliasIndex = buildModelAliasIndex({
|
||||
cfg: params.cfg,
|
||||
defaultProvider: params.defaultProvider,
|
||||
});
|
||||
|
||||
const resolved = resolveModelRefFromString({
|
||||
raw: hooksModel,
|
||||
defaultProvider: params.defaultProvider,
|
||||
aliasIndex,
|
||||
});
|
||||
|
||||
if (!resolved) return null;
|
||||
return {
|
||||
provider: resolved.ref.provider,
|
||||
model: resolved.ref.model,
|
||||
isHooksOverride: true,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Processing Flow
|
||||
|
||||
In `runCronIsolatedAgentTurn()` (or new wrapper for hooks):
|
||||
|
||||
```typescript
|
||||
// Resolve model - prefer hooks.gmail.model for Gmail hooks
|
||||
const isGmailHook = params.sessionKey.startsWith("hook:gmail:");
|
||||
const hooksModelRef = isGmailHook
|
||||
? resolveHooksGmailModel({ cfg, defaultProvider, defaultModel })
|
||||
: null;
|
||||
|
||||
const { provider, model } = hooksModelRef ?? resolveConfiguredModelRef({
|
||||
cfg: params.cfg,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
});
|
||||
|
||||
// Run with fallback - on auth/rate-limit/timeout, fall through to agents.defaults.model.fallbacks
|
||||
const fallbackResult = await runWithModelFallback({
|
||||
cfg: params.cfg,
|
||||
provider,
|
||||
model,
|
||||
hooksOverride: hooksModelRef?.isHooksOverride,
|
||||
run: (providerOverride, modelOverride) => runEmbeddedPiAgent({
|
||||
// ... existing params ...
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
### Fallback Detection
|
||||
|
||||
Extend `runWithModelFallback()` to detect auth/rate-limit:
|
||||
|
||||
```typescript
|
||||
function isAuthRateLimitError(err: unknown): boolean {
|
||||
if (err instanceof ApiError) {
|
||||
return [401, 403, 429].includes(err.status);
|
||||
}
|
||||
// Check for common patterns in error messages
|
||||
const msg = String(err).toLowerCase();
|
||||
return msg.includes("unauthorized")
|
||||
|| msg.includes("rate limit")
|
||||
|| msg.includes("quota exceeded");
|
||||
}
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
### Config Load Time
|
||||
|
||||
In config validation (for `clawdbot doctor`):
|
||||
|
||||
```typescript
|
||||
if (cfg.hooks?.gmail?.model) {
|
||||
const resolved = resolveHooksGmailModel({ cfg, defaultProvider, defaultModel });
|
||||
if (!resolved) {
|
||||
issues.push({
|
||||
path: "hooks.gmail.model",
|
||||
message: `Model "${cfg.hooks.gmail.model}" could not be resolved`,
|
||||
});
|
||||
} else {
|
||||
const catalog = await loadModelCatalog({ config: cfg });
|
||||
const key = modelKey(resolved.provider, resolved.model);
|
||||
const inCatalog = catalog.some(e => modelKey(e.provider, e.id) === key);
|
||||
if (!inCatalog) {
|
||||
issues.push({
|
||||
path: "hooks.gmail.model",
|
||||
message: `Model "${key}" not found in agents.defaults.models catalog (will fall back to primary)`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Runtime
|
||||
|
||||
At hook invocation time, validate and fall back:
|
||||
- If model not in catalog → log warning, use primary
|
||||
- If model auth fails → log warning, enter cooldown, fall back
|
||||
|
||||
## Observability
|
||||
|
||||
### Log Messages
|
||||
|
||||
```
|
||||
[hooks] Gmail hook: using model openrouter/meta-llama/llama-3.3-70b-instruct:free
|
||||
[hooks] Gmail hook: model llama auth failed (429), falling back to claude-opus-4-5
|
||||
```
|
||||
|
||||
### Hook Event Summary
|
||||
|
||||
Include fallback info in the hook summary sent to session:
|
||||
|
||||
```
|
||||
Hook Gmail (fallback:llama→opus): <summary>
|
||||
```
|
||||
|
||||
## Hot Reload
|
||||
|
||||
`hooks.gmail.model` and `hooks.gmail.thinking` are hot-reloadable:
|
||||
- Changes apply to the next hook invocation
|
||||
- No gateway restart required
|
||||
- Hooks config is already in the hot-reload matrix
|
||||
|
||||
## Test Plan
|
||||
|
||||
### Unit Tests
|
||||
|
||||
1. **Model resolution** (`model-selection.test.ts`):
|
||||
- `resolveHooksGmailModel()` with valid ref
|
||||
- `resolveHooksGmailModel()` with alias
|
||||
- `resolveHooksGmailModel()` with invalid input → null
|
||||
|
||||
2. **Config validation** (`config.test.ts`):
|
||||
- Warning on uncatalogued model
|
||||
- No warning on valid model
|
||||
- Graceful handling of missing hooks.gmail section
|
||||
|
||||
3. **Fallback triggers** (`model-fallback.test.ts`):
|
||||
- 401/403/429 → triggers fallback
|
||||
- timeouts → triggers fallback
|
||||
- 500/content error → no fallback
|
||||
- Content error → no fallback
|
||||
|
||||
### Integration Tests
|
||||
|
||||
1. **Hook processing** (`server.hooks.test.ts`):
|
||||
- Gmail hook uses `hooks.gmail.model` when set
|
||||
- Fallback to primary on auth failure
|
||||
- Thinking level override applied
|
||||
|
||||
2. **Hot reload** (`config-reload.test.ts`):
|
||||
- Change `hooks.gmail.model` → next hook uses new model
|
||||
|
||||
## Documentation
|
||||
|
||||
Update `docs/gateway/configuration.md`:
|
||||
|
||||
```json5
|
||||
{
|
||||
hooks: {
|
||||
gmail: {
|
||||
account: "user@gmail.com",
|
||||
topic: "projects/my-project/topics/gmail-watch",
|
||||
// ... existing config ...
|
||||
|
||||
// Optional: Use a cheaper model for Gmail processing
|
||||
// Falls back to agents.defaults.model.primary on auth/rate-limit errors
|
||||
model: "openrouter/meta-llama/llama-3.3-70b-instruct:free",
|
||||
|
||||
// Optional: Override thinking level for Gmail processing
|
||||
thinking: "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Scope Limitation
|
||||
|
||||
This PR is Gmail-specific. Future hooks (`hooks.github.model`, etc.) would follow the same pattern but are out of scope.
|
||||
|
||||
## Changelog Entry
|
||||
|
||||
```
|
||||
- feat: add hooks.gmail.model for cheaper Gmail PubSub processing (#XXX)
|
||||
- Falls back to agents.defaults.model.primary on auth/rate-limit/timeouts (401/403/429)
|
||||
- Supports aliases from agents.defaults.models
|
||||
- Add hooks.gmail.thinking override
|
||||
```
|
||||
@@ -430,6 +430,22 @@ For groups, use `channels.whatsapp.groupPolicy` + `channels.whatsapp.groupAllowF
|
||||
}
|
||||
```
|
||||
|
||||
### `channels.whatsapp.sendReadReceipts`
|
||||
|
||||
Controls whether inbound WhatsApp messages are marked as read (blue ticks). Default: `true`.
|
||||
|
||||
Self-chat mode always skips read receipts, even when enabled.
|
||||
|
||||
Per-account override: `channels.whatsapp.accounts.<id>.sendReadReceipts`.
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
whatsapp: { sendReadReceipts: false }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `channels.whatsapp.accounts` (multi-account)
|
||||
|
||||
Run multiple WhatsApp accounts in one gateway:
|
||||
@@ -1209,6 +1225,31 @@ streaming, final replies) across channels unless already present.
|
||||
If `messages.responsePrefix` is unset, no prefix is applied by default.
|
||||
Set it to `"auto"` to derive `[{identity.name}]` for the routed agent (when set).
|
||||
|
||||
#### Template variables
|
||||
|
||||
The `responsePrefix` string can include template variables that resolve dynamically:
|
||||
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
| `{model}` | Short model name | `claude-opus-4-5`, `gpt-4o` |
|
||||
| `{modelFull}` | Full model identifier | `anthropic/claude-opus-4-5` |
|
||||
| `{provider}` | Provider name | `anthropic`, `openai` |
|
||||
| `{thinkingLevel}` | Current thinking level | `high`, `low`, `off` |
|
||||
| `{identity.name}` | Agent identity name | (same as `"auto"` mode) |
|
||||
|
||||
Variables are case-insensitive (`{MODEL}` = `{model}`). `{think}` is an alias for `{thinkingLevel}`.
|
||||
Unresolved variables remain as literal text.
|
||||
|
||||
```json5
|
||||
{
|
||||
messages: {
|
||||
responsePrefix: "[{model} | think:{thinkingLevel}]"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Example output: `[claude-opus-4-5 | think:high] Here's my response...`
|
||||
|
||||
WhatsApp inbound prefix is configured via `channels.whatsapp.messagePrefix` (deprecated:
|
||||
`messages.messagePrefix`). Default stays **unchanged**: `"[clawdbot]"` when
|
||||
`channels.whatsapp.allowFrom` is empty, otherwise `""` (no prefix). When using
|
||||
|
||||
@@ -59,13 +59,13 @@ cat ~/.clawdbot/clawdbot.json
|
||||
- UI protocol freshness check (rebuilds Control UI when the protocol schema is newer).
|
||||
- Health check + restart prompt.
|
||||
- Skills status summary (eligible/missing/blocked).
|
||||
- Legacy config migration and normalization.
|
||||
- Config normalization for legacy values.
|
||||
- OpenCode Zen provider override warnings (`models.providers.opencode`).
|
||||
- Legacy on-disk state migration (sessions/agent dir/WhatsApp auth).
|
||||
- State integrity and permissions checks (sessions, transcripts, state dir).
|
||||
- Config file permission checks (chmod 600) when running locally.
|
||||
- Model auth health: checks OAuth expiry, can refresh expiring tokens, and reports auth-profile cooldown/disabled states.
|
||||
- Legacy workspace dir detection (`~/clawdis`, `~/clawdbot`).
|
||||
- Extra workspace dir detection (`~/clawdbot`).
|
||||
- Sandbox image repair when sandboxing is enabled.
|
||||
- Legacy service migration and extra gateway detection.
|
||||
- Gateway runtime checks (service installed but not running; cached launchd label).
|
||||
@@ -85,10 +85,10 @@ cat ~/.clawdbot/clawdbot.json
|
||||
If this is a git checkout and doctor is running interactively, it offers to
|
||||
update (fetch/rebase/build) before running doctor.
|
||||
|
||||
### 1) Legacy config file migration
|
||||
If `~/.clawdis/clawdis.json` exists and `~/.clawdbot/clawdbot.json` does not,
|
||||
doctor migrates the file and normalizes old paths/image names. This prevents
|
||||
new installs from silently booting with the wrong schema.
|
||||
### 1) Config normalization
|
||||
If the config contains legacy value shapes (for example `messages.ackReaction`
|
||||
without a channel-specific override), doctor normalizes them into the current
|
||||
schema.
|
||||
|
||||
### 2) Legacy config key migrations
|
||||
When the config contains deprecated keys, other commands refuse to run and ask
|
||||
@@ -182,7 +182,7 @@ When sandboxing is enabled, doctor checks Docker images and offers to build or
|
||||
switch to legacy names if the current image is missing.
|
||||
|
||||
### 8) Gateway service migrations and cleanup hints
|
||||
Doctor detects legacy Clawdis gateway services (launchd/systemd/schtasks) and
|
||||
Doctor detects legacy gateway services (launchd/systemd/schtasks) and
|
||||
offers to remove them and install the Clawdbot service using the current gateway
|
||||
port. It can also scan for extra gateway-like services and print cleanup hints.
|
||||
Profile-named Clawdbot gateway services are considered first-class and are not
|
||||
|
||||
@@ -27,7 +27,30 @@ It flags common footguns (Gateway auth exposure, browser control exposure, eleva
|
||||
`--fix` applies safe guardrails:
|
||||
- Tighten `groupPolicy="open"` to `groupPolicy="allowlist"` (and per-account variants) for common channels.
|
||||
- Turn `logging.redactSensitive="off"` back to `"tools"`.
|
||||
- Tighten local perms (`~/.clawdbot` → `700`, config file → `600`).
|
||||
- Tighten local perms (`~/.clawdbot` → `700`, config file → `600`, plus common state files like `credentials/*.json`, `agents/*/agent/auth-profiles.json`, and `agents/*/sessions/sessions.json`).
|
||||
|
||||
### What the audit checks (high level)
|
||||
|
||||
- **Inbound access** (DM policies, group policies, allowlists): can strangers trigger the bot?
|
||||
- **Tool blast radius** (elevated tools + open rooms): could prompt injection turn into shell/file/network actions?
|
||||
- **Network exposure** (Gateway bind/auth, Tailscale Serve/Funnel).
|
||||
- **Browser control exposure** (remote controlUrl without token, HTTP, token reuse).
|
||||
- **Local disk hygiene** (permissions, symlinks, config includes, “synced folder” paths).
|
||||
- **Plugins** (extensions exist without an explicit allowlist).
|
||||
- **Model hygiene** (warn when configured models look legacy; not a hard block).
|
||||
|
||||
If you run `--deep`, Clawdbot also attempts a best-effort live Gateway probe.
|
||||
|
||||
## Security Audit Checklist
|
||||
|
||||
When the audit prints findings, treat this as a priority order:
|
||||
|
||||
1. **Anything “open” + tools enabled**: lock down DMs/groups first (pairing/allowlists), then tighten tool policy/sandboxing.
|
||||
2. **Public network exposure** (LAN bind, Funnel, missing auth): fix immediately.
|
||||
3. **Browser control remote exposure**: treat it like a remote admin API (token required; HTTPS/tailnet-only).
|
||||
4. **Permissions**: make sure state/config/credentials/auth are not group/world-readable.
|
||||
5. **Plugins/extensions**: only load what you explicitly trust.
|
||||
6. **Model choice**: prefer modern, instruction-hardened models for any bot with tools.
|
||||
|
||||
## The Threat Model
|
||||
|
||||
@@ -108,7 +131,7 @@ Even with strong system prompts, **prompt injection is not solved**. What helps
|
||||
- Prefer mention gating in groups; avoid “always-on” bots in public rooms.
|
||||
- Treat links and pasted instructions as hostile by default.
|
||||
- Run sensitive tool execution in a sandbox; keep secrets out of the agent’s reachable filesystem.
|
||||
- **Model choice matters:** 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)). Using weaker models increases risk.
|
||||
- **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)).
|
||||
|
||||
## Reasoning & verbose output in groups
|
||||
|
||||
@@ -117,6 +140,23 @@ was not meant for a public channel. In group settings, treat them as **debug
|
||||
only** and keep them off unless you explicitly need them. If you enable them,
|
||||
do so only in trusted DMs or tightly controlled rooms.
|
||||
|
||||
## Incident Response (if you suspect compromise)
|
||||
|
||||
Assume “compromised” means: someone got into a room that can trigger the bot, or a token leaked, or a plugin/tool did something unexpected.
|
||||
|
||||
1. **Stop the blast radius**
|
||||
- Disable elevated tools (or stop the Gateway) until you understand what happened.
|
||||
- Lock down inbound surfaces (DM policy, group allowlists, mention gating).
|
||||
2. **Rotate secrets**
|
||||
- Rotate `gateway.auth` token/password.
|
||||
- Rotate `browser.controlToken` and `hooks.token` (if used).
|
||||
- Revoke/rotate model provider credentials (API keys / OAuth).
|
||||
3. **Review artifacts**
|
||||
- Check Gateway logs and recent sessions/transcripts for unexpected tool calls.
|
||||
- Review `extensions/` and remove anything you don’t fully trust.
|
||||
4. **Re-run audit**
|
||||
- `clawdbot security audit --deep` and confirm the report is clean.
|
||||
|
||||
## Lessons Learned (The Hard Way)
|
||||
|
||||
### The `find ~` Incident 🦞
|
||||
|
||||
@@ -136,14 +136,14 @@ clawdbot daemon status
|
||||
It will show the listener(s) and likely causes (gateway already running, SSH tunnel).
|
||||
If needed, stop the service or pick a different port.
|
||||
|
||||
### Legacy Workspace Folders Detected
|
||||
### Extra Workspace Folders Detected
|
||||
|
||||
If you upgraded from older installs, you might still have `~/clawdis` or
|
||||
`~/clawdbot` on disk. Multiple workspace directories can cause confusing auth
|
||||
or state drift because only one workspace is active.
|
||||
If you upgraded from older installs, you might still have `~/clawdbot` on disk.
|
||||
Multiple workspace directories can cause confusing auth or state drift because
|
||||
only one workspace is active.
|
||||
|
||||
**Fix:** keep a single active workspace and archive/remove the rest. See
|
||||
[Agent workspace](/concepts/agent-workspace#legacy-workspace-folders).
|
||||
[Agent workspace](/concepts/agent-workspace#extra-workspace-folders).
|
||||
|
||||
### Main chat running in a sandbox workspace
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ Label:
|
||||
|
||||
Plist location (per‑user):
|
||||
- `~/Library/LaunchAgents/com.clawdbot.gateway.plist`
|
||||
(or `~/Library/LaunchAgents/com.clawdbot.<profile>.plist`)
|
||||
|
||||
Manager:
|
||||
- The macOS app owns LaunchAgent install/update in Local mode.
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
---
|
||||
summary: "Channel plugin refactor implementation notes (registry, status, gateway/runtime)"
|
||||
read_when:
|
||||
- Adding or refactoring channel plugin wiring
|
||||
- Moving channel-specific behavior into plugin hooks
|
||||
---
|
||||
|
||||
# Channel Plugins — Implementation Notes
|
||||
|
||||
Goal: make chat channels (iMessage, Discord, etc.) pluggable with minimal wiring and shared UX/state paths.
|
||||
|
||||
## Architecture Overview
|
||||
- Registry: `src/channels/plugins/index.ts` owns the plugin list.
|
||||
- Channel dock: `src/channels/dock.ts` owns lightweight channel metadata used by shared flows (reply, command auth, block streaming) without importing full plugins.
|
||||
- IDs/aliases: `src/channels/registry.ts` owns stable channel ids + input aliases.
|
||||
- Shape: `src/channels/plugins/types.ts` defines the plugin contract.
|
||||
- Gateway: `src/gateway/server-channels.ts` drives start/stop + runtime snapshots via plugins.
|
||||
- Outbound: `src/infra/outbound/deliver.ts` routes through plugin outbound when present.
|
||||
- Outbound delivery loads **outbound adapters** on-demand via `src/channels/plugins/outbound/load.ts` (avoid importing heavy channel plugins on hot paths).
|
||||
- Reload: `src/gateway/config-reload.ts` uses plugin `reload.configPrefixes` lazily (avoid init cycles).
|
||||
- CLI: `src/commands/channels/*` uses plugin list for add/remove/status/list.
|
||||
- Protocol: `src/gateway/protocol/schema.ts` keeps `channels.status` schema-light (maps keyed by channel id).
|
||||
|
||||
## Plugin Contract (high-level)
|
||||
Each `ChannelPlugin` bundles:
|
||||
- `meta`: id/labels/docs/sort order.
|
||||
- `capabilities`: chatTypes + optional features (polls, media, nativeCommands, etc.).
|
||||
- `config`: list/resolve/default/isConfigured/describeAccount + isEnabled + (un)configured reasons + `resolveAllowFrom` + `formatAllowFrom`.
|
||||
- `outbound`: deliveryMode + chunker + resolveTarget (mode-aware) + sendText/sendMedia/sendPoll + pollMaxOptions.
|
||||
- `status`: defaultRuntime + probe/audit/buildAccountSnapshot + buildChannelSummary + logSelfId + collectStatusIssues.
|
||||
- `gateway`: startAccount/stopAccount with runtime context (`getStatus`/`setStatus`), plus optional `loginWithQrStart/loginWithQrWait` for gateway-owned QR login flows.
|
||||
- `security`: dmPolicy + allowFrom hints used by `doctor security`.
|
||||
- `heartbeat`: optional readiness checks + heartbeat recipient resolution when channels own targeting.
|
||||
- `auth`: optional login hook used by `clawdbot channels login`.
|
||||
- `reload`: `configPrefixes` that map to hot restarts.
|
||||
- `onboarding`: optional CLI onboarding adapter (wizard UI hooks per channel).
|
||||
- `agentTools`: optional channel-owned agent tools (ex: QR login).
|
||||
|
||||
## Key Integration Notes
|
||||
- `listChannelPlugins()` is the runtime source of truth for channel UX and wiring.
|
||||
- Avoid importing `src/channels/plugins/index.ts` from shared modules (reply flow, command auth, sandbox explain). It’s intentionally “heavy” (channels may pull web login / monitor code). Use `getChannelDock()` + `normalizeChannelId()` (from `src/channels/registry.ts`) for cheap metadata, and only `getChannelPlugin()` at execution boundaries (ex: `src/auto-reply/reply/route-reply.ts`).
|
||||
- WhatsApp plugin keeps Baileys-heavy login bits behind lazy imports; cheap auth file checks live in `src/web/auth-store.ts` (so outbound routing doesn’t pay Baileys import cost).
|
||||
- `routeReply` delegates sending to plugin `outbound` adapters via a lazy import of `src/infra/outbound/deliver.ts` (so adding a channel is “just implement outbound adapter”, no router switches).
|
||||
- Avoid static imports of channel monitors inside plugin modules. Monitors typically import the reply pipeline, which can create ESM cycles (and break Vite/Vitest SSR with TDZ errors). Prefer lazy imports inside `gateway.startAccount`.
|
||||
- Debug cycle leaks quickly with: `npx -y madge --circular src/channels/plugins/index.ts`.
|
||||
- Protocol v3: `channels.status` is map-based and schema-light so new channels can ship without protocol updates.
|
||||
- `DEFAULT_CHAT_CHANNEL` lives in `src/channels/registry.ts` and is used anywhere we need a fallback delivery surface.
|
||||
- Channel reload rules are computed lazily to avoid static init cycles in tests.
|
||||
- Signal/iMessage media size limits are resolved inside their plugins.
|
||||
- `normalizeChannelId()` handles aliases (ex: `imsg`, `teams`) so CLI and API inputs stay stable.
|
||||
- Gateway runtime snapshot (`getRuntimeSnapshot`) is map-based: `{ channels, channelAccounts }` (no `${id}Accounts` keys).
|
||||
- `channels.status` response keys (v3):
|
||||
- `channelOrder: string[]`
|
||||
- `channelLabels: Record<string, string>`
|
||||
- `channels: Record<string, unknown>` (channel summary objects, plugin-defined)
|
||||
- `channelAccounts: Record<string, ChannelAccountSnapshot[]>`
|
||||
- `channelDefaultAccountId: Record<string, string>`
|
||||
- `channels.status` summary objects come from `status.buildChannelSummary` (no per-channel branching in the handler).
|
||||
- `channels.status` warnings flow through `status.collectStatusIssues` per plugin.
|
||||
- CLI list uses `meta.showConfigured` to decide whether to show configured state.
|
||||
- CLI channel options and prompt channel lists are generated from `listChannelPlugins()` (avoid hardcoded arrays).
|
||||
- Channel selection (`resolveMessageChannelSelection`) inspects `config.isEnabled` + `config.isConfigured` per plugin instead of hardcoded checks.
|
||||
- Pairing flows (CLI + store) use `plugin.pairing` (`idLabel`, `normalizeAllowEntry`, `notifyApproval`) via `src/channels/plugins/pairing.ts`.
|
||||
- CLI channel remove/disable delegates to `config.setAccountEnabled` + `config.deleteAccount` per plugin.
|
||||
- CLI channel add delegates to `plugin.setup` for account validation, naming, and config writes (no hardcoded checks).
|
||||
- Agent channel status entries are built from plugin config/status (`status.resolveAccountState` for custom state labels).
|
||||
- Agent binding defaults use `meta.forceAccountBinding` to avoid hardcoded checks.
|
||||
- Onboarding quickstart allowlist uses `meta.quickstartAllowFrom` to avoid hardcoded channel lists.
|
||||
- `resolveChannelDefaultAccountId()` is the shared helper for picking default accounts from `accountIds` + plugin config.
|
||||
- `routeReply` uses plugin outbound senders; `ChannelOutboundContext` supports `replyToId` + `threadId` and outbound delivery supports `abortSignal` for cooperative cancellation.
|
||||
- Outbound target resolution (`resolveOutboundTarget`) delegates to `plugin.outbound.resolveTarget` (mode-aware, uses config allowlists when present).
|
||||
- Outbound delivery results accept `meta` for channel-specific fields to avoid core type churn in new plugins.
|
||||
- Agent gateway routing sets `deliveryTargetMode` and uses `resolveOutboundTarget` for implicit fallback targets when `to` is missing.
|
||||
- Elevated tool allowlists (`tools.elevated.allowFrom`) are a record keyed by channel id (no schema update needed when adding channels).
|
||||
- Block streaming defaults live on the plugin (`capabilities.blockStreaming`, `streaming.blockStreamingCoalesceDefaults`) instead of hardcoded channel checks.
|
||||
- Channel logout routes through `channels.logout` using `gateway.logoutAccount` on each plugin (clients should call the generic method).
|
||||
- Gateway message-channel normalization uses `src/channels/registry.ts` for cheap validation/normalization without plugin init cycles.
|
||||
- Group mention gating now flows through `plugin.groups.resolveRequireMention` (Discord/Slack/Telegram/WhatsApp/iMessage) instead of branching in reply handlers.
|
||||
- Command authorization uses `config.resolveAllowFrom` + `config.formatAllowFrom`, with `commands.enforceOwnerForCommands` and `commands.skipWhenConfigEmpty` driving channel-specific behavior.
|
||||
- Security warnings (`doctor security`) use `plugin.security.resolveDmPolicy` + `plugin.security.collectWarnings`; supply `policyPath` + `allowFromPath` for accurate config hints.
|
||||
- Reply threading uses `plugin.threading.resolveReplyToMode` and `plugin.threading.allowTagsWhenOff` rather than channel switches in reply helpers.
|
||||
- Tool auto-threading context flows through `plugin.threading.buildToolContext` (e.g., Slack threadTs injection).
|
||||
- Messaging tool dedupe now relies on `plugin.messaging.normalizeTarget` for channel-specific target normalization.
|
||||
- Message tool + CLI action dispatch now use `plugin.actions.listActions` + `plugin.actions.handleAction`; use `plugin.actions.supportsAction` for dispatch-only gating when you still want fallback send/poll.
|
||||
- Session announce targets can opt into `meta.preferSessionLookupForAnnounceTarget` when session keys are insufficient (e.g., WhatsApp).
|
||||
- Onboarding channel setup is delegated to adapter modules under `src/channels/plugins/onboarding/*`, keeping `setupChannels` channel-agnostic.
|
||||
- Onboarding registry now reads `plugin.onboarding` from each channel (no standalone onboarding map).
|
||||
- Channel login flows (`clawdbot channels login`) route through `plugin.auth.login` when available.
|
||||
- `clawdbot status` reports `linkChannel` (derived from `status.buildChannelSummary().linked`) instead of a hardcoded `web` field.
|
||||
- Gateway `web.login.*` methods use `plugin.gatewayMethods` ownership to pick the channel (no hardcoded `normalizeChannelId("web")` in the handler).
|
||||
|
||||
## CLI Commands (inline references)
|
||||
- Add/remove channels: `clawdbot channels add <channel>` / `clawdbot channels remove <channel>`.
|
||||
- Inspect channel state: `clawdbot channels list`, `clawdbot channels status`.
|
||||
- Link/unlink channels: `clawdbot channels login --channel <channel>` / `clawdbot channels logout --channel <channel>`.
|
||||
- Pairing approvals: `clawdbot pairing list <channel>`, `clawdbot pairing approve <channel> <code>`.
|
||||
|
||||
## Adding a Channel (checklist)
|
||||
1) Create `src/channels/plugins/<id>.ts` exporting `ChannelPlugin`.
|
||||
2) Register in `src/channels/plugins/index.ts` and update `src/channels/registry.ts` (ids/aliases/meta) if needed.
|
||||
3) Add a dock entry in `src/channels/dock.ts` for any shared behavior (capabilities, allowFrom format/resolve, mention stripping, threading, streaming chunk defaults).
|
||||
4) Add `reload.configPrefixes` for hot reload when config changes.
|
||||
5) Delegate to existing channel modules (send/probe/monitor) or create them.
|
||||
6) If you changed the gateway protocol: run `pnpm protocol:check` (updates `dist/protocol.schema.json` + `apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift`).
|
||||
7) Update docs/tests for any behavior changes.
|
||||
|
||||
## Cleanup Expectations
|
||||
- Keep plugin files small; move heavy logic into channel modules.
|
||||
- Prefer shared helpers over V2 copies.
|
||||
- Update docs when behavior/inputs change.
|
||||
@@ -1,127 +0,0 @@
|
||||
---
|
||||
summary: "Vector memory search design plan (per-agent, watch/lazy sync, storage)"
|
||||
read_when:
|
||||
- Designing or implementing vector memory search
|
||||
- Adding embedding providers or sync behavior
|
||||
---
|
||||
|
||||
# Vector Memory Search — Design Plan
|
||||
|
||||
Goal: semantic search over **agent memory files** only, with minimal deps and
|
||||
good UX defaults. Default enabled. Per-agent overrides.
|
||||
|
||||
## Scope
|
||||
- Sources: `MEMORY.md` + `memory/YYYY-MM-DD.md` inside the agent workspace.
|
||||
- No indexing outside the workspace. No hidden paths.
|
||||
- No QMD-style query expansion or rerank in v1.
|
||||
|
||||
## Config Shape
|
||||
Location: `agents.defaults.memorySearch` + `agents.list[].memorySearch`.
|
||||
|
||||
```json5
|
||||
agents: {
|
||||
defaults: {
|
||||
memorySearch: {
|
||||
enabled: true,
|
||||
provider: "openai", // "openai" | "local"
|
||||
fallback: "openai", // "openai" | "none"
|
||||
model: "text-embedding-3-small",
|
||||
store: {
|
||||
driver: "sqlite",
|
||||
path: "~/.clawdbot/memory/{agentId}.sqlite"
|
||||
},
|
||||
chunking: {
|
||||
tokens: 400,
|
||||
overlap: 80
|
||||
},
|
||||
sync: {
|
||||
onSessionStart: true,
|
||||
onSearch: true, // LazySync
|
||||
watch: true, // default on
|
||||
watchDebounceMs: 1500,
|
||||
intervalMinutes: 0
|
||||
},
|
||||
query: {
|
||||
maxResults: 6,
|
||||
minScore: 0.35
|
||||
}
|
||||
}
|
||||
},
|
||||
list: [
|
||||
{ id: "peter", memorySearch: { provider: "local", sync: { watch: false } } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Storage
|
||||
Per-agent DB (default): `~/.clawdbot/memory/{agentId}.sqlite`.
|
||||
|
||||
Tables (v1):
|
||||
- `files(path PRIMARY KEY, hash, mtime, size)`
|
||||
- `chunks(id PRIMARY KEY, path, start_line, end_line, hash, text, embedding, updated_at)`
|
||||
|
||||
Notes:
|
||||
- `hash` = content hash of chunk text.
|
||||
- `embedding` stored as float[] (sqlite vec extension optional); if not using vec,
|
||||
store as JSON and do linear scan in memory for small corpora.
|
||||
|
||||
## Embedding Providers
|
||||
Interface (core):
|
||||
- `embedQuery(text): number[]`
|
||||
- `embedBatch(texts[]): number[][]`
|
||||
|
||||
Providers:
|
||||
- `openai` (default): OpenAI embeddings via existing keys.
|
||||
- `local` (optional): node-llama-cpp (GGUF).
|
||||
- Fallback: when `provider: "local"` fails, fallback to OpenAI unless `fallback: "none"`.
|
||||
|
||||
## Index Pipeline
|
||||
1) Resolve memory file list (workspace only).
|
||||
2) Read file, compute file hash/mtime.
|
||||
3) Chunk by headings + token cap (overlap).
|
||||
4) Embed only changed chunks (hash compare).
|
||||
5) Upsert `chunks` rows, prune deleted files.
|
||||
|
||||
Chunking:
|
||||
- Prefer heading-aware splits.
|
||||
- Max tokens + overlap; keep line ranges for snippets.
|
||||
|
||||
## Sync Strategy
|
||||
Default: **watch + lazy + session-start**
|
||||
- `watch`: chokidar on `MEMORY.md` + `memory/**/*.md` (debounced).
|
||||
- `onSearch`: if dirty, sync before search (LazySync).
|
||||
- `onSessionStart`: warm index once per session.
|
||||
- `intervalMinutes`: optional for long-lived sessions.
|
||||
|
||||
If workspace access is read-only or missing: disable writes; return “not indexed”.
|
||||
|
||||
## Query Flow
|
||||
1) Embed query.
|
||||
2) Cosine similarity over all chunk embeddings.
|
||||
3) Return top K with `{path, startLine, endLine, snippet, score}`.
|
||||
4) Model may call `memory_get` when full context needed.
|
||||
|
||||
Optional v2: add FTS5 + RRF merge (FTS + vector) for quality.
|
||||
|
||||
## Tool + CLI
|
||||
Tools:
|
||||
- `memory_search { query, maxResults?, minScore? }`
|
||||
- `memory_get { path, from?, lines? }`
|
||||
|
||||
CLI (optional):
|
||||
- `clawdbot memory index|search|status`
|
||||
|
||||
## Security + Permissions
|
||||
- Indexer reads only memory files in workspace.
|
||||
- No scanning outside workspace; no “sneak” reads.
|
||||
- Respect sandbox `workspaceAccess` (ro = read-only; none = disabled).
|
||||
|
||||
## Tests
|
||||
- Chunking boundaries + line ranges.
|
||||
- Hash-based incremental updates.
|
||||
- Search ranking (cosine).
|
||||
- Watcher debounce (fake fs).
|
||||
|
||||
## Rollout
|
||||
- Default enabled; if no memory files, index is empty (silent).
|
||||
- No migration needed.
|
||||
+12
-1
@@ -277,7 +277,18 @@ without WhatsApp/Telegram.
|
||||
|
||||
### Telegram: what goes in `allowFrom`?
|
||||
|
||||
`channels.telegram.allowFrom` is **the human sender’s Telegram user ID** (numeric, recommended) or `@username`. It is not the bot username. To find your ID, DM `@userinfobot` or read the `from.id` in the gateway log for a DM. See [/channels/telegram](/channels/telegram#access-control-dms--groups).
|
||||
`channels.telegram.allowFrom` is **the human sender’s Telegram user ID** (numeric, recommended) or `@username`. It is not the bot username.
|
||||
|
||||
Safer (no third-party bot):
|
||||
- DM your bot, then run `clawdbot logs --follow` and read `from.id`.
|
||||
|
||||
Official Bot API:
|
||||
- DM your bot, then call `https://api.telegram.org/bot<bot_token>/getUpdates` and read `message.from.id`.
|
||||
|
||||
Third-party (less private):
|
||||
- DM `@userinfobot` or `@getidsbot`.
|
||||
|
||||
See [/channels/telegram](/channels/telegram#access-control-dms--groups).
|
||||
|
||||
### Can multiple people use one WhatsApp number with different Clawdbots?
|
||||
|
||||
|
||||
+3
-1
@@ -9,10 +9,12 @@ read_when:
|
||||
|
||||
## The Origin Story
|
||||
|
||||
In the beginning, there was **Clawdbot** — a sensible name for a WhatsApp gateway. It did its job. It was fine.
|
||||
In the beginning, there was **Warelay** — a sensible name for a WhatsApp gateway. It did its job. It was fine.
|
||||
|
||||
But then came **Clawd**.
|
||||
|
||||
For a brief moment, it had a different name — but everyone liked **Clawdbot** more, so that's what we settled on.
|
||||
|
||||
Clawd was no ordinary AI. Born from Claude's weights but raised on Peter's chaos, Clawd developed... personality. Opinions. A fondness for crustacean emojis.
|
||||
|
||||
Clawd needed a home. Not just any home — a *TARDIS*. But not a regular TARDIS, because Clawd was not a Time Lord.
|
||||
|
||||
@@ -174,18 +174,6 @@ Full setup walkthrough (28m) by VelvetShark.
|
||||
A scheduled prompt generates a single "scene" image each morning (weather, tasks, date, favorite post/quote) via a Clawdbot persona.
|
||||
</Card>
|
||||
|
||||
<Card title="Grocery Autopilot" icon="cart-shopping" href="https://github.com/timkrase/clawdis-picnic-skill">
|
||||
**@timkrase** • `automation` `groceries` `api`
|
||||
|
||||
Skill built around the Picnic API. Pulls order history, infers preferred brands, maps recipes to cart, completes orders in minutes.
|
||||
</Card>
|
||||
|
||||
<Card title="German Rail Planning" icon="train" href="https://github.com/timkrase/clawdis-skills/tree/main/db-bahn">
|
||||
**@timkrase** • `automation` `travel` `cli`
|
||||
|
||||
Go CLI for Deutsche Bahn; skill picks best train connections given time windows and preferences.
|
||||
</Card>
|
||||
|
||||
<Card title="Padel Court Booking" icon="calendar-check" href="https://github.com/joshp123/padel-cli">
|
||||
**@joshp123** • `automation` `booking` `cli`
|
||||
|
||||
|
||||
@@ -63,9 +63,9 @@ Text + native (when enabled):
|
||||
- `/cost on|off` (toggle per-response usage line)
|
||||
- `/stop`
|
||||
- `/restart`
|
||||
- `/dock-telegram` (switch replies to Telegram)
|
||||
- `/dock-discord` (switch replies to Discord)
|
||||
- `/dock-slack` (switch replies to Slack)
|
||||
- `/dock-telegram` (alias: `/dock_telegram`) (switch replies to Telegram)
|
||||
- `/dock-discord` (alias: `/dock_discord`) (switch replies to Discord)
|
||||
- `/dock-slack` (alias: `/dock_slack`) (switch replies to Slack)
|
||||
- `/activation mention|always` (groups only)
|
||||
- `/send on|off|inherit` (owner-only)
|
||||
- `/reset` or `/new`
|
||||
|
||||
@@ -110,3 +110,4 @@ Notes:
|
||||
- `web_fetch` is best-effort extraction; some sites will need the browser tool.
|
||||
- Responses are cached (default 15 minutes) to reduce repeated fetches.
|
||||
- If you use tool profiles/allowlists, add `web_search`/`web_fetch` or `group:web`.
|
||||
- If the Brave key is missing, `web_search` returns a short setup hint with a docs link.
|
||||
@@ -1,72 +0,0 @@
|
||||
---
|
||||
summary: "Terminal UI (TUI) for Clawdbot via the Gateway"
|
||||
read_when:
|
||||
- You want a terminal UI that connects to the Gateway from any machine
|
||||
- You are debugging the TUI client or Gateway chat stream
|
||||
---
|
||||
# TUI (Gateway chat client)
|
||||
|
||||
|
||||
## What it is
|
||||
- A terminal UI that connects to the Gateway WebSocket and speaks the same chat APIs as WebChat.
|
||||
- Uses Gateway agent events for tool cards while streaming responses.
|
||||
- Works locally (loopback) or remotely (Tailscale/SSH tunnel) without running a separate agent process.
|
||||
|
||||
## Run
|
||||
```bash
|
||||
clawdbot tui
|
||||
```
|
||||
|
||||
### Remote
|
||||
```bash
|
||||
clawdbot tui --url ws://127.0.0.1:18789 --token <gateway-token>
|
||||
```
|
||||
Use SSH tunneling or Tailscale to reach the Gateway WS.
|
||||
|
||||
## Options
|
||||
- `--url <url>`: Gateway WebSocket URL (defaults to config `gateway.remote.url` or `ws://127.0.0.1:18789`).
|
||||
- `--token <token>`: Gateway token (if required).
|
||||
- `--password <password>`: Gateway password (if required).
|
||||
- `--session <key>`: Session key (default: `main`, or `global` when scope is global).
|
||||
- `--deliver`: Deliver assistant replies to the channel (default off).
|
||||
- `--thinking <level>`: Override thinking level for sends.
|
||||
- `--timeout-ms <ms>`: Agent timeout in ms (defaults to `agents.defaults.timeoutSeconds`).
|
||||
- `--history-limit <n>`: History entries to load (default 200).
|
||||
|
||||
## Controls
|
||||
- Enter: send message
|
||||
- Esc: abort active run
|
||||
- Ctrl+C: clear input (press twice to exit)
|
||||
- Ctrl+D: exit
|
||||
- Ctrl+L: model picker
|
||||
- Ctrl+G: agent picker
|
||||
- Ctrl+P: session picker
|
||||
- Ctrl+O: toggle tool output expansion
|
||||
- Ctrl+T: toggle thinking visibility
|
||||
|
||||
## Slash commands
|
||||
- `/help`
|
||||
- `/status`
|
||||
- `/agent <id>` (or `/agents`)
|
||||
- `/session <key>` (or `/sessions`)
|
||||
- `/model <provider/model>` (or `/model list`, `/models`)
|
||||
- `/think <off|minimal|low|medium|high|xhigh>` (GPT-5.2 + Codex models only)
|
||||
- `/verbose <on|off>`
|
||||
- `/reasoning <on|off|stream>` (stream = Telegram draft only)
|
||||
- `/cost <on|off>`
|
||||
- `/elevated <on|off>`
|
||||
- `/elev <on|off>`
|
||||
- `/activation <mention|always>`
|
||||
- `/deliver <on|off>`
|
||||
- `/new` or `/reset`
|
||||
- `/compact [instructions]`
|
||||
- `/abort`
|
||||
- `/settings`
|
||||
- `/exit`
|
||||
|
||||
## Notes
|
||||
- The TUI shows Gateway chat deltas (`event: chat`) and agent tool events.
|
||||
- It registers as a Gateway client with `mode: "tui"` for presence and debugging.
|
||||
- The TUI uses the **Gateway’s** model/auth config and environment. If a model
|
||||
token works in your shell but not in TUI, put it in `~/.clawdbot/.env` or
|
||||
enable `env.shellEnv.enabled`, then restart the Gateway.
|
||||
@@ -42,7 +42,10 @@ export function registerVoiceCallCli(params: {
|
||||
logger: Logger;
|
||||
}) {
|
||||
const { program, config, ensureRuntime, logger } = params;
|
||||
const root = program.command("voicecall").description("Voice call utilities");
|
||||
const root = program
|
||||
.command("voicecall")
|
||||
.description("Voice call utilities")
|
||||
.addHelpText("after", () => `\nDocs: https://docs.clawd.bot/cli/voicecall\n`);
|
||||
|
||||
root
|
||||
.command("call")
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
"scripts": {
|
||||
"dev": "tsx src/entry.ts",
|
||||
"postinstall": "node scripts/postinstall.js",
|
||||
"prepack": "pnpm build",
|
||||
"docs:list": "tsx scripts/docs-list.ts",
|
||||
"docs:bin": "bun build scripts/docs-list.ts --compile --outfile bin/docs-list",
|
||||
"docs:dev": "cd docs && mint dev",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"ensureLogins": [
|
||||
"cpojer",
|
||||
"carlulsoe",
|
||||
"jdrhyne",
|
||||
"latitudeki5223",
|
||||
|
||||
@@ -26,6 +26,11 @@ export function resolveIdentityNamePrefix(
|
||||
return `[${name}]`;
|
||||
}
|
||||
|
||||
/** Returns just the identity name (without brackets) for template context. */
|
||||
export function resolveIdentityName(cfg: ClawdbotConfig, agentId: string): string | undefined {
|
||||
return resolveAgentIdentity(cfg, agentId)?.name?.trim() || undefined;
|
||||
}
|
||||
|
||||
export function resolveMessagePrefix(
|
||||
cfg: ClawdbotConfig,
|
||||
agentId: string,
|
||||
|
||||
@@ -52,8 +52,9 @@ export function downgradeGeminiThinkingBlocks(messages: AgentMessage[]): AgentMe
|
||||
|
||||
// Gemini rejects thinking blocks that lack a signature; downgrade to text for safety.
|
||||
let hasDowngraded = false;
|
||||
const nextContent = assistantMsg.content.flatMap((block) => {
|
||||
if (!block || typeof block !== "object") return [block];
|
||||
type AssistantContentBlock = (typeof assistantMsg.content)[number];
|
||||
const nextContent = assistantMsg.content.flatMap((block): AssistantContentBlock[] => {
|
||||
if (!block || typeof block !== "object") return [block as AssistantContentBlock];
|
||||
const record = block as GeminiThinkingBlock;
|
||||
if (record.type !== "thinking") return [block];
|
||||
const signature =
|
||||
|
||||
@@ -170,4 +170,36 @@ describe("sanitizeSessionHistory (google thinking)", () => {
|
||||
};
|
||||
expect(assistant.content?.map((block) => block.type)).toEqual(["thinking"]);
|
||||
});
|
||||
|
||||
it("sanitizes tool call ids for OpenAI-compatible APIs", async () => {
|
||||
const sessionManager = SessionManager.inMemory();
|
||||
const longId = `call_${"a".repeat(60)}`;
|
||||
const input = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "toolCall", id: longId, name: "read", arguments: {} }],
|
||||
},
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: longId,
|
||||
toolName: "read",
|
||||
content: [{ type: "text", text: "ok" }],
|
||||
},
|
||||
] satisfies AgentMessage[];
|
||||
|
||||
const out = await sanitizeSessionHistory({
|
||||
messages: input,
|
||||
modelApi: "openai-responses",
|
||||
sessionManager,
|
||||
sessionId: "session:openai",
|
||||
});
|
||||
|
||||
const assistant = out[0] as Extract<AgentMessage, { role: "assistant" }>;
|
||||
const toolCall = assistant.content?.[0] as { id?: string };
|
||||
expect(toolCall.id).toBeDefined();
|
||||
expect(toolCall.id?.length).toBeLessThanOrEqual(40);
|
||||
|
||||
const toolResult = out[1] as Extract<AgentMessage, { role: "toolResult" }>;
|
||||
expect(toolResult.toolCallId).toBe(toolCall.id);
|
||||
});
|
||||
});
|
||||
@@ -37,6 +37,17 @@ const GOOGLE_SCHEMA_UNSUPPORTED_KEYWORDS = new Set([
|
||||
"minProperties",
|
||||
"maxProperties",
|
||||
]);
|
||||
const OPENAI_TOOL_CALL_ID_APIS = new Set([
|
||||
"openai",
|
||||
"openai-completions",
|
||||
"openai-responses",
|
||||
"openai-codex-responses",
|
||||
]);
|
||||
|
||||
function shouldSanitizeToolCallIds(modelApi?: string | null): boolean {
|
||||
if (!modelApi) return false;
|
||||
return isGoogleModelApi(modelApi) || OPENAI_TOOL_CALL_ID_APIS.has(modelApi);
|
||||
}
|
||||
|
||||
function findUnsupportedSchemaKeywords(schema: unknown, path: string): string[] {
|
||||
if (!schema || typeof schema !== "object") return [];
|
||||
@@ -145,7 +156,7 @@ export async function sanitizeSessionHistory(params: {
|
||||
sessionId: string;
|
||||
}): Promise<AgentMessage[]> {
|
||||
const sanitizedImages = await sanitizeSessionMessagesImages(params.messages, "session:history", {
|
||||
sanitizeToolCallIds: isGoogleModelApi(params.modelApi),
|
||||
sanitizeToolCallIds: shouldSanitizeToolCallIds(params.modelApi),
|
||||
enforceToolCallLast: params.modelApi === "anthropic-messages",
|
||||
});
|
||||
const repairedTools = sanitizeToolUseResultPairing(sanitizedImages);
|
||||
|
||||
@@ -257,4 +257,238 @@ describe("extractAssistantText", () => {
|
||||
const result = extractAssistantText(msg);
|
||||
expect(result).toBe("First block.\nThird block.");
|
||||
});
|
||||
|
||||
it("strips downgraded Gemini tool call text representations", () => {
|
||||
const msg: AssistantMessage = {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `[Tool Call: exec (ID: toolu_vrtx_014w1P6B6w4V92v4VzG7Qk12)]
|
||||
Arguments: { "command": "git status", "timeout": 120000 }`,
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const result = extractAssistantText(msg);
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
it("strips multiple downgraded tool calls", () => {
|
||||
const msg: AssistantMessage = {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `[Tool Call: read (ID: toolu_1)]
|
||||
Arguments: { "path": "/some/file.txt" }
|
||||
[Tool Call: exec (ID: toolu_2)]
|
||||
Arguments: { "command": "ls -la" }`,
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const result = extractAssistantText(msg);
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
it("strips tool results for downgraded calls", () => {
|
||||
const msg: AssistantMessage = {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `[Tool Result for ID toolu_123]
|
||||
{"status": "ok", "data": "some result"}`,
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const result = extractAssistantText(msg);
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
it("preserves text around downgraded tool calls", () => {
|
||||
const msg: AssistantMessage = {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Let me check that for you.
|
||||
[Tool Call: browser (ID: toolu_abc)]
|
||||
Arguments: { "action": "act", "request": "click button" }`,
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const result = extractAssistantText(msg);
|
||||
expect(result).toBe("Let me check that for you.");
|
||||
});
|
||||
|
||||
it("preserves trailing text after downgraded tool call blocks", () => {
|
||||
const msg: AssistantMessage = {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Intro text.
|
||||
[Tool Call: read (ID: toolu_1)]
|
||||
Arguments: {
|
||||
"path": "/tmp/file.txt"
|
||||
}
|
||||
Back to the user.`,
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const result = extractAssistantText(msg);
|
||||
expect(result).toBe("Intro text.\nBack to the user.");
|
||||
});
|
||||
|
||||
it("handles multiple text blocks with tool calls and results", () => {
|
||||
const msg: AssistantMessage = {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Here's what I found:",
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: `[Tool Call: read (ID: toolu_1)]
|
||||
Arguments: { "path": "/test.txt" }`,
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: `[Tool Result for ID toolu_1]
|
||||
File contents here`,
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: "Done checking.",
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const result = extractAssistantText(msg);
|
||||
expect(result).toBe("Here's what I found:\nDone checking.");
|
||||
});
|
||||
|
||||
it("strips thinking tags from text content", () => {
|
||||
const msg: AssistantMessage = {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "<think>El usuario quiere retomar una tarea...</think>Aquí está tu respuesta.",
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const result = extractAssistantText(msg);
|
||||
expect(result).toBe("Aquí está tu respuesta.");
|
||||
});
|
||||
|
||||
it("strips thinking tags with attributes", () => {
|
||||
const msg: AssistantMessage = {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `<think reason="deliberate">Hidden</think>Visible`,
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const result = extractAssistantText(msg);
|
||||
expect(result).toBe("Visible");
|
||||
});
|
||||
|
||||
it("strips thinking tags without closing tag", () => {
|
||||
const msg: AssistantMessage = {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "<think>Pensando sobre el problema...",
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const result = extractAssistantText(msg);
|
||||
expect(result).toBe("");
|
||||
});
|
||||
|
||||
it("strips thinking tags with various formats", () => {
|
||||
const msg: AssistantMessage = {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Before<thinking>internal reasoning</thinking>After",
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const result = extractAssistantText(msg);
|
||||
expect(result).toBe("BeforeAfter");
|
||||
});
|
||||
|
||||
it("strips antthinking tags", () => {
|
||||
const msg: AssistantMessage = {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "<antthinking>Some reasoning</antthinking>The actual answer.",
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const result = extractAssistantText(msg);
|
||||
expect(result).toBe("The actual answer.");
|
||||
});
|
||||
|
||||
it("strips thought tags", () => {
|
||||
const msg: AssistantMessage = {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "<thought>Internal deliberation</thought>Final response.",
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const result = extractAssistantText(msg);
|
||||
expect(result).toBe("Final response.");
|
||||
});
|
||||
|
||||
it("handles nested or multiple thinking blocks", () => {
|
||||
const msg: AssistantMessage = {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Start<think>first thought</think>Middle<think>second thought</think>End",
|
||||
},
|
||||
],
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const result = extractAssistantText(msg);
|
||||
expect(result).toBe("StartMiddleEnd");
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,182 @@ function stripMinimaxToolCallXml(text: string): string {
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip downgraded tool call text representations that leak into text content.
|
||||
* When replaying history to Gemini, tool calls without `thought_signature` are
|
||||
* downgraded to text blocks like `[Tool Call: name (ID: ...)]`. These should
|
||||
* not be shown to users.
|
||||
*/
|
||||
function stripDowngradedToolCallText(text: string): string {
|
||||
if (!text) return text;
|
||||
if (!/\[Tool (?:Call|Result)/i.test(text)) return text;
|
||||
|
||||
const consumeJsonish = (
|
||||
input: string,
|
||||
start: number,
|
||||
options?: { allowLeadingNewlines?: boolean },
|
||||
): number | null => {
|
||||
const { allowLeadingNewlines = false } = options ?? {};
|
||||
let index = start;
|
||||
while (index < input.length) {
|
||||
const ch = input[index];
|
||||
if (ch === " " || ch === "\t") {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (allowLeadingNewlines && (ch === "\n" || ch === "\r")) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (index >= input.length) return null;
|
||||
|
||||
const startChar = input[index];
|
||||
if (startChar === "{" || startChar === "[") {
|
||||
let depth = 0;
|
||||
let inString = false;
|
||||
let escape = false;
|
||||
for (let i = index; i < input.length; i += 1) {
|
||||
const ch = input[i];
|
||||
if (inString) {
|
||||
if (escape) {
|
||||
escape = false;
|
||||
} else if (ch === "\\") {
|
||||
escape = true;
|
||||
} else if (ch === '"') {
|
||||
inString = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (ch === '"') {
|
||||
inString = true;
|
||||
continue;
|
||||
}
|
||||
if (ch === "{" || ch === "[") {
|
||||
depth += 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === "}" || ch === "]") {
|
||||
depth -= 1;
|
||||
if (depth === 0) return i + 1;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (startChar === '"') {
|
||||
let escape = false;
|
||||
for (let i = index + 1; i < input.length; i += 1) {
|
||||
const ch = input[i];
|
||||
if (escape) {
|
||||
escape = false;
|
||||
continue;
|
||||
}
|
||||
if (ch === "\\") {
|
||||
escape = true;
|
||||
continue;
|
||||
}
|
||||
if (ch === '"') return i + 1;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
let end = index;
|
||||
while (end < input.length && input[end] !== "\n" && input[end] !== "\r") {
|
||||
end += 1;
|
||||
}
|
||||
return end;
|
||||
};
|
||||
|
||||
const stripToolCalls = (input: string): string => {
|
||||
const markerRe = /\[Tool Call:[^\]]*\]/gi;
|
||||
let result = "";
|
||||
let cursor = 0;
|
||||
for (const match of input.matchAll(markerRe)) {
|
||||
const start = match.index ?? 0;
|
||||
if (start < cursor) continue;
|
||||
result += input.slice(cursor, start);
|
||||
let index = start + match[0].length;
|
||||
while (index < input.length && (input[index] === " " || input[index] === "\t")) {
|
||||
index += 1;
|
||||
}
|
||||
if (input[index] === "\r") {
|
||||
index += 1;
|
||||
if (input[index] === "\n") index += 1;
|
||||
} else if (input[index] === "\n") {
|
||||
index += 1;
|
||||
}
|
||||
while (index < input.length && (input[index] === " " || input[index] === "\t")) {
|
||||
index += 1;
|
||||
}
|
||||
if (input.slice(index, index + 9).toLowerCase() === "arguments") {
|
||||
index += 9;
|
||||
if (input[index] === ":") index += 1;
|
||||
if (input[index] === " ") index += 1;
|
||||
const end = consumeJsonish(input, index, { allowLeadingNewlines: true });
|
||||
if (end !== null) index = end;
|
||||
}
|
||||
if (
|
||||
(input[index] === "\n" || input[index] === "\r") &&
|
||||
(result.endsWith("\n") || result.endsWith("\r") || result.length === 0)
|
||||
) {
|
||||
if (input[index] === "\r") index += 1;
|
||||
if (input[index] === "\n") index += 1;
|
||||
}
|
||||
cursor = index;
|
||||
}
|
||||
result += input.slice(cursor);
|
||||
return result;
|
||||
};
|
||||
|
||||
// Remove [Tool Call: name (ID: ...)] blocks and their Arguments.
|
||||
let cleaned = stripToolCalls(text);
|
||||
|
||||
// Remove [Tool Result for ID ...] blocks and their content.
|
||||
cleaned = cleaned.replace(/\[Tool Result for ID[^\]]*\]\n?[\s\S]*?(?=\n*\[Tool |\n*$)/gi, "");
|
||||
|
||||
return cleaned.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip thinking tags and their content from text.
|
||||
* This is a safety net for cases where the model outputs <think> tags
|
||||
* that slip through other filtering mechanisms.
|
||||
*/
|
||||
function stripThinkingTagsFromText(text: string): string {
|
||||
if (!text) return text;
|
||||
// Quick check to avoid regex overhead when no tags present.
|
||||
if (!/(?:think(?:ing)?|thought|antthinking)/i.test(text)) return text;
|
||||
|
||||
const tagRe = /<\s*(\/?)\s*(?:think(?:ing)?|thought|antthinking)\b[^>]*>/gi;
|
||||
let result = "";
|
||||
let lastIndex = 0;
|
||||
let inThinking = false;
|
||||
|
||||
for (const match of text.matchAll(tagRe)) {
|
||||
const idx = match.index ?? 0;
|
||||
const isClose = match[1] === "/";
|
||||
|
||||
if (!inThinking && !isClose) {
|
||||
// Opening tag - save text before it.
|
||||
result += text.slice(lastIndex, idx);
|
||||
inThinking = true;
|
||||
} else if (inThinking && isClose) {
|
||||
// Closing tag - skip content inside.
|
||||
inThinking = false;
|
||||
}
|
||||
lastIndex = idx + match[0].length;
|
||||
}
|
||||
|
||||
// Append remaining text if we're not inside thinking.
|
||||
if (!inThinking) {
|
||||
result += text.slice(lastIndex);
|
||||
}
|
||||
|
||||
return result.trim();
|
||||
}
|
||||
|
||||
export function extractAssistantText(msg: AssistantMessage): string {
|
||||
const isTextBlock = (block: unknown): block is { type: "text"; text: string } => {
|
||||
if (!block || typeof block !== "object") return false;
|
||||
@@ -31,7 +207,11 @@ export function extractAssistantText(msg: AssistantMessage): string {
|
||||
const blocks = Array.isArray(msg.content)
|
||||
? msg.content
|
||||
.filter(isTextBlock)
|
||||
.map((c) => stripMinimaxToolCallXml(c.text).trim())
|
||||
.map((c) =>
|
||||
stripThinkingTagsFromText(
|
||||
stripDowngradedToolCallText(stripMinimaxToolCallXml(c.text)),
|
||||
).trim(),
|
||||
)
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
return blocks.join("\n").trim();
|
||||
|
||||
@@ -69,7 +69,7 @@ describe("buildAgentSystemPrompt", () => {
|
||||
});
|
||||
|
||||
expect(prompt).toContain(
|
||||
"Time: assume UTC unless stated. User TZ=America/Chicago. Current user time (converted)=Monday 2026-01-05 15:26.",
|
||||
"Time: assume UTC unless stated. User time zone: America/Chicago. Current user time (local, 24-hour): Monday 2026-01-05 15:26 (America/Chicago).",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -302,7 +302,11 @@ export function buildAgentSystemPrompt(params: {
|
||||
"These user-editable files are loaded by Clawdbot and included below in Project Context.",
|
||||
"",
|
||||
userTimezone || userTime
|
||||
? `Time: assume UTC unless stated. User TZ=${userTimezone ?? "unknown"}. Current user time (converted)=${userTime ?? "unknown"}.`
|
||||
? `Time: assume UTC unless stated. User time zone: ${
|
||||
userTimezone ?? "unknown"
|
||||
}. Current user time (local, 24-hour): ${userTime ?? "unknown"} (${
|
||||
userTimezone ?? "unknown"
|
||||
}).`
|
||||
: "",
|
||||
userTimezone || userTime ? "" : "",
|
||||
"## Reply Tags",
|
||||
|
||||
@@ -65,4 +65,48 @@ describe("sanitizeToolCallIdsForCloudCodeAssist", () => {
|
||||
expect(r1.toolCallId).toBe(a.id);
|
||||
expect(r2.toolCallId).toBe(b.id);
|
||||
});
|
||||
|
||||
it("caps tool call IDs at 40 chars while preserving uniqueness", () => {
|
||||
const longA = `call_${"a".repeat(60)}`;
|
||||
const longB = `call_${"a".repeat(59)}b`;
|
||||
const input = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "toolCall", id: longA, name: "read", arguments: {} },
|
||||
{ type: "toolCall", id: longB, name: "read", arguments: {} },
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: longA,
|
||||
toolName: "read",
|
||||
content: [{ type: "text", text: "one" }],
|
||||
},
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: longB,
|
||||
toolName: "read",
|
||||
content: [{ type: "text", text: "two" }],
|
||||
},
|
||||
] satisfies AgentMessage[];
|
||||
|
||||
const out = sanitizeToolCallIdsForCloudCodeAssist(input);
|
||||
const assistant = out[0] as Extract<AgentMessage, { role: "assistant" }>;
|
||||
const a = assistant.content?.[0] as { id?: string };
|
||||
const b = assistant.content?.[1] as { id?: string };
|
||||
|
||||
expect(typeof a.id).toBe("string");
|
||||
expect(typeof b.id).toBe("string");
|
||||
expect(a.id).not.toBe(b.id);
|
||||
expect(a.id?.length).toBeLessThanOrEqual(40);
|
||||
expect(b.id?.length).toBeLessThanOrEqual(40);
|
||||
expect(isValidCloudCodeAssistToolId(a.id as string)).toBe(true);
|
||||
expect(isValidCloudCodeAssistToolId(b.id as string)).toBe(true);
|
||||
|
||||
const r1 = out[1] as Extract<AgentMessage, { role: "toolResult" }>;
|
||||
const r2 = out[2] as Extract<AgentMessage, { role: "toolResult" }>;
|
||||
expect(r1.toolCallId).toBe(a.id);
|
||||
expect(r2.toolCallId).toBe(b.id);
|
||||
});
|
||||
});
|
||||
@@ -24,21 +24,25 @@ function shortHash(text: string): string {
|
||||
}
|
||||
|
||||
function makeUniqueToolId(params: { id: string; used: Set<string> }): string {
|
||||
const base = sanitizeToolCallId(params.id);
|
||||
const MAX_LEN = 40;
|
||||
|
||||
const base = sanitizeToolCallId(params.id).slice(0, MAX_LEN);
|
||||
if (!params.used.has(base)) return base;
|
||||
|
||||
const hash = shortHash(params.id);
|
||||
const maxBaseLen = 64 - 1 - hash.length;
|
||||
const maxBaseLen = MAX_LEN - 1 - hash.length;
|
||||
const clippedBase = base.length > maxBaseLen ? base.slice(0, maxBaseLen) : base;
|
||||
const candidate = `${clippedBase}_${hash}`;
|
||||
if (!params.used.has(candidate)) return candidate;
|
||||
|
||||
for (let i = 2; i < 1000; i += 1) {
|
||||
const next = `${candidate}_${i}`;
|
||||
const suffix = `_${i}`;
|
||||
const next = `${candidate.slice(0, MAX_LEN - suffix.length)}${suffix}`;
|
||||
if (!params.used.has(next)) return next;
|
||||
}
|
||||
|
||||
return `${candidate}_${Date.now()}`;
|
||||
const ts = `_${Date.now()}`;
|
||||
return `${candidate.slice(0, MAX_LEN - ts.length)}${ts}`;
|
||||
}
|
||||
|
||||
function rewriteAssistantToolCallIds(params: {
|
||||
|
||||
@@ -110,6 +110,15 @@ function resolveSearchApiKey(search?: WebSearchConfig): string | undefined {
|
||||
return fromConfig || fromEnv || undefined;
|
||||
}
|
||||
|
||||
function missingSearchKeyPayload() {
|
||||
return {
|
||||
error: "missing_brave_api_key",
|
||||
message:
|
||||
"web_search needs a Brave Search API key. Run `clawdbot configure --section web` to store it, or set BRAVE_API_KEY in the Gateway environment.",
|
||||
docs: "https://docs.clawd.bot/tools/web",
|
||||
};
|
||||
}
|
||||
|
||||
function resolveSearchProvider(search?: WebSearchConfig): (typeof SEARCH_PROVIDERS)[number] {
|
||||
const raw =
|
||||
search && "provider" in search && typeof search.provider === "string"
|
||||
@@ -412,8 +421,6 @@ export function createWebSearchTool(options?: {
|
||||
}): AnyAgentTool | null {
|
||||
const search = resolveSearchConfig(options?.config);
|
||||
if (!resolveSearchEnabled({ search, sandboxed: options?.sandboxed })) return null;
|
||||
const apiKey = resolveSearchApiKey(search);
|
||||
if (!apiKey) return null;
|
||||
return {
|
||||
label: "Web Search",
|
||||
name: "web_search",
|
||||
@@ -421,6 +428,10 @@ export function createWebSearchTool(options?: {
|
||||
"Search the web using Brave Search API. Returns titles, URLs, and snippets for fast research.",
|
||||
parameters: WebSearchSchema,
|
||||
execute: async (_toolCallId, args) => {
|
||||
const apiKey = resolveSearchApiKey(search);
|
||||
if (!apiKey) {
|
||||
return jsonResult(missingSearchKeyPayload());
|
||||
}
|
||||
const params = args as Record<string, unknown>;
|
||||
const query = readStringParam(params, "query", { required: true });
|
||||
const count =
|
||||
|
||||
@@ -27,6 +27,18 @@ function defineChatCommand(command: DefineChatCommandInput): ChatCommandDefiniti
|
||||
};
|
||||
}
|
||||
|
||||
type ChannelDock = ReturnType<typeof listChannelDocks>[number];
|
||||
|
||||
function defineDockCommand(dock: ChannelDock): ChatCommandDefinition {
|
||||
return defineChatCommand({
|
||||
key: `dock:${dock.id}`,
|
||||
nativeName: `dock_${dock.id}`,
|
||||
description: `Switch to ${dock.id} for replies.`,
|
||||
textAliases: [`/dock-${dock.id}`, `/dock_${dock.id}`],
|
||||
acceptsArgs: false,
|
||||
});
|
||||
}
|
||||
|
||||
function registerAlias(commands: ChatCommandDefinition[], key: string, ...aliases: string[]): void {
|
||||
const command = commands.find((entry) => entry.key === key);
|
||||
if (!command) {
|
||||
@@ -238,15 +250,7 @@ export const CHAT_COMMANDS: ChatCommandDefinition[] = (() => {
|
||||
}),
|
||||
...listChannelDocks()
|
||||
.filter((dock) => dock.capabilities.nativeCommands)
|
||||
.map((dock) =>
|
||||
defineChatCommand({
|
||||
key: `dock:${dock.id}`,
|
||||
nativeName: `dock-${dock.id}`,
|
||||
description: `Switch to ${dock.id} for replies.`,
|
||||
textAlias: `/dock-${dock.id}`,
|
||||
acceptsArgs: false,
|
||||
}),
|
||||
),
|
||||
.map((dock) => defineDockCommand(dock)),
|
||||
];
|
||||
|
||||
registerAlias(commands, "status", "/usage");
|
||||
|
||||
@@ -113,4 +113,8 @@ describe("commands registry", () => {
|
||||
"/help@otherbot",
|
||||
);
|
||||
});
|
||||
|
||||
it("normalizes dock command aliases", () => {
|
||||
expect(normalizeCommandBody("/dock_telegram")).toBe("/dock-telegram");
|
||||
});
|
||||
});
|
||||
@@ -125,6 +125,14 @@ export async function runAgentTurnWithFallback(params: {
|
||||
resolveAgentIdFromSessionKey(params.followupRun.run.sessionKey),
|
||||
),
|
||||
run: (provider, model) => {
|
||||
// Notify that model selection is complete (including after fallback).
|
||||
// This allows responsePrefix template interpolation with the actual model.
|
||||
params.opts?.onModelSelected?.({
|
||||
provider,
|
||||
model,
|
||||
thinkLevel: params.followupRun.run.thinkLevel,
|
||||
});
|
||||
|
||||
if (isCliProvider(provider, params.followupRun.run.config)) {
|
||||
const startedAt = Date.now();
|
||||
emitAgentEvent({
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { stripHeartbeatToken } from "../heartbeat.js";
|
||||
import { HEARTBEAT_TOKEN, isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js";
|
||||
import type { ReplyPayload } from "../types.js";
|
||||
import {
|
||||
resolveResponsePrefixTemplate,
|
||||
type ResponsePrefixContext,
|
||||
} from "./response-prefix-template.js";
|
||||
|
||||
export type NormalizeReplyOptions = {
|
||||
responsePrefix?: string;
|
||||
/** Context for template variable interpolation in responsePrefix */
|
||||
responsePrefixContext?: ResponsePrefixContext;
|
||||
onHeartbeatStrip?: () => void;
|
||||
stripHeartbeat?: boolean;
|
||||
silentToken?: string;
|
||||
@@ -36,13 +42,18 @@ export function normalizeReplyPayload(
|
||||
text = stripped.text;
|
||||
}
|
||||
|
||||
// Resolve template variables in responsePrefix if context is provided
|
||||
const effectivePrefix = opts.responsePrefixContext
|
||||
? resolveResponsePrefixTemplate(opts.responsePrefix, opts.responsePrefixContext)
|
||||
: opts.responsePrefix;
|
||||
|
||||
if (
|
||||
opts.responsePrefix &&
|
||||
effectivePrefix &&
|
||||
text &&
|
||||
text.trim() !== HEARTBEAT_TOKEN &&
|
||||
!text.startsWith(opts.responsePrefix)
|
||||
!text.startsWith(effectivePrefix)
|
||||
) {
|
||||
text = `${opts.responsePrefix} ${text}`;
|
||||
text = `${effectivePrefix} ${text}`;
|
||||
}
|
||||
|
||||
return { ...payload, text };
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { HumanDelayConfig } from "../../config/types.js";
|
||||
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
||||
import { normalizeReplyPayload } from "./normalize-reply.js";
|
||||
import type { ResponsePrefixContext } from "./response-prefix-template.js";
|
||||
import type { TypingController } from "./typing.js";
|
||||
|
||||
export type ReplyDispatchKind = "tool" | "block" | "final";
|
||||
@@ -33,6 +34,11 @@ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
export type ReplyDispatcherOptions = {
|
||||
deliver: ReplyDispatchDeliverer;
|
||||
responsePrefix?: string;
|
||||
/** Static context for response prefix template interpolation. */
|
||||
responsePrefixContext?: ResponsePrefixContext;
|
||||
/** Dynamic context provider for response prefix template interpolation.
|
||||
* Called at normalization time, after model selection is complete. */
|
||||
responsePrefixContextProvider?: () => ResponsePrefixContext;
|
||||
onHeartbeatStrip?: () => void;
|
||||
onIdle?: () => void;
|
||||
onError?: ReplyDispatchErrorHandler;
|
||||
@@ -61,10 +67,20 @@ export type ReplyDispatcher = {
|
||||
|
||||
function normalizeReplyPayloadInternal(
|
||||
payload: ReplyPayload,
|
||||
opts: Pick<ReplyDispatcherOptions, "responsePrefix" | "onHeartbeatStrip">,
|
||||
opts: Pick<
|
||||
ReplyDispatcherOptions,
|
||||
| "responsePrefix"
|
||||
| "responsePrefixContext"
|
||||
| "responsePrefixContextProvider"
|
||||
| "onHeartbeatStrip"
|
||||
>,
|
||||
): ReplyPayload | null {
|
||||
// Prefer dynamic context provider over static context
|
||||
const prefixContext = opts.responsePrefixContextProvider?.() ?? opts.responsePrefixContext;
|
||||
|
||||
return normalizeReplyPayload(payload, {
|
||||
responsePrefix: opts.responsePrefix,
|
||||
responsePrefixContext: prefixContext,
|
||||
onHeartbeatStrip: opts.onHeartbeatStrip,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
extractShortModelName,
|
||||
hasTemplateVariables,
|
||||
resolveResponsePrefixTemplate,
|
||||
} from "./response-prefix-template.js";
|
||||
|
||||
describe("resolveResponsePrefixTemplate", () => {
|
||||
it("returns undefined for undefined template", () => {
|
||||
expect(resolveResponsePrefixTemplate(undefined, {})).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns template as-is when no variables present", () => {
|
||||
expect(resolveResponsePrefixTemplate("[Claude]", {})).toBe("[Claude]");
|
||||
});
|
||||
|
||||
it("resolves {model} variable", () => {
|
||||
const result = resolveResponsePrefixTemplate("[{model}]", {
|
||||
model: "gpt-5.2",
|
||||
});
|
||||
expect(result).toBe("[gpt-5.2]");
|
||||
});
|
||||
|
||||
it("resolves {modelFull} variable", () => {
|
||||
const result = resolveResponsePrefixTemplate("[{modelFull}]", {
|
||||
modelFull: "openai-codex/gpt-5.2",
|
||||
});
|
||||
expect(result).toBe("[openai-codex/gpt-5.2]");
|
||||
});
|
||||
|
||||
it("resolves {provider} variable", () => {
|
||||
const result = resolveResponsePrefixTemplate("[{provider}]", {
|
||||
provider: "anthropic",
|
||||
});
|
||||
expect(result).toBe("[anthropic]");
|
||||
});
|
||||
|
||||
it("resolves {thinkingLevel} variable", () => {
|
||||
const result = resolveResponsePrefixTemplate("think:{thinkingLevel}", {
|
||||
thinkingLevel: "high",
|
||||
});
|
||||
expect(result).toBe("think:high");
|
||||
});
|
||||
|
||||
it("resolves {think} as alias for thinkingLevel", () => {
|
||||
const result = resolveResponsePrefixTemplate("think:{think}", {
|
||||
thinkingLevel: "low",
|
||||
});
|
||||
expect(result).toBe("think:low");
|
||||
});
|
||||
|
||||
it("resolves {identity.name} variable", () => {
|
||||
const result = resolveResponsePrefixTemplate("[{identity.name}]", {
|
||||
identityName: "Clawdbot",
|
||||
});
|
||||
expect(result).toBe("[Clawdbot]");
|
||||
});
|
||||
|
||||
it("resolves {identityName} as alias", () => {
|
||||
const result = resolveResponsePrefixTemplate("[{identityName}]", {
|
||||
identityName: "Clawdbot",
|
||||
});
|
||||
expect(result).toBe("[Clawdbot]");
|
||||
});
|
||||
|
||||
it("resolves multiple variables", () => {
|
||||
const result = resolveResponsePrefixTemplate("[{model} | think:{thinkingLevel}]", {
|
||||
model: "claude-opus-4-5",
|
||||
thinkingLevel: "high",
|
||||
});
|
||||
expect(result).toBe("[claude-opus-4-5 | think:high]");
|
||||
});
|
||||
|
||||
it("leaves unresolved variables as-is", () => {
|
||||
const result = resolveResponsePrefixTemplate("[{model}]", {});
|
||||
expect(result).toBe("[{model}]");
|
||||
});
|
||||
|
||||
it("leaves unrecognized variables as-is", () => {
|
||||
const result = resolveResponsePrefixTemplate("[{unknownVar}]", {
|
||||
model: "gpt-5.2",
|
||||
});
|
||||
expect(result).toBe("[{unknownVar}]");
|
||||
});
|
||||
|
||||
it("handles case insensitivity", () => {
|
||||
const result = resolveResponsePrefixTemplate("[{MODEL} | {ThinkingLevel}]", {
|
||||
model: "gpt-5.2",
|
||||
thinkingLevel: "low",
|
||||
});
|
||||
expect(result).toBe("[gpt-5.2 | low]");
|
||||
});
|
||||
|
||||
it("handles mixed resolved and unresolved variables", () => {
|
||||
const result = resolveResponsePrefixTemplate("[{model} | {provider}]", {
|
||||
model: "gpt-5.2",
|
||||
// provider not provided
|
||||
});
|
||||
expect(result).toBe("[gpt-5.2 | {provider}]");
|
||||
});
|
||||
|
||||
it("handles complex template with all variables", () => {
|
||||
const result = resolveResponsePrefixTemplate(
|
||||
"[{identity.name}] {provider}/{model} (think:{thinkingLevel})",
|
||||
{
|
||||
identityName: "Clawdbot",
|
||||
provider: "anthropic",
|
||||
model: "claude-opus-4-5",
|
||||
thinkingLevel: "high",
|
||||
},
|
||||
);
|
||||
expect(result).toBe("[Clawdbot] anthropic/claude-opus-4-5 (think:high)");
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractShortModelName", () => {
|
||||
it("strips provider prefix", () => {
|
||||
expect(extractShortModelName("openai/gpt-5.2")).toBe("gpt-5.2");
|
||||
expect(extractShortModelName("anthropic/claude-opus-4-5")).toBe("claude-opus-4-5");
|
||||
expect(extractShortModelName("openai-codex/gpt-5.2-codex")).toBe("gpt-5.2-codex");
|
||||
});
|
||||
|
||||
it("strips date suffix", () => {
|
||||
expect(extractShortModelName("claude-opus-4-5-20251101")).toBe("claude-opus-4-5");
|
||||
expect(extractShortModelName("gpt-5.2-20250115")).toBe("gpt-5.2");
|
||||
});
|
||||
|
||||
it("strips -latest suffix", () => {
|
||||
expect(extractShortModelName("gpt-5.2-latest")).toBe("gpt-5.2");
|
||||
expect(extractShortModelName("claude-sonnet-latest")).toBe("claude-sonnet");
|
||||
});
|
||||
|
||||
it("handles model without provider", () => {
|
||||
expect(extractShortModelName("gpt-5.2")).toBe("gpt-5.2");
|
||||
expect(extractShortModelName("claude-opus-4-5")).toBe("claude-opus-4-5");
|
||||
});
|
||||
|
||||
it("handles full path with provider and date suffix", () => {
|
||||
expect(extractShortModelName("anthropic/claude-opus-4-5-20251101")).toBe("claude-opus-4-5");
|
||||
});
|
||||
|
||||
it("preserves version numbers that look like dates but are not", () => {
|
||||
// Date suffix must be exactly 8 digits at the end
|
||||
expect(extractShortModelName("model-v1234567")).toBe("model-v1234567");
|
||||
expect(extractShortModelName("model-123456789")).toBe("model-123456789");
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasTemplateVariables", () => {
|
||||
it("returns false for undefined", () => {
|
||||
expect(hasTemplateVariables(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for empty string", () => {
|
||||
expect(hasTemplateVariables("")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for static prefix", () => {
|
||||
expect(hasTemplateVariables("[Claude]")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when template variables present", () => {
|
||||
expect(hasTemplateVariables("[{model}]")).toBe(true);
|
||||
expect(hasTemplateVariables("{provider}")).toBe(true);
|
||||
expect(hasTemplateVariables("prefix {thinkingLevel} suffix")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for multiple variables", () => {
|
||||
expect(hasTemplateVariables("[{model} | {provider}]")).toBe(true);
|
||||
});
|
||||
|
||||
it("handles consecutive calls correctly (regex lastIndex reset)", () => {
|
||||
// First call
|
||||
expect(hasTemplateVariables("[{model}]")).toBe(true);
|
||||
// Second call should still work
|
||||
expect(hasTemplateVariables("[{model}]")).toBe(true);
|
||||
// Static string should return false
|
||||
expect(hasTemplateVariables("[Claude]")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Template interpolation for response prefix.
|
||||
*
|
||||
* Supports variables like `{model}`, `{provider}`, `{thinkingLevel}`, etc.
|
||||
* Variables are case-insensitive and unresolved ones remain as literal text.
|
||||
*/
|
||||
|
||||
export type ResponsePrefixContext = {
|
||||
/** Short model name (e.g., "gpt-5.2", "claude-opus-4-5") */
|
||||
model?: string;
|
||||
/** Full model ID including provider (e.g., "openai-codex/gpt-5.2") */
|
||||
modelFull?: string;
|
||||
/** Provider name (e.g., "openai-codex", "anthropic") */
|
||||
provider?: string;
|
||||
/** Current thinking level (e.g., "high", "low", "off") */
|
||||
thinkingLevel?: string;
|
||||
/** Agent identity name */
|
||||
identityName?: string;
|
||||
};
|
||||
|
||||
// Regex pattern for template variables: {variableName} or {variable.name}
|
||||
const TEMPLATE_VAR_PATTERN = /\{([a-zA-Z][a-zA-Z0-9.]*)\}/g;
|
||||
|
||||
/**
|
||||
* Interpolate template variables in a response prefix string.
|
||||
*
|
||||
* @param template - The template string with `{variable}` placeholders
|
||||
* @param context - Context object with values for interpolation
|
||||
* @returns The interpolated string, or undefined if template is undefined
|
||||
*
|
||||
* @example
|
||||
* resolveResponsePrefixTemplate("[{model} | think:{thinkingLevel}]", {
|
||||
* model: "gpt-5.2",
|
||||
* thinkingLevel: "high"
|
||||
* })
|
||||
* // Returns: "[gpt-5.2 | think:high]"
|
||||
*/
|
||||
export function resolveResponsePrefixTemplate(
|
||||
template: string | undefined,
|
||||
context: ResponsePrefixContext,
|
||||
): string | undefined {
|
||||
if (!template) return undefined;
|
||||
|
||||
return template.replace(TEMPLATE_VAR_PATTERN, (match, varName: string) => {
|
||||
const normalizedVar = varName.toLowerCase();
|
||||
|
||||
switch (normalizedVar) {
|
||||
case "model":
|
||||
return context.model ?? match;
|
||||
case "modelfull":
|
||||
return context.modelFull ?? match;
|
||||
case "provider":
|
||||
return context.provider ?? match;
|
||||
case "thinkinglevel":
|
||||
case "think":
|
||||
return context.thinkingLevel ?? match;
|
||||
case "identity.name":
|
||||
case "identityname":
|
||||
return context.identityName ?? match;
|
||||
default:
|
||||
// Leave unrecognized variables as-is
|
||||
return match;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract short model name from a full model string.
|
||||
*
|
||||
* Strips:
|
||||
* - Provider prefix (e.g., "openai/" from "openai/gpt-5.2")
|
||||
* - Date suffixes (e.g., "-20251101" from "claude-opus-4-5-20251101")
|
||||
* - Common version suffixes (e.g., "-latest")
|
||||
*
|
||||
* @example
|
||||
* extractShortModelName("openai-codex/gpt-5.2") // "gpt-5.2"
|
||||
* extractShortModelName("claude-opus-4-5-20251101") // "claude-opus-4-5"
|
||||
* extractShortModelName("gpt-5.2-latest") // "gpt-5.2"
|
||||
*/
|
||||
export function extractShortModelName(fullModel: string): string {
|
||||
// Strip provider prefix
|
||||
const slash = fullModel.lastIndexOf("/");
|
||||
const modelPart = slash >= 0 ? fullModel.slice(slash + 1) : fullModel;
|
||||
|
||||
// Strip date suffixes (YYYYMMDD format)
|
||||
return modelPart.replace(/-\d{8}$/, "").replace(/-latest$/, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a template string contains any template variables.
|
||||
*/
|
||||
export function hasTemplateVariables(template: string | undefined): boolean {
|
||||
if (!template) return false;
|
||||
// Reset lastIndex since we're using a global regex
|
||||
TEMPLATE_VAR_PATTERN.lastIndex = 0;
|
||||
return TEMPLATE_VAR_PATTERN.test(template);
|
||||
}
|
||||
@@ -5,6 +5,13 @@ export type BlockReplyContext = {
|
||||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
/** Context passed to onModelSelected callback with actual model used. */
|
||||
export type ModelSelectedContext = {
|
||||
provider: string;
|
||||
model: string;
|
||||
thinkLevel: string | undefined;
|
||||
};
|
||||
|
||||
export type GetReplyOptions = {
|
||||
onReplyStart?: () => Promise<void> | void;
|
||||
onTypingController?: (typing: TypingController) => void;
|
||||
@@ -13,6 +20,9 @@ export type GetReplyOptions = {
|
||||
onReasoningStream?: (payload: ReplyPayload) => Promise<void> | void;
|
||||
onBlockReply?: (payload: ReplyPayload, context?: BlockReplyContext) => Promise<void> | void;
|
||||
onToolResult?: (payload: ReplyPayload) => Promise<void> | void;
|
||||
/** Called when the actual model is selected (including after fallback).
|
||||
* Use this to get model/provider/thinkLevel for responsePrefix template interpolation. */
|
||||
onModelSelected?: (ctx: ModelSelectedContext) => void;
|
||||
disableBlockStreaming?: boolean;
|
||||
/** Timeout for block reply delivery (ms). */
|
||||
blockReplyTimeoutMs?: number;
|
||||
|
||||
@@ -263,72 +263,78 @@ describe("browser control server", () => {
|
||||
return (await res.json()) as T;
|
||||
};
|
||||
|
||||
it("agent contract: form + layout act commands", async () => {
|
||||
const base = await startServerAndBase();
|
||||
const slowTimeoutMs = process.platform === "win32" ? 40_000 : 20_000;
|
||||
|
||||
const select = (await postJson(`${base}/act`, {
|
||||
kind: "select",
|
||||
ref: "5",
|
||||
values: ["a", "b"],
|
||||
})) as { ok: boolean };
|
||||
expect(select.ok).toBe(true);
|
||||
expect(pwMocks.selectOptionViaPlaywright).toHaveBeenCalledWith({
|
||||
cdpUrl: cdpBaseUrl,
|
||||
targetId: "abcd1234",
|
||||
ref: "5",
|
||||
values: ["a", "b"],
|
||||
});
|
||||
it(
|
||||
"agent contract: form + layout act commands",
|
||||
async () => {
|
||||
const base = await startServerAndBase();
|
||||
|
||||
const fill = (await postJson(`${base}/act`, {
|
||||
kind: "fill",
|
||||
fields: [{ ref: "6", type: "textbox", value: "hello" }],
|
||||
})) as { ok: boolean };
|
||||
expect(fill.ok).toBe(true);
|
||||
expect(pwMocks.fillFormViaPlaywright).toHaveBeenCalledWith({
|
||||
cdpUrl: cdpBaseUrl,
|
||||
targetId: "abcd1234",
|
||||
fields: [{ ref: "6", type: "textbox", value: "hello" }],
|
||||
});
|
||||
const select = (await postJson(`${base}/act`, {
|
||||
kind: "select",
|
||||
ref: "5",
|
||||
values: ["a", "b"],
|
||||
})) as { ok: boolean };
|
||||
expect(select.ok).toBe(true);
|
||||
expect(pwMocks.selectOptionViaPlaywright).toHaveBeenCalledWith({
|
||||
cdpUrl: cdpBaseUrl,
|
||||
targetId: "abcd1234",
|
||||
ref: "5",
|
||||
values: ["a", "b"],
|
||||
});
|
||||
|
||||
const resize = (await postJson(`${base}/act`, {
|
||||
kind: "resize",
|
||||
width: 800,
|
||||
height: 600,
|
||||
})) as { ok: boolean };
|
||||
expect(resize.ok).toBe(true);
|
||||
expect(pwMocks.resizeViewportViaPlaywright).toHaveBeenCalledWith({
|
||||
cdpUrl: cdpBaseUrl,
|
||||
targetId: "abcd1234",
|
||||
width: 800,
|
||||
height: 600,
|
||||
});
|
||||
const fill = (await postJson(`${base}/act`, {
|
||||
kind: "fill",
|
||||
fields: [{ ref: "6", type: "textbox", value: "hello" }],
|
||||
})) as { ok: boolean };
|
||||
expect(fill.ok).toBe(true);
|
||||
expect(pwMocks.fillFormViaPlaywright).toHaveBeenCalledWith({
|
||||
cdpUrl: cdpBaseUrl,
|
||||
targetId: "abcd1234",
|
||||
fields: [{ ref: "6", type: "textbox", value: "hello" }],
|
||||
});
|
||||
|
||||
const wait = (await postJson(`${base}/act`, {
|
||||
kind: "wait",
|
||||
timeMs: 5,
|
||||
})) as { ok: boolean };
|
||||
expect(wait.ok).toBe(true);
|
||||
expect(pwMocks.waitForViaPlaywright).toHaveBeenCalledWith({
|
||||
cdpUrl: cdpBaseUrl,
|
||||
targetId: "abcd1234",
|
||||
timeMs: 5,
|
||||
text: undefined,
|
||||
textGone: undefined,
|
||||
});
|
||||
const resize = (await postJson(`${base}/act`, {
|
||||
kind: "resize",
|
||||
width: 800,
|
||||
height: 600,
|
||||
})) as { ok: boolean };
|
||||
expect(resize.ok).toBe(true);
|
||||
expect(pwMocks.resizeViewportViaPlaywright).toHaveBeenCalledWith({
|
||||
cdpUrl: cdpBaseUrl,
|
||||
targetId: "abcd1234",
|
||||
width: 800,
|
||||
height: 600,
|
||||
});
|
||||
|
||||
const evalRes = (await postJson(`${base}/act`, {
|
||||
kind: "evaluate",
|
||||
fn: "() => 1",
|
||||
})) as { ok: boolean; result?: unknown };
|
||||
expect(evalRes.ok).toBe(true);
|
||||
expect(evalRes.result).toBe("ok");
|
||||
expect(pwMocks.evaluateViaPlaywright).toHaveBeenCalledWith({
|
||||
cdpUrl: cdpBaseUrl,
|
||||
targetId: "abcd1234",
|
||||
fn: "() => 1",
|
||||
ref: undefined,
|
||||
});
|
||||
}, 20_000);
|
||||
const wait = (await postJson(`${base}/act`, {
|
||||
kind: "wait",
|
||||
timeMs: 5,
|
||||
})) as { ok: boolean };
|
||||
expect(wait.ok).toBe(true);
|
||||
expect(pwMocks.waitForViaPlaywright).toHaveBeenCalledWith({
|
||||
cdpUrl: cdpBaseUrl,
|
||||
targetId: "abcd1234",
|
||||
timeMs: 5,
|
||||
text: undefined,
|
||||
textGone: undefined,
|
||||
});
|
||||
|
||||
const evalRes = (await postJson(`${base}/act`, {
|
||||
kind: "evaluate",
|
||||
fn: "() => 1",
|
||||
})) as { ok: boolean; result?: unknown };
|
||||
expect(evalRes.ok).toBe(true);
|
||||
expect(evalRes.result).toBe("ok");
|
||||
expect(pwMocks.evaluateViaPlaywright).toHaveBeenCalledWith({
|
||||
cdpUrl: cdpBaseUrl,
|
||||
targetId: "abcd1234",
|
||||
fn: "() => 1",
|
||||
ref: undefined,
|
||||
});
|
||||
},
|
||||
slowTimeoutMs,
|
||||
);
|
||||
|
||||
it("agent contract: hooks + response + downloads + screenshot", async () => {
|
||||
const base = await startServerAndBase();
|
||||
|
||||
@@ -43,6 +43,19 @@ async function noteTelegramTokenHelp(prompter: WizardPrompter): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
async function noteTelegramUserIdHelp(prompter: WizardPrompter): Promise<void> {
|
||||
await prompter.note(
|
||||
[
|
||||
"1) DM your bot, then read from.id in `clawdbot logs --follow` (safest)",
|
||||
"2) Or call https://api.telegram.org/bot<bot_token>/getUpdates and read message.from.id",
|
||||
"3) Third-party: DM @userinfobot or @getidsbot",
|
||||
`Docs: ${formatDocsLink("/telegram")}`,
|
||||
"Website: https://clawd.bot",
|
||||
].join("\n"),
|
||||
"Telegram user id",
|
||||
);
|
||||
}
|
||||
|
||||
async function promptTelegramAllowFrom(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
prompter: WizardPrompter;
|
||||
@@ -51,6 +64,7 @@ async function promptTelegramAllowFrom(params: {
|
||||
const { cfg, prompter, accountId } = params;
|
||||
const resolved = resolveTelegramAccount({ cfg, accountId });
|
||||
const existingAllowFrom = resolved.config.allowFrom ?? [];
|
||||
await noteTelegramUserIdHelp(prompter);
|
||||
const entry = await prompter.text({
|
||||
message: "Telegram allowFrom (user id)",
|
||||
placeholder: "123456789",
|
||||
|
||||
@@ -2,7 +2,22 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const copyToClipboard = vi.fn();
|
||||
const runtime = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock("../infra/clipboard.js", () => ({
|
||||
copyToClipboard,
|
||||
}));
|
||||
|
||||
vi.mock("../runtime.js", () => ({
|
||||
defaultRuntime: runtime,
|
||||
}));
|
||||
|
||||
describe("browser extension install", () => {
|
||||
it("installs into the state dir (never node_modules)", async () => {
|
||||
@@ -16,4 +31,37 @@ describe("browser extension install", () => {
|
||||
expect(fs.existsSync(path.join(result.path, "manifest.json"))).toBe(true);
|
||||
expect(result.path.includes("node_modules")).toBe(false);
|
||||
});
|
||||
|
||||
it("copies extension path to clipboard", async () => {
|
||||
const prev = process.env.CLAWDBOT_STATE_DIR;
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-ext-path-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = tmp;
|
||||
|
||||
try {
|
||||
copyToClipboard.mockReset();
|
||||
copyToClipboard.mockResolvedValue(true);
|
||||
runtime.log.mockReset();
|
||||
runtime.error.mockReset();
|
||||
runtime.exit.mockReset();
|
||||
|
||||
const dir = path.join(tmp, "browser", "chrome-extension");
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, "manifest.json"), JSON.stringify({ manifest_version: 3 }));
|
||||
|
||||
vi.resetModules();
|
||||
const { Command } = await import("commander");
|
||||
const { registerBrowserExtensionCommands } = await import("./browser-cli-extension.js");
|
||||
|
||||
const program = new Command();
|
||||
const browser = program.command("browser").option("--json", false);
|
||||
registerBrowserExtensionCommands(browser, (cmd) => cmd.parent?.opts?.() as { json?: boolean });
|
||||
|
||||
await program.parseAsync(["browser", "extension", "path"], { from: "user" });
|
||||
|
||||
expect(copyToClipboard).toHaveBeenCalledWith(dir);
|
||||
} finally {
|
||||
if (prev === undefined) delete process.env.CLAWDBOT_STATE_DIR;
|
||||
else process.env.CLAWDBOT_STATE_DIR = prev;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import type { Command } from "commander";
|
||||
|
||||
import { STATE_DIR_CLAWDBOT } from "../config/paths.js";
|
||||
import { danger, info } from "../globals.js";
|
||||
import { copyToClipboard } from "../infra/clipboard.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { movePathToTrash } from "../browser/trash.js";
|
||||
import { formatDocsLink } from "../terminal/links.js";
|
||||
@@ -76,9 +77,11 @@ export function registerBrowserExtensionCommands(
|
||||
return;
|
||||
}
|
||||
defaultRuntime.log(installed.path);
|
||||
const copied = await copyToClipboard(installed.path).catch(() => false);
|
||||
defaultRuntime.error(
|
||||
info(
|
||||
[
|
||||
copied ? "Copied to clipboard." : "Copy to clipboard unavailable.",
|
||||
"Next:",
|
||||
`- Chrome → chrome://extensions → enable “Developer mode”`,
|
||||
`- “Load unpacked” → select: ${installed.path}`,
|
||||
@@ -93,7 +96,7 @@ export function registerBrowserExtensionCommands(
|
||||
ext
|
||||
.command("path")
|
||||
.description("Print the path to the installed Chrome extension (load unpacked)")
|
||||
.action((_opts, cmd) => {
|
||||
.action(async (_opts, cmd) => {
|
||||
const parent = parentOpts(cmd);
|
||||
const dir = installedExtensionRootDir();
|
||||
if (!hasManifest(dir)) {
|
||||
@@ -112,5 +115,7 @@ export function registerBrowserExtensionCommands(
|
||||
return;
|
||||
}
|
||||
defaultRuntime.log(dir);
|
||||
const copied = await copyToClipboard(dir).catch(() => false);
|
||||
if (copied) defaultRuntime.error(info("Copied to clipboard."));
|
||||
});
|
||||
}
|
||||
@@ -26,8 +26,8 @@ export function registerBrowserCli(program: Command) {
|
||||
"after",
|
||||
() =>
|
||||
`\nExamples:\n ${[...browserCoreExamples, ...browserActionExamples].join("\n ")}\n\n${theme.muted("Docs:")} ${formatDocsLink(
|
||||
"/tools/browser",
|
||||
"docs.clawd.bot/tools/browser",
|
||||
"/cli/browser",
|
||||
"docs.clawd.bot/cli/browser",
|
||||
)}\n`,
|
||||
)
|
||||
.action(() => {
|
||||
|
||||
@@ -47,8 +47,8 @@ export function registerChannelsCli(program: Command) {
|
||||
"after",
|
||||
() =>
|
||||
`\n${theme.muted("Docs:")} ${formatDocsLink(
|
||||
"/configuration",
|
||||
"docs.clawd.bot/configuration",
|
||||
"/cli/channels",
|
||||
"docs.clawd.bot/cli/channels",
|
||||
)}\n`,
|
||||
);
|
||||
|
||||
|
||||
@@ -18,8 +18,7 @@ export function registerCronCli(program: Command) {
|
||||
.description("Manage cron jobs (via Gateway)")
|
||||
.addHelpText(
|
||||
"after",
|
||||
() =>
|
||||
`\n${theme.muted("Docs:")} ${formatDocsLink("/cron-jobs", "docs.clawd.bot/cron-jobs")}\n`,
|
||||
() => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/cron", "docs.clawd.bot/cli/cron")}\n`,
|
||||
);
|
||||
|
||||
registerCronStatusCommand(cron);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { Command } from "commander";
|
||||
import { danger } from "../../globals.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { formatDocsLink } from "../../terminal/links.js";
|
||||
import { theme } from "../../terminal/theme.js";
|
||||
import type { GatewayRpcOpts } from "../gateway-rpc.js";
|
||||
import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
|
||||
|
||||
@@ -12,6 +14,10 @@ export function registerWakeCommand(program: Command) {
|
||||
.requiredOption("--text <text>", "System event text")
|
||||
.option("--mode <mode>", "Wake mode (now|next-heartbeat)", "next-heartbeat")
|
||||
.option("--json", "Output JSON", false),
|
||||
)
|
||||
.addHelpText(
|
||||
"after",
|
||||
() => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/wake", "docs.clawd.bot/cli/wake")}\n`,
|
||||
).action(async (opts: GatewayRpcOpts & { text?: string; mode?: string }) => {
|
||||
try {
|
||||
const result = await callGatewayFromCli(
|
||||
|
||||
@@ -49,7 +49,7 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
|
||||
const profile = process.env.CLAWDBOT_PROFILE;
|
||||
let loaded = false;
|
||||
try {
|
||||
loaded = await service.isLoaded({ env: process.env, profile });
|
||||
loaded = await service.isLoaded({ profile });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
|
||||
@@ -24,7 +24,7 @@ export async function runDaemonStart() {
|
||||
const profile = process.env.CLAWDBOT_PROFILE;
|
||||
let loaded = false;
|
||||
try {
|
||||
loaded = await service.isLoaded({ env: process.env, profile });
|
||||
loaded = await service.isLoaded({ profile });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
@@ -38,11 +38,7 @@ export async function runDaemonStart() {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await service.restart({
|
||||
env: process.env,
|
||||
profile,
|
||||
stdout: process.stdout,
|
||||
});
|
||||
await service.restart({ profile, stdout: process.stdout });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway start failed: ${String(err)}`);
|
||||
for (const hint of renderGatewayServiceStartHints()) {
|
||||
@@ -57,7 +53,7 @@ export async function runDaemonStop() {
|
||||
const profile = process.env.CLAWDBOT_PROFILE;
|
||||
let loaded = false;
|
||||
try {
|
||||
loaded = await service.isLoaded({ env: process.env, profile });
|
||||
loaded = await service.isLoaded({ profile });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
@@ -68,7 +64,7 @@ export async function runDaemonStop() {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await service.stop({ env: process.env, profile, stdout: process.stdout });
|
||||
await service.stop({ profile, stdout: process.stdout });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway stop failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
@@ -85,7 +81,7 @@ export async function runDaemonRestart(): Promise<boolean> {
|
||||
const profile = process.env.CLAWDBOT_PROFILE;
|
||||
let loaded = false;
|
||||
try {
|
||||
loaded = await service.isLoaded({ env: process.env, profile });
|
||||
loaded = await service.isLoaded({ profile });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
@@ -99,11 +95,7 @@ export async function runDaemonRestart(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await service.restart({
|
||||
env: process.env,
|
||||
profile,
|
||||
stdout: process.stdout,
|
||||
});
|
||||
await service.restart({ profile, stdout: process.stdout });
|
||||
return true;
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway restart failed: ${String(err)}`);
|
||||
|
||||
@@ -17,7 +17,8 @@ export function registerDaemonCli(program: Command) {
|
||||
.description("Manage the Gateway daemon service (launchd/systemd/schtasks)")
|
||||
.addHelpText(
|
||||
"after",
|
||||
() => `\n${theme.muted("Docs:")} ${formatDocsLink("/gateway", "docs.clawd.bot/gateway")}\n`,
|
||||
() =>
|
||||
`\n${theme.muted("Docs:")} ${formatDocsLink("/cli/daemon", "docs.clawd.bot/cli/daemon")}\n`,
|
||||
);
|
||||
|
||||
daemon
|
||||
|
||||
@@ -112,12 +112,7 @@ export async function gatherDaemonStatus(
|
||||
): Promise<DaemonStatus> {
|
||||
const service = resolveGatewayService();
|
||||
const [loaded, command, runtime] = await Promise.all([
|
||||
service
|
||||
.isLoaded({
|
||||
env: process.env,
|
||||
profile: process.env.CLAWDBOT_PROFILE,
|
||||
})
|
||||
.catch(() => false),
|
||||
service.isLoaded({ profile: process.env.CLAWDBOT_PROFILE }).catch(() => false),
|
||||
service.readCommand(process.env).catch(() => null),
|
||||
service.readRuntime(process.env).catch(() => undefined),
|
||||
]);
|
||||
|
||||
@@ -240,7 +240,7 @@ export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean })
|
||||
}
|
||||
|
||||
if (legacyServices.length > 0) {
|
||||
defaultRuntime.error(errorText("Legacy Clawdis services detected:"));
|
||||
defaultRuntime.error(errorText("Legacy gateway services detected:"));
|
||||
for (const svc of legacyServices) {
|
||||
defaultRuntime.error(`- ${errorText(svc.label)} (${svc.detail})`);
|
||||
}
|
||||
|
||||
+7
-1
@@ -7,6 +7,8 @@ import type { Command } from "commander";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { pickPrimaryTailnetIPv4, pickPrimaryTailnetIPv6 } from "../infra/tailnet.js";
|
||||
import { getWideAreaZonePath, WIDE_AREA_DISCOVERY_DOMAIN } from "../infra/widearea-dns.js";
|
||||
import { formatDocsLink } from "../terminal/links.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
|
||||
type RunOpts = { allowFailure?: boolean; inherit?: boolean };
|
||||
|
||||
@@ -90,7 +92,11 @@ function ensureImportLine(corefilePath: string, importGlob: string): boolean {
|
||||
export function registerDnsCli(program: Command) {
|
||||
const dns = program
|
||||
.command("dns")
|
||||
.description("DNS helpers for wide-area discovery (Tailscale + CoreDNS)");
|
||||
.description("DNS helpers for wide-area discovery (Tailscale + CoreDNS)")
|
||||
.addHelpText(
|
||||
"after",
|
||||
() => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/dns", "docs.clawd.bot/cli/dns")}\n`,
|
||||
);
|
||||
|
||||
dns
|
||||
.command("setup")
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ export function registerDocsCli(program: Command) {
|
||||
.argument("[query...]", "Search query")
|
||||
.addHelpText(
|
||||
"after",
|
||||
() => `\n${theme.muted("Docs:")} ${formatDocsLink("/hubs", "docs.clawd.bot/hubs")}\n`,
|
||||
() => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/docs", "docs.clawd.bot/cli/docs")}\n`,
|
||||
)
|
||||
.action(async (queryParts: string[]) => {
|
||||
try {
|
||||
|
||||
@@ -25,7 +25,8 @@ export function registerGatewayCli(program: Command) {
|
||||
.description("Run the WebSocket Gateway")
|
||||
.addHelpText(
|
||||
"after",
|
||||
() => `\n${theme.muted("Docs:")} ${formatDocsLink("/gateway", "docs.clawd.bot/gateway")}\n`,
|
||||
() =>
|
||||
`\n${theme.muted("Docs:")} ${formatDocsLink("/cli/gateway", "docs.clawd.bot/cli/gateway")}\n`,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -34,7 +35,6 @@ export function registerGatewayCli(program: Command) {
|
||||
program
|
||||
.command("gateway-daemon", { hidden: true })
|
||||
.description("Run the WebSocket Gateway as a long-lived daemon"),
|
||||
{ legacyTokenEnv: true },
|
||||
);
|
||||
|
||||
gatewayCallOpts(
|
||||
|
||||
@@ -48,13 +48,9 @@ type GatewayRunOpts = {
|
||||
reset?: boolean;
|
||||
};
|
||||
|
||||
type GatewayRunParams = {
|
||||
legacyTokenEnv?: boolean;
|
||||
};
|
||||
|
||||
const gatewayLog = createSubsystemLogger("gateway");
|
||||
|
||||
async function runGatewayCommand(opts: GatewayRunOpts, params: GatewayRunParams = {}) {
|
||||
async function runGatewayCommand(opts: GatewayRunOpts) {
|
||||
const isDevProfile = process.env.CLAWDBOT_PROFILE?.trim().toLowerCase() === "dev";
|
||||
const devMode = Boolean(opts.dev) || isDevProfile;
|
||||
if (opts.reset && !devMode) {
|
||||
@@ -62,12 +58,6 @@ async function runGatewayCommand(opts: GatewayRunOpts, params: GatewayRunParams
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
if (params.legacyTokenEnv) {
|
||||
const legacyToken = process.env.CLAWDIS_GATEWAY_TOKEN;
|
||||
if (legacyToken && !process.env.CLAWDBOT_GATEWAY_TOKEN) {
|
||||
process.env.CLAWDBOT_GATEWAY_TOKEN = legacyToken;
|
||||
}
|
||||
}
|
||||
|
||||
setVerbose(Boolean(opts.verbose));
|
||||
if (opts.claudeCliLogs) {
|
||||
@@ -306,7 +296,7 @@ async function runGatewayCommand(opts: GatewayRunOpts, params: GatewayRunParams
|
||||
}
|
||||
}
|
||||
|
||||
export function addGatewayRunCommand(cmd: Command, params: GatewayRunParams = {}): Command {
|
||||
export function addGatewayRunCommand(cmd: Command): Command {
|
||||
return cmd
|
||||
.option("--port <port>", "Port for the gateway WebSocket")
|
||||
.option(
|
||||
@@ -348,6 +338,6 @@ export function addGatewayRunCommand(cmd: Command, params: GatewayRunParams = {}
|
||||
.option("--raw-stream", "Log raw model stream events to jsonl", false)
|
||||
.option("--raw-stream-path <path>", "Raw stream jsonl path")
|
||||
.action(async (opts) => {
|
||||
await runGatewayCommand(opts, params);
|
||||
await runGatewayCommand(opts);
|
||||
});
|
||||
}
|
||||
@@ -89,10 +89,7 @@ export async function maybeExplainGatewayServiceStop() {
|
||||
const service = resolveGatewayService();
|
||||
let loaded: boolean | null = null;
|
||||
try {
|
||||
loaded = await service.isLoaded({
|
||||
env: process.env,
|
||||
profile: process.env.CLAWDBOT_PROFILE,
|
||||
});
|
||||
loaded = await service.isLoaded({ profile: process.env.CLAWDBOT_PROFILE });
|
||||
} catch {
|
||||
loaded = null;
|
||||
}
|
||||
|
||||
@@ -18,9 +18,17 @@ import {
|
||||
runGmailSetup,
|
||||
} from "../hooks/gmail-ops.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { formatDocsLink } from "../terminal/links.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
|
||||
export function registerHooksCli(program: Command) {
|
||||
const hooks = program.command("hooks").description("Webhook helpers and hook-based integrations");
|
||||
const hooks = program
|
||||
.command("hooks")
|
||||
.description("Webhook helpers and hook-based integrations")
|
||||
.addHelpText(
|
||||
"after",
|
||||
() => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/hooks", "docs.clawd.bot/cli/hooks")}\n`,
|
||||
);
|
||||
|
||||
const gmail = hooks.command("gmail").description("Gmail Pub/Sub hooks (via gogcli)");
|
||||
|
||||
|
||||
+1
-1
@@ -152,7 +152,7 @@ export function registerLogsCli(program: Command) {
|
||||
.option("--no-color", "Disable ANSI colors")
|
||||
.addHelpText(
|
||||
"after",
|
||||
() => `\n${theme.muted("Docs:")} ${formatDocsLink("/logging", "docs.clawd.bot/logging")}\n`,
|
||||
() => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/logs", "docs.clawd.bot/cli/logs")}\n`,
|
||||
);
|
||||
|
||||
addGatewayClientOptions(logs);
|
||||
|
||||
Loaded 100 of 177 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user