Merge branch 'main' into commands-list-clean
This commit is contained in:
commit
e52a2888cc
11
CHANGELOG.md
11
CHANGELOG.md
@ -24,7 +24,10 @@
|
|||||||
- Auto-reply: block reply ordering fix (duplicate PR superseded by #503). (#483) — thanks @AbhisekBasu1
|
- Auto-reply: block reply ordering fix (duplicate PR superseded by #503). (#483) — thanks @AbhisekBasu1
|
||||||
- Auto-reply: avoid splitting outbound chunks inside parentheses. (#499) — thanks @philipp-spiess
|
- Auto-reply: avoid splitting outbound chunks inside parentheses. (#499) — thanks @philipp-spiess
|
||||||
- Auto-reply: preserve spacing when stripping inline directives. (#539) — thanks @joshp123
|
- Auto-reply: preserve spacing when stripping inline directives. (#539) — thanks @joshp123
|
||||||
|
- Auto-reply: fix /status usage summary filtering for the active provider.
|
||||||
- Status: show provider prefix in /status model display. (#506) — thanks @mcinteerj
|
- Status: show provider prefix in /status model display. (#506) — thanks @mcinteerj
|
||||||
|
- Status: compact /status with session token usage + estimated cost, add `/cost` per-response usage lines (tokens-only for OAuth).
|
||||||
|
- Status: show active auth profile and key snippet in /status.
|
||||||
- macOS: package ClawdbotKit resources and Swift 6.2 compatibility dylib to avoid launch/tool crashes. (#473) — thanks @gupsammy
|
- macOS: package ClawdbotKit resources and Swift 6.2 compatibility dylib to avoid launch/tool crashes. (#473) — thanks @gupsammy
|
||||||
- WhatsApp: group `/model list` output by provider for scannability. (#456) - thanks @mcinteerj
|
- WhatsApp: group `/model list` output by provider for scannability. (#456) - thanks @mcinteerj
|
||||||
- Hooks: allow per-hook model overrides for webhook/Gmail runs (e.g. GPT 5 Mini).
|
- Hooks: allow per-hook model overrides for webhook/Gmail runs (e.g. GPT 5 Mini).
|
||||||
@ -56,7 +59,15 @@
|
|||||||
- Docs: expand parameter descriptions for agent/wake hooks. (#532) — thanks @mcinteerj
|
- Docs: expand parameter descriptions for agent/wake hooks. (#532) — thanks @mcinteerj
|
||||||
- Docs: add community showcase entries from Discord. (#476) — thanks @gupsammy
|
- Docs: add community showcase entries from Discord. (#476) — thanks @gupsammy
|
||||||
- TUI: refresh status bar after think/verbose/reasoning changes. (#519) — thanks @jdrhyne
|
- TUI: refresh status bar after think/verbose/reasoning changes. (#519) — thanks @jdrhyne
|
||||||
|
- Status: show Verbose/Elevated only when enabled.
|
||||||
|
- Status: filter usage summary to the active model provider.
|
||||||
|
- Status: map model providers to usage sources so unrelated usage doesn’t appear.
|
||||||
|
- Commands: allow /elevated off in groups without a mention; keep /elevated on mention-gated.
|
||||||
|
- Commands: keep multi-directive messages from clearing directive handling.
|
||||||
|
- Commands: warn when /elevated runs in direct (unsandboxed) runtime.
|
||||||
- Commands: treat mention-bypassed group command messages as mentioned so elevated directives respond.
|
- Commands: treat mention-bypassed group command messages as mentioned so elevated directives respond.
|
||||||
|
- Commands: return /status in directive-only multi-line messages.
|
||||||
|
- Agent system prompt: add messaging guidance for reply routing and cross-session sends. (#526) — thanks @neist
|
||||||
|
|
||||||
## 2026.1.8
|
## 2026.1.8
|
||||||
|
|
||||||
|
|||||||
17
README.md
17
README.md
@ -240,11 +240,12 @@ ClawdHub is a minimal skill registry. With ClawdHub enabled, the agent can searc
|
|||||||
|
|
||||||
Send these in WhatsApp/Telegram/Slack/WebChat (group commands are owner-only):
|
Send these in WhatsApp/Telegram/Slack/WebChat (group commands are owner-only):
|
||||||
|
|
||||||
- `/status` — health + session info (group shows activation mode)
|
- `/status` — compact session status (model + tokens, cost when available)
|
||||||
- `/new` or `/reset` — reset the session
|
- `/new` or `/reset` — reset the session
|
||||||
- `/compact` — compact session context (summary)
|
- `/compact` — compact session context (summary)
|
||||||
- `/think <level>` — off|minimal|low|medium|high
|
- `/think <level>` — off|minimal|low|medium|high
|
||||||
- `/verbose on|off`
|
- `/verbose on|off`
|
||||||
|
- `/cost on|off` — append per-response token/cost usage lines
|
||||||
- `/restart` — restart the gateway (owner-only in groups)
|
- `/restart` — restart the gateway (owner-only in groups)
|
||||||
- `/activation mention|always` — group activation toggle (groups only)
|
- `/activation mention|always` — group activation toggle (groups only)
|
||||||
|
|
||||||
@ -459,11 +460,11 @@ Thanks to all clawtributors:
|
|||||||
<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/omniwired"><img src="https://avatars.githubusercontent.com/u/322761?v=4&s=48" width="48" height="48" alt="Eng. Juan Combetto" title="Eng. Juan Combetto"/></a> <a href="https://github.com/mbelinky"><img src="https://avatars.githubusercontent.com/u/132747814?v=4&s=48" width="48" height="48" alt="Mariano Belinky" title="Mariano Belinky"/></a> <a href="https://github.com/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/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/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/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/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/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/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/omniwired"><img src="https://avatars.githubusercontent.com/u/322761?v=4&s=48" width="48" height="48" alt="Eng. Juan Combetto" title="Eng. Juan Combetto"/></a> <a href="https://github.com/mbelinky"><img src="https://avatars.githubusercontent.com/u/132747814?v=4&s=48" width="48" height="48" alt="Mariano Belinky" title="Mariano Belinky"/></a> <a href="https://github.com/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></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/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/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/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/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/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/search?q=sheeek"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="sheeek" title="sheeek"/></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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/RandyVentures"><img src="https://avatars.githubusercontent.com/u/149904821?v=4&s=48" width="48" height="48" alt="RandyVentures" title="RandyVentures"/></a> <a href="https://github.com/search?q=Yurii%20Chukhlib"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Yurii Chukhlib" title="Yurii Chukhlib"/></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/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/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/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/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/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/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/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/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></a> <a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a> <a href="https://github.com/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/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/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/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/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/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/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a> <a href="https://github.com/Syhids"><img src="https://avatars.githubusercontent.com/u/671202?v=4&s=48" width="48" height="48" alt="Syhids" title="Syhids"/></a> <a href="https://github.com/erik-agens"><img src="https://avatars.githubusercontent.com/u/80908960?v=4&s=48" width="48" height="48" alt="erik-agens" title="erik-agens"/></a> <a href="https://github.com/fcatuhe"><img src="https://avatars.githubusercontent.com/u/17382215?v=4&s=48" width="48" height="48" alt="fcatuhe" title="fcatuhe"/></a> <a href="https://github.com/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/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/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/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/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/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/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/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/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/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/jdrhyne"><img src="https://avatars.githubusercontent.com/u/7828464?v=4&s=48" width="48" height="48" alt="Jonathan D. Rhyne (DJ-D)" title="Jonathan D. Rhyne (DJ-D)"/></a> <a href="https://github.com/jverdi"><img src="https://avatars.githubusercontent.com/u/345050?v=4&s=48" width="48" height="48" alt="jverdi" title="jverdi"/></a> <a href="https://github.com/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/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/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/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/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/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/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/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/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/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/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/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/jdrhyne"><img src="https://avatars.githubusercontent.com/u/7828464?v=4&s=48" width="48" height="48" alt="Jonathan D. Rhyne (DJ-D)" title="Jonathan D. Rhyne (DJ-D)"/></a> <a href="https://github.com/search?q=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/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/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/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/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/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/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=Kit"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kit" title="Kit"/></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/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/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/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/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/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/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/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=Kit"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Kit" title="Kit"/></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/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/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/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/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/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/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/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/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=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></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/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/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/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/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/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=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></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=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/search?q=Tobias%20Bischoff"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></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/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/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/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/search?q=Tobias%20Bischoff"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Tobias Bischoff" title="Tobias Bischoff"/></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>
|
</p>
|
||||||
|
|||||||
@ -664,19 +664,22 @@ public struct SessionsListParams: Codable, Sendable {
|
|||||||
public let includeglobal: Bool?
|
public let includeglobal: Bool?
|
||||||
public let includeunknown: Bool?
|
public let includeunknown: Bool?
|
||||||
public let spawnedby: String?
|
public let spawnedby: String?
|
||||||
|
public let agentid: String?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
limit: Int?,
|
limit: Int?,
|
||||||
activeminutes: Int?,
|
activeminutes: Int?,
|
||||||
includeglobal: Bool?,
|
includeglobal: Bool?,
|
||||||
includeunknown: Bool?,
|
includeunknown: Bool?,
|
||||||
spawnedby: String?
|
spawnedby: String?,
|
||||||
|
agentid: String?
|
||||||
) {
|
) {
|
||||||
self.limit = limit
|
self.limit = limit
|
||||||
self.activeminutes = activeminutes
|
self.activeminutes = activeminutes
|
||||||
self.includeglobal = includeglobal
|
self.includeglobal = includeglobal
|
||||||
self.includeunknown = includeunknown
|
self.includeunknown = includeunknown
|
||||||
self.spawnedby = spawnedby
|
self.spawnedby = spawnedby
|
||||||
|
self.agentid = agentid
|
||||||
}
|
}
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case limit
|
case limit
|
||||||
@ -684,6 +687,7 @@ public struct SessionsListParams: Codable, Sendable {
|
|||||||
case includeglobal = "includeGlobal"
|
case includeglobal = "includeGlobal"
|
||||||
case includeunknown = "includeUnknown"
|
case includeunknown = "includeUnknown"
|
||||||
case spawnedby = "spawnedBy"
|
case spawnedby = "spawnedBy"
|
||||||
|
case agentid = "agentId"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -692,6 +696,7 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||||||
public let thinkinglevel: AnyCodable?
|
public let thinkinglevel: AnyCodable?
|
||||||
public let verboselevel: AnyCodable?
|
public let verboselevel: AnyCodable?
|
||||||
public let reasoninglevel: AnyCodable?
|
public let reasoninglevel: AnyCodable?
|
||||||
|
public let responseusage: AnyCodable?
|
||||||
public let elevatedlevel: AnyCodable?
|
public let elevatedlevel: AnyCodable?
|
||||||
public let model: AnyCodable?
|
public let model: AnyCodable?
|
||||||
public let spawnedby: AnyCodable?
|
public let spawnedby: AnyCodable?
|
||||||
@ -703,6 +708,7 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||||||
thinkinglevel: AnyCodable?,
|
thinkinglevel: AnyCodable?,
|
||||||
verboselevel: AnyCodable?,
|
verboselevel: AnyCodable?,
|
||||||
reasoninglevel: AnyCodable?,
|
reasoninglevel: AnyCodable?,
|
||||||
|
responseusage: AnyCodable?,
|
||||||
elevatedlevel: AnyCodable?,
|
elevatedlevel: AnyCodable?,
|
||||||
model: AnyCodable?,
|
model: AnyCodable?,
|
||||||
spawnedby: AnyCodable?,
|
spawnedby: AnyCodable?,
|
||||||
@ -713,6 +719,7 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||||||
self.thinkinglevel = thinkinglevel
|
self.thinkinglevel = thinkinglevel
|
||||||
self.verboselevel = verboselevel
|
self.verboselevel = verboselevel
|
||||||
self.reasoninglevel = reasoninglevel
|
self.reasoninglevel = reasoninglevel
|
||||||
|
self.responseusage = responseusage
|
||||||
self.elevatedlevel = elevatedlevel
|
self.elevatedlevel = elevatedlevel
|
||||||
self.model = model
|
self.model = model
|
||||||
self.spawnedby = spawnedby
|
self.spawnedby = spawnedby
|
||||||
@ -724,6 +731,7 @@ public struct SessionsPatchParams: Codable, Sendable {
|
|||||||
case thinkinglevel = "thinkingLevel"
|
case thinkinglevel = "thinkingLevel"
|
||||||
case verboselevel = "verboseLevel"
|
case verboselevel = "verboseLevel"
|
||||||
case reasoninglevel = "reasoningLevel"
|
case reasoninglevel = "reasoningLevel"
|
||||||
|
case responseusage = "responseUsage"
|
||||||
case elevatedlevel = "elevatedLevel"
|
case elevatedlevel = "elevatedLevel"
|
||||||
case model
|
case model
|
||||||
case spawnedby = "spawnedBy"
|
case spawnedby = "spawnedBy"
|
||||||
@ -1100,6 +1108,51 @@ public struct WebLoginWaitParams: Codable, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct AgentSummary: Codable, Sendable {
|
||||||
|
public let id: String
|
||||||
|
public let name: String?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
id: String,
|
||||||
|
name: String?
|
||||||
|
) {
|
||||||
|
self.id = id
|
||||||
|
self.name = name
|
||||||
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
|
case name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct AgentsListParams: Codable, Sendable {
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct AgentsListResult: Codable, Sendable {
|
||||||
|
public let defaultid: String
|
||||||
|
public let mainkey: String
|
||||||
|
public let scope: AnyCodable
|
||||||
|
public let agents: [AgentSummary]
|
||||||
|
|
||||||
|
public init(
|
||||||
|
defaultid: String,
|
||||||
|
mainkey: String,
|
||||||
|
scope: AnyCodable,
|
||||||
|
agents: [AgentSummary]
|
||||||
|
) {
|
||||||
|
self.defaultid = defaultid
|
||||||
|
self.mainkey = mainkey
|
||||||
|
self.scope = scope
|
||||||
|
self.agents = agents
|
||||||
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case defaultid = "defaultId"
|
||||||
|
case mainkey = "mainKey"
|
||||||
|
case scope
|
||||||
|
case agents
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct ModelChoice: Codable, Sendable {
|
public struct ModelChoice: Codable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let name: String
|
public let name: String
|
||||||
|
|||||||
@ -16,6 +16,8 @@ sessions.
|
|||||||
resolve relative paths against the workspace, but absolute paths can still reach
|
resolve relative paths against the workspace, but absolute paths can still reach
|
||||||
elsewhere on the host unless sandboxing is enabled. If you need isolation, use
|
elsewhere on the host unless sandboxing is enabled. If you need isolation, use
|
||||||
[`agent.sandbox`](/gateway/sandboxing) (and/or per‑agent sandbox config).
|
[`agent.sandbox`](/gateway/sandboxing) (and/or per‑agent sandbox config).
|
||||||
|
When sandboxing is enabled and `workspaceAccess` is not `"rw"`, tools operate
|
||||||
|
inside a sandbox workspace under `~/.clawdbot/sandboxes`, not your host workspace.
|
||||||
|
|
||||||
## Default location
|
## Default location
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,8 @@ read_when:
|
|||||||
- No estimated costs; only the provider-reported windows.
|
- No estimated costs; only the provider-reported windows.
|
||||||
|
|
||||||
## Where it shows up
|
## Where it shows up
|
||||||
- `/status` in chats: adds a short “Usage” line (only if available).
|
- `/status` in chats: compact one‑liner with session tokens + estimated cost (API key only) and provider quota windows when available.
|
||||||
|
- `/cost on|off` in chats: toggles per‑response usage lines (OAuth shows tokens only).
|
||||||
- CLI: `clawdbot status --usage` prints a full per-provider breakdown.
|
- CLI: `clawdbot status --usage` prints a full per-provider breakdown.
|
||||||
- CLI: `clawdbot providers list` prints the same usage snapshot alongside provider config (use `--no-usage` to skip).
|
- CLI: `clawdbot providers list` prints the same usage snapshot alongside provider config (use `--no-usage` to skip).
|
||||||
- macOS menu bar: “Usage” section under Context (only if available).
|
- macOS menu bar: “Usage” section under Context (only if available).
|
||||||
|
|||||||
@ -556,6 +556,7 @@
|
|||||||
"concepts/agent",
|
"concepts/agent",
|
||||||
"concepts/agent-loop",
|
"concepts/agent-loop",
|
||||||
"concepts/system-prompt",
|
"concepts/system-prompt",
|
||||||
|
"token-use",
|
||||||
"concepts/oauth",
|
"concepts/oauth",
|
||||||
"concepts/agent-workspace",
|
"concepts/agent-workspace",
|
||||||
"concepts/multi-agent",
|
"concepts/multi-agent",
|
||||||
|
|||||||
@ -1473,6 +1473,7 @@ Controls session scoping, idle expiry, reset triggers, and where the session sto
|
|||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
- `mainKey`: direct-chat bucket key (default: `"main"`). Useful when you want to “rename” the primary DM thread without changing `agentId`.
|
- `mainKey`: direct-chat bucket key (default: `"main"`). Useful when you want to “rename” the primary DM thread without changing `agentId`.
|
||||||
|
- Sandbox note: `agent.sandbox.mode: "non-main"` uses this key to detect the main session. Any session key that does not match `mainKey` (groups/channels) is sandboxed.
|
||||||
- `agentToAgent.maxPingPongTurns`: max reply-back turns between requester/target (0–5, default 5).
|
- `agentToAgent.maxPingPongTurns`: max reply-back turns between requester/target (0–5, default 5).
|
||||||
- `sendPolicy.default`: `allow` or `deny` fallback when no rule matches.
|
- `sendPolicy.default`: `allow` or `deny` fallback when no rule matches.
|
||||||
- `sendPolicy.rules[]`: match by `provider`, `chatType` (`direct|group|room`), or `keyPrefix` (e.g. `cron:`). First deny wins; otherwise allow.
|
- `sendPolicy.rules[]`: match by `provider`, `chatType` (`direct|group|room`), or `keyPrefix` (e.g. `cron:`). First deny wins; otherwise allow.
|
||||||
|
|||||||
@ -31,6 +31,8 @@ Not sandboxed:
|
|||||||
- `"off"`: no sandboxing.
|
- `"off"`: no sandboxing.
|
||||||
- `"non-main"`: sandbox only **non-main** sessions (default if you want normal chats on host).
|
- `"non-main"`: sandbox only **non-main** sessions (default if you want normal chats on host).
|
||||||
- `"all"`: every session runs in a sandbox.
|
- `"all"`: every session runs in a sandbox.
|
||||||
|
Note: `"non-main"` is based on `session.mainKey` (default `"main"`), not agent id.
|
||||||
|
Group/channel sessions use their own keys, so they count as non-main and will be sandboxed.
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
`agent.sandbox.scope` controls **how many containers** are created:
|
`agent.sandbox.scope` controls **how many containers** are created:
|
||||||
|
|||||||
@ -122,6 +122,19 @@ or state drift because only one workspace is active.
|
|||||||
**Fix:** keep a single active workspace and archive/remove the rest. See
|
**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#legacy-workspace-folders).
|
||||||
|
|
||||||
|
### Main chat running in a sandbox workspace
|
||||||
|
|
||||||
|
Symptoms: `pwd` or file tools show `~/.clawdbot/sandboxes/...` even though you
|
||||||
|
expected the host workspace.
|
||||||
|
|
||||||
|
**Why:** `agent.sandbox.mode: "non-main"` keys off `session.mainKey` (default `"main"`).
|
||||||
|
Group/channel sessions use their own keys, so they are treated as non-main and
|
||||||
|
get sandbox workspaces.
|
||||||
|
|
||||||
|
**Fix options:**
|
||||||
|
- If you want host workspaces for an agent: set `routing.agents.<id>.sandbox.mode: "off"`.
|
||||||
|
- If you want host workspace access inside sandbox: set `workspaceAccess: "rw"` for that agent.
|
||||||
|
|
||||||
### "Agent was aborted"
|
### "Agent was aborted"
|
||||||
|
|
||||||
The agent was interrupted mid-response.
|
The agent was interrupted mid-response.
|
||||||
|
|||||||
@ -252,6 +252,15 @@ The global `agent.workspace` and `agent.sandbox` are still supported for backwar
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Common Pitfall: "non-main"
|
||||||
|
|
||||||
|
`sandbox.mode: "non-main"` is based on `session.mainKey` (default `"main"`),
|
||||||
|
not the agent id. Group/channel sessions always get their own keys, so they
|
||||||
|
are treated as non-main and will be sandboxed. If you want an agent to never
|
||||||
|
sandbox, set `routing.agents.<id>.sandbox.mode: "off"`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
After configuring multi-agent sandbox and tools:
|
After configuring multi-agent sandbox and tools:
|
||||||
|
|||||||
@ -19,6 +19,23 @@ Recommended path: use the **CLI onboarding wizard** (`clawdbot onboard`). It set
|
|||||||
|
|
||||||
If you want the deeper reference pages, jump to: [Wizard](/start/wizard), [Setup](/start/setup), [Pairing](/start/pairing), [Security](/gateway/security).
|
If you want the deeper reference pages, jump to: [Wizard](/start/wizard), [Setup](/start/setup), [Pairing](/start/pairing), [Security](/gateway/security).
|
||||||
|
|
||||||
|
Sandboxing note: `agent.sandbox.mode: "non-main"` uses `session.mainKey` (default `"main"`),
|
||||||
|
so group/channel sessions are sandboxed. If you want the main agent to always
|
||||||
|
run on host, set an explicit per-agent override:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"routing": {
|
||||||
|
"agents": {
|
||||||
|
"main": {
|
||||||
|
"workspace": "~/clawd",
|
||||||
|
"sandbox": { "mode": "off" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## 0) Prereqs
|
## 0) Prereqs
|
||||||
|
|
||||||
- Node `>=22`
|
- Node `>=22`
|
||||||
|
|||||||
72
docs/token-use.md
Normal file
72
docs/token-use.md
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
summary: "How Clawdbot builds prompt context and reports token usage + costs"
|
||||||
|
read_when:
|
||||||
|
- Explaining token usage, costs, or context windows
|
||||||
|
- Debugging context growth or compaction behavior
|
||||||
|
---
|
||||||
|
# Token use & costs
|
||||||
|
|
||||||
|
Clawdbot tracks **tokens**, not characters. Tokens are model-specific, but most
|
||||||
|
OpenAI-style models average ~4 characters per token for English text.
|
||||||
|
|
||||||
|
## How the system prompt is built
|
||||||
|
|
||||||
|
Clawdbot assembles its own system prompt on every run. It includes:
|
||||||
|
|
||||||
|
- Tool list + short descriptions
|
||||||
|
- Skills list (only metadata; instructions are loaded on demand with `read`)
|
||||||
|
- Self-update instructions
|
||||||
|
- Workspace + bootstrap files (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, `BOOTSTRAP.md` when new)
|
||||||
|
- Time (UTC + user timezone)
|
||||||
|
- Reply tags + heartbeat behavior
|
||||||
|
- Runtime metadata (host/OS/model/thinking)
|
||||||
|
|
||||||
|
See the full breakdown in [System Prompt](/concepts/system-prompt).
|
||||||
|
|
||||||
|
## What counts in the context window
|
||||||
|
|
||||||
|
Everything the model receives counts toward the context limit:
|
||||||
|
|
||||||
|
- System prompt (all sections listed above)
|
||||||
|
- Conversation history (user + assistant messages)
|
||||||
|
- Tool calls and tool results
|
||||||
|
- Attachments/transcripts (images, audio, files)
|
||||||
|
- Compaction summaries and pruning artifacts
|
||||||
|
- Provider wrappers or safety headers (not visible, but still counted)
|
||||||
|
|
||||||
|
## How to see current token usage
|
||||||
|
|
||||||
|
Use these in chat:
|
||||||
|
|
||||||
|
- `/status` → **compact one‑liner** with the session model, context usage,
|
||||||
|
last response input/output tokens, and **estimated cost** (API key only).
|
||||||
|
- `/cost on|off` → appends a **per-response usage line** to every reply.
|
||||||
|
- Persists per session (stored as `responseUsage`).
|
||||||
|
- OAuth auth **hides cost** (tokens only).
|
||||||
|
|
||||||
|
Other surfaces:
|
||||||
|
|
||||||
|
- **TUI/Web TUI:** `/status` + `/cost` are supported.
|
||||||
|
- **CLI:** `clawdbot status --usage` and `clawdbot providers list` show
|
||||||
|
provider quota windows (not per-response costs).
|
||||||
|
|
||||||
|
## Cost estimation (when shown)
|
||||||
|
|
||||||
|
Costs are estimated from your model pricing config:
|
||||||
|
|
||||||
|
```
|
||||||
|
models.providers.<provider>.models[].cost
|
||||||
|
```
|
||||||
|
|
||||||
|
These are **USD per 1M tokens** for `input`, `output`, `cacheRead`, and
|
||||||
|
`cacheWrite`. If pricing is missing, Clawdbot shows tokens only. OAuth tokens
|
||||||
|
never show dollar cost.
|
||||||
|
|
||||||
|
## Tips for reducing token pressure
|
||||||
|
|
||||||
|
- Use `/compact` to summarize long sessions.
|
||||||
|
- Trim large tool outputs in your workflows.
|
||||||
|
- Keep skill descriptions short (skill list is injected into the prompt).
|
||||||
|
- Prefer smaller models for verbose, exploratory work.
|
||||||
|
|
||||||
|
See [Skills](/tools/skills) for the exact skill list overhead formula.
|
||||||
@ -163,6 +163,23 @@ This is **scoped to the agent run**, not a global shell environment.
|
|||||||
|
|
||||||
Clawdbot snapshots the eligible skills **when a session starts** and reuses that list for subsequent turns in the same session. Changes to skills or config take effect on the next new session.
|
Clawdbot snapshots the eligible skills **when a session starts** and reuses that list for subsequent turns in the same session. Changes to skills or config take effect on the next new session.
|
||||||
|
|
||||||
|
## Token impact (skills list)
|
||||||
|
|
||||||
|
When skills are eligible, Clawdbot injects a compact XML list of available skills into the system prompt (via `formatSkillsForPrompt` in `pi-coding-agent`). The cost is deterministic:
|
||||||
|
|
||||||
|
- **Base overhead (only when ≥1 skill):** 195 characters.
|
||||||
|
- **Per skill:** 97 characters + the length of the XML-escaped `<name>`, `<description>`, and `<location>` values.
|
||||||
|
|
||||||
|
Formula (characters):
|
||||||
|
|
||||||
|
```
|
||||||
|
total = 195 + Σ (97 + len(name_escaped) + len(description_escaped) + len(location_escaped))
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- XML escaping expands `& < > " '` into entities (`&`, `<`, etc.), increasing length.
|
||||||
|
- Token counts vary by model tokenizer. A rough OpenAI-style estimate is ~4 chars/token, so **97 chars ≈ 24 tokens** per skill plus your actual field lengths.
|
||||||
|
|
||||||
## Managed skills lifecycle
|
## Managed skills lifecycle
|
||||||
|
|
||||||
Clawdbot ships a baseline set of skills as **bundled skills** as part of the
|
Clawdbot ships a baseline set of skills as **bundled skills** as part of the
|
||||||
|
|||||||
@ -36,6 +36,7 @@ Text + native (when enabled):
|
|||||||
- `/help`
|
- `/help`
|
||||||
- `/commands`
|
- `/commands`
|
||||||
- `/status`
|
- `/status`
|
||||||
|
- `/cost on|off` (toggle per-response usage line)
|
||||||
- `/stop`
|
- `/stop`
|
||||||
- `/restart`
|
- `/restart`
|
||||||
- `/activation mention|always` (groups only)
|
- `/activation mention|always` (groups only)
|
||||||
@ -53,6 +54,7 @@ Text-only:
|
|||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Commands accept an optional `:` between the command and args (e.g. `/think: high`, `/send: on`, `/help:`).
|
- Commands accept an optional `:` between the command and args (e.g. `/think: high`, `/send: on`, `/help:`).
|
||||||
|
- `/cost` appends per-response token usage; it only shows dollar cost when the model uses an API key (OAuth hides cost).
|
||||||
- `/verbose` is meant for debugging and extra visibility; keep it **off** in normal use.
|
- `/verbose` is meant for debugging and extra visibility; keep it **off** in normal use.
|
||||||
- `/reasoning` (and `/verbose`) are risky in group settings: they may reveal internal reasoning or tool output you did not intend to expose. Prefer leaving them off, especially in group chats.
|
- `/reasoning` (and `/verbose`) are risky in group settings: they may reveal internal reasoning or tool output you did not intend to expose. Prefer leaving them off, especially in group chats.
|
||||||
|
|
||||||
|
|||||||
@ -77,6 +77,7 @@ Session controls:
|
|||||||
- `/think <off|minimal|low|medium|high>`
|
- `/think <off|minimal|low|medium|high>`
|
||||||
- `/verbose <on|off>`
|
- `/verbose <on|off>`
|
||||||
- `/reasoning <on|off|stream>`
|
- `/reasoning <on|off|stream>`
|
||||||
|
- `/cost <on|off>`
|
||||||
- `/elevated <on|off>` (alias: `/elev`)
|
- `/elevated <on|off>` (alias: `/elev`)
|
||||||
- `/activation <mention|always>`
|
- `/activation <mention|always>`
|
||||||
- `/deliver <on|off>`
|
- `/deliver <on|off>`
|
||||||
|
|||||||
@ -53,6 +53,7 @@ Use SSH tunneling or Tailscale to reach the Gateway WS.
|
|||||||
- `/think <off|minimal|low|medium|high>`
|
- `/think <off|minimal|low|medium|high>`
|
||||||
- `/verbose <on|off>`
|
- `/verbose <on|off>`
|
||||||
- `/reasoning <on|off|stream>` (stream = Telegram draft only)
|
- `/reasoning <on|off|stream>` (stream = Telegram draft only)
|
||||||
|
- `/cost <on|off>`
|
||||||
- `/elevated <on|off>`
|
- `/elevated <on|off>`
|
||||||
- `/elev <on|off>`
|
- `/elev <on|off>`
|
||||||
- `/activation <mention|always>`
|
- `/activation <mention|always>`
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"ensureLogins": [
|
"ensureLogins": [
|
||||||
"jdrhyne",
|
"jdrhyne",
|
||||||
|
"latitudeki5223",
|
||||||
"manmal"
|
"manmal"
|
||||||
],
|
],
|
||||||
"seedCommit": "d6863f87",
|
"seedCommit": "d6863f87",
|
||||||
|
|||||||
@ -100,6 +100,7 @@ export async function resolveApiKeyForProvider(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type EnvApiKeyResult = { apiKey: string; source: string };
|
export type EnvApiKeyResult = { apiKey: string; source: string };
|
||||||
|
export type ModelAuthMode = "api-key" | "oauth" | "mixed" | "unknown";
|
||||||
|
|
||||||
export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
|
export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
|
||||||
const applied = new Set(getShellEnvAppliedKeys());
|
const applied = new Set(getShellEnvAppliedKeys());
|
||||||
@ -143,6 +144,37 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
|
|||||||
return pick(envVar);
|
return pick(envVar);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveModelAuthMode(
|
||||||
|
provider?: string,
|
||||||
|
cfg?: ClawdbotConfig,
|
||||||
|
store?: AuthProfileStore,
|
||||||
|
): ModelAuthMode | undefined {
|
||||||
|
const resolved = provider?.trim();
|
||||||
|
if (!resolved) return undefined;
|
||||||
|
|
||||||
|
const authStore = store ?? ensureAuthProfileStore();
|
||||||
|
const profiles = listProfilesForProvider(authStore, resolved);
|
||||||
|
if (profiles.length > 0) {
|
||||||
|
const modes = new Set(
|
||||||
|
profiles
|
||||||
|
.map((id) => authStore.profiles[id]?.type)
|
||||||
|
.filter((mode): mode is "api_key" | "oauth" => Boolean(mode)),
|
||||||
|
);
|
||||||
|
if (modes.has("oauth") && modes.has("api_key")) return "mixed";
|
||||||
|
if (modes.has("oauth")) return "oauth";
|
||||||
|
if (modes.has("api_key")) return "api-key";
|
||||||
|
}
|
||||||
|
|
||||||
|
const envKey = resolveEnvApiKey(resolved);
|
||||||
|
if (envKey?.apiKey) {
|
||||||
|
return envKey.source.includes("OAUTH_TOKEN") ? "oauth" : "api-key";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getCustomProviderApiKey(cfg, resolved)) return "api-key";
|
||||||
|
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
export async function getApiKeyForModel(params: {
|
export async function getApiKeyForModel(params: {
|
||||||
model: Model<Api>;
|
model: Model<Api>;
|
||||||
cfg?: ClawdbotConfig;
|
cfg?: ClawdbotConfig;
|
||||||
|
|||||||
@ -224,6 +224,11 @@ export function buildAgentSystemPrompt(params: {
|
|||||||
"- [[reply_to:<id>]] replies to a specific message id when you have it.",
|
"- [[reply_to:<id>]] replies to a specific message id when you have it.",
|
||||||
"Tags are stripped before sending; support depends on the current provider config.",
|
"Tags are stripped before sending; support depends on the current provider config.",
|
||||||
"",
|
"",
|
||||||
|
"## Messaging",
|
||||||
|
"- Reply in current session → automatically routes to the source provider (Signal, Telegram, etc.)",
|
||||||
|
"- Cross-session messaging → use sessions_send(sessionKey, message)",
|
||||||
|
"- Never use bash/curl for provider messaging; Clawdbot handles all routing internally.",
|
||||||
|
"",
|
||||||
];
|
];
|
||||||
|
|
||||||
if (extraSystemPrompt) {
|
if (extraSystemPrompt) {
|
||||||
|
|||||||
@ -34,6 +34,13 @@ const CHAT_COMMANDS: ChatCommandDefinition[] = [
|
|||||||
description: "Show current status.",
|
description: "Show current status.",
|
||||||
textAliases: ["/status"],
|
textAliases: ["/status"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "cost",
|
||||||
|
nativeName: "cost",
|
||||||
|
description: "Toggle per-response usage line.",
|
||||||
|
textAliases: ["/cost"],
|
||||||
|
acceptsArgs: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "stop",
|
key: "stop",
|
||||||
nativeName: "stop",
|
nativeName: "stop",
|
||||||
|
|||||||
@ -472,6 +472,40 @@ describe("directive behavior", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("warns when elevated is used in direct runtime", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
vi.mocked(runEmbeddedPiAgent).mockReset();
|
||||||
|
|
||||||
|
const res = await getReplyFromConfig(
|
||||||
|
{
|
||||||
|
Body: "/elevated off",
|
||||||
|
From: "+1222",
|
||||||
|
To: "+1222",
|
||||||
|
Provider: "whatsapp",
|
||||||
|
SenderE164: "+1222",
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
agent: {
|
||||||
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
elevated: {
|
||||||
|
allowFrom: { whatsapp: ["+1222"] },
|
||||||
|
},
|
||||||
|
sandbox: { mode: "off" },
|
||||||
|
},
|
||||||
|
whatsapp: { allowFrom: ["+1222"] },
|
||||||
|
session: { store: path.join(home, "sessions.json") },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
|
expect(text).toContain("Elevated mode disabled.");
|
||||||
|
expect(text).toContain("Runtime is direct; sandboxing does not apply.");
|
||||||
|
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects invalid elevated level", async () => {
|
it("rejects invalid elevated level", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockReset();
|
vi.mocked(runEmbeddedPiAgent).mockReset();
|
||||||
@ -504,6 +538,72 @@ describe("directive behavior", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("handles multiple directives in a single message", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
vi.mocked(runEmbeddedPiAgent).mockReset();
|
||||||
|
|
||||||
|
const res = await getReplyFromConfig(
|
||||||
|
{
|
||||||
|
Body: "/elevated off\n/verbose on",
|
||||||
|
From: "+1222",
|
||||||
|
To: "+1222",
|
||||||
|
Provider: "whatsapp",
|
||||||
|
SenderE164: "+1222",
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
agent: {
|
||||||
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
elevated: {
|
||||||
|
allowFrom: { whatsapp: ["+1222"] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
whatsapp: { allowFrom: ["+1222"] },
|
||||||
|
session: { store: path.join(home, "sessions.json") },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
|
expect(text).toContain("Elevated mode disabled.");
|
||||||
|
expect(text).toContain("Verbose logging enabled.");
|
||||||
|
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns status alongside directive-only acks", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
vi.mocked(runEmbeddedPiAgent).mockReset();
|
||||||
|
|
||||||
|
const res = await getReplyFromConfig(
|
||||||
|
{
|
||||||
|
Body: "/elevated off\n/status",
|
||||||
|
From: "+1222",
|
||||||
|
To: "+1222",
|
||||||
|
Provider: "whatsapp",
|
||||||
|
SenderE164: "+1222",
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
agent: {
|
||||||
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: path.join(home, "clawd"),
|
||||||
|
elevated: {
|
||||||
|
allowFrom: { whatsapp: ["+1222"] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
whatsapp: { allowFrom: ["+1222"] },
|
||||||
|
session: { store: path.join(home, "sessions.json") },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
|
expect(text).toContain("Elevated mode disabled.");
|
||||||
|
expect(text).toContain("status agent:main:main");
|
||||||
|
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("acks queue directive and persists override", async () => {
|
it("acks queue directive and persists override", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
vi.mocked(runEmbeddedPiAgent).mockReset();
|
vi.mocked(runEmbeddedPiAgent).mockReset();
|
||||||
|
|||||||
@ -14,6 +14,17 @@ vi.mock("../agents/pi-embedded.js", () => ({
|
|||||||
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const usageMocks = vi.hoisted(() => ({
|
||||||
|
loadProviderUsageSummary: vi.fn().mockResolvedValue({
|
||||||
|
updatedAt: 0,
|
||||||
|
providers: [],
|
||||||
|
}),
|
||||||
|
formatUsageSummaryLine: vi.fn().mockReturnValue("📊 Usage: Claude 80% left"),
|
||||||
|
resolveUsageProviderId: vi.fn((provider: string) => provider.split("/")[0]),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../infra/provider-usage.js", () => usageMocks);
|
||||||
|
|
||||||
import {
|
import {
|
||||||
abortEmbeddedPiRun,
|
abortEmbeddedPiRun,
|
||||||
compactEmbeddedPiSession,
|
compactEmbeddedPiSession,
|
||||||
@ -66,6 +77,30 @@ afterEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("trigger handling", () => {
|
describe("trigger handling", () => {
|
||||||
|
it("filters usage summary to the current model provider", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
usageMocks.loadProviderUsageSummary.mockClear();
|
||||||
|
|
||||||
|
const res = await getReplyFromConfig(
|
||||||
|
{
|
||||||
|
Body: "/status",
|
||||||
|
From: "+1000",
|
||||||
|
To: "+2000",
|
||||||
|
Provider: "whatsapp",
|
||||||
|
SenderE164: "+1000",
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
makeCfg(home),
|
||||||
|
);
|
||||||
|
|
||||||
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
|
expect(text).toContain("📊 Usage: Claude 80% left");
|
||||||
|
expect(usageMocks.loadProviderUsageSummary).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ providers: ["anthropic"] }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("aborts even with timestamp prefix", async () => {
|
it("aborts even with timestamp prefix", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const res = await getReplyFromConfig(
|
const res = await getReplyFromConfig(
|
||||||
@ -178,7 +213,71 @@ describe("trigger handling", () => {
|
|||||||
makeCfg(home),
|
makeCfg(home),
|
||||||
);
|
);
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toContain("ClawdBot");
|
expect(text).toContain("status");
|
||||||
|
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports active auth profile and key snippet in status", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
const cfg = makeCfg(home);
|
||||||
|
const agentDir = join(home, ".clawdbot", "agents", "main", "agent");
|
||||||
|
await fs.mkdir(agentDir, { recursive: true });
|
||||||
|
await fs.writeFile(
|
||||||
|
join(agentDir, "auth-profiles.json"),
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
version: 1,
|
||||||
|
profiles: {
|
||||||
|
"anthropic:work": {
|
||||||
|
type: "api_key",
|
||||||
|
provider: "anthropic",
|
||||||
|
key: "sk-test-1234567890abcdef",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lastGood: { anthropic: "anthropic:work" },
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const sessionKey = resolveSessionKey("per-sender", {
|
||||||
|
From: "+1002",
|
||||||
|
To: "+2000",
|
||||||
|
Provider: "whatsapp",
|
||||||
|
} as Parameters<typeof resolveSessionKey>[1]);
|
||||||
|
await fs.writeFile(
|
||||||
|
cfg.session.store,
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
[sessionKey]: {
|
||||||
|
sessionId: "session-auth",
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
authProfileOverride: "anthropic:work",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const res = await getReplyFromConfig(
|
||||||
|
{
|
||||||
|
Body: "/status",
|
||||||
|
From: "+1002",
|
||||||
|
To: "+2000",
|
||||||
|
Provider: "whatsapp",
|
||||||
|
SenderE164: "+1002",
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
|
expect(text).toContain("🔑 api-key");
|
||||||
|
expect(text).toContain("…");
|
||||||
|
expect(text).toContain("(anthropic:work)");
|
||||||
|
expect(text).not.toContain("mixed");
|
||||||
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -383,6 +482,48 @@ describe("trigger handling", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("allows elevated off in groups without mention", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
|
||||||
|
payloads: [{ text: "ok" }],
|
||||||
|
meta: {
|
||||||
|
durationMs: 1,
|
||||||
|
agentMeta: { sessionId: "s", provider: "p", model: "m" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const cfg = {
|
||||||
|
agent: {
|
||||||
|
model: "anthropic/claude-opus-4-5",
|
||||||
|
workspace: join(home, "clawd"),
|
||||||
|
elevated: {
|
||||||
|
allowFrom: { whatsapp: ["+1000"] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
whatsapp: {
|
||||||
|
allowFrom: ["+1000"],
|
||||||
|
groups: { "*": { requireMention: false } },
|
||||||
|
},
|
||||||
|
session: { store: join(home, "sessions.json") },
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await getReplyFromConfig(
|
||||||
|
{
|
||||||
|
Body: "/elevated off",
|
||||||
|
From: "group:123@g.us",
|
||||||
|
To: "whatsapp:+2000",
|
||||||
|
Provider: "whatsapp",
|
||||||
|
SenderE164: "+1000",
|
||||||
|
ChatType: "group",
|
||||||
|
WasMentioned: false,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
|
expect(text).toContain("Elevated mode disabled.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("allows elevated directive in groups when mentioned", async () => {
|
it("allows elevated directive in groups when mentioned", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
|
|||||||
@ -40,7 +40,11 @@ import { getAbortMemory } from "./reply/abort.js";
|
|||||||
import { runReplyAgent } from "./reply/agent-runner.js";
|
import { runReplyAgent } from "./reply/agent-runner.js";
|
||||||
import { resolveBlockStreamingChunking } from "./reply/block-streaming.js";
|
import { resolveBlockStreamingChunking } from "./reply/block-streaming.js";
|
||||||
import { applySessionHints } from "./reply/body.js";
|
import { applySessionHints } from "./reply/body.js";
|
||||||
import { buildCommandContext, handleCommands } from "./reply/commands.js";
|
import {
|
||||||
|
buildCommandContext,
|
||||||
|
buildStatusReply,
|
||||||
|
handleCommands,
|
||||||
|
} from "./reply/commands.js";
|
||||||
import {
|
import {
|
||||||
handleDirectiveOnly,
|
handleDirectiveOnly,
|
||||||
type InlineDirectives,
|
type InlineDirectives,
|
||||||
@ -329,11 +333,23 @@ export async function getReplyFromConfig(
|
|||||||
.map((entry) => entry.alias?.trim())
|
.map((entry) => entry.alias?.trim())
|
||||||
.filter((alias): alias is string => Boolean(alias))
|
.filter((alias): alias is string => Boolean(alias))
|
||||||
.filter((alias) => !reservedCommands.has(alias.toLowerCase()));
|
.filter((alias) => !reservedCommands.has(alias.toLowerCase()));
|
||||||
const disableElevatedInGroup = isGroup && ctx.WasMentioned !== true;
|
|
||||||
let parsedDirectives = parseInlineDirectives(rawBody, {
|
let parsedDirectives = parseInlineDirectives(rawBody, {
|
||||||
modelAliases: configuredAliases,
|
modelAliases: configuredAliases,
|
||||||
disableElevated: disableElevatedInGroup,
|
|
||||||
});
|
});
|
||||||
|
if (
|
||||||
|
isGroup &&
|
||||||
|
ctx.WasMentioned !== true &&
|
||||||
|
parsedDirectives.hasElevatedDirective
|
||||||
|
) {
|
||||||
|
if (parsedDirectives.elevatedLevel !== "off") {
|
||||||
|
parsedDirectives = {
|
||||||
|
...parsedDirectives,
|
||||||
|
hasElevatedDirective: false,
|
||||||
|
elevatedLevel: undefined,
|
||||||
|
rawElevatedLevel: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
const hasDirective =
|
const hasDirective =
|
||||||
parsedDirectives.hasThinkDirective ||
|
parsedDirectives.hasThinkDirective ||
|
||||||
parsedDirectives.hasVerboseDirective ||
|
parsedDirectives.hasVerboseDirective ||
|
||||||
@ -348,7 +364,12 @@ export async function getReplyFromConfig(
|
|||||||
? stripMentions(stripped, ctx, cfg, agentId)
|
? stripMentions(stripped, ctx, cfg, agentId)
|
||||||
: stripped;
|
: stripped;
|
||||||
if (noMentions.trim().length > 0) {
|
if (noMentions.trim().length > 0) {
|
||||||
parsedDirectives = clearInlineDirectives(parsedDirectives.cleaned);
|
const directiveOnlyCheck = parseInlineDirectives(noMentions, {
|
||||||
|
modelAliases: configuredAliases,
|
||||||
|
});
|
||||||
|
if (directiveOnlyCheck.cleaned.trim().length > 0) {
|
||||||
|
parsedDirectives = clearInlineDirectives(parsedDirectives.cleaned);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const directives = commandAuthorized
|
const directives = commandAuthorized
|
||||||
@ -465,6 +486,21 @@ export async function getReplyFromConfig(
|
|||||||
? undefined
|
? undefined
|
||||||
: directives.rawModelDirective;
|
: directives.rawModelDirective;
|
||||||
|
|
||||||
|
const command = buildCommandContext({
|
||||||
|
ctx,
|
||||||
|
cfg,
|
||||||
|
agentId,
|
||||||
|
sessionKey,
|
||||||
|
isGroup,
|
||||||
|
triggerBodyNormalized,
|
||||||
|
commandAuthorized,
|
||||||
|
});
|
||||||
|
const allowTextCommands = shouldHandleTextCommands({
|
||||||
|
cfg,
|
||||||
|
surface: command.surface,
|
||||||
|
commandSource: ctx.CommandSource,
|
||||||
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isDirectiveOnly({
|
isDirectiveOnly({
|
||||||
directives,
|
directives,
|
||||||
@ -510,8 +546,36 @@ export async function getReplyFromConfig(
|
|||||||
currentReasoningLevel,
|
currentReasoningLevel,
|
||||||
currentElevatedLevel,
|
currentElevatedLevel,
|
||||||
});
|
});
|
||||||
|
let statusReply: ReplyPayload | undefined;
|
||||||
|
if (directives.hasStatusDirective && allowTextCommands) {
|
||||||
|
statusReply = await buildStatusReply({
|
||||||
|
cfg,
|
||||||
|
command,
|
||||||
|
sessionEntry,
|
||||||
|
sessionKey,
|
||||||
|
sessionScope,
|
||||||
|
provider,
|
||||||
|
model,
|
||||||
|
contextTokens,
|
||||||
|
resolvedThinkLevel:
|
||||||
|
currentThinkLevel ??
|
||||||
|
(agentCfg?.thinkingDefault as ThinkLevel | undefined),
|
||||||
|
resolvedVerboseLevel: (currentVerboseLevel ?? "off") as VerboseLevel,
|
||||||
|
resolvedReasoningLevel: (currentReasoningLevel ??
|
||||||
|
"off") as ReasoningLevel,
|
||||||
|
resolvedElevatedLevel: currentElevatedLevel,
|
||||||
|
resolveDefaultThinkingLevel: async () =>
|
||||||
|
currentThinkLevel ??
|
||||||
|
(agentCfg?.thinkingDefault as ThinkLevel | undefined),
|
||||||
|
isGroup,
|
||||||
|
defaultGroupActivation: () => defaultActivation,
|
||||||
|
});
|
||||||
|
}
|
||||||
typing.cleanup();
|
typing.cleanup();
|
||||||
return directiveReply;
|
if (statusReply?.text && directiveReply?.text) {
|
||||||
|
return { text: `${directiveReply.text}\n${statusReply.text}` };
|
||||||
|
}
|
||||||
|
return statusReply ?? directiveReply;
|
||||||
}
|
}
|
||||||
|
|
||||||
const persisted = await persistInlineDirectives({
|
const persisted = await persistInlineDirectives({
|
||||||
@ -551,20 +615,6 @@ export async function getReplyFromConfig(
|
|||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const command = buildCommandContext({
|
|
||||||
ctx,
|
|
||||||
cfg,
|
|
||||||
agentId,
|
|
||||||
sessionKey,
|
|
||||||
isGroup,
|
|
||||||
triggerBodyNormalized,
|
|
||||||
commandAuthorized,
|
|
||||||
});
|
|
||||||
const allowTextCommands = shouldHandleTextCommands({
|
|
||||||
cfg,
|
|
||||||
surface: command.surface,
|
|
||||||
commandSource: ctx.CommandSource,
|
|
||||||
});
|
|
||||||
const isEmptyConfig = Object.keys(cfg).length === 0;
|
const isEmptyConfig = Object.keys(cfg).length === 0;
|
||||||
if (
|
if (
|
||||||
command.isWhatsAppProvider &&
|
command.isWhatsAppProvider &&
|
||||||
|
|||||||
@ -2,12 +2,13 @@ import crypto from "node:crypto";
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { lookupContextTokens } from "../../agents/context.js";
|
import { lookupContextTokens } from "../../agents/context.js";
|
||||||
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
||||||
|
import { resolveModelAuthMode } from "../../agents/model-auth.js";
|
||||||
import { runWithModelFallback } from "../../agents/model-fallback.js";
|
import { runWithModelFallback } from "../../agents/model-fallback.js";
|
||||||
import {
|
import {
|
||||||
queueEmbeddedPiMessage,
|
queueEmbeddedPiMessage,
|
||||||
runEmbeddedPiAgent,
|
runEmbeddedPiAgent,
|
||||||
} from "../../agents/pi-embedded.js";
|
} from "../../agents/pi-embedded.js";
|
||||||
import { hasNonzeroUsage } from "../../agents/usage.js";
|
import { hasNonzeroUsage, type NormalizedUsage } from "../../agents/usage.js";
|
||||||
import {
|
import {
|
||||||
loadSessionStore,
|
loadSessionStore,
|
||||||
resolveSessionTranscriptPath,
|
resolveSessionTranscriptPath,
|
||||||
@ -18,6 +19,12 @@ import type { TypingMode } from "../../config/types.js";
|
|||||||
import { logVerbose } from "../../globals.js";
|
import { logVerbose } from "../../globals.js";
|
||||||
import { registerAgentRunContext } from "../../infra/agent-events.js";
|
import { registerAgentRunContext } from "../../infra/agent-events.js";
|
||||||
import { defaultRuntime } from "../../runtime.js";
|
import { defaultRuntime } from "../../runtime.js";
|
||||||
|
import {
|
||||||
|
estimateUsageCost,
|
||||||
|
formatTokenCount,
|
||||||
|
formatUsd,
|
||||||
|
resolveModelCostConfig,
|
||||||
|
} from "../../utils/usage-format.js";
|
||||||
import { stripHeartbeatToken } from "../heartbeat.js";
|
import { stripHeartbeatToken } from "../heartbeat.js";
|
||||||
import type { OriginatingChannelType, TemplateContext } from "../templating.js";
|
import type { OriginatingChannelType, TemplateContext } from "../templating.js";
|
||||||
import { normalizeVerboseLevel, type VerboseLevel } from "../thinking.js";
|
import { normalizeVerboseLevel, type VerboseLevel } from "../thinking.js";
|
||||||
@ -62,6 +69,65 @@ const formatBunFetchSocketError = (message: string) => {
|
|||||||
].join("\n");
|
].join("\n");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatResponseUsageLine = (params: {
|
||||||
|
usage?: NormalizedUsage;
|
||||||
|
showCost: boolean;
|
||||||
|
costConfig?: {
|
||||||
|
input: number;
|
||||||
|
output: number;
|
||||||
|
cacheRead: number;
|
||||||
|
cacheWrite: number;
|
||||||
|
};
|
||||||
|
}): string | null => {
|
||||||
|
const usage = params.usage;
|
||||||
|
if (!usage) return null;
|
||||||
|
const input = usage.input;
|
||||||
|
const output = usage.output;
|
||||||
|
if (typeof input !== "number" && typeof output !== "number") return null;
|
||||||
|
const inputLabel = typeof input === "number" ? formatTokenCount(input) : "?";
|
||||||
|
const outputLabel =
|
||||||
|
typeof output === "number" ? formatTokenCount(output) : "?";
|
||||||
|
const cost =
|
||||||
|
params.showCost && typeof input === "number" && typeof output === "number"
|
||||||
|
? estimateUsageCost({
|
||||||
|
usage: {
|
||||||
|
input,
|
||||||
|
output,
|
||||||
|
cacheRead: usage.cacheRead,
|
||||||
|
cacheWrite: usage.cacheWrite,
|
||||||
|
},
|
||||||
|
cost: params.costConfig,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
const costLabel = params.showCost ? formatUsd(cost) : undefined;
|
||||||
|
const suffix = costLabel ? ` · est ${costLabel}` : "";
|
||||||
|
return `Usage: ${inputLabel} in / ${outputLabel} out${suffix}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const appendUsageLine = (
|
||||||
|
payloads: ReplyPayload[],
|
||||||
|
line: string,
|
||||||
|
): ReplyPayload[] => {
|
||||||
|
let index = -1;
|
||||||
|
for (let i = payloads.length - 1; i >= 0; i -= 1) {
|
||||||
|
if (payloads[i]?.text) {
|
||||||
|
index = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (index === -1) return [...payloads, { text: line }];
|
||||||
|
const existing = payloads[index];
|
||||||
|
const existingText = existing.text ?? "";
|
||||||
|
const separator = existingText.endsWith("\n") ? "" : "\n";
|
||||||
|
const next = {
|
||||||
|
...existing,
|
||||||
|
text: `${existingText}${separator}${line}`,
|
||||||
|
};
|
||||||
|
const updated = payloads.slice();
|
||||||
|
updated[index] = next;
|
||||||
|
return updated;
|
||||||
|
};
|
||||||
|
|
||||||
const withTimeout = async <T>(
|
const withTimeout = async <T>(
|
||||||
promise: Promise<T>,
|
promise: Promise<T>,
|
||||||
timeoutMs: number,
|
timeoutMs: number,
|
||||||
@ -191,6 +257,7 @@ export async function runReplyAgent(params: {
|
|||||||
replyToChannel,
|
replyToChannel,
|
||||||
);
|
);
|
||||||
const applyReplyToMode = createReplyToModeFilter(replyToMode);
|
const applyReplyToMode = createReplyToModeFilter(replyToMode);
|
||||||
|
const cfg = followupRun.run.config;
|
||||||
|
|
||||||
if (shouldSteer && isStreaming) {
|
if (shouldSteer && isStreaming) {
|
||||||
const steered = queueEmbeddedPiMessage(
|
const steered = queueEmbeddedPiMessage(
|
||||||
@ -242,6 +309,7 @@ export async function runReplyAgent(params: {
|
|||||||
|
|
||||||
let didLogHeartbeatStrip = false;
|
let didLogHeartbeatStrip = false;
|
||||||
let autoCompactionCompleted = false;
|
let autoCompactionCompleted = false;
|
||||||
|
let responseUsageLine: string | undefined;
|
||||||
try {
|
try {
|
||||||
const runId = crypto.randomUUID();
|
const runId = crypto.randomUUID();
|
||||||
if (sessionKey) {
|
if (sessionKey) {
|
||||||
@ -641,20 +709,20 @@ export async function runReplyAgent(params: {
|
|||||||
await typingSignals.signalRunStart();
|
await typingSignals.signalRunStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sessionStore && sessionKey) {
|
const usage = runResult.meta.agentMeta?.usage;
|
||||||
const usage = runResult.meta.agentMeta?.usage;
|
const modelUsed =
|
||||||
const modelUsed =
|
runResult.meta.agentMeta?.model ?? fallbackModel ?? defaultModel;
|
||||||
runResult.meta.agentMeta?.model ?? fallbackModel ?? defaultModel;
|
const providerUsed =
|
||||||
const providerUsed =
|
runResult.meta.agentMeta?.provider ??
|
||||||
runResult.meta.agentMeta?.provider ??
|
fallbackProvider ??
|
||||||
fallbackProvider ??
|
followupRun.run.provider;
|
||||||
followupRun.run.provider;
|
const contextTokensUsed =
|
||||||
const contextTokensUsed =
|
agentCfgContextTokens ??
|
||||||
agentCfgContextTokens ??
|
lookupContextTokens(modelUsed) ??
|
||||||
lookupContextTokens(modelUsed) ??
|
sessionEntry?.contextTokens ??
|
||||||
sessionEntry?.contextTokens ??
|
DEFAULT_CONTEXT_TOKENS;
|
||||||
DEFAULT_CONTEXT_TOKENS;
|
|
||||||
|
|
||||||
|
if (sessionStore && sessionKey) {
|
||||||
if (hasNonzeroUsage(usage)) {
|
if (hasNonzeroUsage(usage)) {
|
||||||
const entry = sessionEntry ?? sessionStore[sessionKey];
|
const entry = sessionEntry ?? sessionStore[sessionKey];
|
||||||
if (entry) {
|
if (entry) {
|
||||||
@ -694,6 +762,29 @@ export async function runReplyAgent(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const responseUsageEnabled =
|
||||||
|
(sessionEntry?.responseUsage ??
|
||||||
|
(sessionKey
|
||||||
|
? sessionStore?.[sessionKey]?.responseUsage
|
||||||
|
: undefined)) === "on";
|
||||||
|
if (responseUsageEnabled && hasNonzeroUsage(usage)) {
|
||||||
|
const authMode = resolveModelAuthMode(providerUsed, cfg);
|
||||||
|
const showCost = authMode === "api-key";
|
||||||
|
const costConfig = showCost
|
||||||
|
? resolveModelCostConfig({
|
||||||
|
provider: providerUsed,
|
||||||
|
model: modelUsed,
|
||||||
|
config: cfg,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
const formatted = formatResponseUsageLine({
|
||||||
|
usage,
|
||||||
|
showCost,
|
||||||
|
costConfig,
|
||||||
|
});
|
||||||
|
if (formatted) responseUsageLine = formatted;
|
||||||
|
}
|
||||||
|
|
||||||
// If verbose is enabled and this is a new session, prepend a session hint.
|
// If verbose is enabled and this is a new session, prepend a session hint.
|
||||||
let finalPayloads = replyPayloads;
|
let finalPayloads = replyPayloads;
|
||||||
if (autoCompactionCompleted) {
|
if (autoCompactionCompleted) {
|
||||||
@ -717,6 +808,9 @@ export async function runReplyAgent(params: {
|
|||||||
...finalPayloads,
|
...finalPayloads,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
if (responseUsageLine) {
|
||||||
|
finalPayloads = appendUsageLine(finalPayloads, responseUsageLine);
|
||||||
|
}
|
||||||
|
|
||||||
return finalizeWithFollowup(
|
return finalizeWithFollowup(
|
||||||
finalPayloads.length === 1 ? finalPayloads[0] : finalPayloads,
|
finalPayloads.length === 1 ? finalPayloads[0] : finalPayloads,
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
ensureAuthProfileStore,
|
ensureAuthProfileStore,
|
||||||
listProfilesForProvider,
|
resolveAuthProfileDisplayLabel,
|
||||||
|
resolveAuthProfileOrder,
|
||||||
} from "../../agents/auth-profiles.js";
|
} from "../../agents/auth-profiles.js";
|
||||||
import {
|
import {
|
||||||
getCustomProviderApiKey,
|
getCustomProviderApiKey,
|
||||||
resolveEnvApiKey,
|
resolveEnvApiKey,
|
||||||
} from "../../agents/model-auth.js";
|
} from "../../agents/model-auth.js";
|
||||||
|
import { normalizeProviderId } from "../../agents/model-selection.js";
|
||||||
import {
|
import {
|
||||||
abortEmbeddedPiRun,
|
abortEmbeddedPiRun,
|
||||||
compactEmbeddedPiSession,
|
compactEmbeddedPiSession,
|
||||||
@ -23,6 +25,7 @@ import { logVerbose } from "../../globals.js";
|
|||||||
import {
|
import {
|
||||||
formatUsageSummaryLine,
|
formatUsageSummaryLine,
|
||||||
loadProviderUsageSummary,
|
loadProviderUsageSummary,
|
||||||
|
resolveUsageProviderId,
|
||||||
} from "../../infra/provider-usage.js";
|
} from "../../infra/provider-usage.js";
|
||||||
import {
|
import {
|
||||||
scheduleGatewaySigusr1Restart,
|
scheduleGatewaySigusr1Restart,
|
||||||
@ -92,32 +95,166 @@ export type CommandContext = {
|
|||||||
to?: string;
|
to?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export async function buildStatusReply(params: {
|
||||||
|
cfg: ClawdbotConfig;
|
||||||
|
command: CommandContext;
|
||||||
|
sessionEntry?: SessionEntry;
|
||||||
|
sessionKey?: string;
|
||||||
|
sessionScope?: SessionScope;
|
||||||
|
provider: string;
|
||||||
|
model: string;
|
||||||
|
contextTokens: number;
|
||||||
|
resolvedThinkLevel?: ThinkLevel;
|
||||||
|
resolvedVerboseLevel: VerboseLevel;
|
||||||
|
resolvedReasoningLevel: ReasoningLevel;
|
||||||
|
resolvedElevatedLevel?: ElevatedLevel;
|
||||||
|
resolveDefaultThinkingLevel: () => Promise<ThinkLevel | undefined>;
|
||||||
|
isGroup: boolean;
|
||||||
|
defaultGroupActivation: () => "always" | "mention";
|
||||||
|
}): Promise<ReplyPayload | undefined> {
|
||||||
|
const {
|
||||||
|
cfg,
|
||||||
|
command,
|
||||||
|
sessionEntry,
|
||||||
|
sessionKey,
|
||||||
|
sessionScope,
|
||||||
|
provider,
|
||||||
|
model,
|
||||||
|
contextTokens,
|
||||||
|
resolvedThinkLevel,
|
||||||
|
resolvedVerboseLevel,
|
||||||
|
resolvedReasoningLevel,
|
||||||
|
resolvedElevatedLevel,
|
||||||
|
resolveDefaultThinkingLevel,
|
||||||
|
isGroup,
|
||||||
|
defaultGroupActivation,
|
||||||
|
} = params;
|
||||||
|
if (!command.isAuthorizedSender) {
|
||||||
|
logVerbose(
|
||||||
|
`Ignoring /status from unauthorized sender: ${command.senderE164 || "<unknown>"}`,
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let usageLine: string | null = null;
|
||||||
|
try {
|
||||||
|
const usageProvider = resolveUsageProviderId(provider);
|
||||||
|
if (usageProvider) {
|
||||||
|
const usageSummary = await loadProviderUsageSummary({
|
||||||
|
timeoutMs: 3500,
|
||||||
|
providers: [usageProvider],
|
||||||
|
});
|
||||||
|
usageLine = formatUsageSummaryLine(usageSummary, { now: Date.now() });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
usageLine = null;
|
||||||
|
}
|
||||||
|
const queueSettings = resolveQueueSettings({
|
||||||
|
cfg,
|
||||||
|
provider: command.provider,
|
||||||
|
sessionEntry,
|
||||||
|
});
|
||||||
|
const queueKey = sessionKey ?? sessionEntry?.sessionId;
|
||||||
|
const queueDepth = queueKey ? getFollowupQueueDepth(queueKey) : 0;
|
||||||
|
const queueOverrides = Boolean(
|
||||||
|
sessionEntry?.queueDebounceMs ??
|
||||||
|
sessionEntry?.queueCap ??
|
||||||
|
sessionEntry?.queueDrop,
|
||||||
|
);
|
||||||
|
const groupActivation = isGroup
|
||||||
|
? (normalizeGroupActivation(sessionEntry?.groupActivation) ??
|
||||||
|
defaultGroupActivation())
|
||||||
|
: undefined;
|
||||||
|
const statusText = buildStatusMessage({
|
||||||
|
agent: {
|
||||||
|
...cfg.agent,
|
||||||
|
model: {
|
||||||
|
...cfg.agent?.model,
|
||||||
|
primary: `${provider}/${model}`,
|
||||||
|
},
|
||||||
|
contextTokens,
|
||||||
|
thinkingDefault: cfg.agent?.thinkingDefault,
|
||||||
|
verboseDefault: cfg.agent?.verboseDefault,
|
||||||
|
elevatedDefault: cfg.agent?.elevatedDefault,
|
||||||
|
},
|
||||||
|
sessionEntry,
|
||||||
|
sessionKey,
|
||||||
|
sessionScope,
|
||||||
|
groupActivation,
|
||||||
|
resolvedThink: resolvedThinkLevel ?? (await resolveDefaultThinkingLevel()),
|
||||||
|
resolvedVerbose: resolvedVerboseLevel,
|
||||||
|
resolvedReasoning: resolvedReasoningLevel,
|
||||||
|
resolvedElevated: resolvedElevatedLevel,
|
||||||
|
modelAuth: resolveModelAuthLabel(provider, cfg, sessionEntry),
|
||||||
|
usageLine: usageLine ?? undefined,
|
||||||
|
queue: {
|
||||||
|
mode: queueSettings.mode,
|
||||||
|
depth: queueDepth,
|
||||||
|
debounceMs: queueSettings.debounceMs,
|
||||||
|
cap: queueSettings.cap,
|
||||||
|
dropPolicy: queueSettings.dropPolicy,
|
||||||
|
showDetails: queueOverrides,
|
||||||
|
},
|
||||||
|
includeTranscriptUsage: false,
|
||||||
|
});
|
||||||
|
return { text: statusText };
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatApiKeySnippet(apiKey: string): string {
|
||||||
|
const compact = apiKey.replace(/\s+/g, "");
|
||||||
|
if (!compact) return "unknown";
|
||||||
|
const edge = compact.length >= 12 ? 6 : 4;
|
||||||
|
const head = compact.slice(0, edge);
|
||||||
|
const tail = compact.slice(-edge);
|
||||||
|
return `${head}…${tail}`;
|
||||||
|
}
|
||||||
|
|
||||||
function resolveModelAuthLabel(
|
function resolveModelAuthLabel(
|
||||||
provider?: string,
|
provider?: string,
|
||||||
cfg?: ClawdbotConfig,
|
cfg?: ClawdbotConfig,
|
||||||
|
sessionEntry?: SessionEntry,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
const resolved = provider?.trim();
|
const resolved = provider?.trim();
|
||||||
if (!resolved) return undefined;
|
if (!resolved) return undefined;
|
||||||
|
|
||||||
|
const providerKey = normalizeProviderId(resolved);
|
||||||
const store = ensureAuthProfileStore();
|
const store = ensureAuthProfileStore();
|
||||||
const profiles = listProfilesForProvider(store, resolved);
|
const profileOverride = sessionEntry?.authProfileOverride?.trim();
|
||||||
if (profiles.length > 0) {
|
const lastGood = store.lastGood?.[providerKey] ?? store.lastGood?.[resolved];
|
||||||
const modes = new Set(
|
const order = resolveAuthProfileOrder({
|
||||||
profiles
|
cfg,
|
||||||
.map((id) => store.profiles[id]?.type)
|
store,
|
||||||
.filter((mode): mode is "api_key" | "oauth" => Boolean(mode)),
|
provider: providerKey,
|
||||||
);
|
preferredProfile: profileOverride,
|
||||||
if (modes.has("oauth") && modes.has("api_key")) return "mixed";
|
});
|
||||||
if (modes.has("oauth")) return "oauth";
|
const candidates = [profileOverride, lastGood, ...order].filter(
|
||||||
if (modes.has("api_key")) return "api-key";
|
Boolean,
|
||||||
|
) as string[];
|
||||||
|
|
||||||
|
for (const profileId of candidates) {
|
||||||
|
const profile = store.profiles[profileId];
|
||||||
|
if (!profile || normalizeProviderId(profile.provider) !== providerKey) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const label = resolveAuthProfileDisplayLabel({ cfg, store, profileId });
|
||||||
|
if (profile.type === "oauth") {
|
||||||
|
return `oauth${label ? ` (${label})` : ""}`;
|
||||||
|
}
|
||||||
|
const snippet = formatApiKeySnippet(profile.key);
|
||||||
|
return `api-key ${snippet}${label ? ` (${label})` : ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const envKey = resolveEnvApiKey(resolved);
|
const envKey = resolveEnvApiKey(providerKey);
|
||||||
if (envKey?.apiKey) {
|
if (envKey?.apiKey) {
|
||||||
return envKey.source.includes("OAUTH_TOKEN") ? "oauth" : "api-key";
|
if (envKey.source.includes("OAUTH_TOKEN")) {
|
||||||
|
return `oauth (${envKey.source})`;
|
||||||
|
}
|
||||||
|
return `api-key ${formatApiKeySnippet(envKey.apiKey)} (${envKey.source})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getCustomProviderApiKey(cfg, resolved)) return "api-key";
|
const customKey = getCustomProviderApiKey(cfg, providerKey);
|
||||||
|
if (customKey) {
|
||||||
|
return `api-key ${formatApiKeySnippet(customKey)} (models.json)`;
|
||||||
|
}
|
||||||
|
|
||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
@ -426,71 +563,24 @@ export async function handleCommands(params: {
|
|||||||
directives.hasStatusDirective ||
|
directives.hasStatusDirective ||
|
||||||
command.commandBodyNormalized === "/status";
|
command.commandBodyNormalized === "/status";
|
||||||
if (allowTextCommands && statusRequested) {
|
if (allowTextCommands && statusRequested) {
|
||||||
if (!command.isAuthorizedSender) {
|
const reply = await buildStatusReply({
|
||||||
logVerbose(
|
|
||||||
`Ignoring /status from unauthorized sender: ${command.senderE164 || "<unknown>"}`,
|
|
||||||
);
|
|
||||||
return { shouldContinue: false };
|
|
||||||
}
|
|
||||||
let usageLine: string | null = null;
|
|
||||||
try {
|
|
||||||
const usageSummary = await loadProviderUsageSummary({
|
|
||||||
timeoutMs: 3500,
|
|
||||||
});
|
|
||||||
usageLine = formatUsageSummaryLine(usageSummary, { now: Date.now() });
|
|
||||||
} catch {
|
|
||||||
usageLine = null;
|
|
||||||
}
|
|
||||||
const queueSettings = resolveQueueSettings({
|
|
||||||
cfg,
|
cfg,
|
||||||
provider: command.provider,
|
command,
|
||||||
sessionEntry,
|
|
||||||
});
|
|
||||||
const queueKey = sessionKey ?? sessionEntry?.sessionId;
|
|
||||||
const queueDepth = queueKey ? getFollowupQueueDepth(queueKey) : 0;
|
|
||||||
const queueOverrides = Boolean(
|
|
||||||
sessionEntry?.queueDebounceMs ??
|
|
||||||
sessionEntry?.queueCap ??
|
|
||||||
sessionEntry?.queueDrop,
|
|
||||||
);
|
|
||||||
const groupActivation = isGroup
|
|
||||||
? (normalizeGroupActivation(sessionEntry?.groupActivation) ??
|
|
||||||
defaultGroupActivation())
|
|
||||||
: undefined;
|
|
||||||
const statusText = buildStatusMessage({
|
|
||||||
agent: {
|
|
||||||
...cfg.agent,
|
|
||||||
model: {
|
|
||||||
...cfg.agent?.model,
|
|
||||||
primary: `${provider}/${model}`,
|
|
||||||
},
|
|
||||||
contextTokens,
|
|
||||||
thinkingDefault: cfg.agent?.thinkingDefault,
|
|
||||||
verboseDefault: cfg.agent?.verboseDefault,
|
|
||||||
elevatedDefault: cfg.agent?.elevatedDefault,
|
|
||||||
},
|
|
||||||
sessionEntry,
|
sessionEntry,
|
||||||
sessionKey,
|
sessionKey,
|
||||||
sessionScope,
|
sessionScope,
|
||||||
groupActivation,
|
provider,
|
||||||
resolvedThink:
|
model,
|
||||||
resolvedThinkLevel ?? (await resolveDefaultThinkingLevel()),
|
contextTokens,
|
||||||
resolvedVerbose: resolvedVerboseLevel,
|
resolvedThinkLevel,
|
||||||
resolvedReasoning: resolvedReasoningLevel,
|
resolvedVerboseLevel,
|
||||||
resolvedElevated: resolvedElevatedLevel,
|
resolvedReasoningLevel,
|
||||||
modelAuth: resolveModelAuthLabel(provider, cfg),
|
resolvedElevatedLevel,
|
||||||
usageLine: usageLine ?? undefined,
|
resolveDefaultThinkingLevel,
|
||||||
queue: {
|
isGroup,
|
||||||
mode: queueSettings.mode,
|
defaultGroupActivation,
|
||||||
depth: queueDepth,
|
|
||||||
debounceMs: queueSettings.debounceMs,
|
|
||||||
cap: queueSettings.cap,
|
|
||||||
dropPolicy: queueSettings.dropPolicy,
|
|
||||||
showDetails: queueOverrides,
|
|
||||||
},
|
|
||||||
includeTranscriptUsage: false,
|
|
||||||
});
|
});
|
||||||
return { shouldContinue: false, reply: { text: statusText } };
|
return { shouldContinue: false, reply };
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopRequested = command.commandBodyNormalized === "/stop";
|
const stopRequested = command.commandBodyNormalized === "/stop";
|
||||||
|
|||||||
@ -24,7 +24,12 @@ import {
|
|||||||
resolveModelRefFromString,
|
resolveModelRefFromString,
|
||||||
} from "../../agents/model-selection.js";
|
} from "../../agents/model-selection.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import { type SessionEntry, saveSessionStore } from "../../config/sessions.js";
|
import {
|
||||||
|
resolveAgentIdFromSessionKey,
|
||||||
|
resolveAgentMainSessionKey,
|
||||||
|
type SessionEntry,
|
||||||
|
saveSessionStore,
|
||||||
|
} from "../../config/sessions.js";
|
||||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||||
import { shortenHomePath } from "../../utils.js";
|
import { shortenHomePath } from "../../utils.js";
|
||||||
import { extractModelDirective } from "../model.js";
|
import { extractModelDirective } from "../model.js";
|
||||||
@ -57,6 +62,8 @@ const SYSTEM_MARK = "⚙️";
|
|||||||
const formatOptionsLine = (options: string) => `Options: ${options}.`;
|
const formatOptionsLine = (options: string) => `Options: ${options}.`;
|
||||||
const withOptions = (line: string, options: string) =>
|
const withOptions = (line: string, options: string) =>
|
||||||
`${line}\n${formatOptionsLine(options)}`;
|
`${line}\n${formatOptionsLine(options)}`;
|
||||||
|
const formatElevatedRuntimeHint = () =>
|
||||||
|
`${SYSTEM_MARK} Runtime is direct; sandboxing does not apply.`;
|
||||||
|
|
||||||
const maskApiKey = (value: string): string => {
|
const maskApiKey = (value: string): string => {
|
||||||
const trimmed = value.trim();
|
const trimmed = value.trim();
|
||||||
@ -350,6 +357,21 @@ export async function handleDirectiveOnly(params: {
|
|||||||
currentReasoningLevel,
|
currentReasoningLevel,
|
||||||
currentElevatedLevel,
|
currentElevatedLevel,
|
||||||
} = params;
|
} = params;
|
||||||
|
const runtimeIsSandboxed = (() => {
|
||||||
|
const sandboxMode = params.cfg.agent?.sandbox?.mode ?? "off";
|
||||||
|
if (sandboxMode === "off") return false;
|
||||||
|
const sessionKey = params.sessionKey?.trim();
|
||||||
|
if (!sessionKey) return false;
|
||||||
|
const agentId = resolveAgentIdFromSessionKey(sessionKey);
|
||||||
|
const mainKey = resolveAgentMainSessionKey({
|
||||||
|
cfg: params.cfg,
|
||||||
|
agentId,
|
||||||
|
});
|
||||||
|
if (sandboxMode === "all") return true;
|
||||||
|
return sessionKey !== mainKey;
|
||||||
|
})();
|
||||||
|
const shouldHintDirectRuntime =
|
||||||
|
directives.hasElevatedDirective && !runtimeIsSandboxed;
|
||||||
|
|
||||||
if (directives.hasModelDirective) {
|
if (directives.hasModelDirective) {
|
||||||
const modelDirective = directives.rawModelDirective?.trim().toLowerCase();
|
const modelDirective = directives.rawModelDirective?.trim().toLowerCase();
|
||||||
@ -463,7 +485,12 @@ export async function handleDirectiveOnly(params: {
|
|||||||
}
|
}
|
||||||
const level = currentElevatedLevel ?? "off";
|
const level = currentElevatedLevel ?? "off";
|
||||||
return {
|
return {
|
||||||
text: withOptions(`Current elevated level: ${level}.`, "on, off"),
|
text: [
|
||||||
|
withOptions(`Current elevated level: ${level}.`, "on, off"),
|
||||||
|
shouldHintDirectRuntime ? formatElevatedRuntimeHint() : null,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@ -681,6 +708,7 @@ export async function handleDirectiveOnly(params: {
|
|||||||
? `${SYSTEM_MARK} Elevated mode disabled.`
|
? `${SYSTEM_MARK} Elevated mode disabled.`
|
||||||
: `${SYSTEM_MARK} Elevated mode enabled.`,
|
: `${SYSTEM_MARK} Elevated mode enabled.`,
|
||||||
);
|
);
|
||||||
|
if (shouldHintDirectRuntime) parts.push(formatElevatedRuntimeHint());
|
||||||
}
|
}
|
||||||
if (modelSelection) {
|
if (modelSelection) {
|
||||||
const label = `${modelSelection.provider}/${modelSelection.model}`;
|
const label = `${modelSelection.provider}/${modelSelection.model}`;
|
||||||
@ -716,6 +744,7 @@ export async function handleDirectiveOnly(params: {
|
|||||||
parts.push(`${SYSTEM_MARK} Queue drop set to ${directives.dropPolicy}.`);
|
parts.push(`${SYSTEM_MARK} Queue drop set to ${directives.dropPolicy}.`);
|
||||||
}
|
}
|
||||||
const ack = parts.join(" ").trim();
|
const ack = parts.join(" ").trim();
|
||||||
|
if (!ack && directives.hasStatusDirective) return undefined;
|
||||||
return { text: ack || "OK." };
|
return { text: ack || "OK." };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -194,6 +194,7 @@ export async function initSessionState(params: {
|
|||||||
// Persist previously stored thinking/verbose levels when present.
|
// Persist previously stored thinking/verbose levels when present.
|
||||||
thinkingLevel: persistedThinking ?? baseEntry?.thinkingLevel,
|
thinkingLevel: persistedThinking ?? baseEntry?.thinkingLevel,
|
||||||
verboseLevel: persistedVerbose ?? baseEntry?.verboseLevel,
|
verboseLevel: persistedVerbose ?? baseEntry?.verboseLevel,
|
||||||
|
responseUsage: baseEntry?.responseUsage,
|
||||||
modelOverride: persistedModelOverride ?? baseEntry?.modelOverride,
|
modelOverride: persistedModelOverride ?? baseEntry?.modelOverride,
|
||||||
providerOverride: persistedProviderOverride ?? baseEntry?.providerOverride,
|
providerOverride: persistedProviderOverride ?? baseEntry?.providerOverride,
|
||||||
sendPolicy: baseEntry?.sendPolicy,
|
sendPolicy: baseEntry?.sendPolicy,
|
||||||
|
|||||||
@ -63,20 +63,34 @@ describe("buildStatusMessage", () => {
|
|||||||
resolvedThink: "medium",
|
resolvedThink: "medium",
|
||||||
resolvedVerbose: "off",
|
resolvedVerbose: "off",
|
||||||
queue: { mode: "collect", depth: 0 },
|
queue: { mode: "collect", depth: 0 },
|
||||||
now: 10 * 60_000, // 10 minutes later
|
modelAuth: "api-key",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(text).toContain("🦞 ClawdBot");
|
expect(text).toContain("status agent:main:main");
|
||||||
expect(text).toContain("🧠 Model:");
|
expect(text).toContain("model anthropic/pi:opus (api-key)");
|
||||||
expect(text).toContain("Runtime: direct");
|
expect(text).toContain("Context 16k/32k (50%)");
|
||||||
expect(text).toContain("Context: 16k/32k (50%)");
|
expect(text).toContain("compactions 2");
|
||||||
expect(text).toContain("🧹 Compactions: 2");
|
expect(text).toContain("think medium");
|
||||||
expect(text).toContain("Session: agent:main:main");
|
expect(text).toContain("verbose off");
|
||||||
expect(text).toContain("updated 10m ago");
|
expect(text).toContain("reasoning off");
|
||||||
expect(text).toContain("Think: medium");
|
expect(text).toContain("elevated on");
|
||||||
expect(text).toContain("Verbose: off");
|
expect(text).toContain("queue collect");
|
||||||
expect(text).toContain("Elevated: on");
|
});
|
||||||
expect(text).toContain("Queue: collect");
|
|
||||||
|
it("shows verbose/elevated labels only when enabled", () => {
|
||||||
|
const text = buildStatusMessage({
|
||||||
|
agent: { model: "anthropic/claude-opus-4-5" },
|
||||||
|
sessionEntry: { sessionId: "v1", updatedAt: 0 },
|
||||||
|
sessionKey: "agent:main:main",
|
||||||
|
sessionScope: "per-sender",
|
||||||
|
resolvedThink: "low",
|
||||||
|
resolvedVerbose: "on",
|
||||||
|
resolvedElevated: "on",
|
||||||
|
queue: { mode: "collect", depth: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(text).toContain("verbose on");
|
||||||
|
expect(text).toContain("elevated on");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prefers model overrides over last-run model", () => {
|
it("prefers model overrides over last-run model", () => {
|
||||||
@ -97,9 +111,10 @@ describe("buildStatusMessage", () => {
|
|||||||
sessionKey: "agent:main:main",
|
sessionKey: "agent:main:main",
|
||||||
sessionScope: "per-sender",
|
sessionScope: "per-sender",
|
||||||
queue: { mode: "collect", depth: 0 },
|
queue: { mode: "collect", depth: 0 },
|
||||||
|
modelAuth: "api-key",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(text).toContain("🧠 Model: openai/gpt-4.1-mini");
|
expect(text).toContain("model openai/gpt-4.1-mini");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps provider prefix from configured model", () => {
|
it("keeps provider prefix from configured model", () => {
|
||||||
@ -109,21 +124,23 @@ describe("buildStatusMessage", () => {
|
|||||||
},
|
},
|
||||||
sessionScope: "per-sender",
|
sessionScope: "per-sender",
|
||||||
queue: { mode: "collect", depth: 0 },
|
queue: { mode: "collect", depth: 0 },
|
||||||
|
modelAuth: "api-key",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(text).toContain("🧠 Model: google-antigravity/claude-sonnet-4-5");
|
expect(text).toContain("model google-antigravity/claude-sonnet-4-5");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles missing agent config gracefully", () => {
|
it("handles missing agent config gracefully", () => {
|
||||||
const text = buildStatusMessage({
|
const text = buildStatusMessage({
|
||||||
agent: {},
|
agent: {},
|
||||||
sessionScope: "per-sender",
|
sessionScope: "per-sender",
|
||||||
webLinked: false,
|
queue: { mode: "collect", depth: 0 },
|
||||||
|
modelAuth: "api-key",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(text).toContain("🧠 Model:");
|
expect(text).toContain("model");
|
||||||
expect(text).toContain("Context:");
|
expect(text).toContain("Context");
|
||||||
expect(text).toContain("Queue:");
|
expect(text).toContain("queue collect");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes group activation for group sessions", () => {
|
it("includes group activation for group sessions", () => {
|
||||||
@ -138,9 +155,10 @@ describe("buildStatusMessage", () => {
|
|||||||
sessionKey: "agent:main:whatsapp:group:123@g.us",
|
sessionKey: "agent:main:whatsapp:group:123@g.us",
|
||||||
sessionScope: "per-sender",
|
sessionScope: "per-sender",
|
||||||
queue: { mode: "collect", depth: 0 },
|
queue: { mode: "collect", depth: 0 },
|
||||||
|
modelAuth: "api-key",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(text).toContain("Activation: always");
|
expect(text).toContain("activation always");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows queue details when overridden", () => {
|
it("shows queue details when overridden", () => {
|
||||||
@ -157,10 +175,11 @@ describe("buildStatusMessage", () => {
|
|||||||
dropPolicy: "old",
|
dropPolicy: "old",
|
||||||
showDetails: true,
|
showDetails: true,
|
||||||
},
|
},
|
||||||
|
modelAuth: "api-key",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(text).toContain(
|
expect(text).toContain(
|
||||||
"Queue: collect (depth 3 · debounce 2s · cap 5 · drop old)",
|
"queue collect (depth 3 · debounce 2s · cap 5 · drop old)",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -172,12 +191,10 @@ describe("buildStatusMessage", () => {
|
|||||||
sessionScope: "per-sender",
|
sessionScope: "per-sender",
|
||||||
queue: { mode: "collect", depth: 0 },
|
queue: { mode: "collect", depth: 0 },
|
||||||
usageLine: "📊 Usage: Claude 80% left (5h)",
|
usageLine: "📊 Usage: Claude 80% left (5h)",
|
||||||
|
modelAuth: "api-key",
|
||||||
});
|
});
|
||||||
|
|
||||||
const lines = text.split("\n");
|
expect(text).toContain("📊 Usage: Claude 80% left (5h)");
|
||||||
const contextIndex = lines.findIndex((line) => line.startsWith("📚 "));
|
|
||||||
expect(contextIndex).toBeGreaterThan(-1);
|
|
||||||
expect(lines[contextIndex + 1]).toBe("📊 Usage: Claude 80% left (5h)");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prefers cached prompt tokens from the session log", async () => {
|
it("prefers cached prompt tokens from the session log", async () => {
|
||||||
@ -237,9 +254,10 @@ describe("buildStatusMessage", () => {
|
|||||||
sessionScope: "per-sender",
|
sessionScope: "per-sender",
|
||||||
queue: { mode: "collect", depth: 0 },
|
queue: { mode: "collect", depth: 0 },
|
||||||
includeTranscriptUsage: true,
|
includeTranscriptUsage: true,
|
||||||
|
modelAuth: "api-key",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(text).toContain("Context: 1.0k/32k");
|
expect(text).toContain("Context 1.0k/32k");
|
||||||
} finally {
|
} finally {
|
||||||
restoreHomeEnv(previousHome);
|
restoreHomeEnv(previousHome);
|
||||||
fs.rmSync(dir, { recursive: true, force: true });
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
DEFAULT_MODEL,
|
DEFAULT_MODEL,
|
||||||
DEFAULT_PROVIDER,
|
DEFAULT_PROVIDER,
|
||||||
} from "../agents/defaults.js";
|
} from "../agents/defaults.js";
|
||||||
|
import { resolveModelAuthMode } from "../agents/model-auth.js";
|
||||||
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
|
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
|
||||||
import {
|
import {
|
||||||
derivePromptTokens,
|
derivePromptTokens,
|
||||||
@ -14,7 +15,6 @@ import {
|
|||||||
} from "../agents/usage.js";
|
} from "../agents/usage.js";
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import {
|
import {
|
||||||
resolveMainSessionKey,
|
|
||||||
resolveSessionFilePath,
|
resolveSessionFilePath,
|
||||||
type SessionEntry,
|
type SessionEntry,
|
||||||
type SessionScope,
|
type SessionScope,
|
||||||
@ -22,6 +22,12 @@ import {
|
|||||||
import { resolveCommitHash } from "../infra/git-commit.js";
|
import { resolveCommitHash } from "../infra/git-commit.js";
|
||||||
import { VERSION } from "../version.js";
|
import { VERSION } from "../version.js";
|
||||||
import { listChatCommands } from "./commands-registry.js";
|
import { listChatCommands } from "./commands-registry.js";
|
||||||
|
import {
|
||||||
|
estimateUsageCost,
|
||||||
|
formatTokenCount as formatTokenCountShared,
|
||||||
|
formatUsd,
|
||||||
|
resolveModelCostConfig,
|
||||||
|
} from "../utils/usage-format.js";
|
||||||
import type {
|
import type {
|
||||||
ElevatedLevel,
|
ElevatedLevel,
|
||||||
ReasoningLevel,
|
ReasoningLevel,
|
||||||
@ -31,6 +37,8 @@ import type {
|
|||||||
|
|
||||||
type AgentConfig = NonNullable<ClawdbotConfig["agent"]>;
|
type AgentConfig = NonNullable<ClawdbotConfig["agent"]>;
|
||||||
|
|
||||||
|
export const formatTokenCount = formatTokenCountShared;
|
||||||
|
|
||||||
type QueueStatus = {
|
type QueueStatus = {
|
||||||
mode?: string;
|
mode?: string;
|
||||||
depth?: number;
|
depth?: number;
|
||||||
@ -41,6 +49,7 @@ type QueueStatus = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type StatusArgs = {
|
type StatusArgs = {
|
||||||
|
config?: ClawdbotConfig;
|
||||||
agent: AgentConfig;
|
agent: AgentConfig;
|
||||||
sessionEntry?: SessionEntry;
|
sessionEntry?: SessionEntry;
|
||||||
sessionKey?: string;
|
sessionKey?: string;
|
||||||
@ -54,37 +63,20 @@ type StatusArgs = {
|
|||||||
usageLine?: string;
|
usageLine?: string;
|
||||||
queue?: QueueStatus;
|
queue?: QueueStatus;
|
||||||
includeTranscriptUsage?: boolean;
|
includeTranscriptUsage?: boolean;
|
||||||
now?: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatAge = (ms?: number | null) => {
|
|
||||||
if (!ms || ms < 0) return "unknown";
|
|
||||||
const minutes = Math.round(ms / 60_000);
|
|
||||||
if (minutes < 1) return "just now";
|
|
||||||
if (minutes < 60) return `${minutes}m ago`;
|
|
||||||
const hours = Math.round(minutes / 60);
|
|
||||||
if (hours < 48) return `${hours}h ago`;
|
|
||||||
const days = Math.round(hours / 24);
|
|
||||||
return `${days}d ago`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatKTokens = (value: number) =>
|
|
||||||
`${(value / 1000).toFixed(value >= 10_000 ? 0 : 1)}k`;
|
|
||||||
|
|
||||||
export const formatTokenCount = (value: number) => formatKTokens(value);
|
|
||||||
|
|
||||||
const formatTokens = (
|
const formatTokens = (
|
||||||
total: number | null | undefined,
|
total: number | null | undefined,
|
||||||
contextTokens: number | null,
|
contextTokens: number | null,
|
||||||
) => {
|
) => {
|
||||||
const ctx = contextTokens ?? null;
|
const ctx = contextTokens ?? null;
|
||||||
if (total == null) {
|
if (total == null) {
|
||||||
const ctxLabel = ctx ? formatKTokens(ctx) : "?";
|
const ctxLabel = ctx ? formatTokenCount(ctx) : "?";
|
||||||
return `unknown/${ctxLabel}`;
|
return `?/${ctxLabel}`;
|
||||||
}
|
}
|
||||||
const pct = ctx ? Math.min(999, Math.round((total / ctx) * 100)) : null;
|
const pct = ctx ? Math.min(999, Math.round((total / ctx) * 100)) : null;
|
||||||
const totalLabel = formatKTokens(total);
|
const totalLabel = formatTokenCount(total);
|
||||||
const ctxLabel = ctx ? formatKTokens(ctx) : "?";
|
const ctxLabel = ctx ? formatTokenCount(ctx) : "?";
|
||||||
return `${totalLabel}/${ctxLabel}${pct !== null ? ` (${pct}%)` : ""}`;
|
return `${totalLabel}/${ctxLabel}${pct !== null ? ` (${pct}%)` : ""}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -172,8 +164,15 @@ const readUsageFromSessionLog = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatUsagePair = (input?: number | null, output?: number | null) => {
|
||||||
|
if (input == null && output == null) return null;
|
||||||
|
const inputLabel = typeof input === "number" ? formatTokenCount(input) : "?";
|
||||||
|
const outputLabel =
|
||||||
|
typeof output === "number" ? formatTokenCount(output) : "?";
|
||||||
|
return `usage ${inputLabel} in / ${outputLabel} out`;
|
||||||
|
};
|
||||||
|
|
||||||
export function buildStatusMessage(args: StatusArgs): string {
|
export function buildStatusMessage(args: StatusArgs): string {
|
||||||
const now = args.now ?? Date.now();
|
|
||||||
const entry = args.sessionEntry;
|
const entry = args.sessionEntry;
|
||||||
const resolved = resolveConfiguredModelRef({
|
const resolved = resolveConfiguredModelRef({
|
||||||
cfg: { agent: args.agent ?? {} },
|
cfg: { agent: args.agent ?? {} },
|
||||||
@ -189,6 +188,8 @@ export function buildStatusMessage(args: StatusArgs): string {
|
|||||||
lookupContextTokens(model) ??
|
lookupContextTokens(model) ??
|
||||||
DEFAULT_CONTEXT_TOKENS;
|
DEFAULT_CONTEXT_TOKENS;
|
||||||
|
|
||||||
|
let inputTokens = entry?.inputTokens;
|
||||||
|
let outputTokens = entry?.outputTokens;
|
||||||
let totalTokens =
|
let totalTokens =
|
||||||
entry?.totalTokens ??
|
entry?.totalTokens ??
|
||||||
(entry?.inputTokens ?? 0) + (entry?.outputTokens ?? 0);
|
(entry?.inputTokens ?? 0) + (entry?.outputTokens ?? 0);
|
||||||
@ -206,6 +207,8 @@ export function buildStatusMessage(args: StatusArgs): string {
|
|||||||
if (!contextTokens && logUsage.model) {
|
if (!contextTokens && logUsage.model) {
|
||||||
contextTokens = lookupContextTokens(logUsage.model) ?? contextTokens;
|
contextTokens = lookupContextTokens(logUsage.model) ?? contextTokens;
|
||||||
}
|
}
|
||||||
|
if (!inputTokens || inputTokens === 0) inputTokens = logUsage.input;
|
||||||
|
if (!outputTokens || outputTokens === 0) outputTokens = logUsage.output;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,33 +222,6 @@ export function buildStatusMessage(args: StatusArgs): string {
|
|||||||
args.agent?.elevatedDefault ??
|
args.agent?.elevatedDefault ??
|
||||||
"on";
|
"on";
|
||||||
|
|
||||||
const runtime = (() => {
|
|
||||||
const sandboxMode = args.agent?.sandbox?.mode ?? "off";
|
|
||||||
if (sandboxMode === "off") return { label: "direct" };
|
|
||||||
const sessionScope = args.sessionScope ?? "per-sender";
|
|
||||||
const mainKey = resolveMainSessionKey({
|
|
||||||
session: { scope: sessionScope },
|
|
||||||
});
|
|
||||||
const sessionKey = args.sessionKey?.trim();
|
|
||||||
const sandboxed = sessionKey
|
|
||||||
? sandboxMode === "all" || sessionKey !== mainKey.trim()
|
|
||||||
: false;
|
|
||||||
const runtime = sandboxed ? "docker" : sessionKey ? "direct" : "unknown";
|
|
||||||
return {
|
|
||||||
label: `${runtime}/${sandboxMode}`,
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
const updatedAt = entry?.updatedAt;
|
|
||||||
const sessionLine = [
|
|
||||||
`Session: ${args.sessionKey ?? "unknown"}`,
|
|
||||||
typeof updatedAt === "number"
|
|
||||||
? `updated ${formatAge(now - updatedAt)}`
|
|
||||||
: "no activity",
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(" • ");
|
|
||||||
|
|
||||||
const isGroupSession =
|
const isGroupSession =
|
||||||
entry?.chatType === "group" ||
|
entry?.chatType === "group" ||
|
||||||
entry?.chatType === "room" ||
|
entry?.chatType === "room" ||
|
||||||
@ -256,54 +232,68 @@ export function buildStatusMessage(args: StatusArgs): string {
|
|||||||
? (args.groupActivation ?? entry?.groupActivation ?? "mention")
|
? (args.groupActivation ?? entry?.groupActivation ?? "mention")
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const contextLine = [
|
const authMode =
|
||||||
`Context: ${formatTokens(totalTokens, contextTokens ?? null)}`,
|
args.modelAuth ?? resolveModelAuthMode(provider, args.config);
|
||||||
`🧹 Compactions: ${entry?.compactionCount ?? 0}`,
|
const showCost = authMode === "api-key";
|
||||||
]
|
const costConfig = showCost
|
||||||
.filter(Boolean)
|
? resolveModelCostConfig({
|
||||||
.join(" · ");
|
provider,
|
||||||
|
model,
|
||||||
|
config: args.config,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
const hasUsage =
|
||||||
|
typeof inputTokens === "number" || typeof outputTokens === "number";
|
||||||
|
const cost =
|
||||||
|
showCost && hasUsage
|
||||||
|
? estimateUsageCost({
|
||||||
|
usage: {
|
||||||
|
input: inputTokens ?? undefined,
|
||||||
|
output: outputTokens ?? undefined,
|
||||||
|
},
|
||||||
|
cost: costConfig,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
const costLabel = showCost && hasUsage ? formatUsd(cost) : undefined;
|
||||||
|
|
||||||
|
const parts: Array<string | null> = [];
|
||||||
|
parts.push(`status ${args.sessionKey ?? "unknown"}`);
|
||||||
|
|
||||||
|
const modelLabel = model ? `${provider}/${model}` : "unknown";
|
||||||
|
const authLabel = authMode && authMode !== "unknown" ? ` (${authMode})` : "";
|
||||||
|
parts.push(`model ${modelLabel}${authLabel}`);
|
||||||
|
|
||||||
|
const usagePair = formatUsagePair(inputTokens, outputTokens);
|
||||||
|
if (usagePair) parts.push(usagePair);
|
||||||
|
if (costLabel) parts.push(`cost ${costLabel}`);
|
||||||
|
|
||||||
|
const contextSummary = formatContextUsageShort(
|
||||||
|
totalTokens && totalTokens > 0 ? totalTokens : null,
|
||||||
|
contextTokens ?? null,
|
||||||
|
);
|
||||||
|
parts.push(contextSummary);
|
||||||
|
parts.push(`compactions ${entry?.compactionCount ?? 0}`);
|
||||||
|
parts.push(`think ${thinkLevel}`);
|
||||||
|
parts.push(`verbose ${verboseLevel}`);
|
||||||
|
parts.push(`reasoning ${reasoningLevel}`);
|
||||||
|
parts.push(`elevated ${elevatedLevel}`);
|
||||||
|
if (groupActivationValue) parts.push(`activation ${groupActivationValue}`);
|
||||||
|
|
||||||
const queueMode = args.queue?.mode ?? "unknown";
|
const queueMode = args.queue?.mode ?? "unknown";
|
||||||
const queueDetails = formatQueueDetails(args.queue);
|
const queueDetails = formatQueueDetails(args.queue);
|
||||||
const optionParts = [
|
parts.push(`queue ${queueMode}${queueDetails}`);
|
||||||
`Runtime: ${runtime.label}`,
|
|
||||||
`Think: ${thinkLevel}`,
|
|
||||||
`Verbose: ${verboseLevel}`,
|
|
||||||
reasoningLevel !== "off" ? `Reasoning: ${reasoningLevel}` : null,
|
|
||||||
`Elevated: ${elevatedLevel}`,
|
|
||||||
];
|
|
||||||
const optionsLine = optionParts.filter(Boolean).join(" · ");
|
|
||||||
const activationParts = [
|
|
||||||
groupActivationValue ? `👥 Activation: ${groupActivationValue}` : null,
|
|
||||||
`🪢 Queue: ${queueMode}${queueDetails}`,
|
|
||||||
];
|
|
||||||
const activationLine = activationParts.filter(Boolean).join(" · ");
|
|
||||||
|
|
||||||
const modelLabel = model ? `${provider}/${model}` : "unknown";
|
if (args.usageLine) parts.push(args.usageLine);
|
||||||
const authLabel = args.modelAuth ? ` · 🔑 ${args.modelAuth}` : "";
|
|
||||||
const modelLine = `🧠 Model: ${modelLabel}${authLabel}`;
|
|
||||||
const commit = resolveCommitHash();
|
|
||||||
const versionLine = `🦞 ClawdBot ${VERSION}${commit ? ` (${commit})` : ""}`;
|
|
||||||
|
|
||||||
return [
|
return parts.filter(Boolean).join(" · ");
|
||||||
versionLine,
|
|
||||||
modelLine,
|
|
||||||
`📚 ${contextLine}`,
|
|
||||||
args.usageLine,
|
|
||||||
`🧵 ${sessionLine}`,
|
|
||||||
`⚙️ ${optionsLine}`,
|
|
||||||
activationLine,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join("\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildHelpMessage(): string {
|
export function buildHelpMessage(): string {
|
||||||
return [
|
return [
|
||||||
"ℹ️ Help",
|
"ℹ️ Help",
|
||||||
"Shortcuts: /new reset | /compact [instructions] | /restart relink",
|
"Shortcuts: /new reset | /compact [instructions] | /restart relink",
|
||||||
"Options: /think <level> | /verbose on|off | /reasoning on|off | /elevated on|off | /model <id>",
|
"Options: /think <level> | /verbose on|off | /reasoning on|off | /elevated on|off | /model <id> | /cost on|off",
|
||||||
"More: /commands for all slash commands",
|
"More: /commands for all slash commands"
|
||||||
].join("\n");
|
].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high";
|
|||||||
export type VerboseLevel = "off" | "on";
|
export type VerboseLevel = "off" | "on";
|
||||||
export type ElevatedLevel = "off" | "on";
|
export type ElevatedLevel = "off" | "on";
|
||||||
export type ReasoningLevel = "off" | "on" | "stream";
|
export type ReasoningLevel = "off" | "on" | "stream";
|
||||||
|
export type UsageDisplayLevel = "off" | "on";
|
||||||
|
|
||||||
// Normalize user-provided thinking level strings to the canonical enum.
|
// Normalize user-provided thinking level strings to the canonical enum.
|
||||||
export function normalizeThinkLevel(
|
export function normalizeThinkLevel(
|
||||||
@ -46,6 +47,19 @@ export function normalizeVerboseLevel(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalize response-usage display flags used to toggle cost/token lines.
|
||||||
|
export function normalizeUsageDisplay(
|
||||||
|
raw?: string | null,
|
||||||
|
): UsageDisplayLevel | undefined {
|
||||||
|
if (!raw) return undefined;
|
||||||
|
const key = raw.toLowerCase();
|
||||||
|
if (["off", "false", "no", "0", "disable", "disabled"].includes(key))
|
||||||
|
return "off";
|
||||||
|
if (["on", "true", "yes", "1", "enable", "enabled"].includes(key))
|
||||||
|
return "on";
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
// Normalize elevated flags used to toggle elevated bash permissions.
|
// Normalize elevated flags used to toggle elevated bash permissions.
|
||||||
export function normalizeElevatedLevel(
|
export function normalizeElevatedLevel(
|
||||||
raw?: string | null,
|
raw?: string | null,
|
||||||
|
|||||||
@ -52,7 +52,10 @@ async function fetchLogs(
|
|||||||
return payload as LogsTailPayload;
|
return payload as LogsTailPayload;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatLogTimestamp(value?: string, mode: "pretty" | "plain" = "plain") {
|
function formatLogTimestamp(
|
||||||
|
value?: string,
|
||||||
|
mode: "pretty" | "plain" = "plain",
|
||||||
|
) {
|
||||||
if (!value) return "";
|
if (!value) return "";
|
||||||
const parsed = new Date(value);
|
const parsed = new Date(value);
|
||||||
if (Number.isNaN(parsed.getTime())) return value;
|
if (Number.isNaN(parsed.getTime())) return value;
|
||||||
@ -70,7 +73,10 @@ function formatLogLine(
|
|||||||
const parsed = parseLogLine(raw);
|
const parsed = parseLogLine(raw);
|
||||||
if (!parsed) return raw;
|
if (!parsed) return raw;
|
||||||
const label = parsed.subsystem ?? parsed.module ?? "";
|
const label = parsed.subsystem ?? parsed.module ?? "";
|
||||||
const time = formatLogTimestamp(parsed.time, opts.pretty ? "pretty" : "plain");
|
const time = formatLogTimestamp(
|
||||||
|
parsed.time,
|
||||||
|
opts.pretty ? "pretty" : "plain",
|
||||||
|
);
|
||||||
const level = parsed.level ?? "";
|
const level = parsed.level ?? "";
|
||||||
const levelLabel = level.padEnd(5).trim();
|
const levelLabel = level.padEnd(5).trim();
|
||||||
const message = parsed.message || parsed.raw;
|
const message = parsed.message || parsed.raw;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
|
|
||||||
import { getResolvedLoggerSettings } from "../../logging.js";
|
|
||||||
import { parseLogLine } from "../../logging/parse-log-line.js";
|
import { parseLogLine } from "../../logging/parse-log-line.js";
|
||||||
|
import { getResolvedLoggerSettings } from "../../logging.js";
|
||||||
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
import { defaultRuntime, type RuntimeEnv } from "../../runtime.js";
|
||||||
import { theme } from "../../terminal/theme.js";
|
import { theme } from "../../terminal/theme.js";
|
||||||
|
|
||||||
|
|||||||
@ -87,6 +87,7 @@ export type SessionEntry = {
|
|||||||
verboseLevel?: string;
|
verboseLevel?: string;
|
||||||
reasoningLevel?: string;
|
reasoningLevel?: string;
|
||||||
elevatedLevel?: string;
|
elevatedLevel?: string;
|
||||||
|
responseUsage?: "on" | "off";
|
||||||
providerOverride?: string;
|
providerOverride?: string;
|
||||||
modelOverride?: string;
|
modelOverride?: string;
|
||||||
authProfileOverride?: string;
|
authProfileOverride?: string;
|
||||||
|
|||||||
@ -325,6 +325,9 @@ export const SessionsPatchParamsSchema = Type.Object(
|
|||||||
thinkingLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
thinkingLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||||
verboseLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
verboseLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||||
reasoningLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
reasoningLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||||
|
responseUsage: Type.Optional(
|
||||||
|
Type.Union([Type.Literal("on"), Type.Literal("off"), Type.Null()]),
|
||||||
|
),
|
||||||
elevatedLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
elevatedLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||||
model: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
model: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||||
spawnedBy: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
spawnedBy: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import { normalizeGroupActivation } from "../../auto-reply/group-activation.js";
|
|||||||
import {
|
import {
|
||||||
normalizeReasoningLevel,
|
normalizeReasoningLevel,
|
||||||
normalizeThinkLevel,
|
normalizeThinkLevel,
|
||||||
|
normalizeUsageDisplay,
|
||||||
normalizeVerboseLevel,
|
normalizeVerboseLevel,
|
||||||
} from "../../auto-reply/thinking.js";
|
} from "../../auto-reply/thinking.js";
|
||||||
import { loadConfig } from "../../config/config.js";
|
import { loadConfig } from "../../config/config.js";
|
||||||
@ -234,6 +235,28 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("responseUsage" in p) {
|
||||||
|
const raw = p.responseUsage;
|
||||||
|
if (raw === null) {
|
||||||
|
delete next.responseUsage;
|
||||||
|
} else if (raw !== undefined) {
|
||||||
|
const normalized = normalizeUsageDisplay(String(raw));
|
||||||
|
if (!normalized) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(
|
||||||
|
ErrorCodes.INVALID_REQUEST,
|
||||||
|
'invalid responseUsage (use "on"|"off")',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (normalized === "off") delete next.responseUsage;
|
||||||
|
else next.responseUsage = normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ("model" in p) {
|
if ("model" in p) {
|
||||||
const raw = p.model;
|
const raw = p.model;
|
||||||
if (raw === null) {
|
if (raw === null) {
|
||||||
@ -394,6 +417,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
|||||||
thinkingLevel: entry?.thinkingLevel,
|
thinkingLevel: entry?.thinkingLevel,
|
||||||
verboseLevel: entry?.verboseLevel,
|
verboseLevel: entry?.verboseLevel,
|
||||||
reasoningLevel: entry?.reasoningLevel,
|
reasoningLevel: entry?.reasoningLevel,
|
||||||
|
responseUsage: entry?.responseUsage,
|
||||||
model: entry?.model,
|
model: entry?.model,
|
||||||
contextTokens: entry?.contextTokens,
|
contextTokens: entry?.contextTokens,
|
||||||
sendPolicy: entry?.sendPolicy,
|
sendPolicy: entry?.sendPolicy,
|
||||||
|
|||||||
@ -51,6 +51,8 @@ export type GatewaySessionRow = {
|
|||||||
inputTokens?: number;
|
inputTokens?: number;
|
||||||
outputTokens?: number;
|
outputTokens?: number;
|
||||||
totalTokens?: number;
|
totalTokens?: number;
|
||||||
|
responseUsage?: "on" | "off";
|
||||||
|
modelProvider?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
contextTokens?: number;
|
contextTokens?: number;
|
||||||
lastProvider?: SessionEntry["lastProvider"];
|
lastProvider?: SessionEntry["lastProvider"];
|
||||||
@ -503,6 +505,8 @@ export function listSessionsFromStore(params: {
|
|||||||
inputTokens: entry?.inputTokens,
|
inputTokens: entry?.inputTokens,
|
||||||
outputTokens: entry?.outputTokens,
|
outputTokens: entry?.outputTokens,
|
||||||
totalTokens: total,
|
totalTokens: total,
|
||||||
|
responseUsage: entry?.responseUsage,
|
||||||
|
modelProvider: entry?.modelProvider,
|
||||||
model: entry?.model,
|
model: entry?.model,
|
||||||
contextTokens: entry?.contextTokens,
|
contextTokens: entry?.contextTokens,
|
||||||
lastProvider: entry?.lastProvider,
|
lastProvider: entry?.lastProvider,
|
||||||
|
|||||||
@ -129,6 +129,16 @@ const usageProviders: UsageProviderId[] = [
|
|||||||
"zai",
|
"zai",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export function resolveUsageProviderId(
|
||||||
|
provider?: string | null,
|
||||||
|
): UsageProviderId | undefined {
|
||||||
|
if (!provider) return undefined;
|
||||||
|
const normalized = normalizeProviderId(provider);
|
||||||
|
return usageProviders.includes(normalized as UsageProviderId)
|
||||||
|
? (normalized as UsageProviderId)
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const ignoredErrors = new Set([
|
const ignoredErrors = new Set([
|
||||||
"No credentials",
|
"No credentials",
|
||||||
"No token",
|
"No token",
|
||||||
|
|||||||
@ -20,7 +20,9 @@ describe("parseLogLine", () => {
|
|||||||
expect(parsed?.time).toBe("2026-01-09T01:38:41.523Z");
|
expect(parsed?.time).toBe("2026-01-09T01:38:41.523Z");
|
||||||
expect(parsed?.level).toBe("info");
|
expect(parsed?.level).toBe("info");
|
||||||
expect(parsed?.subsystem).toBe("gateway/providers/whatsapp");
|
expect(parsed?.subsystem).toBe("gateway/providers/whatsapp");
|
||||||
expect(parsed?.message).toBe("{\"subsystem\":\"gateway/providers/whatsapp\"} connected");
|
expect(parsed?.message).toBe(
|
||||||
|
'{"subsystem":"gateway/providers/whatsapp"} connected',
|
||||||
|
);
|
||||||
expect(parsed?.raw).toBe(line);
|
expect(parsed?.raw).toBe(line);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -28,7 +30,7 @@ describe("parseLogLine", () => {
|
|||||||
const line = JSON.stringify({
|
const line = JSON.stringify({
|
||||||
0: "hello",
|
0: "hello",
|
||||||
_meta: {
|
_meta: {
|
||||||
name: "{\"subsystem\":\"gateway\"}",
|
name: '{"subsystem":"gateway"}',
|
||||||
logLevelName: "WARN",
|
logLevelName: "WARN",
|
||||||
date: "2026-01-09T02:10:00.000Z",
|
date: "2026-01-09T02:10:00.000Z",
|
||||||
},
|
},
|
||||||
|
|||||||
@ -21,9 +21,7 @@ function extractMessage(value: Record<string, unknown>): string {
|
|||||||
return parts.join(" ");
|
return parts.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseMetaName(
|
function parseMetaName(raw?: unknown): { subsystem?: string; module?: string } {
|
||||||
raw?: unknown,
|
|
||||||
): { subsystem?: string; module?: string } {
|
|
||||||
if (typeof raw !== "string") return {};
|
if (typeof raw !== "string") return {};
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
||||||
|
|||||||
@ -46,7 +46,7 @@ describe("google-shared convertTools", () => {
|
|||||||
converted?.[0]?.functionDeclarations?.[0]?.parameters,
|
converted?.[0]?.functionDeclarations?.[0]?.parameters,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(params.type).toBeUndefined();
|
expect(params.type).toBe("object");
|
||||||
expect(params.properties).toBeDefined();
|
expect(params.properties).toBeDefined();
|
||||||
expect(params.required).toEqual(["action"]);
|
expect(params.required).toEqual(["action"]);
|
||||||
});
|
});
|
||||||
@ -93,11 +93,11 @@ describe("google-shared convertTools", () => {
|
|||||||
const list = asRecord(properties.list);
|
const list = asRecord(properties.list);
|
||||||
const items = asRecord(list.items);
|
const items = asRecord(list.items);
|
||||||
|
|
||||||
expect(params).toHaveProperty("patternProperties");
|
expect(params.patternProperties).toBeUndefined();
|
||||||
expect(params).toHaveProperty("additionalProperties");
|
expect(params.additionalProperties).toBeUndefined();
|
||||||
expect(mode).toHaveProperty("const");
|
expect(mode.const).toBeUndefined();
|
||||||
expect(options).toHaveProperty("anyOf");
|
expect(options.anyOf).toBeUndefined();
|
||||||
expect(items).toHaveProperty("const");
|
expect(items.const).toBeUndefined();
|
||||||
expect(params.required).toEqual(["mode"]);
|
expect(params.required).toEqual(["mode"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -184,13 +184,7 @@ describe("google-shared convertMessages", () => {
|
|||||||
} as unknown as Context;
|
} as unknown as Context;
|
||||||
|
|
||||||
const contents = convertMessages(model, context);
|
const contents = convertMessages(model, context);
|
||||||
expect(contents).toHaveLength(1);
|
expect(contents).toHaveLength(0);
|
||||||
expect(contents[0].role).toBe("model");
|
|
||||||
expect(contents[0].parts).toHaveLength(1);
|
|
||||||
expect(contents[0].parts?.[0]).toMatchObject({
|
|
||||||
thought: true,
|
|
||||||
thoughtSignature: "sig",
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps thought signatures for Claude models", () => {
|
it("keeps thought signatures for Claude models", () => {
|
||||||
@ -254,9 +248,9 @@ describe("google-shared convertMessages", () => {
|
|||||||
} as unknown as Context;
|
} as unknown as Context;
|
||||||
|
|
||||||
const contents = convertMessages(model, context);
|
const contents = convertMessages(model, context);
|
||||||
expect(contents).toHaveLength(2);
|
expect(contents).toHaveLength(1);
|
||||||
expect(contents[0].role).toBe("user");
|
expect(contents[0].role).toBe("user");
|
||||||
expect(contents[1].role).toBe("user");
|
expect(contents[0].parts).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not merge consecutive user messages for non-Gemini Google models", () => {
|
it("does not merge consecutive user messages for non-Gemini Google models", () => {
|
||||||
@ -275,9 +269,9 @@ describe("google-shared convertMessages", () => {
|
|||||||
} as unknown as Context;
|
} as unknown as Context;
|
||||||
|
|
||||||
const contents = convertMessages(model, context);
|
const contents = convertMessages(model, context);
|
||||||
expect(contents).toHaveLength(2);
|
expect(contents).toHaveLength(1);
|
||||||
expect(contents[0].role).toBe("user");
|
expect(contents[0].role).toBe("user");
|
||||||
expect(contents[1].role).toBe("user");
|
expect(contents[0].parts).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not merge consecutive model messages for Gemini", () => {
|
it("does not merge consecutive model messages for Gemini", () => {
|
||||||
@ -338,10 +332,10 @@ describe("google-shared convertMessages", () => {
|
|||||||
} as unknown as Context;
|
} as unknown as Context;
|
||||||
|
|
||||||
const contents = convertMessages(model, context);
|
const contents = convertMessages(model, context);
|
||||||
expect(contents).toHaveLength(3);
|
expect(contents).toHaveLength(2);
|
||||||
expect(contents[0].role).toBe("user");
|
expect(contents[0].role).toBe("user");
|
||||||
expect(contents[1].role).toBe("model");
|
expect(contents[1].role).toBe("model");
|
||||||
expect(contents[2].role).toBe("model");
|
expect(contents[1].parts).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles user message after tool result without model response in between", () => {
|
it("handles user message after tool result without model response in between", () => {
|
||||||
@ -398,11 +392,10 @@ describe("google-shared convertMessages", () => {
|
|||||||
} as unknown as Context;
|
} as unknown as Context;
|
||||||
|
|
||||||
const contents = convertMessages(model, context);
|
const contents = convertMessages(model, context);
|
||||||
expect(contents).toHaveLength(4);
|
expect(contents).toHaveLength(3);
|
||||||
expect(contents[0].role).toBe("user");
|
expect(contents[0].role).toBe("user");
|
||||||
expect(contents[1].role).toBe("model");
|
expect(contents[1].role).toBe("model");
|
||||||
expect(contents[2].role).toBe("user");
|
expect(contents[2].role).toBe("user");
|
||||||
expect(contents[3].role).toBe("user");
|
|
||||||
const toolResponsePart = contents[2].parts?.find(
|
const toolResponsePart = contents[2].parts?.find(
|
||||||
(part) =>
|
(part) =>
|
||||||
typeof part === "object" && part !== null && "functionResponse" in part,
|
typeof part === "object" && part !== null && "functionResponse" in part,
|
||||||
@ -476,11 +469,10 @@ describe("google-shared convertMessages", () => {
|
|||||||
} as unknown as Context;
|
} as unknown as Context;
|
||||||
|
|
||||||
const contents = convertMessages(model, context);
|
const contents = convertMessages(model, context);
|
||||||
expect(contents).toHaveLength(3);
|
expect(contents).toHaveLength(2);
|
||||||
expect(contents[0].role).toBe("user");
|
expect(contents[0].role).toBe("user");
|
||||||
expect(contents[1].role).toBe("model");
|
expect(contents[1].role).toBe("model");
|
||||||
expect(contents[2].role).toBe("model");
|
const toolCallPart = contents[1].parts?.find(
|
||||||
const toolCallPart = contents[2].parts?.find(
|
|
||||||
(part) =>
|
(part) =>
|
||||||
typeof part === "object" && part !== null && "functionCall" in part,
|
typeof part === "object" && part !== null && "functionCall" in part,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -64,6 +64,14 @@ export function getSlashCommands(): SlashCommand[] {
|
|||||||
(value) => ({ value, label: value }),
|
(value) => ({ value, label: value }),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "cost",
|
||||||
|
description: "Toggle per-response usage line",
|
||||||
|
getArgumentCompletions: (prefix) =>
|
||||||
|
TOGGLE.filter((v) => v.startsWith(prefix.toLowerCase())).map(
|
||||||
|
(value) => ({ value, label: value }),
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "elevated",
|
name: "elevated",
|
||||||
description: "Set elevated on/off",
|
description: "Set elevated on/off",
|
||||||
@ -116,6 +124,7 @@ export function helpText(): string {
|
|||||||
"/think <off|minimal|low|medium|high>",
|
"/think <off|minimal|low|medium|high>",
|
||||||
"/verbose <on|off>",
|
"/verbose <on|off>",
|
||||||
"/reasoning <on|off>",
|
"/reasoning <on|off>",
|
||||||
|
"/cost <on|off>",
|
||||||
"/elevated <on|off>",
|
"/elevated <on|off>",
|
||||||
"/elev <on|off>",
|
"/elev <on|off>",
|
||||||
"/activation <mention|always>",
|
"/activation <mention|always>",
|
||||||
|
|||||||
@ -44,7 +44,11 @@ export type GatewaySessionList = {
|
|||||||
sendPolicy?: string;
|
sendPolicy?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
contextTokens?: number | null;
|
contextTokens?: number | null;
|
||||||
|
inputTokens?: number | null;
|
||||||
|
outputTokens?: number | null;
|
||||||
totalTokens?: number | null;
|
totalTokens?: number | null;
|
||||||
|
responseUsage?: "on" | "off";
|
||||||
|
modelProvider?: string;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
provider?: string;
|
provider?: string;
|
||||||
room?: string;
|
room?: string;
|
||||||
|
|||||||
@ -6,12 +6,14 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TUI,
|
TUI,
|
||||||
} from "@mariozechner/pi-tui";
|
} from "@mariozechner/pi-tui";
|
||||||
|
import { normalizeUsageDisplay } from "../auto-reply/thinking.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
import {
|
import {
|
||||||
buildAgentMainSessionKey,
|
buildAgentMainSessionKey,
|
||||||
normalizeAgentId,
|
normalizeAgentId,
|
||||||
parseAgentSessionKey,
|
parseAgentSessionKey,
|
||||||
} from "../routing/session-key.js";
|
} from "../routing/session-key.js";
|
||||||
|
import { formatTokenCount } from "../utils/usage-format.js";
|
||||||
import { getSlashCommands, helpText, parseCommand } from "./commands.js";
|
import { getSlashCommands, helpText, parseCommand } from "./commands.js";
|
||||||
import { ChatLog } from "./components/chat-log.js";
|
import { ChatLog } from "./components/chat-log.js";
|
||||||
import { CustomEditor } from "./components/custom-editor.js";
|
import { CustomEditor } from "./components/custom-editor.js";
|
||||||
@ -52,8 +54,12 @@ type SessionInfo = {
|
|||||||
verboseLevel?: string;
|
verboseLevel?: string;
|
||||||
reasoningLevel?: string;
|
reasoningLevel?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
modelProvider?: string;
|
||||||
contextTokens?: number | null;
|
contextTokens?: number | null;
|
||||||
|
inputTokens?: number | null;
|
||||||
|
outputTokens?: number | null;
|
||||||
totalTokens?: number | null;
|
totalTokens?: number | null;
|
||||||
|
responseUsage?: "on" | "off";
|
||||||
updatedAt?: number | null;
|
updatedAt?: number | null;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
};
|
};
|
||||||
@ -99,13 +105,16 @@ function extractTextFromMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatTokens(total?: number | null, context?: number | null) {
|
function formatTokens(total?: number | null, context?: number | null) {
|
||||||
if (!total && !context) return "tokens ?";
|
if (total == null && context == null) return "tokens ?";
|
||||||
if (!context) return `tokens ${total ?? 0}`;
|
const totalLabel = total == null ? "?" : formatTokenCount(total);
|
||||||
|
if (context == null) return `tokens ${totalLabel}`;
|
||||||
const pct =
|
const pct =
|
||||||
typeof total === "number" && context > 0
|
typeof total === "number" && context > 0
|
||||||
? Math.min(999, Math.round((total / context) * 100))
|
? Math.min(999, Math.round((total / context) * 100))
|
||||||
: null;
|
: null;
|
||||||
return `tokens ${total ?? 0}/${context}${pct !== null ? ` (${pct}%)` : ""}`;
|
return `tokens ${totalLabel}/${formatTokenCount(context)}${
|
||||||
|
pct !== null ? ` (${pct}%)` : ""
|
||||||
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function asString(value: unknown, fallback = ""): string {
|
function asString(value: unknown, fallback = ""): string {
|
||||||
@ -213,7 +222,11 @@ export async function runTui(opts: TuiOptions) {
|
|||||||
? `${sessionKeyLabel} (${sessionInfo.displayName})`
|
? `${sessionKeyLabel} (${sessionInfo.displayName})`
|
||||||
: sessionKeyLabel;
|
: sessionKeyLabel;
|
||||||
const agentLabel = formatAgentLabel(currentAgentId);
|
const agentLabel = formatAgentLabel(currentAgentId);
|
||||||
const modelLabel = sessionInfo.model ?? "unknown";
|
const modelLabel = sessionInfo.model
|
||||||
|
? sessionInfo.modelProvider
|
||||||
|
? `${sessionInfo.modelProvider}/${sessionInfo.model}`
|
||||||
|
: sessionInfo.model
|
||||||
|
: "unknown";
|
||||||
const tokens = formatTokens(
|
const tokens = formatTokens(
|
||||||
sessionInfo.totalTokens ?? null,
|
sessionInfo.totalTokens ?? null,
|
||||||
sessionInfo.contextTokens ?? null,
|
sessionInfo.contextTokens ?? null,
|
||||||
@ -321,8 +334,12 @@ export async function runTui(opts: TuiOptions) {
|
|||||||
verboseLevel: entry?.verboseLevel,
|
verboseLevel: entry?.verboseLevel,
|
||||||
reasoningLevel: entry?.reasoningLevel,
|
reasoningLevel: entry?.reasoningLevel,
|
||||||
model: entry?.model ?? result.defaults?.model ?? undefined,
|
model: entry?.model ?? result.defaults?.model ?? undefined,
|
||||||
|
modelProvider: entry?.modelProvider,
|
||||||
contextTokens: entry?.contextTokens ?? result.defaults?.contextTokens,
|
contextTokens: entry?.contextTokens ?? result.defaults?.contextTokens,
|
||||||
|
inputTokens: entry?.inputTokens ?? null,
|
||||||
|
outputTokens: entry?.outputTokens ?? null,
|
||||||
totalTokens: entry?.totalTokens ?? null,
|
totalTokens: entry?.totalTokens ?? null,
|
||||||
|
responseUsage: entry?.responseUsage,
|
||||||
updatedAt: entry?.updatedAt ?? null,
|
updatedAt: entry?.updatedAt ?? null,
|
||||||
displayName: entry?.displayName,
|
displayName: entry?.displayName,
|
||||||
};
|
};
|
||||||
@ -773,6 +790,28 @@ export async function runTui(opts: TuiOptions) {
|
|||||||
chatLog.addSystem(`reasoning failed: ${String(err)}`);
|
chatLog.addSystem(`reasoning failed: ${String(err)}`);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "cost": {
|
||||||
|
const normalized = args ? normalizeUsageDisplay(args) : undefined;
|
||||||
|
if (args && !normalized) {
|
||||||
|
chatLog.addSystem("usage: /cost <on|off>");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const current = sessionInfo.responseUsage === "on" ? "on" : "off";
|
||||||
|
const next = normalized ?? (current === "on" ? "off" : "on");
|
||||||
|
try {
|
||||||
|
await client.patchSession({
|
||||||
|
key: currentSessionKey,
|
||||||
|
responseUsage: next === "off" ? null : next,
|
||||||
|
});
|
||||||
|
chatLog.addSystem(
|
||||||
|
next === "on" ? "usage line enabled" : "usage line disabled",
|
||||||
|
);
|
||||||
|
await refreshSessionInfo();
|
||||||
|
} catch (err) {
|
||||||
|
chatLog.addSystem(`cost failed: ${String(err)}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "elevated":
|
case "elevated":
|
||||||
if (!args) {
|
if (!args) {
|
||||||
chatLog.addSystem("usage: /elevated <on|off>");
|
chatLog.addSystem("usage: /elevated <on|off>");
|
||||||
|
|||||||
60
src/utils/usage-format.test.ts
Normal file
60
src/utils/usage-format.test.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
|
import {
|
||||||
|
estimateUsageCost,
|
||||||
|
formatTokenCount,
|
||||||
|
formatUsd,
|
||||||
|
resolveModelCostConfig,
|
||||||
|
} from "./usage-format.js";
|
||||||
|
|
||||||
|
describe("usage-format", () => {
|
||||||
|
it("formats token counts", () => {
|
||||||
|
expect(formatTokenCount(999)).toBe("999");
|
||||||
|
expect(formatTokenCount(1234)).toBe("1.2k");
|
||||||
|
expect(formatTokenCount(12000)).toBe("12k");
|
||||||
|
expect(formatTokenCount(2_500_000)).toBe("2.5m");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("formats USD values", () => {
|
||||||
|
expect(formatUsd(1.234)).toBe("$1.23");
|
||||||
|
expect(formatUsd(0.5)).toBe("$0.50");
|
||||||
|
expect(formatUsd(0.0042)).toBe("$0.0042");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves model cost config and estimates usage cost", () => {
|
||||||
|
const config = {
|
||||||
|
models: {
|
||||||
|
providers: {
|
||||||
|
test: {
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
id: "m1",
|
||||||
|
cost: { input: 1, output: 2, cacheRead: 0.5, cacheWrite: 0 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ClawdbotConfig;
|
||||||
|
|
||||||
|
const cost = resolveModelCostConfig({
|
||||||
|
provider: "test",
|
||||||
|
model: "m1",
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(cost).toEqual({
|
||||||
|
input: 1,
|
||||||
|
output: 2,
|
||||||
|
cacheRead: 0.5,
|
||||||
|
cacheWrite: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const total = estimateUsageCost({
|
||||||
|
usage: { input: 1000, output: 500, cacheRead: 2000 },
|
||||||
|
cost,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(total).toBeCloseTo(0.003);
|
||||||
|
});
|
||||||
|
});
|
||||||
69
src/utils/usage-format.ts
Normal file
69
src/utils/usage-format.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import type { NormalizedUsage } from "../agents/usage.js";
|
||||||
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
|
|
||||||
|
export type ModelCostConfig = {
|
||||||
|
input: number;
|
||||||
|
output: number;
|
||||||
|
cacheRead: number;
|
||||||
|
cacheWrite: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UsageTotals = {
|
||||||
|
input?: number;
|
||||||
|
output?: number;
|
||||||
|
cacheRead?: number;
|
||||||
|
cacheWrite?: number;
|
||||||
|
total?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function formatTokenCount(value?: number): string {
|
||||||
|
if (value === undefined || !Number.isFinite(value)) return "0";
|
||||||
|
const safe = Math.max(0, value);
|
||||||
|
if (safe >= 1_000_000) return `${(safe / 1_000_000).toFixed(1)}m`;
|
||||||
|
if (safe >= 1_000)
|
||||||
|
return `${(safe / 1_000).toFixed(safe >= 10_000 ? 0 : 1)}k`;
|
||||||
|
return String(Math.round(safe));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatUsd(value?: number): string | undefined {
|
||||||
|
if (value === undefined || !Number.isFinite(value)) return undefined;
|
||||||
|
if (value >= 1) return `$${value.toFixed(2)}`;
|
||||||
|
if (value >= 0.01) return `$${value.toFixed(2)}`;
|
||||||
|
return `$${value.toFixed(4)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveModelCostConfig(params: {
|
||||||
|
provider?: string;
|
||||||
|
model?: string;
|
||||||
|
config?: ClawdbotConfig;
|
||||||
|
}): ModelCostConfig | undefined {
|
||||||
|
const provider = params.provider?.trim();
|
||||||
|
const model = params.model?.trim();
|
||||||
|
if (!provider || !model) return undefined;
|
||||||
|
const providers = params.config?.models?.providers ?? {};
|
||||||
|
const entry = providers[provider]?.models?.find((item) => item.id === model);
|
||||||
|
return entry?.cost;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toNumber = (value: number | undefined): number =>
|
||||||
|
typeof value === "number" && Number.isFinite(value) ? value : 0;
|
||||||
|
|
||||||
|
export function estimateUsageCost(params: {
|
||||||
|
usage?: NormalizedUsage | UsageTotals | null;
|
||||||
|
cost?: ModelCostConfig;
|
||||||
|
}): number | undefined {
|
||||||
|
const usage = params.usage;
|
||||||
|
const cost = params.cost;
|
||||||
|
if (!usage || !cost) return undefined;
|
||||||
|
const input = toNumber(usage.input);
|
||||||
|
const output = toNumber(usage.output);
|
||||||
|
const cacheRead = toNumber(usage.cacheRead);
|
||||||
|
const cacheWrite = toNumber(usage.cacheWrite);
|
||||||
|
const total =
|
||||||
|
input * cost.input +
|
||||||
|
output * cost.output +
|
||||||
|
cacheRead * cost.cacheRead +
|
||||||
|
cacheWrite * cost.cacheWrite;
|
||||||
|
if (!Number.isFinite(total)) return undefined;
|
||||||
|
return total / 1_000_000;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user