Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a36dc285d6 | ||
|
|
704a3ba51c |
No files matched your search
+1
-15
@@ -5,32 +5,22 @@ Docs: https://docs.clawd.bot
|
|||||||
## 2026.1.23 (Unreleased)
|
## 2026.1.23 (Unreleased)
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
- Agents: keep system prompt time zone-only and move current time to `session_status` for better cache hits.
|
|
||||||
- Browser: add node-host proxy auto-routing for remote gateways (configurable per gateway/node).
|
|
||||||
- Plugins: add optional llm-task JSON-only tool for workflows. (#1498) Thanks @vignesh07.
|
- Plugins: add optional llm-task JSON-only tool for workflows. (#1498) Thanks @vignesh07.
|
||||||
- CLI: restart the gateway by default after `clawdbot update`; add `--no-restart` to skip it.
|
- CLI: restart the gateway by default after `clawdbot update`; add `--no-restart` to skip it.
|
||||||
- CLI: add live auth probes to `clawdbot models status` for per-profile verification.
|
- CLI: add live auth probes to `clawdbot models status` for per-profile verification.
|
||||||
- CLI: add `clawdbot system` for system events + heartbeat controls; remove standalone `wake`.
|
|
||||||
- Agents: add Bedrock auto-discovery defaults + config overrides. (#1553) Thanks @fal3.
|
- Agents: add Bedrock auto-discovery defaults + config overrides. (#1553) Thanks @fal3.
|
||||||
- Agents: add diagnostics-configured Anthropic payload logging. (#1501) Thanks @parubets.
|
|
||||||
- Docs: add cron vs heartbeat decision guide (with Lobster workflow notes). (#1533) Thanks @JustYannicc.
|
- Docs: add cron vs heartbeat decision guide (with Lobster workflow notes). (#1533) Thanks @JustYannicc.
|
||||||
- Docs: clarify HEARTBEAT.md empty file skips heartbeats, missing file still runs. (#1535) Thanks @JustYannicc.
|
|
||||||
- Markdown: add per-channel table conversion (bullets for Signal/WhatsApp, code blocks elsewhere). (#1495) Thanks @odysseus0.
|
- Markdown: add per-channel table conversion (bullets for Signal/WhatsApp, code blocks elsewhere). (#1495) Thanks @odysseus0.
|
||||||
- Tlon: add Urbit channel plugin (DMs, group mentions, thread replies). (#1544) Thanks @wca4a.
|
- Tlon: add Urbit channel plugin (DMs, group mentions, thread replies). (#1544) Thanks @wca4a.
|
||||||
- Channels: allow per-group tool allow/deny policies across built-in + plugin channels. (#1546) Thanks @adam91holt.
|
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- Agents: ignore IDENTITY.md template placeholders when parsing identity to avoid placeholder replies. (#1556)
|
- Logging: guard console settings resolution to avoid recursion on config warnings. (#1555) Thanks @travisp.
|
||||||
- Docker: update gateway command in docker-compose and Hetzner guide. (#1514)
|
|
||||||
- Sessions: reject array-backed session stores to prevent silent wipes. (#1469)
|
|
||||||
- Voice wake: auto-save wake words on blur/submit across iOS/Android and align limits with macOS.
|
- Voice wake: auto-save wake words on blur/submit across iOS/Android and align limits with macOS.
|
||||||
- UI: keep the Control UI sidebar visible while scrolling long pages. (#1515) Thanks @pookNast.
|
- UI: keep the Control UI sidebar visible while scrolling long pages. (#1515) Thanks @pookNast.
|
||||||
- UI: cache Control UI markdown rendering + memoize chat text extraction to reduce Safari typing jank.
|
|
||||||
- Tailscale: retry serve/funnel with sudo only for permission errors and keep original failure details. (#1551) Thanks @sweepies.
|
- Tailscale: retry serve/funnel with sudo only for permission errors and keep original failure details. (#1551) Thanks @sweepies.
|
||||||
- Agents: add CLI log hint to "agent failed before reply" messages. (#1550) Thanks @sweepies.
|
- Agents: add CLI log hint to "agent failed before reply" messages. (#1550) Thanks @sweepies.
|
||||||
- Discord: limit autoThread mention bypass to bot-owned threads; keep ack reactions mention-gated. (#1511) Thanks @pvoo.
|
- Discord: limit autoThread mention bypass to bot-owned threads; keep ack reactions mention-gated. (#1511) Thanks @pvoo.
|
||||||
- Gateway: accept null optional fields in exec approval requests. (#1511) Thanks @pvoo.
|
- Gateway: accept null optional fields in exec approval requests. (#1511) Thanks @pvoo.
|
||||||
- Exec: honor tools.exec ask/security defaults for elevated approvals (avoid unwanted prompts).
|
|
||||||
- TUI: forward unknown slash commands (for example, `/context`) to the Gateway.
|
- TUI: forward unknown slash commands (for example, `/context`) to the Gateway.
|
||||||
- TUI: include Gateway slash commands in autocomplete and `/help`.
|
- TUI: include Gateway slash commands in autocomplete and `/help`.
|
||||||
- CLI: skip usage lines in `clawdbot models status` when provider usage is unavailable.
|
- CLI: skip usage lines in `clawdbot models status` when provider usage is unavailable.
|
||||||
@@ -43,19 +33,15 @@ Docs: https://docs.clawd.bot
|
|||||||
- CLI: explain when auth profiles are excluded by auth.order in probe details.
|
- CLI: explain when auth profiles are excluded by auth.order in probe details.
|
||||||
- CLI: drop the em dash when the banner tagline wraps to a second line.
|
- CLI: drop the em dash when the banner tagline wraps to a second line.
|
||||||
- CLI: inline auth probe errors in status rows to reduce wrapping.
|
- CLI: inline auth probe errors in status rows to reduce wrapping.
|
||||||
- Telegram: render markdown in media captions. (#1478)
|
|
||||||
- Agents: honor enqueue overrides for embedded runs to avoid queue deadlocks in tests.
|
- Agents: honor enqueue overrides for embedded runs to avoid queue deadlocks in tests.
|
||||||
- Agents: trigger model fallback when auth profiles are all in cooldown or unavailable. (#1522)
|
|
||||||
- Daemon: use platform PATH delimiters when building minimal service paths.
|
- Daemon: use platform PATH delimiters when building minimal service paths.
|
||||||
- Tests: skip embedded runner ordering assertion on Windows to avoid CI timeouts.
|
- Tests: skip embedded runner ordering assertion on Windows to avoid CI timeouts.
|
||||||
- Linux: include env-configured user bin roots in systemd PATH and align PATH audits. (#1512) Thanks @robbyczgw-cla.
|
- Linux: include env-configured user bin roots in systemd PATH and align PATH audits. (#1512) Thanks @robbyczgw-cla.
|
||||||
- TUI: render Gateway slash-command replies as system output (for example, `/context`).
|
- TUI: render Gateway slash-command replies as system output (for example, `/context`).
|
||||||
- Media: only parse `MEDIA:` tags when they start the line to avoid stripping prose mentions. (#1206)
|
|
||||||
- Media: preserve PNG alpha when possible; fall back to JPEG when still over size cap. (#1491) Thanks @robbyczgw-cla.
|
- Media: preserve PNG alpha when possible; fall back to JPEG when still over size cap. (#1491) Thanks @robbyczgw-cla.
|
||||||
- Agents: treat plugin-only tool allowlists as opt-ins; keep core tools enabled. (#1467)
|
- Agents: treat plugin-only tool allowlists as opt-ins; keep core tools enabled. (#1467)
|
||||||
- Exec approvals: persist allowlist entry ids to keep macOS allowlist rows stable. (#1521) Thanks @ngutman.
|
- Exec approvals: persist allowlist entry ids to keep macOS allowlist rows stable. (#1521) Thanks @ngutman.
|
||||||
- MS Teams (plugin): remove `.default` suffix from Graph scopes to avoid double-appending. (#1507) Thanks @Evizero.
|
- MS Teams (plugin): remove `.default` suffix from Graph scopes to avoid double-appending. (#1507) Thanks @Evizero.
|
||||||
- Browser: keep extension relay tabs controllable when the extension reuses a session id after switching tabs. (#1160)
|
|
||||||
|
|
||||||
## 2026.1.22
|
## 2026.1.22
|
||||||
|
|
||||||
|
|||||||
@@ -385,7 +385,6 @@ public struct SendParams: Codable, Sendable {
|
|||||||
public let to: String
|
public let to: String
|
||||||
public let message: String
|
public let message: String
|
||||||
public let mediaurl: String?
|
public let mediaurl: String?
|
||||||
public let mediaurls: [String]?
|
|
||||||
public let gifplayback: Bool?
|
public let gifplayback: Bool?
|
||||||
public let channel: String?
|
public let channel: String?
|
||||||
public let accountid: String?
|
public let accountid: String?
|
||||||
@@ -396,7 +395,6 @@ public struct SendParams: Codable, Sendable {
|
|||||||
to: String,
|
to: String,
|
||||||
message: String,
|
message: String,
|
||||||
mediaurl: String?,
|
mediaurl: String?,
|
||||||
mediaurls: [String]?,
|
|
||||||
gifplayback: Bool?,
|
gifplayback: Bool?,
|
||||||
channel: String?,
|
channel: String?,
|
||||||
accountid: String?,
|
accountid: String?,
|
||||||
@@ -406,7 +404,6 @@ public struct SendParams: Codable, Sendable {
|
|||||||
self.to = to
|
self.to = to
|
||||||
self.message = message
|
self.message = message
|
||||||
self.mediaurl = mediaurl
|
self.mediaurl = mediaurl
|
||||||
self.mediaurls = mediaurls
|
|
||||||
self.gifplayback = gifplayback
|
self.gifplayback = gifplayback
|
||||||
self.channel = channel
|
self.channel = channel
|
||||||
self.accountid = accountid
|
self.accountid = accountid
|
||||||
@@ -417,7 +414,6 @@ public struct SendParams: Codable, Sendable {
|
|||||||
case to
|
case to
|
||||||
case message
|
case message
|
||||||
case mediaurl = "mediaUrl"
|
case mediaurl = "mediaUrl"
|
||||||
case mediaurls = "mediaUrls"
|
|
||||||
case gifplayback = "gifPlayback"
|
case gifplayback = "gifPlayback"
|
||||||
case channel
|
case channel
|
||||||
case accountid = "accountId"
|
case accountid = "accountId"
|
||||||
@@ -482,9 +478,6 @@ public struct AgentParams: Codable, Sendable {
|
|||||||
public let accountid: String?
|
public let accountid: String?
|
||||||
public let replyaccountid: String?
|
public let replyaccountid: String?
|
||||||
public let threadid: String?
|
public let threadid: String?
|
||||||
public let groupid: String?
|
|
||||||
public let groupchannel: String?
|
|
||||||
public let groupspace: String?
|
|
||||||
public let timeout: Int?
|
public let timeout: Int?
|
||||||
public let lane: String?
|
public let lane: String?
|
||||||
public let extrasystemprompt: String?
|
public let extrasystemprompt: String?
|
||||||
@@ -507,9 +500,6 @@ public struct AgentParams: Codable, Sendable {
|
|||||||
accountid: String?,
|
accountid: String?,
|
||||||
replyaccountid: String?,
|
replyaccountid: String?,
|
||||||
threadid: String?,
|
threadid: String?,
|
||||||
groupid: String?,
|
|
||||||
groupchannel: String?,
|
|
||||||
groupspace: String?,
|
|
||||||
timeout: Int?,
|
timeout: Int?,
|
||||||
lane: String?,
|
lane: String?,
|
||||||
extrasystemprompt: String?,
|
extrasystemprompt: String?,
|
||||||
@@ -531,9 +521,6 @@ public struct AgentParams: Codable, Sendable {
|
|||||||
self.accountid = accountid
|
self.accountid = accountid
|
||||||
self.replyaccountid = replyaccountid
|
self.replyaccountid = replyaccountid
|
||||||
self.threadid = threadid
|
self.threadid = threadid
|
||||||
self.groupid = groupid
|
|
||||||
self.groupchannel = groupchannel
|
|
||||||
self.groupspace = groupspace
|
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.lane = lane
|
self.lane = lane
|
||||||
self.extrasystemprompt = extrasystemprompt
|
self.extrasystemprompt = extrasystemprompt
|
||||||
@@ -556,9 +543,6 @@ public struct AgentParams: Codable, Sendable {
|
|||||||
case accountid = "accountId"
|
case accountid = "accountId"
|
||||||
case replyaccountid = "replyAccountId"
|
case replyaccountid = "replyAccountId"
|
||||||
case threadid = "threadId"
|
case threadid = "threadId"
|
||||||
case groupid = "groupId"
|
|
||||||
case groupchannel = "groupChannel"
|
|
||||||
case groupspace = "groupSpace"
|
|
||||||
case timeout
|
case timeout
|
||||||
case lane
|
case lane
|
||||||
case extrasystemprompt = "extraSystemPrompt"
|
case extrasystemprompt = "extraSystemPrompt"
|
||||||
|
|||||||
@@ -385,7 +385,6 @@ public struct SendParams: Codable, Sendable {
|
|||||||
public let to: String
|
public let to: String
|
||||||
public let message: String
|
public let message: String
|
||||||
public let mediaurl: String?
|
public let mediaurl: String?
|
||||||
public let mediaurls: [String]?
|
|
||||||
public let gifplayback: Bool?
|
public let gifplayback: Bool?
|
||||||
public let channel: String?
|
public let channel: String?
|
||||||
public let accountid: String?
|
public let accountid: String?
|
||||||
@@ -396,7 +395,6 @@ public struct SendParams: Codable, Sendable {
|
|||||||
to: String,
|
to: String,
|
||||||
message: String,
|
message: String,
|
||||||
mediaurl: String?,
|
mediaurl: String?,
|
||||||
mediaurls: [String]?,
|
|
||||||
gifplayback: Bool?,
|
gifplayback: Bool?,
|
||||||
channel: String?,
|
channel: String?,
|
||||||
accountid: String?,
|
accountid: String?,
|
||||||
@@ -406,7 +404,6 @@ public struct SendParams: Codable, Sendable {
|
|||||||
self.to = to
|
self.to = to
|
||||||
self.message = message
|
self.message = message
|
||||||
self.mediaurl = mediaurl
|
self.mediaurl = mediaurl
|
||||||
self.mediaurls = mediaurls
|
|
||||||
self.gifplayback = gifplayback
|
self.gifplayback = gifplayback
|
||||||
self.channel = channel
|
self.channel = channel
|
||||||
self.accountid = accountid
|
self.accountid = accountid
|
||||||
@@ -417,7 +414,6 @@ public struct SendParams: Codable, Sendable {
|
|||||||
case to
|
case to
|
||||||
case message
|
case message
|
||||||
case mediaurl = "mediaUrl"
|
case mediaurl = "mediaUrl"
|
||||||
case mediaurls = "mediaUrls"
|
|
||||||
case gifplayback = "gifPlayback"
|
case gifplayback = "gifPlayback"
|
||||||
case channel
|
case channel
|
||||||
case accountid = "accountId"
|
case accountid = "accountId"
|
||||||
@@ -482,9 +478,6 @@ public struct AgentParams: Codable, Sendable {
|
|||||||
public let accountid: String?
|
public let accountid: String?
|
||||||
public let replyaccountid: String?
|
public let replyaccountid: String?
|
||||||
public let threadid: String?
|
public let threadid: String?
|
||||||
public let groupid: String?
|
|
||||||
public let groupchannel: String?
|
|
||||||
public let groupspace: String?
|
|
||||||
public let timeout: Int?
|
public let timeout: Int?
|
||||||
public let lane: String?
|
public let lane: String?
|
||||||
public let extrasystemprompt: String?
|
public let extrasystemprompt: String?
|
||||||
@@ -507,9 +500,6 @@ public struct AgentParams: Codable, Sendable {
|
|||||||
accountid: String?,
|
accountid: String?,
|
||||||
replyaccountid: String?,
|
replyaccountid: String?,
|
||||||
threadid: String?,
|
threadid: String?,
|
||||||
groupid: String?,
|
|
||||||
groupchannel: String?,
|
|
||||||
groupspace: String?,
|
|
||||||
timeout: Int?,
|
timeout: Int?,
|
||||||
lane: String?,
|
lane: String?,
|
||||||
extrasystemprompt: String?,
|
extrasystemprompt: String?,
|
||||||
@@ -531,9 +521,6 @@ public struct AgentParams: Codable, Sendable {
|
|||||||
self.accountid = accountid
|
self.accountid = accountid
|
||||||
self.replyaccountid = replyaccountid
|
self.replyaccountid = replyaccountid
|
||||||
self.threadid = threadid
|
self.threadid = threadid
|
||||||
self.groupid = groupid
|
|
||||||
self.groupchannel = groupchannel
|
|
||||||
self.groupspace = groupspace
|
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.lane = lane
|
self.lane = lane
|
||||||
self.extrasystemprompt = extrasystemprompt
|
self.extrasystemprompt = extrasystemprompt
|
||||||
@@ -556,9 +543,6 @@ public struct AgentParams: Codable, Sendable {
|
|||||||
case accountid = "accountId"
|
case accountid = "accountId"
|
||||||
case replyaccountid = "replyAccountId"
|
case replyaccountid = "replyAccountId"
|
||||||
case threadid = "threadId"
|
case threadid = "threadId"
|
||||||
case groupid = "groupId"
|
|
||||||
case groupchannel = "groupChannel"
|
|
||||||
case groupspace = "groupSpace"
|
|
||||||
case timeout
|
case timeout
|
||||||
case lane
|
case lane
|
||||||
case extrasystemprompt = "extraSystemPrompt"
|
case extrasystemprompt = "extraSystemPrompt"
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ services:
|
|||||||
[
|
[
|
||||||
"node",
|
"node",
|
||||||
"dist/index.js",
|
"dist/index.js",
|
||||||
"gateway",
|
"gateway-daemon",
|
||||||
"--bind",
|
"--bind",
|
||||||
"${CLAWDBOT_GATEWAY_BIND:-lan}",
|
"${CLAWDBOT_GATEWAY_BIND:-lan}",
|
||||||
"--port",
|
"--port",
|
||||||
|
|||||||
@@ -263,15 +263,15 @@ Run history:
|
|||||||
clawdbot cron runs --id <jobId> --limit 50
|
clawdbot cron runs --id <jobId> --limit 50
|
||||||
```
|
```
|
||||||
|
|
||||||
Immediate system event without creating a job:
|
Immediate wake without creating a job:
|
||||||
```bash
|
```bash
|
||||||
clawdbot system event --mode now --text "Next heartbeat: check battery."
|
clawdbot wake --mode now --text "Next heartbeat: check battery."
|
||||||
```
|
```
|
||||||
|
|
||||||
## Gateway API surface
|
## Gateway API surface
|
||||||
- `cron.list`, `cron.status`, `cron.add`, `cron.update`, `cron.remove`
|
- `cron.list`, `cron.status`, `cron.add`, `cron.update`, `cron.remove`
|
||||||
- `cron.run` (force or due), `cron.runs`
|
- `cron.run` (force or due), `cron.runs`
|
||||||
For immediate system events without a job, use [`clawdbot system event`](/cli/system).
|
- `wake` (enqueue system event + optional heartbeat)
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
|||||||
@@ -271,4 +271,4 @@ clawdbot cron add \
|
|||||||
|
|
||||||
- [Heartbeat](/gateway/heartbeat) - full heartbeat configuration
|
- [Heartbeat](/gateway/heartbeat) - full heartbeat configuration
|
||||||
- [Cron jobs](/automation/cron-jobs) - full cron CLI and API reference
|
- [Cron jobs](/automation/cron-jobs) - full cron CLI and API reference
|
||||||
- [System](/cli/system) - system events + heartbeat controls
|
- [Wake](/cli/wake) - manual wake command
|
||||||
@@ -44,7 +44,6 @@ clawdbot channels logout --channel whatsapp
|
|||||||
|
|
||||||
- Run `clawdbot status --deep` for a broad probe.
|
- Run `clawdbot status --deep` for a broad probe.
|
||||||
- Use `clawdbot doctor` for guided fixes.
|
- Use `clawdbot doctor` for guided fixes.
|
||||||
- `clawdbot channels list` prints `Claude: HTTP 403 ... user:profile` → usage snapshot needs the `user:profile` scope. Use `--no-usage`, or provide a claude.ai session key (`CLAUDE_WEB_SESSION_KEY` / `CLAUDE_WEB_COOKIE`), or re-auth via Claude Code CLI.
|
|
||||||
|
|
||||||
## Capabilities probe
|
## Capabilities probe
|
||||||
|
|
||||||
|
|||||||
+5
-22
@@ -29,7 +29,6 @@ This page describes the current CLI behavior. If commands change, update this do
|
|||||||
- [`sessions`](/cli/sessions)
|
- [`sessions`](/cli/sessions)
|
||||||
- [`gateway`](/cli/gateway)
|
- [`gateway`](/cli/gateway)
|
||||||
- [`logs`](/cli/logs)
|
- [`logs`](/cli/logs)
|
||||||
- [`system`](/cli/system)
|
|
||||||
- [`models`](/cli/models)
|
- [`models`](/cli/models)
|
||||||
- [`memory`](/cli/memory)
|
- [`memory`](/cli/memory)
|
||||||
- [`nodes`](/cli/nodes)
|
- [`nodes`](/cli/nodes)
|
||||||
@@ -39,6 +38,7 @@ This page describes the current CLI behavior. If commands change, update this do
|
|||||||
- [`sandbox`](/cli/sandbox)
|
- [`sandbox`](/cli/sandbox)
|
||||||
- [`tui`](/cli/tui)
|
- [`tui`](/cli/tui)
|
||||||
- [`browser`](/cli/browser)
|
- [`browser`](/cli/browser)
|
||||||
|
- [`wake`](/cli/wake)
|
||||||
- [`cron`](/cli/cron)
|
- [`cron`](/cli/cron)
|
||||||
- [`dns`](/cli/dns)
|
- [`dns`](/cli/dns)
|
||||||
- [`docs`](/cli/docs)
|
- [`docs`](/cli/docs)
|
||||||
@@ -145,10 +145,6 @@ clawdbot [--dev] [--profile <name>] <command>
|
|||||||
restart
|
restart
|
||||||
run
|
run
|
||||||
logs
|
logs
|
||||||
system
|
|
||||||
event
|
|
||||||
heartbeat last|enable|disable
|
|
||||||
presence
|
|
||||||
models
|
models
|
||||||
list
|
list
|
||||||
status
|
status
|
||||||
@@ -164,6 +160,7 @@ clawdbot [--dev] [--profile <name>] <command>
|
|||||||
list
|
list
|
||||||
recreate
|
recreate
|
||||||
explain
|
explain
|
||||||
|
wake
|
||||||
cron
|
cron
|
||||||
status
|
status
|
||||||
list
|
list
|
||||||
@@ -766,9 +763,9 @@ Options:
|
|||||||
- `set`: `--provider <name>`, `--agent <id>`, `<profileIds...>`
|
- `set`: `--provider <name>`, `--agent <id>`, `<profileIds...>`
|
||||||
- `clear`: `--provider <name>`, `--agent <id>`
|
- `clear`: `--provider <name>`, `--agent <id>`
|
||||||
|
|
||||||
## System
|
## Cron + wake
|
||||||
|
|
||||||
### `system event`
|
### `wake`
|
||||||
Enqueue a system event and optionally trigger a heartbeat (Gateway RPC).
|
Enqueue a system event and optionally trigger a heartbeat (Gateway RPC).
|
||||||
|
|
||||||
Required:
|
Required:
|
||||||
@@ -779,21 +776,7 @@ Options:
|
|||||||
- `--json`
|
- `--json`
|
||||||
- `--url`, `--token`, `--timeout`, `--expect-final`
|
- `--url`, `--token`, `--timeout`, `--expect-final`
|
||||||
|
|
||||||
### `system heartbeat last|enable|disable`
|
### `cron`
|
||||||
Heartbeat controls (Gateway RPC).
|
|
||||||
|
|
||||||
Options:
|
|
||||||
- `--json`
|
|
||||||
- `--url`, `--token`, `--timeout`, `--expect-final`
|
|
||||||
|
|
||||||
### `system presence`
|
|
||||||
List system presence entries (Gateway RPC).
|
|
||||||
|
|
||||||
Options:
|
|
||||||
- `--json`
|
|
||||||
- `--url`, `--token`, `--timeout`, `--expect-final`
|
|
||||||
|
|
||||||
## Cron
|
|
||||||
Manage scheduled jobs (Gateway RPC). See [/automation/cron-jobs](/automation/cron-jobs).
|
Manage scheduled jobs (Gateway RPC). See [/automation/cron-jobs](/automation/cron-jobs).
|
||||||
|
|
||||||
Subcommands:
|
Subcommands:
|
||||||
|
|||||||
@@ -23,24 +23,6 @@ Common use cases:
|
|||||||
Execution is still guarded by **exec approvals** and per‑agent allowlists on the
|
Execution is still guarded by **exec approvals** and per‑agent allowlists on the
|
||||||
node host, so you can keep command access scoped and explicit.
|
node host, so you can keep command access scoped and explicit.
|
||||||
|
|
||||||
## Browser proxy (zero-config)
|
|
||||||
|
|
||||||
Node hosts automatically advertise a browser proxy if `browser.enabled` is not
|
|
||||||
disabled on the node. This lets the agent use browser automation on that node
|
|
||||||
without extra configuration.
|
|
||||||
|
|
||||||
Disable it on the node if needed:
|
|
||||||
|
|
||||||
```json5
|
|
||||||
{
|
|
||||||
nodeHost: {
|
|
||||||
browserProxy: {
|
|
||||||
enabled: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Run (foreground)
|
## Run (foreground)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
---
|
|
||||||
summary: "CLI reference for `clawdbot system` (system events, heartbeat, presence)"
|
|
||||||
read_when:
|
|
||||||
- You want to enqueue a system event without creating a cron job
|
|
||||||
- You need to enable or disable heartbeats
|
|
||||||
- You want to inspect system presence entries
|
|
||||||
---
|
|
||||||
|
|
||||||
# `clawdbot system`
|
|
||||||
|
|
||||||
System-level helpers for the Gateway: enqueue system events, control heartbeats,
|
|
||||||
and view presence.
|
|
||||||
|
|
||||||
## Common commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
clawdbot system event --text "Check for urgent follow-ups" --mode now
|
|
||||||
clawdbot system heartbeat enable
|
|
||||||
clawdbot system heartbeat last
|
|
||||||
clawdbot system presence
|
|
||||||
```
|
|
||||||
|
|
||||||
## `system event`
|
|
||||||
|
|
||||||
Enqueue a system event on the **main** session. The next heartbeat will inject
|
|
||||||
it as a `System:` line in the prompt. Use `--mode now` to trigger the heartbeat
|
|
||||||
immediately; `next-heartbeat` waits for the next scheduled tick.
|
|
||||||
|
|
||||||
Flags:
|
|
||||||
- `--text <text>`: required system event text.
|
|
||||||
- `--mode <mode>`: `now` or `next-heartbeat` (default).
|
|
||||||
- `--json`: machine-readable output.
|
|
||||||
|
|
||||||
## `system heartbeat last|enable|disable`
|
|
||||||
|
|
||||||
Heartbeat controls:
|
|
||||||
- `last`: show the last heartbeat event.
|
|
||||||
- `enable`: turn heartbeats back on (use this if they were disabled).
|
|
||||||
- `disable`: pause heartbeats.
|
|
||||||
|
|
||||||
Flags:
|
|
||||||
- `--json`: machine-readable output.
|
|
||||||
|
|
||||||
## `system presence`
|
|
||||||
|
|
||||||
List the current system presence entries the Gateway knows about (nodes,
|
|
||||||
instances, and similar status lines).
|
|
||||||
|
|
||||||
Flags:
|
|
||||||
- `--json`: machine-readable output.
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Requires a running Gateway reachable by your current config (local or remote).
|
|
||||||
- System events are ephemeral and not persisted across restarts.
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
summary: "CLI reference for `clawdbot wake` (enqueue a system event and optionally trigger an immediate heartbeat)"
|
||||||
|
read_when:
|
||||||
|
- You want to “poke” a running Gateway to process a system event
|
||||||
|
- You use `wake` with cron jobs or remote nodes
|
||||||
|
---
|
||||||
|
|
||||||
|
# `clawdbot wake`
|
||||||
|
|
||||||
|
Enqueue a system event on the Gateway and optionally trigger an immediate heartbeat.
|
||||||
|
|
||||||
|
This is a lightweight “poke” for automation flows where you don’t want to run a full command, but you do want the Gateway to react quickly.
|
||||||
|
|
||||||
|
Related:
|
||||||
|
- Cron jobs: [Cron](/cli/cron)
|
||||||
|
- Gateway heartbeat: [Heartbeat](/gateway/heartbeat)
|
||||||
|
|
||||||
|
## Common commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
clawdbot wake --text "sync"
|
||||||
|
clawdbot wake --text "sync" --mode now
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flags
|
||||||
|
|
||||||
|
- `--text <text>`: system event text.
|
||||||
|
- `--mode <mode>`: `now` or `next-heartbeat` (default).
|
||||||
|
- `--json`: machine-readable output.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Requires a running Gateway reachable by your current config (local or remote).
|
||||||
|
- If you’re using sandboxing, `wake` still targets the Gateway; sandboxing does not block the command itself.
|
||||||
|
|
||||||
@@ -66,12 +66,12 @@ To inspect how much each injected file contributes (raw vs injected, truncation,
|
|||||||
|
|
||||||
## Time handling
|
## Time handling
|
||||||
|
|
||||||
The system prompt includes a dedicated **Current Date & Time** section when the
|
The system prompt includes a dedicated **Current Date & Time** section when user
|
||||||
user timezone is known. To keep the prompt cache-stable, it now only includes
|
time or timezone is known. It is explicit about:
|
||||||
the **time zone** (no dynamic clock or time format).
|
|
||||||
|
|
||||||
Use `session_status` when the agent needs the current time; the status card
|
- The user’s **local time** (already converted).
|
||||||
includes a timestamp line.
|
- The **time zone** used for the conversion.
|
||||||
|
- The **time format** (12-hour / 24-hour).
|
||||||
|
|
||||||
Configure with:
|
Configure with:
|
||||||
|
|
||||||
|
|||||||
+8
-8
@@ -7,8 +7,8 @@ read_when:
|
|||||||
|
|
||||||
# Date & Time
|
# Date & Time
|
||||||
|
|
||||||
Clawdbot defaults to **host-local time for transport timestamps** and **user timezone only in the system prompt**.
|
Clawdbot defaults to **host-local time for transport timestamps** and **user-local time only in the system prompt**.
|
||||||
Provider timestamps are preserved so tools keep their native semantics (current time is available via `session_status`).
|
Provider timestamps are preserved so tools keep their native semantics.
|
||||||
|
|
||||||
## Message envelopes (local by default)
|
## Message envelopes (local by default)
|
||||||
|
|
||||||
@@ -63,16 +63,16 @@ You can override this behavior:
|
|||||||
|
|
||||||
## System prompt: Current Date & Time
|
## System prompt: Current Date & Time
|
||||||
|
|
||||||
If the user timezone is known, the system prompt includes a dedicated
|
If the user timezone or local time is known, the system prompt includes a dedicated
|
||||||
**Current Date & Time** section with the **time zone only** (no clock/time format)
|
**Current Date & Time** section:
|
||||||
to keep prompt caching stable:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
Time zone: America/Chicago
|
Thursday, January 15th, 2026 — 3:07 PM (America/Chicago)
|
||||||
|
Time format: 12-hour
|
||||||
```
|
```
|
||||||
|
|
||||||
When the agent needs the current time, use the `session_status` tool; the status
|
If only the timezone is known, we still include the section and instruct the model
|
||||||
card includes a timestamp line.
|
to assume UTC for unknown time references.
|
||||||
|
|
||||||
## System event lines (local by default)
|
## System event lines (local by default)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -850,12 +850,12 @@
|
|||||||
"cli/memory",
|
"cli/memory",
|
||||||
"cli/models",
|
"cli/models",
|
||||||
"cli/logs",
|
"cli/logs",
|
||||||
"cli/system",
|
|
||||||
"cli/nodes",
|
"cli/nodes",
|
||||||
"cli/approvals",
|
"cli/approvals",
|
||||||
"cli/gateway",
|
"cli/gateway",
|
||||||
"cli/tui",
|
"cli/tui",
|
||||||
"cli/voicecall",
|
"cli/voicecall",
|
||||||
|
"cli/wake",
|
||||||
"cli/cron",
|
"cli/cron",
|
||||||
"cli/dns",
|
"cli/dns",
|
||||||
"cli/docs",
|
"cli/docs",
|
||||||
|
|||||||
@@ -162,10 +162,6 @@ If a `HEARTBEAT.md` file exists in the workspace, the default prompt tells the
|
|||||||
agent to read it. Think of it as your “heartbeat checklist”: small, stable, and
|
agent to read it. Think of it as your “heartbeat checklist”: small, stable, and
|
||||||
safe to include every 30 minutes.
|
safe to include every 30 minutes.
|
||||||
|
|
||||||
If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown
|
|
||||||
headers like `# Heading`), Clawdbot skips the heartbeat run to save API calls.
|
|
||||||
If the file is missing, the heartbeat still runs and the model decides what to do.
|
|
||||||
|
|
||||||
Keep it tiny (short checklist or reminders) to avoid prompt bloat.
|
Keep it tiny (short checklist or reminders) to avoid prompt bloat.
|
||||||
|
|
||||||
Example `HEARTBEAT.md`:
|
Example `HEARTBEAT.md`:
|
||||||
@@ -199,7 +195,7 @@ Safety note: don’t put secrets (API keys, phone numbers, private tokens) into
|
|||||||
You can enqueue a system event and trigger an immediate heartbeat with:
|
You can enqueue a system event and trigger an immediate heartbeat with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
clawdbot system event --text "Check for urgent follow-ups" --mode now
|
clawdbot wake --text "Check for urgent follow-ups" --mode now
|
||||||
```
|
```
|
||||||
|
|
||||||
If multiple agents have `heartbeat` configured, a manual wake runs each of those
|
If multiple agents have `heartbeat` configured, a manual wake runs each of those
|
||||||
|
|||||||
@@ -95,29 +95,6 @@ Console logs are **TTY-aware** and formatted for readability:
|
|||||||
|
|
||||||
Console formatting is controlled by `logging.consoleStyle`.
|
Console formatting is controlled by `logging.consoleStyle`.
|
||||||
|
|
||||||
## Anthropic payload log (debugging)
|
|
||||||
|
|
||||||
For Anthropic-only runs, you can enable a dedicated JSONL log that captures the
|
|
||||||
exact request payload (as sent) plus per-run usage stats. This log includes full
|
|
||||||
prompt/message data; treat it as sensitive.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"diagnostics": {
|
|
||||||
"anthropicPayloadLog": {
|
|
||||||
"enabled": true,
|
|
||||||
"filePath": "/path/to/anthropic-payload.jsonl"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Defaults + overrides:
|
|
||||||
|
|
||||||
- Default path: `$CLAWDBOT_STATE_DIR/logs/anthropic-payload.jsonl`
|
|
||||||
- Env enable: `CLAWDBOT_ANTHROPIC_PAYLOAD_LOG=1`
|
|
||||||
- Env path: `CLAWDBOT_ANTHROPIC_PAYLOAD_LOG_FILE=/path/to/anthropic-payload.jsonl`
|
|
||||||
|
|
||||||
## Configuring logging
|
## Configuring logging
|
||||||
|
|
||||||
All logging configuration lives under `logging` in `~/.clawdbot/clawdbot.json`.
|
All logging configuration lives under `logging` in `~/.clawdbot/clawdbot.json`.
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ services:
|
|||||||
[
|
[
|
||||||
"node",
|
"node",
|
||||||
"dist/index.js",
|
"dist/index.js",
|
||||||
"gateway",
|
"gateway-daemon",
|
||||||
"--bind",
|
"--bind",
|
||||||
"${CLAWDBOT_GATEWAY_BIND}",
|
"${CLAWDBOT_GATEWAY_BIND}",
|
||||||
"--port",
|
"--port",
|
||||||
|
|||||||
@@ -5,5 +5,4 @@ read_when:
|
|||||||
---
|
---
|
||||||
# HEARTBEAT.md
|
# HEARTBEAT.md
|
||||||
|
|
||||||
# Keep this file empty (or with only comments) to skip heartbeat API calls.
|
Keep this file empty unless you want a tiny checklist. Keep it small.
|
||||||
# Add tasks below when you want the agent to check something periodically.
|
|
||||||
@@ -7,16 +7,11 @@ read_when:
|
|||||||
|
|
||||||
*Fill this in during your first conversation. Make it yours.*
|
*Fill this in during your first conversation. Make it yours.*
|
||||||
|
|
||||||
- **Name:**
|
- **Name:** *(pick something you like)*
|
||||||
*(pick something you like)*
|
- **Creature:** *(AI? robot? familiar? ghost in the machine? something weirder?)*
|
||||||
- **Creature:**
|
- **Vibe:** *(how do you come across? sharp? warm? chaotic? calm?)*
|
||||||
*(AI? robot? familiar? ghost in the machine? something weirder?)*
|
- **Emoji:** *(your signature — pick one that feels right)*
|
||||||
- **Vibe:**
|
- **Avatar:** *(workspace-relative path, http(s) URL, or data URI)*
|
||||||
*(how do you come across? sharp? warm? chaotic? calm?)*
|
|
||||||
- **Emoji:**
|
|
||||||
*(your signature — pick one that feels right)*
|
|
||||||
- **Avatar:**
|
|
||||||
*(workspace-relative path, http(s) URL, or data URI)*
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -182,8 +182,6 @@ By default, Clawdbot runs a heartbeat every 30 minutes with the prompt:
|
|||||||
`Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`
|
`Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`
|
||||||
Set `agents.defaults.heartbeat.every: "0m"` to disable.
|
Set `agents.defaults.heartbeat.every: "0m"` to disable.
|
||||||
|
|
||||||
- If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown headers like `# Heading`), Clawdbot skips the heartbeat run to save API calls.
|
|
||||||
- If the file is missing, the heartbeat still runs and the model decides what to do.
|
|
||||||
- If the agent replies with `HEARTBEAT_OK` (optionally with short padding; see `agents.defaults.heartbeat.ackMaxChars`), Clawdbot suppresses outbound delivery for that heartbeat.
|
- If the agent replies with `HEARTBEAT_OK` (optionally with short padding; see `agents.defaults.heartbeat.ackMaxChars`), Clawdbot suppresses outbound delivery for that heartbeat.
|
||||||
- Heartbeats run full agent turns — shorter intervals burn more tokens.
|
- Heartbeats run full agent turns — shorter intervals burn more tokens.
|
||||||
|
|
||||||
|
|||||||
@@ -971,10 +971,6 @@ Heartbeats run every **30m** by default. Tune or disable them:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
If `HEARTBEAT.md` exists but is effectively empty (only blank lines and markdown
|
|
||||||
headers like `# Heading`), Clawdbot skips the heartbeat run to save API calls.
|
|
||||||
If the file is missing, the heartbeat still runs and the model decides what to do.
|
|
||||||
|
|
||||||
Per-agent overrides use `agents.list[].heartbeat`. Docs: [Heartbeat](/gateway/heartbeat).
|
Per-agent overrides use `agents.list[].heartbeat`. Docs: [Heartbeat](/gateway/heartbeat).
|
||||||
|
|
||||||
### Do I need to add a “bot account” to a WhatsApp group?
|
### Do I need to add a “bot account” to a WhatsApp group?
|
||||||
|
|||||||
@@ -166,19 +166,6 @@ Clawdbot preserves the auth when calling `/json/*` endpoints and when connecting
|
|||||||
to the CDP WebSocket. Prefer environment variables or secrets managers for
|
to the CDP WebSocket. Prefer environment variables or secrets managers for
|
||||||
tokens instead of committing them to config files.
|
tokens instead of committing them to config files.
|
||||||
|
|
||||||
### Node browser proxy (zero-config default)
|
|
||||||
|
|
||||||
If you run a **node host** on the machine that has your browser, Clawdbot can
|
|
||||||
auto-route browser tool calls to that node without any custom `controlUrl`
|
|
||||||
setup. This is the default path for remote gateways.
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- The node host exposes its local browser control server via a **proxy command**.
|
|
||||||
- Profiles come from the node’s own `browser.profiles` config (same as local).
|
|
||||||
- Disable if you don’t want it:
|
|
||||||
- On the node: `nodeHost.browserProxy.enabled=false`
|
|
||||||
- On the gateway: `gateway.nodes.browser.mode="off"`
|
|
||||||
|
|
||||||
### Browserless (hosted remote CDP)
|
### Browserless (hosted remote CDP)
|
||||||
|
|
||||||
[Browserless](https://browserless.io) is a hosted Chromium service that exposes
|
[Browserless](https://browserless.io) is a hosted Chromium service that exposes
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ Exec approvals are the **companion app / node host guardrail** for letting a san
|
|||||||
commands on a real host (`gateway` or `node`). Think of it like a safety interlock:
|
commands on a real host (`gateway` or `node`). Think of it like a safety interlock:
|
||||||
commands are allowed only when policy + allowlist + (optional) user approval all agree.
|
commands are allowed only when policy + allowlist + (optional) user approval all agree.
|
||||||
Exec approvals are **in addition** to tool policy and elevated gating (unless elevated is set to `full`, which skips approvals).
|
Exec approvals are **in addition** to tool policy and elevated gating (unless elevated is set to `full`, which skips approvals).
|
||||||
Effective policy is the **stricter** of `tools.exec.*` and approvals defaults; if an approvals field is omitted, the `tools.exec` value is used.
|
|
||||||
|
|
||||||
If the companion app UI is **not available**, any request that requires a prompt is
|
If the companion app UI is **not available**, any request that requires a prompt is
|
||||||
resolved by the **ask fallback** (default: deny).
|
resolved by the **ask fallback** (default: deny).
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
normalizeAccountId,
|
normalizeAccountId,
|
||||||
PAIRING_APPROVED_MESSAGE,
|
PAIRING_APPROVED_MESSAGE,
|
||||||
resolveBlueBubblesGroupRequireMention,
|
resolveBlueBubblesGroupRequireMention,
|
||||||
resolveBlueBubblesGroupToolPolicy,
|
|
||||||
setAccountEnabledInConfigSection,
|
setAccountEnabledInConfigSection,
|
||||||
} from "clawdbot/plugin-sdk";
|
} from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
@@ -63,7 +62,6 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveBlueBubblesGroupRequireMention,
|
resolveRequireMention: resolveBlueBubblesGroupRequireMention,
|
||||||
resolveToolPolicy: resolveBlueBubblesGroupToolPolicy,
|
|
||||||
},
|
},
|
||||||
threading: {
|
threading: {
|
||||||
buildToolContext: ({ context, hasRepliedRef }) => ({
|
buildToolContext: ({ context, hasRepliedRef }) => ({
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MarkdownConfigSchema, ToolPolicySchema } from "clawdbot/plugin-sdk";
|
import { MarkdownConfigSchema } from "clawdbot/plugin-sdk";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const allowFromEntry = z.union([z.string(), z.number()]);
|
const allowFromEntry = z.union([z.string(), z.number()]);
|
||||||
@@ -21,7 +21,6 @@ const bluebubblesActionSchema = z
|
|||||||
|
|
||||||
const bluebubblesGroupConfigSchema = z.object({
|
const bluebubblesGroupConfigSchema = z.object({
|
||||||
requireMention: z.boolean().optional(),
|
requireMention: z.boolean().optional(),
|
||||||
tools: ToolPolicySchema,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const bluebubblesAccountSchema = z.object({
|
const bluebubblesAccountSchema = z.object({
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ export type GroupPolicy = "open" | "disabled" | "allowlist";
|
|||||||
export type BlueBubblesGroupConfig = {
|
export type BlueBubblesGroupConfig = {
|
||||||
/** If true, only respond in this group when mentioned. */
|
/** If true, only respond in this group when mentioned. */
|
||||||
requireMention?: boolean;
|
requireMention?: boolean;
|
||||||
/** Optional tool policy overrides for this group. */
|
|
||||||
tools?: { allow?: string[]; deny?: string[] };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BlueBubblesAccountConfig = {
|
export type BlueBubblesAccountConfig = {
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import {
|
|||||||
resolveDiscordAccount,
|
resolveDiscordAccount,
|
||||||
resolveDefaultDiscordAccountId,
|
resolveDefaultDiscordAccountId,
|
||||||
resolveDiscordGroupRequireMention,
|
resolveDiscordGroupRequireMention,
|
||||||
resolveDiscordGroupToolPolicy,
|
|
||||||
setAccountEnabledInConfigSection,
|
setAccountEnabledInConfigSection,
|
||||||
type ChannelMessageActionAdapter,
|
type ChannelMessageActionAdapter,
|
||||||
type ChannelPlugin,
|
type ChannelPlugin,
|
||||||
@@ -145,7 +144,6 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveDiscordGroupRequireMention,
|
resolveRequireMention: resolveDiscordGroupRequireMention,
|
||||||
resolveToolPolicy: resolveDiscordGroupToolPolicy,
|
|
||||||
},
|
},
|
||||||
mentions: {
|
mentions: {
|
||||||
stripPatterns: () => ["<@!?\\d+>"],
|
stripPatterns: () => ["<@!?\\d+>"],
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
resolveDefaultIMessageAccountId,
|
resolveDefaultIMessageAccountId,
|
||||||
resolveIMessageAccount,
|
resolveIMessageAccount,
|
||||||
resolveIMessageGroupRequireMention,
|
resolveIMessageGroupRequireMention,
|
||||||
resolveIMessageGroupToolPolicy,
|
|
||||||
setAccountEnabledInConfigSection,
|
setAccountEnabledInConfigSection,
|
||||||
type ChannelPlugin,
|
type ChannelPlugin,
|
||||||
type ResolvedIMessageAccount,
|
type ResolvedIMessageAccount,
|
||||||
@@ -107,7 +106,6 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveIMessageGroupRequireMention,
|
resolveRequireMention: resolveIMessageGroupRequireMention,
|
||||||
resolveToolPolicy: resolveIMessageGroupToolPolicy,
|
|
||||||
},
|
},
|
||||||
messaging: {
|
messaging: {
|
||||||
targetResolver: {
|
targetResolver: {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
|
|
||||||
import { matrixMessageActions } from "./actions.js";
|
import { matrixMessageActions } from "./actions.js";
|
||||||
import { MatrixConfigSchema } from "./config-schema.js";
|
import { MatrixConfigSchema } from "./config-schema.js";
|
||||||
import { resolveMatrixGroupRequireMention, resolveMatrixGroupToolPolicy } from "./group-mentions.js";
|
import { resolveMatrixGroupRequireMention } from "./group-mentions.js";
|
||||||
import type { CoreConfig } from "./types.js";
|
import type { CoreConfig } from "./types.js";
|
||||||
import {
|
import {
|
||||||
listMatrixAccountIds,
|
listMatrixAccountIds,
|
||||||
@@ -167,7 +167,6 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveMatrixGroupRequireMention,
|
resolveRequireMention: resolveMatrixGroupRequireMention,
|
||||||
resolveToolPolicy: resolveMatrixGroupToolPolicy,
|
|
||||||
},
|
},
|
||||||
threading: {
|
threading: {
|
||||||
resolveReplyToMode: ({ cfg }) =>
|
resolveReplyToMode: ({ cfg }) =>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MarkdownConfigSchema, ToolPolicySchema } from "clawdbot/plugin-sdk";
|
import { MarkdownConfigSchema } from "clawdbot/plugin-sdk";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const allowFromEntry = z.union([z.string(), z.number()]);
|
const allowFromEntry = z.union([z.string(), z.number()]);
|
||||||
@@ -26,7 +26,6 @@ const matrixRoomSchema = z
|
|||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
allow: z.boolean().optional(),
|
allow: z.boolean().optional(),
|
||||||
requireMention: z.boolean().optional(),
|
requireMention: z.boolean().optional(),
|
||||||
tools: ToolPolicySchema,
|
|
||||||
autoReply: z.boolean().optional(),
|
autoReply: z.boolean().optional(),
|
||||||
users: z.array(allowFromEntry).optional(),
|
users: z.array(allowFromEntry).optional(),
|
||||||
skills: z.array(z.string()).optional(),
|
skills: z.array(z.string()).optional(),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ChannelGroupContext, GroupToolPolicyConfig } from "clawdbot/plugin-sdk";
|
import type { ChannelGroupContext } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
import { resolveMatrixRoomConfig } from "./matrix/monitor/rooms.js";
|
import { resolveMatrixRoomConfig } from "./matrix/monitor/rooms.js";
|
||||||
import type { CoreConfig } from "./types.js";
|
import type { CoreConfig } from "./types.js";
|
||||||
@@ -32,30 +32,3 @@ export function resolveMatrixGroupRequireMention(params: ChannelGroupContext): b
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveMatrixGroupToolPolicy(
|
|
||||||
params: ChannelGroupContext,
|
|
||||||
): GroupToolPolicyConfig | undefined {
|
|
||||||
const rawGroupId = params.groupId?.trim() ?? "";
|
|
||||||
let roomId = rawGroupId;
|
|
||||||
const lower = roomId.toLowerCase();
|
|
||||||
if (lower.startsWith("matrix:")) {
|
|
||||||
roomId = roomId.slice("matrix:".length).trim();
|
|
||||||
}
|
|
||||||
if (roomId.toLowerCase().startsWith("channel:")) {
|
|
||||||
roomId = roomId.slice("channel:".length).trim();
|
|
||||||
}
|
|
||||||
if (roomId.toLowerCase().startsWith("room:")) {
|
|
||||||
roomId = roomId.slice("room:".length).trim();
|
|
||||||
}
|
|
||||||
const groupChannel = params.groupChannel?.trim() ?? "";
|
|
||||||
const aliases = groupChannel ? [groupChannel] : [];
|
|
||||||
const cfg = params.cfg as CoreConfig;
|
|
||||||
const resolved = resolveMatrixRoomConfig({
|
|
||||||
rooms: cfg.channels?.matrix?.groups ?? cfg.channels?.matrix?.rooms,
|
|
||||||
roomId,
|
|
||||||
aliases,
|
|
||||||
name: groupChannel || undefined,
|
|
||||||
}).config;
|
|
||||||
return resolved?.tools;
|
|
||||||
}
|
|
||||||
@@ -18,8 +18,6 @@ export type MatrixRoomConfig = {
|
|||||||
allow?: boolean;
|
allow?: boolean;
|
||||||
/** Require mentioning the bot to trigger replies. */
|
/** Require mentioning the bot to trigger replies. */
|
||||||
requireMention?: boolean;
|
requireMention?: boolean;
|
||||||
/** Optional tool policy overrides for this room. */
|
|
||||||
tools?: { allow?: string[]; deny?: string[] };
|
|
||||||
/** If true, reply without mention requirements. */
|
/** If true, reply without mention requirements. */
|
||||||
autoReply?: boolean;
|
autoReply?: boolean;
|
||||||
/** Optional allowlist for room senders (user IDs or localparts). */
|
/** Optional allowlist for room senders (user IDs or localparts). */
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
import { msteamsOnboardingAdapter } from "./onboarding.js";
|
import { msteamsOnboardingAdapter } from "./onboarding.js";
|
||||||
import { msteamsOutbound } from "./outbound.js";
|
import { msteamsOutbound } from "./outbound.js";
|
||||||
import { probeMSTeams } from "./probe.js";
|
import { probeMSTeams } from "./probe.js";
|
||||||
import { resolveMSTeamsGroupToolPolicy } from "./policy.js";
|
|
||||||
import {
|
import {
|
||||||
normalizeMSTeamsMessagingTarget,
|
normalizeMSTeamsMessagingTarget,
|
||||||
normalizeMSTeamsUserInput,
|
normalizeMSTeamsUserInput,
|
||||||
@@ -78,9 +77,6 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
|
|||||||
hasRepliedRef,
|
hasRepliedRef,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
groups: {
|
|
||||||
resolveToolPolicy: resolveMSTeamsGroupToolPolicy,
|
|
||||||
},
|
|
||||||
reload: { configPrefixes: ["channels.msteams"] },
|
reload: { configPrefixes: ["channels.msteams"] },
|
||||||
configSchema: buildChannelConfigSchema(MSTeamsConfigSchema),
|
configSchema: buildChannelConfigSchema(MSTeamsConfigSchema),
|
||||||
config: {
|
config: {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
AllowlistMatch,
|
AllowlistMatch,
|
||||||
ChannelGroupContext,
|
|
||||||
GroupPolicy,
|
GroupPolicy,
|
||||||
GroupToolPolicyConfig,
|
|
||||||
MSTeamsChannelConfig,
|
MSTeamsChannelConfig,
|
||||||
MSTeamsConfig,
|
MSTeamsConfig,
|
||||||
MSTeamsReplyStyle,
|
MSTeamsReplyStyle,
|
||||||
@@ -88,50 +86,6 @@ export function resolveMSTeamsRouteConfig(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveMSTeamsGroupToolPolicy(
|
|
||||||
params: ChannelGroupContext,
|
|
||||||
): GroupToolPolicyConfig | undefined {
|
|
||||||
const cfg = params.cfg.channels?.msteams;
|
|
||||||
if (!cfg) return undefined;
|
|
||||||
const groupId = params.groupId?.trim();
|
|
||||||
const groupChannel = params.groupChannel?.trim();
|
|
||||||
const groupSpace = params.groupSpace?.trim();
|
|
||||||
|
|
||||||
const resolved = resolveMSTeamsRouteConfig({
|
|
||||||
cfg,
|
|
||||||
teamId: groupSpace,
|
|
||||||
teamName: groupSpace,
|
|
||||||
conversationId: groupId,
|
|
||||||
channelName: groupChannel,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (resolved.channelConfig) {
|
|
||||||
return resolved.channelConfig.tools ?? resolved.teamConfig?.tools;
|
|
||||||
}
|
|
||||||
if (resolved.teamConfig?.tools) return resolved.teamConfig.tools;
|
|
||||||
|
|
||||||
if (!groupId) return undefined;
|
|
||||||
|
|
||||||
const channelCandidates = buildChannelKeyCandidates(
|
|
||||||
groupId,
|
|
||||||
groupChannel,
|
|
||||||
groupChannel ? normalizeChannelSlug(groupChannel) : undefined,
|
|
||||||
);
|
|
||||||
for (const teamConfig of Object.values(cfg.teams ?? {})) {
|
|
||||||
const match = resolveChannelEntryMatchWithFallback({
|
|
||||||
entries: teamConfig?.channels ?? {},
|
|
||||||
keys: channelCandidates,
|
|
||||||
wildcardKey: "*",
|
|
||||||
normalizeKey: normalizeChannelSlug,
|
|
||||||
});
|
|
||||||
if (match.entry) {
|
|
||||||
return match.entry.tools ?? teamConfig?.tools;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MSTeamsReplyPolicy = {
|
export type MSTeamsReplyPolicy = {
|
||||||
requireMention: boolean;
|
requireMention: boolean;
|
||||||
replyStyle: MSTeamsReplyStyle;
|
replyStyle: MSTeamsReplyStyle;
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import { nextcloudTalkOnboardingAdapter } from "./onboarding.js";
|
|||||||
import { getNextcloudTalkRuntime } from "./runtime.js";
|
import { getNextcloudTalkRuntime } from "./runtime.js";
|
||||||
import { sendMessageNextcloudTalk } from "./send.js";
|
import { sendMessageNextcloudTalk } from "./send.js";
|
||||||
import type { CoreConfig } from "./types.js";
|
import type { CoreConfig } from "./types.js";
|
||||||
import { resolveNextcloudTalkGroupToolPolicy } from "./policy.js";
|
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
id: "nextcloud-talk",
|
id: "nextcloud-talk",
|
||||||
@@ -160,7 +159,6 @@ export const nextcloudTalkPlugin: ChannelPlugin<ResolvedNextcloudTalkAccount> =
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
resolveToolPolicy: resolveNextcloudTalkGroupToolPolicy,
|
|
||||||
},
|
},
|
||||||
messaging: {
|
messaging: {
|
||||||
normalizeTarget: normalizeNextcloudTalkMessagingTarget,
|
normalizeTarget: normalizeNextcloudTalkMessagingTarget,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
DmPolicySchema,
|
DmPolicySchema,
|
||||||
GroupPolicySchema,
|
GroupPolicySchema,
|
||||||
MarkdownConfigSchema,
|
MarkdownConfigSchema,
|
||||||
ToolPolicySchema,
|
|
||||||
requireOpenAllowFrom,
|
requireOpenAllowFrom,
|
||||||
} from "clawdbot/plugin-sdk";
|
} from "clawdbot/plugin-sdk";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -12,7 +11,6 @@ import { z } from "zod";
|
|||||||
export const NextcloudTalkRoomSchema = z
|
export const NextcloudTalkRoomSchema = z
|
||||||
.object({
|
.object({
|
||||||
requireMention: z.boolean().optional(),
|
requireMention: z.boolean().optional(),
|
||||||
tools: ToolPolicySchema,
|
|
||||||
skills: z.array(z.string()).optional(),
|
skills: z.array(z.string()).optional(),
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
allowFrom: z.array(z.string()).optional(),
|
allowFrom: z.array(z.string()).optional(),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { AllowlistMatch, ChannelGroupContext, GroupPolicy, GroupToolPolicyConfig } from "clawdbot/plugin-sdk";
|
import type { AllowlistMatch, GroupPolicy } from "clawdbot/plugin-sdk";
|
||||||
import {
|
import {
|
||||||
buildChannelKeyCandidates,
|
buildChannelKeyCandidates,
|
||||||
normalizeChannelSlug,
|
normalizeChannelSlug,
|
||||||
@@ -86,21 +86,6 @@ export function resolveNextcloudTalkRoomMatch(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveNextcloudTalkGroupToolPolicy(
|
|
||||||
params: ChannelGroupContext,
|
|
||||||
): GroupToolPolicyConfig | undefined {
|
|
||||||
const cfg = params.cfg as { channels?: { "nextcloud-talk"?: { rooms?: Record<string, NextcloudTalkRoomConfig> } } };
|
|
||||||
const roomToken = params.groupId?.trim();
|
|
||||||
if (!roomToken) return undefined;
|
|
||||||
const roomName = params.groupChannel?.trim() || undefined;
|
|
||||||
const match = resolveNextcloudTalkRoomMatch({
|
|
||||||
rooms: cfg.channels?.["nextcloud-talk"]?.rooms,
|
|
||||||
roomToken,
|
|
||||||
roomName,
|
|
||||||
});
|
|
||||||
return match.roomConfig?.tools ?? match.wildcardConfig?.tools;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveNextcloudTalkRequireMention(params: {
|
export function resolveNextcloudTalkRequireMention(params: {
|
||||||
roomConfig?: NextcloudTalkRoomConfig;
|
roomConfig?: NextcloudTalkRoomConfig;
|
||||||
wildcardConfig?: NextcloudTalkRoomConfig;
|
wildcardConfig?: NextcloudTalkRoomConfig;
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import type {
|
|||||||
|
|
||||||
export type NextcloudTalkRoomConfig = {
|
export type NextcloudTalkRoomConfig = {
|
||||||
requireMention?: boolean;
|
requireMention?: boolean;
|
||||||
/** Optional tool policy overrides for this room. */
|
|
||||||
tools?: { allow?: string[]; deny?: string[] };
|
|
||||||
/** If specified, only load these skills for this room. Omit = all skills; empty = no skills. */
|
/** If specified, only load these skills for this room. Omit = all skills; empty = no skills. */
|
||||||
skills?: string[];
|
skills?: string[];
|
||||||
/** If false, disable the bot for this room. */
|
/** If false, disable the bot for this room. */
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
resolveSlackAccount,
|
resolveSlackAccount,
|
||||||
resolveSlackReplyToMode,
|
resolveSlackReplyToMode,
|
||||||
resolveSlackGroupRequireMention,
|
resolveSlackGroupRequireMention,
|
||||||
resolveSlackGroupToolPolicy,
|
|
||||||
buildSlackThreadingToolContext,
|
buildSlackThreadingToolContext,
|
||||||
setAccountEnabledInConfigSection,
|
setAccountEnabledInConfigSection,
|
||||||
slackOnboardingAdapter,
|
slackOnboardingAdapter,
|
||||||
@@ -162,7 +161,6 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveSlackGroupRequireMention,
|
resolveRequireMention: resolveSlackGroupRequireMention,
|
||||||
resolveToolPolicy: resolveSlackGroupToolPolicy,
|
|
||||||
},
|
},
|
||||||
threading: {
|
threading: {
|
||||||
resolveReplyToMode: ({ cfg, accountId, chatType }) =>
|
resolveReplyToMode: ({ cfg, accountId, chatType }) =>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
resolveDefaultTelegramAccountId,
|
resolveDefaultTelegramAccountId,
|
||||||
resolveTelegramAccount,
|
resolveTelegramAccount,
|
||||||
resolveTelegramGroupRequireMention,
|
resolveTelegramGroupRequireMention,
|
||||||
resolveTelegramGroupToolPolicy,
|
|
||||||
setAccountEnabledInConfigSection,
|
setAccountEnabledInConfigSection,
|
||||||
telegramOnboardingAdapter,
|
telegramOnboardingAdapter,
|
||||||
TelegramConfigSchema,
|
TelegramConfigSchema,
|
||||||
@@ -155,7 +154,6 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveTelegramGroupRequireMention,
|
resolveRequireMention: resolveTelegramGroupRequireMention,
|
||||||
resolveToolPolicy: resolveTelegramGroupToolPolicy,
|
|
||||||
},
|
},
|
||||||
threading: {
|
threading: {
|
||||||
resolveReplyToMode: ({ cfg }) => cfg.channels?.telegram?.replyToMode ?? "first",
|
resolveReplyToMode: ({ cfg }) => cfg.channels?.telegram?.replyToMode ?? "first",
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
resolveDefaultWhatsAppAccountId,
|
resolveDefaultWhatsAppAccountId,
|
||||||
resolveWhatsAppAccount,
|
resolveWhatsAppAccount,
|
||||||
resolveWhatsAppGroupRequireMention,
|
resolveWhatsAppGroupRequireMention,
|
||||||
resolveWhatsAppGroupToolPolicy,
|
|
||||||
resolveWhatsAppHeartbeatRecipients,
|
resolveWhatsAppHeartbeatRecipients,
|
||||||
whatsappOnboardingAdapter,
|
whatsappOnboardingAdapter,
|
||||||
WhatsAppConfigSchema,
|
WhatsAppConfigSchema,
|
||||||
@@ -199,7 +198,6 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveWhatsAppGroupRequireMention,
|
resolveRequireMention: resolveWhatsAppGroupRequireMention,
|
||||||
resolveToolPolicy: resolveWhatsAppGroupToolPolicy,
|
|
||||||
resolveGroupIntroHint: () =>
|
resolveGroupIntroHint: () =>
|
||||||
"WhatsApp IDs: SenderId is the participant JID; [message_id: ...] is the message id for reactions (use SenderId as participant).",
|
"WhatsApp IDs: SenderId is the participant JID; [message_id: ...] is the message id for reactions (use SenderId as participant).",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ import type {
|
|||||||
ChannelAccountSnapshot,
|
ChannelAccountSnapshot,
|
||||||
ChannelDirectoryEntry,
|
ChannelDirectoryEntry,
|
||||||
ChannelDock,
|
ChannelDock,
|
||||||
ChannelGroupContext,
|
|
||||||
ChannelPlugin,
|
ChannelPlugin,
|
||||||
ClawdbotConfig,
|
ClawdbotConfig,
|
||||||
GroupToolPolicyConfig,
|
|
||||||
} from "clawdbot/plugin-sdk";
|
} from "clawdbot/plugin-sdk";
|
||||||
import {
|
import {
|
||||||
applyAccountNameToChannelSection,
|
applyAccountNameToChannelSection,
|
||||||
@@ -81,26 +79,6 @@ function mapGroup(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveZalouserGroupToolPolicy(
|
|
||||||
params: ChannelGroupContext,
|
|
||||||
): GroupToolPolicyConfig | undefined {
|
|
||||||
const account = resolveZalouserAccountSync({
|
|
||||||
cfg: params.cfg as ClawdbotConfig,
|
|
||||||
accountId: params.accountId ?? undefined,
|
|
||||||
});
|
|
||||||
const groups = account.config.groups ?? {};
|
|
||||||
const groupId = params.groupId?.trim();
|
|
||||||
const groupChannel = params.groupChannel?.trim();
|
|
||||||
const candidates = [groupId, groupChannel, "*"].filter(
|
|
||||||
(value): value is string => Boolean(value),
|
|
||||||
);
|
|
||||||
for (const key of candidates) {
|
|
||||||
const entry = groups[key];
|
|
||||||
if (entry?.tools) return entry.tools;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const zalouserDock: ChannelDock = {
|
export const zalouserDock: ChannelDock = {
|
||||||
id: "zalouser",
|
id: "zalouser",
|
||||||
capabilities: {
|
capabilities: {
|
||||||
@@ -123,7 +101,6 @@ export const zalouserDock: ChannelDock = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: () => true,
|
resolveRequireMention: () => true,
|
||||||
resolveToolPolicy: resolveZalouserGroupToolPolicy,
|
|
||||||
},
|
},
|
||||||
threading: {
|
threading: {
|
||||||
resolveReplyToMode: () => "off",
|
resolveReplyToMode: () => "off",
|
||||||
@@ -211,7 +188,6 @@ export const zalouserPlugin: ChannelPlugin<ResolvedZalouserAccount> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: () => true,
|
resolveRequireMention: () => true,
|
||||||
resolveToolPolicy: resolveZalouserGroupToolPolicy,
|
|
||||||
},
|
},
|
||||||
threading: {
|
threading: {
|
||||||
resolveReplyToMode: () => "off",
|
resolveReplyToMode: () => "off",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MarkdownConfigSchema, ToolPolicySchema } from "clawdbot/plugin-sdk";
|
import { MarkdownConfigSchema } from "clawdbot/plugin-sdk";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const allowFromEntry = z.union([z.string(), z.number()]);
|
const allowFromEntry = z.union([z.string(), z.number()]);
|
||||||
@@ -6,7 +6,6 @@ const allowFromEntry = z.union([z.string(), z.number()]);
|
|||||||
const groupConfigSchema = z.object({
|
const groupConfigSchema = z.object({
|
||||||
allow: z.boolean().optional(),
|
allow: z.boolean().optional(),
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
tools: ToolPolicySchema,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const zalouserAccountSchema = z.object({
|
const zalouserAccountSchema = z.object({
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export type ZalouserAccountConfig = {
|
|||||||
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
||||||
allowFrom?: Array<string | number>;
|
allowFrom?: Array<string | number>;
|
||||||
groupPolicy?: "open" | "allowlist" | "disabled";
|
groupPolicy?: "open" | "allowlist" | "disabled";
|
||||||
groups?: Record<string, { allow?: boolean; enabled?: boolean; tools?: { allow?: string[]; deny?: string[] } }>;
|
groups?: Record<string, { allow?: boolean; enabled?: boolean }>;
|
||||||
messagePrefix?: string;
|
messagePrefix?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ export type ZalouserConfig = {
|
|||||||
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
||||||
allowFrom?: Array<string | number>;
|
allowFrom?: Array<string | number>;
|
||||||
groupPolicy?: "open" | "allowlist" | "disabled";
|
groupPolicy?: "open" | "allowlist" | "disabled";
|
||||||
groups?: Record<string, { allow?: boolean; enabled?: boolean; tools?: { allow?: string[]; deny?: string[] } }>;
|
groups?: Record<string, { allow?: boolean; enabled?: boolean }>;
|
||||||
messagePrefix?: string;
|
messagePrefix?: string;
|
||||||
accounts?: Record<string, ZalouserAccountConfig>;
|
accounts?: Record<string, ZalouserAccountConfig>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,182 +0,0 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
|
||||||
|
|
||||||
import type { StreamFn } from "@mariozechner/pi-agent-core";
|
|
||||||
|
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
|
||||||
import { resolveUserPath } from "../utils.js";
|
|
||||||
import { createAnthropicPayloadLogger } from "./anthropic-payload-log.js";
|
|
||||||
|
|
||||||
describe("createAnthropicPayloadLogger", () => {
|
|
||||||
it("returns null when diagnostics payload logging is disabled", () => {
|
|
||||||
const logger = createAnthropicPayloadLogger({
|
|
||||||
cfg: {} as ClawdbotConfig,
|
|
||||||
env: {},
|
|
||||||
modelApi: "anthropic-messages",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(logger).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns null when model api is not anthropic", () => {
|
|
||||||
const logger = createAnthropicPayloadLogger({
|
|
||||||
cfg: {
|
|
||||||
diagnostics: {
|
|
||||||
anthropicPayloadLog: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
env: {},
|
|
||||||
modelApi: "openai",
|
|
||||||
writer: {
|
|
||||||
filePath: "memory",
|
|
||||||
write: () => undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(logger).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("honors diagnostics config and expands file paths", () => {
|
|
||||||
const lines: string[] = [];
|
|
||||||
const logger = createAnthropicPayloadLogger({
|
|
||||||
cfg: {
|
|
||||||
diagnostics: {
|
|
||||||
anthropicPayloadLog: {
|
|
||||||
enabled: true,
|
|
||||||
filePath: "~/.clawdbot/logs/anthropic-payload.jsonl",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
env: {},
|
|
||||||
modelApi: "anthropic-messages",
|
|
||||||
writer: {
|
|
||||||
filePath: "memory",
|
|
||||||
write: (line) => lines.push(line),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(logger).not.toBeNull();
|
|
||||||
expect(logger?.filePath).toBe(resolveUserPath("~/.clawdbot/logs/anthropic-payload.jsonl"));
|
|
||||||
|
|
||||||
logger?.recordUsage([
|
|
||||||
{
|
|
||||||
role: "assistant",
|
|
||||||
usage: {
|
|
||||||
input: 12,
|
|
||||||
},
|
|
||||||
} as unknown as {
|
|
||||||
role: string;
|
|
||||||
usage: { input: number };
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const event = JSON.parse(lines[0]?.trim() ?? "{}") as Record<string, unknown>;
|
|
||||||
expect(event.stage).toBe("usage");
|
|
||||||
expect(event.usage).toEqual({ input: 12 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("skips usage when no new assistant message was added", () => {
|
|
||||||
const lines: string[] = [];
|
|
||||||
const logger = createAnthropicPayloadLogger({
|
|
||||||
cfg: {
|
|
||||||
diagnostics: {
|
|
||||||
anthropicPayloadLog: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
env: {},
|
|
||||||
modelApi: "anthropic-messages",
|
|
||||||
writer: {
|
|
||||||
filePath: "memory",
|
|
||||||
write: (line) => lines.push(line),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
logger?.recordUsage(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
role: "assistant",
|
|
||||||
usage: {
|
|
||||||
input: 1,
|
|
||||||
},
|
|
||||||
} as unknown as {
|
|
||||||
role: string;
|
|
||||||
usage: { input: number };
|
|
||||||
},
|
|
||||||
],
|
|
||||||
undefined,
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(lines.length).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("records request payloads and forwards onPayload", async () => {
|
|
||||||
const lines: string[] = [];
|
|
||||||
let forwarded: unknown;
|
|
||||||
const logger = createAnthropicPayloadLogger({
|
|
||||||
cfg: {
|
|
||||||
diagnostics: {
|
|
||||||
anthropicPayloadLog: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
env: {},
|
|
||||||
modelApi: "anthropic-messages",
|
|
||||||
writer: {
|
|
||||||
filePath: "memory",
|
|
||||||
write: (line) => lines.push(line),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const streamFn = ((_, __, options) => {
|
|
||||||
options?.onPayload?.({ hello: "world" });
|
|
||||||
return Promise.resolve(undefined);
|
|
||||||
}) as StreamFn;
|
|
||||||
|
|
||||||
const wrapped = logger?.wrapStreamFn(streamFn);
|
|
||||||
await wrapped?.(
|
|
||||||
{ api: "anthropic-messages" } as unknown as { api: string },
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
onPayload: (payload) => {
|
|
||||||
forwarded = payload;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const event = JSON.parse(lines[0]?.trim() ?? "{}") as Record<string, unknown>;
|
|
||||||
expect(event.stage).toBe("request");
|
|
||||||
expect(event.payload).toEqual({ hello: "world" });
|
|
||||||
expect(event.payloadDigest).toBeTruthy();
|
|
||||||
expect(forwarded).toEqual({ hello: "world" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("records errors when usage is missing", () => {
|
|
||||||
const lines: string[] = [];
|
|
||||||
const logger = createAnthropicPayloadLogger({
|
|
||||||
cfg: {
|
|
||||||
diagnostics: {
|
|
||||||
anthropicPayloadLog: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
env: {},
|
|
||||||
modelApi: "anthropic-messages",
|
|
||||||
writer: {
|
|
||||||
filePath: "memory",
|
|
||||||
write: (line) => lines.push(line),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
logger?.recordUsage([], new Error("boom"));
|
|
||||||
|
|
||||||
const event = JSON.parse(lines[0]?.trim() ?? "{}") as Record<string, unknown>;
|
|
||||||
expect(event.stage).toBe("usage");
|
|
||||||
expect(event.error).toContain("boom");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
import crypto from "node:crypto";
|
|
||||||
import fs from "node:fs/promises";
|
|
||||||
import path from "node:path";
|
|
||||||
|
|
||||||
import type { AgentMessage, StreamFn } from "@mariozechner/pi-agent-core";
|
|
||||||
import type { Api, Model } from "@mariozechner/pi-ai";
|
|
||||||
|
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
|
||||||
import { resolveStateDir } from "../config/paths.js";
|
|
||||||
import { formatErrorMessage } from "../infra/errors.js";
|
|
||||||
import { parseBooleanValue } from "../utils/boolean.js";
|
|
||||||
import { resolveUserPath } from "../utils.js";
|
|
||||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
|
||||||
|
|
||||||
type PayloadLogStage = "request" | "usage";
|
|
||||||
|
|
||||||
type PayloadLogEvent = {
|
|
||||||
ts: string;
|
|
||||||
stage: PayloadLogStage;
|
|
||||||
runId?: string;
|
|
||||||
sessionId?: string;
|
|
||||||
sessionKey?: string;
|
|
||||||
provider?: string;
|
|
||||||
modelId?: string;
|
|
||||||
modelApi?: string | null;
|
|
||||||
workspaceDir?: string;
|
|
||||||
payload?: unknown;
|
|
||||||
usage?: Record<string, unknown>;
|
|
||||||
error?: string;
|
|
||||||
payloadDigest?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type PayloadLogConfig = {
|
|
||||||
enabled: boolean;
|
|
||||||
filePath: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type PayloadLogInit = {
|
|
||||||
cfg?: ClawdbotConfig;
|
|
||||||
env?: NodeJS.ProcessEnv;
|
|
||||||
};
|
|
||||||
|
|
||||||
type PayloadLogWriter = {
|
|
||||||
filePath: string;
|
|
||||||
write: (line: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const writers = new Map<string, PayloadLogWriter>();
|
|
||||||
const log = createSubsystemLogger("agent/anthropic-payload");
|
|
||||||
|
|
||||||
function resolvePayloadLogConfig(params: PayloadLogInit): PayloadLogConfig {
|
|
||||||
const env = params.env ?? process.env;
|
|
||||||
const config = params.cfg?.diagnostics?.anthropicPayloadLog;
|
|
||||||
const envEnabled = parseBooleanValue(env.CLAWDBOT_ANTHROPIC_PAYLOAD_LOG);
|
|
||||||
const enabled = envEnabled ?? config?.enabled ?? false;
|
|
||||||
const fileOverride = config?.filePath?.trim() || env.CLAWDBOT_ANTHROPIC_PAYLOAD_LOG_FILE?.trim();
|
|
||||||
const filePath = fileOverride
|
|
||||||
? resolveUserPath(fileOverride)
|
|
||||||
: path.join(resolveStateDir(env), "logs", "anthropic-payload.jsonl");
|
|
||||||
return { enabled, filePath };
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWriter(filePath: string): PayloadLogWriter {
|
|
||||||
const existing = writers.get(filePath);
|
|
||||||
if (existing) return existing;
|
|
||||||
|
|
||||||
const dir = path.dirname(filePath);
|
|
||||||
const ready = fs.mkdir(dir, { recursive: true }).catch(() => undefined);
|
|
||||||
let queue = Promise.resolve();
|
|
||||||
|
|
||||||
const writer: PayloadLogWriter = {
|
|
||||||
filePath,
|
|
||||||
write: (line: string) => {
|
|
||||||
queue = queue
|
|
||||||
.then(() => ready)
|
|
||||||
.then(() => fs.appendFile(filePath, line, "utf8"))
|
|
||||||
.catch(() => undefined);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
writers.set(filePath, writer);
|
|
||||||
return writer;
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeJsonStringify(value: unknown): string | null {
|
|
||||||
try {
|
|
||||||
return JSON.stringify(value, (_key, val) => {
|
|
||||||
if (typeof val === "bigint") return val.toString();
|
|
||||||
if (typeof val === "function") return "[Function]";
|
|
||||||
if (val instanceof Error) {
|
|
||||||
return { name: val.name, message: val.message, stack: val.stack };
|
|
||||||
}
|
|
||||||
if (val instanceof Uint8Array) {
|
|
||||||
return { type: "Uint8Array", data: Buffer.from(val).toString("base64") };
|
|
||||||
}
|
|
||||||
return val;
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function digest(value: unknown): string | undefined {
|
|
||||||
const serialized = safeJsonStringify(value);
|
|
||||||
if (!serialized) return undefined;
|
|
||||||
return crypto.createHash("sha256").update(serialized).digest("hex");
|
|
||||||
}
|
|
||||||
|
|
||||||
function isAnthropicModel(model: Model<Api> | undefined | null): boolean {
|
|
||||||
return (model as { api?: unknown })?.api === "anthropic-messages";
|
|
||||||
}
|
|
||||||
|
|
||||||
function findLastAssistantUsage(
|
|
||||||
messages: AgentMessage[],
|
|
||||||
minIndex = 0,
|
|
||||||
): Record<string, unknown> | null {
|
|
||||||
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
||||||
if (i < minIndex) break;
|
|
||||||
const msg = messages[i] as { role?: unknown; usage?: unknown };
|
|
||||||
if (msg?.role === "assistant" && msg.usage && typeof msg.usage === "object") {
|
|
||||||
return msg.usage as Record<string, unknown>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AnthropicPayloadLogger = {
|
|
||||||
enabled: true;
|
|
||||||
filePath: string;
|
|
||||||
wrapStreamFn: (streamFn: StreamFn) => StreamFn;
|
|
||||||
recordUsage: (messages: AgentMessage[], error?: unknown, baselineMessageCount?: number) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function createAnthropicPayloadLogger(params: {
|
|
||||||
cfg?: ClawdbotConfig;
|
|
||||||
env?: NodeJS.ProcessEnv;
|
|
||||||
runId?: string;
|
|
||||||
sessionId?: string;
|
|
||||||
sessionKey?: string;
|
|
||||||
provider?: string;
|
|
||||||
modelId?: string;
|
|
||||||
modelApi?: string | null;
|
|
||||||
workspaceDir?: string;
|
|
||||||
writer?: PayloadLogWriter;
|
|
||||||
}): AnthropicPayloadLogger | null {
|
|
||||||
const env = params.env ?? process.env;
|
|
||||||
const cfg = resolvePayloadLogConfig({ env, cfg: params.cfg });
|
|
||||||
if (!cfg.enabled) return null;
|
|
||||||
if (params.modelApi !== "anthropic-messages") return null;
|
|
||||||
|
|
||||||
const writer = params.writer ?? getWriter(cfg.filePath);
|
|
||||||
const base: Omit<PayloadLogEvent, "ts" | "stage"> = {
|
|
||||||
runId: params.runId,
|
|
||||||
sessionId: params.sessionId,
|
|
||||||
sessionKey: params.sessionKey,
|
|
||||||
provider: params.provider,
|
|
||||||
modelId: params.modelId,
|
|
||||||
modelApi: params.modelApi,
|
|
||||||
workspaceDir: params.workspaceDir,
|
|
||||||
};
|
|
||||||
|
|
||||||
const record = (event: PayloadLogEvent) => {
|
|
||||||
const line = safeJsonStringify(event);
|
|
||||||
if (!line) return;
|
|
||||||
writer.write(`${line}\n`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapStreamFn: AnthropicPayloadLogger["wrapStreamFn"] = (streamFn) => {
|
|
||||||
const wrapped: StreamFn = (model, context, options) => {
|
|
||||||
if (!isAnthropicModel(model as Model<Api>)) {
|
|
||||||
return streamFn(model, context, options);
|
|
||||||
}
|
|
||||||
const nextOnPayload = (payload: unknown) => {
|
|
||||||
record({
|
|
||||||
...base,
|
|
||||||
ts: new Date().toISOString(),
|
|
||||||
stage: "request",
|
|
||||||
payload,
|
|
||||||
payloadDigest: digest(payload),
|
|
||||||
});
|
|
||||||
options?.onPayload?.(payload);
|
|
||||||
};
|
|
||||||
return streamFn(model, context, {
|
|
||||||
...options,
|
|
||||||
onPayload: nextOnPayload,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return wrapped;
|
|
||||||
};
|
|
||||||
|
|
||||||
const recordUsage: AnthropicPayloadLogger["recordUsage"] = (
|
|
||||||
messages,
|
|
||||||
error,
|
|
||||||
baselineMessageCount,
|
|
||||||
) => {
|
|
||||||
const usage = findLastAssistantUsage(messages, baselineMessageCount ?? 0);
|
|
||||||
if (!usage) {
|
|
||||||
if (error) {
|
|
||||||
record({
|
|
||||||
...base,
|
|
||||||
ts: new Date().toISOString(),
|
|
||||||
stage: "usage",
|
|
||||||
error: formatErrorMessage(error),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
record({
|
|
||||||
...base,
|
|
||||||
ts: new Date().toISOString(),
|
|
||||||
stage: "usage",
|
|
||||||
usage,
|
|
||||||
error: error ? formatErrorMessage(error) : undefined,
|
|
||||||
});
|
|
||||||
log.info("anthropic usage", {
|
|
||||||
runId: params.runId,
|
|
||||||
sessionId: params.sessionId,
|
|
||||||
usage,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
log.info("anthropic payload logger enabled", { filePath: cfg.filePath });
|
|
||||||
return { enabled: true, filePath: cfg.filePath, wrapStreamFn, recordUsage };
|
|
||||||
}
|
|
||||||
@@ -129,25 +129,4 @@ describe("exec approvals", () => {
|
|||||||
expect(calls).toContain("node.invoke");
|
expect(calls).toContain("node.invoke");
|
||||||
expect(calls).not.toContain("exec.approval.request");
|
expect(calls).not.toContain("exec.approval.request");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("honors ask=off for elevated gateway exec without prompting", async () => {
|
|
||||||
const { callGatewayTool } = await import("./tools/gateway.js");
|
|
||||||
const calls: string[] = [];
|
|
||||||
vi.mocked(callGatewayTool).mockImplementation(async (method) => {
|
|
||||||
calls.push(method);
|
|
||||||
return { ok: true };
|
|
||||||
});
|
|
||||||
|
|
||||||
const { createExecTool } = await import("./bash-tools.exec.js");
|
|
||||||
const tool = createExecTool({
|
|
||||||
ask: "off",
|
|
||||||
security: "full",
|
|
||||||
approvalRunningNoticeMs: 0,
|
|
||||||
elevated: { enabled: true, allowed: true, defaultLevel: "ask" },
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await tool.execute("call3", { command: "echo ok", elevated: true });
|
|
||||||
expect(result.details.status).toBe("completed");
|
|
||||||
expect(calls).not.toContain("exec.approval.request");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
@@ -838,7 +838,10 @@ export function createExecTool(
|
|||||||
applyPathPrepend(env, defaultPathPrepend);
|
applyPathPrepend(env, defaultPathPrepend);
|
||||||
|
|
||||||
if (host === "node") {
|
if (host === "node") {
|
||||||
const approvals = resolveExecApprovals(agentId, { security, ask });
|
const approvals = resolveExecApprovals(
|
||||||
|
agentId,
|
||||||
|
host === "node" ? { security: "allowlist" } : undefined,
|
||||||
|
);
|
||||||
const hostSecurity = minSecurity(security, approvals.agent.security);
|
const hostSecurity = minSecurity(security, approvals.agent.security);
|
||||||
const hostAsk = maxAsk(ask, approvals.agent.ask);
|
const hostAsk = maxAsk(ask, approvals.agent.ask);
|
||||||
const askFallback = approvals.agent.askFallback;
|
const askFallback = approvals.agent.askFallback;
|
||||||
@@ -1109,7 +1112,7 @@ export function createExecTool(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (host === "gateway" && !bypassApprovals) {
|
if (host === "gateway" && !bypassApprovals) {
|
||||||
const approvals = resolveExecApprovals(agentId, { security, ask });
|
const approvals = resolveExecApprovals(agentId, { security: "allowlist" });
|
||||||
const hostSecurity = minSecurity(security, approvals.agent.security);
|
const hostSecurity = minSecurity(security, approvals.agent.security);
|
||||||
const hostAsk = maxAsk(ask, approvals.agent.ask);
|
const hostAsk = maxAsk(ask, approvals.agent.ask);
|
||||||
const askFallback = approvals.agent.askFallback;
|
const askFallback = approvals.agent.askFallback;
|
||||||
|
|||||||
@@ -31,12 +31,6 @@ export function createClawdbotTools(options?: {
|
|||||||
agentTo?: string;
|
agentTo?: string;
|
||||||
/** Thread/topic identifier for routing replies to the originating thread. */
|
/** Thread/topic identifier for routing replies to the originating thread. */
|
||||||
agentThreadId?: string | number;
|
agentThreadId?: string | number;
|
||||||
/** Group id for channel-level tool policy inheritance. */
|
|
||||||
agentGroupId?: string | null;
|
|
||||||
/** Group channel label for channel-level tool policy inheritance. */
|
|
||||||
agentGroupChannel?: string | null;
|
|
||||||
/** Group space label for channel-level tool policy inheritance. */
|
|
||||||
agentGroupSpace?: string | null;
|
|
||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
sandboxRoot?: string;
|
sandboxRoot?: string;
|
||||||
workspaceDir?: string;
|
workspaceDir?: string;
|
||||||
@@ -120,9 +114,6 @@ export function createClawdbotTools(options?: {
|
|||||||
agentAccountId: options?.agentAccountId,
|
agentAccountId: options?.agentAccountId,
|
||||||
agentTo: options?.agentTo,
|
agentTo: options?.agentTo,
|
||||||
agentThreadId: options?.agentThreadId,
|
agentThreadId: options?.agentThreadId,
|
||||||
agentGroupId: options?.agentGroupId,
|
|
||||||
agentGroupChannel: options?.agentGroupChannel,
|
|
||||||
agentGroupSpace: options?.agentGroupSpace,
|
|
||||||
sandboxed: options?.sandboxed,
|
sandboxed: options?.sandboxed,
|
||||||
}),
|
}),
|
||||||
createSessionStatusTool({
|
createSessionStatusTool({
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
|
||||||
|
|
||||||
import { parseIdentityMarkdown } from "./identity-file.js";
|
|
||||||
|
|
||||||
describe("parseIdentityMarkdown", () => {
|
|
||||||
it("ignores identity template placeholders", () => {
|
|
||||||
const content = `
|
|
||||||
# IDENTITY.md - Who Am I?
|
|
||||||
|
|
||||||
- **Name:** *(pick something you like)*
|
|
||||||
- **Creature:** *(AI? robot? familiar? ghost in the machine? something weirder?)*
|
|
||||||
- **Vibe:** *(how do you come across? sharp? warm? chaotic? calm?)*
|
|
||||||
- **Emoji:** *(your signature - pick one that feels right)*
|
|
||||||
- **Avatar:** *(workspace-relative path, http(s) URL, or data URI)*
|
|
||||||
`;
|
|
||||||
const parsed = parseIdentityMarkdown(content);
|
|
||||||
expect(parsed).toEqual({});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("parses explicit identity values", () => {
|
|
||||||
const content = `
|
|
||||||
- **Name:** Samantha
|
|
||||||
- **Creature:** Robot
|
|
||||||
- **Vibe:** Warm
|
|
||||||
- **Emoji:** :robot:
|
|
||||||
- **Avatar:** avatars/clawd.png
|
|
||||||
`;
|
|
||||||
const parsed = parseIdentityMarkdown(content);
|
|
||||||
expect(parsed).toEqual({
|
|
||||||
name: "Samantha",
|
|
||||||
creature: "Robot",
|
|
||||||
vibe: "Warm",
|
|
||||||
emoji: ":robot:",
|
|
||||||
avatar: "avatars/clawd.png",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -12,30 +12,6 @@ export type AgentIdentityFile = {
|
|||||||
avatar?: string;
|
avatar?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const IDENTITY_PLACEHOLDER_VALUES = new Set([
|
|
||||||
"pick something you like",
|
|
||||||
"ai? robot? familiar? ghost in the machine? something weirder?",
|
|
||||||
"how do you come across? sharp? warm? chaotic? calm?",
|
|
||||||
"your signature - pick one that feels right",
|
|
||||||
"workspace-relative path, http(s) url, or data uri",
|
|
||||||
]);
|
|
||||||
|
|
||||||
function normalizeIdentityValue(value: string): string {
|
|
||||||
let normalized = value.trim();
|
|
||||||
normalized = normalized.replace(/^[*_]+|[*_]+$/g, "").trim();
|
|
||||||
if (normalized.startsWith("(") && normalized.endsWith(")")) {
|
|
||||||
normalized = normalized.slice(1, -1).trim();
|
|
||||||
}
|
|
||||||
normalized = normalized.replace(/[\u2013\u2014]/g, "-");
|
|
||||||
normalized = normalized.replace(/\s+/g, " ").toLowerCase();
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isIdentityPlaceholder(value: string): boolean {
|
|
||||||
const normalized = normalizeIdentityValue(value);
|
|
||||||
return IDENTITY_PLACEHOLDER_VALUES.has(normalized);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseIdentityMarkdown(content: string): AgentIdentityFile {
|
export function parseIdentityMarkdown(content: string): AgentIdentityFile {
|
||||||
const identity: AgentIdentityFile = {};
|
const identity: AgentIdentityFile = {};
|
||||||
const lines = content.split(/\r?\n/);
|
const lines = content.split(/\r?\n/);
|
||||||
@@ -49,7 +25,6 @@ export function parseIdentityMarkdown(content: string): AgentIdentityFile {
|
|||||||
.replace(/^[*_]+|[*_]+$/g, "")
|
.replace(/^[*_]+|[*_]+$/g, "")
|
||||||
.trim();
|
.trim();
|
||||||
if (!value) continue;
|
if (!value) continue;
|
||||||
if (isIdentityPlaceholder(value)) continue;
|
|
||||||
if (label === "name") identity.name = value;
|
if (label === "name") identity.name = value;
|
||||||
if (label === "emoji") identity.emoji = value;
|
if (label === "emoji") identity.emoji = value;
|
||||||
if (label === "creature") identity.creature = value;
|
if (label === "creature") identity.creature = value;
|
||||||
|
|||||||
@@ -63,12 +63,12 @@ const makeAttempt = (overrides: Partial<EmbeddedRunAttemptResult>): EmbeddedRunA
|
|||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeConfig = (opts?: { fallbacks?: string[]; apiKey?: string }): ClawdbotConfig =>
|
const makeConfig = (): ClawdbotConfig =>
|
||||||
({
|
({
|
||||||
agents: {
|
agents: {
|
||||||
defaults: {
|
defaults: {
|
||||||
model: {
|
model: {
|
||||||
fallbacks: opts?.fallbacks ?? [],
|
fallbacks: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -76,7 +76,7 @@ const makeConfig = (opts?: { fallbacks?: string[]; apiKey?: string }): ClawdbotC
|
|||||||
providers: {
|
providers: {
|
||||||
openai: {
|
openai: {
|
||||||
api: "openai-responses",
|
api: "openai-responses",
|
||||||
apiKey: opts?.apiKey ?? "sk-test",
|
apiKey: "sk-test",
|
||||||
baseUrl: "https://example.com",
|
baseUrl: "https://example.com",
|
||||||
models: [
|
models: [
|
||||||
{
|
{
|
||||||
@@ -94,13 +94,7 @@ const makeConfig = (opts?: { fallbacks?: string[]; apiKey?: string }): ClawdbotC
|
|||||||
},
|
},
|
||||||
}) satisfies ClawdbotConfig;
|
}) satisfies ClawdbotConfig;
|
||||||
|
|
||||||
const writeAuthStore = async (
|
const writeAuthStore = async (agentDir: string, opts?: { includeAnthropic?: boolean }) => {
|
||||||
agentDir: string,
|
|
||||||
opts?: {
|
|
||||||
includeAnthropic?: boolean;
|
|
||||||
usageStats?: Record<string, { lastUsed?: number; cooldownUntil?: number }>;
|
|
||||||
},
|
|
||||||
) => {
|
|
||||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||||
const payload = {
|
const payload = {
|
||||||
version: 1,
|
version: 1,
|
||||||
@@ -111,12 +105,10 @@ const writeAuthStore = async (
|
|||||||
? { "anthropic:default": { type: "api_key", provider: "anthropic", key: "sk-anth" } }
|
? { "anthropic:default": { type: "api_key", provider: "anthropic", key: "sk-anth" } }
|
||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
usageStats:
|
usageStats: {
|
||||||
opts?.usageStats ??
|
"openai:p1": { lastUsed: 1 },
|
||||||
({
|
"openai:p2": { lastUsed: 2 },
|
||||||
"openai:p1": { lastUsed: 1 },
|
},
|
||||||
"openai:p2": { lastUsed: 2 },
|
|
||||||
} as Record<string, { lastUsed?: number }>),
|
|
||||||
};
|
};
|
||||||
await fs.writeFile(authPath, JSON.stringify(payload));
|
await fs.writeFile(authPath, JSON.stringify(payload));
|
||||||
};
|
};
|
||||||
@@ -392,92 +384,6 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("fails over when all profiles are in cooldown and fallbacks are configured", async () => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
try {
|
|
||||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-agent-"));
|
|
||||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-workspace-"));
|
|
||||||
const now = Date.now();
|
|
||||||
vi.setSystemTime(now);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await writeAuthStore(agentDir, {
|
|
||||||
usageStats: {
|
|
||||||
"openai:p1": { lastUsed: 1, cooldownUntil: now + 60 * 60 * 1000 },
|
|
||||||
"openai:p2": { lastUsed: 2, cooldownUntil: now + 60 * 60 * 1000 },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
runEmbeddedPiAgent({
|
|
||||||
sessionId: "session:test",
|
|
||||||
sessionKey: "agent:test:cooldown-failover",
|
|
||||||
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
|
||||||
workspaceDir,
|
|
||||||
agentDir,
|
|
||||||
config: makeConfig({ fallbacks: ["openai/mock-2"] }),
|
|
||||||
prompt: "hello",
|
|
||||||
provider: "openai",
|
|
||||||
model: "mock-1",
|
|
||||||
authProfileIdSource: "auto",
|
|
||||||
timeoutMs: 5_000,
|
|
||||||
runId: "run:cooldown-failover",
|
|
||||||
}),
|
|
||||||
).rejects.toMatchObject({
|
|
||||||
name: "FailoverError",
|
|
||||||
reason: "rate_limit",
|
|
||||||
provider: "openai",
|
|
||||||
model: "mock-1",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(runEmbeddedAttemptMock).not.toHaveBeenCalled();
|
|
||||||
} finally {
|
|
||||||
await fs.rm(agentDir, { recursive: true, force: true });
|
|
||||||
await fs.rm(workspaceDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
vi.useRealTimers();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("fails over when auth is unavailable and fallbacks are configured", async () => {
|
|
||||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-agent-"));
|
|
||||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-workspace-"));
|
|
||||||
const previousOpenAiKey = process.env.OPENAI_API_KEY;
|
|
||||||
delete process.env.OPENAI_API_KEY;
|
|
||||||
try {
|
|
||||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
|
||||||
await fs.writeFile(authPath, JSON.stringify({ version: 1, profiles: {}, usageStats: {} }));
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
runEmbeddedPiAgent({
|
|
||||||
sessionId: "session:test",
|
|
||||||
sessionKey: "agent:test:auth-unavailable",
|
|
||||||
sessionFile: path.join(workspaceDir, "session.jsonl"),
|
|
||||||
workspaceDir,
|
|
||||||
agentDir,
|
|
||||||
config: makeConfig({ fallbacks: ["openai/mock-2"], apiKey: "" }),
|
|
||||||
prompt: "hello",
|
|
||||||
provider: "openai",
|
|
||||||
model: "mock-1",
|
|
||||||
authProfileIdSource: "auto",
|
|
||||||
timeoutMs: 5_000,
|
|
||||||
runId: "run:auth-unavailable",
|
|
||||||
}),
|
|
||||||
).rejects.toMatchObject({ name: "FailoverError", reason: "auth" });
|
|
||||||
|
|
||||||
expect(runEmbeddedAttemptMock).not.toHaveBeenCalled();
|
|
||||||
} finally {
|
|
||||||
if (previousOpenAiKey === undefined) {
|
|
||||||
delete process.env.OPENAI_API_KEY;
|
|
||||||
} else {
|
|
||||||
process.env.OPENAI_API_KEY = previousOpenAiKey;
|
|
||||||
}
|
|
||||||
await fs.rm(agentDir, { recursive: true, force: true });
|
|
||||||
await fs.rm(workspaceDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("skips profiles in cooldown when rotating after failure", async () => {
|
it("skips profiles in cooldown when rotating after failure", async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -73,14 +73,6 @@ export async function compactEmbeddedPiSession(params: {
|
|||||||
messageChannel?: string;
|
messageChannel?: string;
|
||||||
messageProvider?: string;
|
messageProvider?: string;
|
||||||
agentAccountId?: string;
|
agentAccountId?: string;
|
||||||
/** Group id for channel-level tool policy resolution. */
|
|
||||||
groupId?: string | null;
|
|
||||||
/** Group channel label (e.g. #general) for channel-level tool policy resolution. */
|
|
||||||
groupChannel?: string | null;
|
|
||||||
/** Group space label (e.g. guild/team id) for channel-level tool policy resolution. */
|
|
||||||
groupSpace?: string | null;
|
|
||||||
/** Parent session key for subagent policy inheritance. */
|
|
||||||
spawnedBy?: string | null;
|
|
||||||
sessionFile: string;
|
sessionFile: string;
|
||||||
workspaceDir: string;
|
workspaceDir: string;
|
||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
@@ -215,10 +207,6 @@ export async function compactEmbeddedPiSession(params: {
|
|||||||
messageProvider: params.messageChannel ?? params.messageProvider,
|
messageProvider: params.messageChannel ?? params.messageProvider,
|
||||||
agentAccountId: params.agentAccountId,
|
agentAccountId: params.agentAccountId,
|
||||||
sessionKey: params.sessionKey ?? params.sessionId,
|
sessionKey: params.sessionKey ?? params.sessionId,
|
||||||
groupId: params.groupId,
|
|
||||||
groupChannel: params.groupChannel,
|
|
||||||
groupSpace: params.groupSpace,
|
|
||||||
spawnedBy: params.spawnedBy,
|
|
||||||
agentDir,
|
agentDir,
|
||||||
workspaceDir: effectiveWorkspace,
|
workspaceDir: effectiveWorkspace,
|
||||||
config: params.config,
|
config: params.config,
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ import {
|
|||||||
isRateLimitAssistantError,
|
isRateLimitAssistantError,
|
||||||
isTimeoutErrorMessage,
|
isTimeoutErrorMessage,
|
||||||
pickFallbackThinkingLevel,
|
pickFallbackThinkingLevel,
|
||||||
type FailoverReason,
|
|
||||||
} from "../pi-embedded-helpers.js";
|
} from "../pi-embedded-helpers.js";
|
||||||
import { normalizeUsage, type UsageLike } from "../usage.js";
|
import { normalizeUsage, type UsageLike } from "../usage.js";
|
||||||
|
|
||||||
@@ -93,8 +92,6 @@ export async function runEmbeddedPiAgent(
|
|||||||
const provider = (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER;
|
const provider = (params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER;
|
||||||
const modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL;
|
const modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL;
|
||||||
const agentDir = params.agentDir ?? resolveClawdbotAgentDir();
|
const agentDir = params.agentDir ?? resolveClawdbotAgentDir();
|
||||||
const fallbackConfigured =
|
|
||||||
(params.config?.agents?.defaults?.model?.fallbacks?.length ?? 0) > 0;
|
|
||||||
await ensureClawdbotModelsJson(params.config, agentDir);
|
await ensureClawdbotModelsJson(params.config, agentDir);
|
||||||
|
|
||||||
const { model, error, authStorage, modelRegistry } = resolveModel(
|
const { model, error, authStorage, modelRegistry } = resolveModel(
|
||||||
@@ -168,42 +165,6 @@ export async function runEmbeddedPiAgent(
|
|||||||
let apiKeyInfo: ApiKeyInfo | null = null;
|
let apiKeyInfo: ApiKeyInfo | null = null;
|
||||||
let lastProfileId: string | undefined;
|
let lastProfileId: string | undefined;
|
||||||
|
|
||||||
const resolveAuthProfileFailoverReason = (params: {
|
|
||||||
allInCooldown: boolean;
|
|
||||||
message: string;
|
|
||||||
}): FailoverReason => {
|
|
||||||
if (params.allInCooldown) return "rate_limit";
|
|
||||||
const classified = classifyFailoverReason(params.message);
|
|
||||||
return classified ?? "auth";
|
|
||||||
};
|
|
||||||
|
|
||||||
const throwAuthProfileFailover = (params: {
|
|
||||||
allInCooldown: boolean;
|
|
||||||
message?: string;
|
|
||||||
error?: unknown;
|
|
||||||
}): never => {
|
|
||||||
const fallbackMessage = `No available auth profile for ${provider} (all in cooldown or unavailable).`;
|
|
||||||
const message =
|
|
||||||
params.message?.trim() ||
|
|
||||||
(params.error ? describeUnknownError(params.error).trim() : "") ||
|
|
||||||
fallbackMessage;
|
|
||||||
const reason = resolveAuthProfileFailoverReason({
|
|
||||||
allInCooldown: params.allInCooldown,
|
|
||||||
message,
|
|
||||||
});
|
|
||||||
if (fallbackConfigured) {
|
|
||||||
throw new FailoverError(message, {
|
|
||||||
reason,
|
|
||||||
provider,
|
|
||||||
model: modelId,
|
|
||||||
status: resolveFailoverStatus(reason),
|
|
||||||
cause: params.error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (params.error instanceof Error) throw params.error;
|
|
||||||
throw new Error(message);
|
|
||||||
};
|
|
||||||
|
|
||||||
const resolveApiKeyForCandidate = async (candidate?: string) => {
|
const resolveApiKeyForCandidate = async (candidate?: string) => {
|
||||||
return getApiKeyForModel({
|
return getApiKeyForModel({
|
||||||
model,
|
model,
|
||||||
@@ -277,17 +238,14 @@ export async function runEmbeddedPiAgent(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (profileIndex >= profileCandidates.length) {
|
if (profileIndex >= profileCandidates.length) {
|
||||||
throwAuthProfileFailover({ allInCooldown: true });
|
throw new Error(
|
||||||
|
`No available auth profile for ${provider} (all in cooldown or unavailable).`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof FailoverError) throw err;
|
if (profileCandidates[profileIndex] === lockedProfileId) throw err;
|
||||||
if (profileCandidates[profileIndex] === lockedProfileId) {
|
|
||||||
throwAuthProfileFailover({ allInCooldown: false, error: err });
|
|
||||||
}
|
|
||||||
const advanced = await advanceAuthProfile();
|
const advanced = await advanceAuthProfile();
|
||||||
if (!advanced) {
|
if (!advanced) throw err;
|
||||||
throwAuthProfileFailover({ allInCooldown: false, error: err });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -306,10 +264,6 @@ export async function runEmbeddedPiAgent(
|
|||||||
agentAccountId: params.agentAccountId,
|
agentAccountId: params.agentAccountId,
|
||||||
messageTo: params.messageTo,
|
messageTo: params.messageTo,
|
||||||
messageThreadId: params.messageThreadId,
|
messageThreadId: params.messageThreadId,
|
||||||
groupId: params.groupId,
|
|
||||||
groupChannel: params.groupChannel,
|
|
||||||
groupSpace: params.groupSpace,
|
|
||||||
spawnedBy: params.spawnedBy,
|
|
||||||
currentChannelId: params.currentChannelId,
|
currentChannelId: params.currentChannelId,
|
||||||
currentThreadTs: params.currentThreadTs,
|
currentThreadTs: params.currentThreadTs,
|
||||||
replyToMode: params.replyToMode,
|
replyToMode: params.replyToMode,
|
||||||
@@ -435,7 +389,9 @@ export async function runEmbeddedPiAgent(
|
|||||||
}
|
}
|
||||||
// FIX: Throw FailoverError for prompt errors when fallbacks configured
|
// FIX: Throw FailoverError for prompt errors when fallbacks configured
|
||||||
// This enables model fallback for quota/rate limit errors during prompt submission
|
// This enables model fallback for quota/rate limit errors during prompt submission
|
||||||
if (fallbackConfigured && isFailoverErrorMessage(errorText)) {
|
const promptFallbackConfigured =
|
||||||
|
(params.config?.agents?.defaults?.model?.fallbacks?.length ?? 0) > 0;
|
||||||
|
if (promptFallbackConfigured && isFailoverErrorMessage(errorText)) {
|
||||||
throw new FailoverError(errorText, {
|
throw new FailoverError(errorText, {
|
||||||
reason: promptFailoverReason ?? "unknown",
|
reason: promptFailoverReason ?? "unknown",
|
||||||
provider,
|
provider,
|
||||||
@@ -459,6 +415,8 @@ export async function runEmbeddedPiAgent(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fallbackConfigured =
|
||||||
|
(params.config?.agents?.defaults?.model?.fallbacks?.length ?? 0) > 0;
|
||||||
const authFailure = isAuthAssistantError(lastAssistant);
|
const authFailure = isAuthAssistantError(lastAssistant);
|
||||||
const rateLimitFailure = isRateLimitAssistantError(lastAssistant);
|
const rateLimitFailure = isRateLimitAssistantError(lastAssistant);
|
||||||
const failoverFailure = isFailoverAssistantError(lastAssistant);
|
const failoverFailure = isFailoverAssistantError(lastAssistant);
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import { isReasoningTagProvider } from "../../../utils/provider-utils.js";
|
|||||||
import { isSubagentSessionKey } from "../../../routing/session-key.js";
|
import { isSubagentSessionKey } from "../../../routing/session-key.js";
|
||||||
import { resolveUserPath } from "../../../utils.js";
|
import { resolveUserPath } from "../../../utils.js";
|
||||||
import { createCacheTrace } from "../../cache-trace.js";
|
import { createCacheTrace } from "../../cache-trace.js";
|
||||||
import { createAnthropicPayloadLogger } from "../../anthropic-payload-log.js";
|
|
||||||
import { resolveClawdbotAgentDir } from "../../agent-paths.js";
|
import { resolveClawdbotAgentDir } from "../../agent-paths.js";
|
||||||
import { resolveSessionAgentIds } from "../../agent-scope.js";
|
import { resolveSessionAgentIds } from "../../agent-scope.js";
|
||||||
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../../bootstrap-files.js";
|
import { makeBootstrapWarn, resolveBootstrapContextForRun } from "../../bootstrap-files.js";
|
||||||
@@ -209,10 +208,6 @@ export async function runEmbeddedAttempt(
|
|||||||
agentAccountId: params.agentAccountId,
|
agentAccountId: params.agentAccountId,
|
||||||
messageTo: params.messageTo,
|
messageTo: params.messageTo,
|
||||||
messageThreadId: params.messageThreadId,
|
messageThreadId: params.messageThreadId,
|
||||||
groupId: params.groupId,
|
|
||||||
groupChannel: params.groupChannel,
|
|
||||||
groupSpace: params.groupSpace,
|
|
||||||
spawnedBy: params.spawnedBy,
|
|
||||||
sessionKey: params.sessionKey ?? params.sessionId,
|
sessionKey: params.sessionKey ?? params.sessionId,
|
||||||
agentDir,
|
agentDir,
|
||||||
workspaceDir: effectiveWorkspace,
|
workspaceDir: effectiveWorkspace,
|
||||||
@@ -459,17 +454,6 @@ export async function runEmbeddedAttempt(
|
|||||||
modelApi: params.model.api,
|
modelApi: params.model.api,
|
||||||
workspaceDir: params.workspaceDir,
|
workspaceDir: params.workspaceDir,
|
||||||
});
|
});
|
||||||
const anthropicPayloadLogger = createAnthropicPayloadLogger({
|
|
||||||
cfg: params.config,
|
|
||||||
env: process.env,
|
|
||||||
runId: params.runId,
|
|
||||||
sessionId: activeSession.sessionId,
|
|
||||||
sessionKey: params.sessionKey,
|
|
||||||
provider: params.provider,
|
|
||||||
modelId: params.modelId,
|
|
||||||
modelApi: params.model.api,
|
|
||||||
workspaceDir: params.workspaceDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Force a stable streamFn reference so vitest can reliably mock @mariozechner/pi-ai.
|
// Force a stable streamFn reference so vitest can reliably mock @mariozechner/pi-ai.
|
||||||
activeSession.agent.streamFn = streamSimple;
|
activeSession.agent.streamFn = streamSimple;
|
||||||
@@ -490,11 +474,6 @@ export async function runEmbeddedAttempt(
|
|||||||
});
|
});
|
||||||
activeSession.agent.streamFn = cacheTrace.wrapStreamFn(activeSession.agent.streamFn);
|
activeSession.agent.streamFn = cacheTrace.wrapStreamFn(activeSession.agent.streamFn);
|
||||||
}
|
}
|
||||||
if (anthropicPayloadLogger) {
|
|
||||||
activeSession.agent.streamFn = anthropicPayloadLogger.wrapStreamFn(
|
|
||||||
activeSession.agent.streamFn,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const prior = await sanitizeSessionHistory({
|
const prior = await sanitizeSessionHistory({
|
||||||
@@ -643,7 +622,6 @@ export async function runEmbeddedAttempt(
|
|||||||
|
|
||||||
let messagesSnapshot: AgentMessage[] = [];
|
let messagesSnapshot: AgentMessage[] = [];
|
||||||
let sessionIdUsed = activeSession.sessionId;
|
let sessionIdUsed = activeSession.sessionId;
|
||||||
let promptStartMessageCount = activeSession.messages.length;
|
|
||||||
const onAbort = () => {
|
const onAbort = () => {
|
||||||
const reason = params.abortSignal ? getAbortReason(params.abortSignal) : undefined;
|
const reason = params.abortSignal ? getAbortReason(params.abortSignal) : undefined;
|
||||||
const timeout = reason ? isTimeoutError(reason) : false;
|
const timeout = reason ? isTimeoutError(reason) : false;
|
||||||
@@ -715,8 +693,6 @@ export async function runEmbeddedAttempt(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
promptStartMessageCount = activeSession.messages.length;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Detect and load images referenced in the prompt for vision-capable models.
|
// Detect and load images referenced in the prompt for vision-capable models.
|
||||||
// This eliminates the need for an explicit "view" tool call by injecting
|
// This eliminates the need for an explicit "view" tool call by injecting
|
||||||
@@ -792,7 +768,6 @@ export async function runEmbeddedAttempt(
|
|||||||
messages: messagesSnapshot,
|
messages: messagesSnapshot,
|
||||||
note: promptError ? "prompt error" : undefined,
|
note: promptError ? "prompt error" : undefined,
|
||||||
});
|
});
|
||||||
anthropicPayloadLogger?.recordUsage(messagesSnapshot, promptError, promptStartMessageCount);
|
|
||||||
|
|
||||||
// Run agent_end hooks to allow plugins to analyze the conversation
|
// Run agent_end hooks to allow plugins to analyze the conversation
|
||||||
// This is fire-and-forget, so we don't await
|
// This is fire-and-forget, so we don't await
|
||||||
|
|||||||
@@ -27,14 +27,6 @@ export type RunEmbeddedPiAgentParams = {
|
|||||||
messageTo?: string;
|
messageTo?: string;
|
||||||
/** Thread/topic identifier for routing replies to the originating thread. */
|
/** Thread/topic identifier for routing replies to the originating thread. */
|
||||||
messageThreadId?: string | number;
|
messageThreadId?: string | number;
|
||||||
/** Group id for channel-level tool policy resolution. */
|
|
||||||
groupId?: string | null;
|
|
||||||
/** Group channel label (e.g. #general) for channel-level tool policy resolution. */
|
|
||||||
groupChannel?: string | null;
|
|
||||||
/** Group space label (e.g. guild/team id) for channel-level tool policy resolution. */
|
|
||||||
groupSpace?: string | null;
|
|
||||||
/** Parent session key for subagent policy inheritance. */
|
|
||||||
spawnedBy?: string | null;
|
|
||||||
/** Current channel ID for auto-threading (Slack). */
|
/** Current channel ID for auto-threading (Slack). */
|
||||||
currentChannelId?: string;
|
currentChannelId?: string;
|
||||||
/** Current thread timestamp for auto-threading (Slack). */
|
/** Current thread timestamp for auto-threading (Slack). */
|
||||||
|
|||||||
@@ -23,14 +23,6 @@ export type EmbeddedRunAttemptParams = {
|
|||||||
agentAccountId?: string;
|
agentAccountId?: string;
|
||||||
messageTo?: string;
|
messageTo?: string;
|
||||||
messageThreadId?: string | number;
|
messageThreadId?: string | number;
|
||||||
/** Group id for channel-level tool policy resolution. */
|
|
||||||
groupId?: string | null;
|
|
||||||
/** Group channel label (e.g. #general) for channel-level tool policy resolution. */
|
|
||||||
groupChannel?: string | null;
|
|
||||||
/** Group space label (e.g. guild/team id) for channel-level tool policy resolution. */
|
|
||||||
groupSpace?: string | null;
|
|
||||||
/** Parent session key for subagent policy inheritance. */
|
|
||||||
spawnedBy?: string | null;
|
|
||||||
currentChannelId?: string;
|
currentChannelId?: string;
|
||||||
currentThreadTs?: string;
|
currentThreadTs?: string;
|
||||||
replyToMode?: "off" | "first" | "all";
|
replyToMode?: "off" | "first" | "all";
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function abortEmbeddedPiRun(sessionId: string): boolean {
|
|||||||
diag.debug(`abort failed: sessionId=${sessionId} reason=no_active_run`);
|
diag.debug(`abort failed: sessionId=${sessionId} reason=no_active_run`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
diag.debug(`aborting run: sessionId=${sessionId}`);
|
diag.info(`aborting run: sessionId=${sessionId}`);
|
||||||
handle.abort();
|
handle.abort();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -110,7 +110,7 @@ export function setActiveEmbeddedRun(sessionId: string, handle: EmbeddedPiQueueH
|
|||||||
reason: wasActive ? "run_replaced" : "run_started",
|
reason: wasActive ? "run_replaced" : "run_started",
|
||||||
});
|
});
|
||||||
if (!sessionId.startsWith("probe-")) {
|
if (!sessionId.startsWith("probe-")) {
|
||||||
diag.debug(`run registered: sessionId=${sessionId} totalActive=${ACTIVE_EMBEDDED_RUNS.size}`);
|
diag.info(`run registered: sessionId=${sessionId} totalActive=${ACTIVE_EMBEDDED_RUNS.size}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ export function clearActiveEmbeddedRun(sessionId: string, handle: EmbeddedPiQueu
|
|||||||
ACTIVE_EMBEDDED_RUNS.delete(sessionId);
|
ACTIVE_EMBEDDED_RUNS.delete(sessionId);
|
||||||
logSessionStateChange({ sessionId, state: "idle", reason: "run_completed" });
|
logSessionStateChange({ sessionId, state: "idle", reason: "run_completed" });
|
||||||
if (!sessionId.startsWith("probe-")) {
|
if (!sessionId.startsWith("probe-")) {
|
||||||
diag.debug(`run cleared: sessionId=${sessionId} totalActive=${ACTIVE_EMBEDDED_RUNS.size}`);
|
diag.info(`run cleared: sessionId=${sessionId} totalActive=${ACTIVE_EMBEDDED_RUNS.size}`);
|
||||||
}
|
}
|
||||||
notifyEmbeddedRunEnded(sessionId);
|
notifyEmbeddedRunEnded(sessionId);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -231,95 +231,6 @@ describe("Agent-specific tool filtering", () => {
|
|||||||
expect(familyToolNames).not.toContain("apply_patch");
|
expect(familyToolNames).not.toContain("apply_patch");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should apply group tool policy overrides (group-specific beats wildcard)", () => {
|
|
||||||
const cfg: ClawdbotConfig = {
|
|
||||||
channels: {
|
|
||||||
whatsapp: {
|
|
||||||
groups: {
|
|
||||||
"*": {
|
|
||||||
tools: { allow: ["read"] },
|
|
||||||
},
|
|
||||||
trusted: {
|
|
||||||
tools: { allow: ["read", "exec"] },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const trustedTools = createClawdbotCodingTools({
|
|
||||||
config: cfg,
|
|
||||||
sessionKey: "agent:main:whatsapp:group:trusted",
|
|
||||||
messageProvider: "whatsapp",
|
|
||||||
workspaceDir: "/tmp/test-group-trusted",
|
|
||||||
agentDir: "/tmp/agent-group",
|
|
||||||
});
|
|
||||||
const trustedNames = trustedTools.map((t) => t.name);
|
|
||||||
expect(trustedNames).toContain("read");
|
|
||||||
expect(trustedNames).toContain("exec");
|
|
||||||
|
|
||||||
const defaultTools = createClawdbotCodingTools({
|
|
||||||
config: cfg,
|
|
||||||
sessionKey: "agent:main:whatsapp:group:unknown",
|
|
||||||
messageProvider: "whatsapp",
|
|
||||||
workspaceDir: "/tmp/test-group-default",
|
|
||||||
agentDir: "/tmp/agent-group",
|
|
||||||
});
|
|
||||||
const defaultNames = defaultTools.map((t) => t.name);
|
|
||||||
expect(defaultNames).toContain("read");
|
|
||||||
expect(defaultNames).not.toContain("exec");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should resolve telegram group tool policy for topic session keys", () => {
|
|
||||||
const cfg: ClawdbotConfig = {
|
|
||||||
channels: {
|
|
||||||
telegram: {
|
|
||||||
groups: {
|
|
||||||
"123": {
|
|
||||||
tools: { allow: ["read"] },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const tools = createClawdbotCodingTools({
|
|
||||||
config: cfg,
|
|
||||||
sessionKey: "agent:main:telegram:group:123:topic:456",
|
|
||||||
messageProvider: "telegram",
|
|
||||||
workspaceDir: "/tmp/test-telegram-topic",
|
|
||||||
agentDir: "/tmp/agent-telegram",
|
|
||||||
});
|
|
||||||
const names = tools.map((t) => t.name);
|
|
||||||
expect(names).toContain("read");
|
|
||||||
expect(names).not.toContain("exec");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should inherit group tool policy for subagents from spawnedBy session keys", () => {
|
|
||||||
const cfg: ClawdbotConfig = {
|
|
||||||
channels: {
|
|
||||||
whatsapp: {
|
|
||||||
groups: {
|
|
||||||
trusted: {
|
|
||||||
tools: { allow: ["read"] },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const tools = createClawdbotCodingTools({
|
|
||||||
config: cfg,
|
|
||||||
sessionKey: "agent:main:subagent:test",
|
|
||||||
spawnedBy: "agent:main:whatsapp:group:trusted",
|
|
||||||
workspaceDir: "/tmp/test-subagent-group",
|
|
||||||
agentDir: "/tmp/agent-subagent",
|
|
||||||
});
|
|
||||||
const names = tools.map((t) => t.name);
|
|
||||||
expect(names).toContain("read");
|
|
||||||
expect(names).not.toContain("exec");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should apply global tool policy before agent-specific policy", () => {
|
it("should apply global tool policy before agent-specific policy", () => {
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
tools: {
|
tools: {
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import { getChannelDock } from "../channels/dock.js";
|
|
||||||
import { resolveChannelGroupToolsPolicy } from "../config/group-policy.js";
|
|
||||||
import { resolveAgentConfig, resolveAgentIdFromSessionKey } from "./agent-scope.js";
|
import { resolveAgentConfig, resolveAgentIdFromSessionKey } from "./agent-scope.js";
|
||||||
import type { AnyAgentTool } from "./pi-tools.types.js";
|
import type { AnyAgentTool } from "./pi-tools.types.js";
|
||||||
import type { SandboxToolPolicy } from "./sandbox.js";
|
import type { SandboxToolPolicy } from "./sandbox.js";
|
||||||
import { expandToolGroups, normalizeToolName } from "./tool-policy.js";
|
import { expandToolGroups, normalizeToolName } from "./tool-policy.js";
|
||||||
import { normalizeMessageChannel } from "../utils/message-channel.js";
|
|
||||||
import { resolveThreadParentSessionKey } from "../sessions/session-key-utils.js";
|
|
||||||
|
|
||||||
type CompiledPattern =
|
type CompiledPattern =
|
||||||
| { kind: "all" }
|
| { kind: "all" }
|
||||||
@@ -112,26 +108,6 @@ function normalizeProviderKey(value: string): string {
|
|||||||
return value.trim().toLowerCase();
|
return value.trim().toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveGroupContextFromSessionKey(sessionKey?: string | null): {
|
|
||||||
channel?: string;
|
|
||||||
groupId?: string;
|
|
||||||
} {
|
|
||||||
const raw = (sessionKey ?? "").trim();
|
|
||||||
if (!raw) return {};
|
|
||||||
const base = resolveThreadParentSessionKey(raw) ?? raw;
|
|
||||||
const parts = base.split(":").filter(Boolean);
|
|
||||||
let body = parts[0] === "agent" ? parts.slice(2) : parts;
|
|
||||||
if (body[0] === "subagent") {
|
|
||||||
body = body.slice(1);
|
|
||||||
}
|
|
||||||
if (body.length < 3) return {};
|
|
||||||
const [channel, kind, ...rest] = body;
|
|
||||||
if (kind !== "group" && kind !== "channel") return {};
|
|
||||||
const groupId = rest.join(":").trim();
|
|
||||||
if (!groupId) return {};
|
|
||||||
return { channel: channel.trim().toLowerCase(), groupId };
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveProviderToolPolicy(params: {
|
function resolveProviderToolPolicy(params: {
|
||||||
byProvider?: Record<string, ToolPolicyConfig>;
|
byProvider?: Record<string, ToolPolicyConfig>;
|
||||||
modelProvider?: string;
|
modelProvider?: string;
|
||||||
@@ -198,47 +174,6 @@ export function resolveEffectiveToolPolicy(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveGroupToolPolicy(params: {
|
|
||||||
config?: ClawdbotConfig;
|
|
||||||
sessionKey?: string;
|
|
||||||
spawnedBy?: string | null;
|
|
||||||
messageProvider?: string;
|
|
||||||
groupId?: string | null;
|
|
||||||
groupChannel?: string | null;
|
|
||||||
groupSpace?: string | null;
|
|
||||||
accountId?: string | null;
|
|
||||||
}): SandboxToolPolicy | undefined {
|
|
||||||
if (!params.config) return undefined;
|
|
||||||
const sessionContext = resolveGroupContextFromSessionKey(params.sessionKey);
|
|
||||||
const spawnedContext = resolveGroupContextFromSessionKey(params.spawnedBy);
|
|
||||||
const groupId = params.groupId ?? sessionContext.groupId ?? spawnedContext.groupId;
|
|
||||||
if (!groupId) return undefined;
|
|
||||||
const channelRaw = params.messageProvider ?? sessionContext.channel ?? spawnedContext.channel;
|
|
||||||
const channel = normalizeMessageChannel(channelRaw);
|
|
||||||
if (!channel) return undefined;
|
|
||||||
let dock;
|
|
||||||
try {
|
|
||||||
dock = getChannelDock(channel);
|
|
||||||
} catch {
|
|
||||||
dock = undefined;
|
|
||||||
}
|
|
||||||
const toolsConfig =
|
|
||||||
dock?.groups?.resolveToolPolicy?.({
|
|
||||||
cfg: params.config,
|
|
||||||
groupId,
|
|
||||||
groupChannel: params.groupChannel,
|
|
||||||
groupSpace: params.groupSpace,
|
|
||||||
accountId: params.accountId,
|
|
||||||
}) ??
|
|
||||||
resolveChannelGroupToolsPolicy({
|
|
||||||
cfg: params.config,
|
|
||||||
channel,
|
|
||||||
groupId,
|
|
||||||
accountId: params.accountId,
|
|
||||||
});
|
|
||||||
return pickToolPolicy(toolsConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isToolAllowedByPolicies(
|
export function isToolAllowedByPolicies(
|
||||||
name: string,
|
name: string,
|
||||||
policies: Array<SandboxToolPolicy | undefined>,
|
policies: Array<SandboxToolPolicy | undefined>,
|
||||||
|
|||||||
+2
-33
@@ -23,7 +23,6 @@ import {
|
|||||||
filterToolsByPolicy,
|
filterToolsByPolicy,
|
||||||
isToolAllowedByPolicies,
|
isToolAllowedByPolicies,
|
||||||
resolveEffectiveToolPolicy,
|
resolveEffectiveToolPolicy,
|
||||||
resolveGroupToolPolicy,
|
|
||||||
resolveSubagentToolPolicy,
|
resolveSubagentToolPolicy,
|
||||||
} from "./pi-tools.policy.js";
|
} from "./pi-tools.policy.js";
|
||||||
import {
|
import {
|
||||||
@@ -129,14 +128,6 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
currentChannelId?: string;
|
currentChannelId?: string;
|
||||||
/** Current thread timestamp for auto-threading (Slack). */
|
/** Current thread timestamp for auto-threading (Slack). */
|
||||||
currentThreadTs?: string;
|
currentThreadTs?: string;
|
||||||
/** Group id for channel-level tool policy resolution. */
|
|
||||||
groupId?: string | null;
|
|
||||||
/** Group channel label (e.g. #general) for channel-level tool policy resolution. */
|
|
||||||
groupChannel?: string | null;
|
|
||||||
/** Group space label (e.g. guild/team id) for channel-level tool policy resolution. */
|
|
||||||
groupSpace?: string | null;
|
|
||||||
/** Parent session key for subagent group policy inheritance. */
|
|
||||||
spawnedBy?: string | null;
|
|
||||||
/** Reply-to mode for Slack auto-threading. */
|
/** Reply-to mode for Slack auto-threading. */
|
||||||
replyToMode?: "off" | "first" | "all";
|
replyToMode?: "off" | "first" | "all";
|
||||||
/** Mutable ref to track if a reply was sent (for "first" mode). */
|
/** Mutable ref to track if a reply was sent (for "first" mode). */
|
||||||
@@ -160,16 +151,6 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
modelProvider: options?.modelProvider,
|
modelProvider: options?.modelProvider,
|
||||||
modelId: options?.modelId,
|
modelId: options?.modelId,
|
||||||
});
|
});
|
||||||
const groupPolicy = resolveGroupToolPolicy({
|
|
||||||
config: options?.config,
|
|
||||||
sessionKey: options?.sessionKey,
|
|
||||||
spawnedBy: options?.spawnedBy,
|
|
||||||
messageProvider: options?.messageProvider,
|
|
||||||
groupId: options?.groupId,
|
|
||||||
groupChannel: options?.groupChannel,
|
|
||||||
groupSpace: options?.groupSpace,
|
|
||||||
accountId: options?.agentAccountId,
|
|
||||||
});
|
|
||||||
const profilePolicy = resolveToolProfilePolicy(profile);
|
const profilePolicy = resolveToolProfilePolicy(profile);
|
||||||
const providerProfilePolicy = resolveToolProfilePolicy(providerProfile);
|
const providerProfilePolicy = resolveToolProfilePolicy(providerProfile);
|
||||||
const scopeKey = options?.exec?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined);
|
const scopeKey = options?.exec?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined);
|
||||||
@@ -184,7 +165,6 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
globalProviderPolicy,
|
globalProviderPolicy,
|
||||||
agentPolicy,
|
agentPolicy,
|
||||||
agentProviderPolicy,
|
agentProviderPolicy,
|
||||||
groupPolicy,
|
|
||||||
sandbox?.tools,
|
sandbox?.tools,
|
||||||
subagentPolicy,
|
subagentPolicy,
|
||||||
]);
|
]);
|
||||||
@@ -293,9 +273,6 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
agentAccountId: options?.agentAccountId,
|
agentAccountId: options?.agentAccountId,
|
||||||
agentTo: options?.messageTo,
|
agentTo: options?.messageTo,
|
||||||
agentThreadId: options?.messageThreadId,
|
agentThreadId: options?.messageThreadId,
|
||||||
agentGroupId: options?.groupId ?? null,
|
|
||||||
agentGroupChannel: options?.groupChannel ?? null,
|
|
||||||
agentGroupSpace: options?.groupSpace ?? null,
|
|
||||||
agentDir: options?.agentDir,
|
agentDir: options?.agentDir,
|
||||||
sandboxRoot,
|
sandboxRoot,
|
||||||
workspaceDir: options?.workspaceDir,
|
workspaceDir: options?.workspaceDir,
|
||||||
@@ -308,7 +285,6 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
globalProviderPolicy,
|
globalProviderPolicy,
|
||||||
agentPolicy,
|
agentPolicy,
|
||||||
agentProviderPolicy,
|
agentProviderPolicy,
|
||||||
groupPolicy,
|
|
||||||
sandbox?.tools,
|
sandbox?.tools,
|
||||||
subagentPolicy,
|
subagentPolicy,
|
||||||
]),
|
]),
|
||||||
@@ -347,10 +323,6 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
stripPluginOnlyAllowlist(agentProviderPolicy, pluginGroups),
|
stripPluginOnlyAllowlist(agentProviderPolicy, pluginGroups),
|
||||||
pluginGroups,
|
pluginGroups,
|
||||||
);
|
);
|
||||||
const groupPolicyExpanded = expandPolicyWithPluginGroups(
|
|
||||||
stripPluginOnlyAllowlist(groupPolicy, pluginGroups),
|
|
||||||
pluginGroups,
|
|
||||||
);
|
|
||||||
const sandboxPolicyExpanded = expandPolicyWithPluginGroups(sandbox?.tools, pluginGroups);
|
const sandboxPolicyExpanded = expandPolicyWithPluginGroups(sandbox?.tools, pluginGroups);
|
||||||
const subagentPolicyExpanded = expandPolicyWithPluginGroups(subagentPolicy, pluginGroups);
|
const subagentPolicyExpanded = expandPolicyWithPluginGroups(subagentPolicy, pluginGroups);
|
||||||
|
|
||||||
@@ -372,12 +344,9 @@ export function createClawdbotCodingTools(options?: {
|
|||||||
const agentProviderFiltered = agentProviderExpanded
|
const agentProviderFiltered = agentProviderExpanded
|
||||||
? filterToolsByPolicy(agentFiltered, agentProviderExpanded)
|
? filterToolsByPolicy(agentFiltered, agentProviderExpanded)
|
||||||
: agentFiltered;
|
: agentFiltered;
|
||||||
const groupFiltered = groupPolicyExpanded
|
|
||||||
? filterToolsByPolicy(agentProviderFiltered, groupPolicyExpanded)
|
|
||||||
: agentProviderFiltered;
|
|
||||||
const sandboxed = sandboxPolicyExpanded
|
const sandboxed = sandboxPolicyExpanded
|
||||||
? filterToolsByPolicy(groupFiltered, sandboxPolicyExpanded)
|
? filterToolsByPolicy(agentProviderFiltered, sandboxPolicyExpanded)
|
||||||
: groupFiltered;
|
: agentProviderFiltered;
|
||||||
const subagentFiltered = subagentPolicyExpanded
|
const subagentFiltered = subagentPolicyExpanded
|
||||||
? filterToolsByPolicy(sandboxed, subagentPolicyExpanded)
|
? filterToolsByPolicy(sandboxed, subagentPolicyExpanded)
|
||||||
: sandboxed;
|
: sandboxed;
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ describe("buildAgentSystemPrompt", () => {
|
|||||||
expect(prompt).toContain("Reminder: commit your changes in this workspace after edits.");
|
expect(prompt).toContain("Reminder: commit your changes in this workspace after edits.");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes user timezone when provided (12-hour)", () => {
|
it("includes user time when provided (12-hour)", () => {
|
||||||
const prompt = buildAgentSystemPrompt({
|
const prompt = buildAgentSystemPrompt({
|
||||||
workspaceDir: "/tmp/clawd",
|
workspaceDir: "/tmp/clawd",
|
||||||
userTimezone: "America/Chicago",
|
userTimezone: "America/Chicago",
|
||||||
@@ -133,10 +133,11 @@ describe("buildAgentSystemPrompt", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(prompt).toContain("## Current Date & Time");
|
expect(prompt).toContain("## Current Date & Time");
|
||||||
expect(prompt).toContain("Time zone: America/Chicago");
|
expect(prompt).toContain("Monday, January 5th, 2026 — 3:26 PM (America/Chicago)");
|
||||||
|
expect(prompt).toContain("Time format: 12-hour");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes user timezone when provided (24-hour)", () => {
|
it("includes user time when provided (24-hour)", () => {
|
||||||
const prompt = buildAgentSystemPrompt({
|
const prompt = buildAgentSystemPrompt({
|
||||||
workspaceDir: "/tmp/clawd",
|
workspaceDir: "/tmp/clawd",
|
||||||
userTimezone: "America/Chicago",
|
userTimezone: "America/Chicago",
|
||||||
@@ -145,10 +146,11 @@ describe("buildAgentSystemPrompt", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(prompt).toContain("## Current Date & Time");
|
expect(prompt).toContain("## Current Date & Time");
|
||||||
expect(prompt).toContain("Time zone: America/Chicago");
|
expect(prompt).toContain("Monday, January 5th, 2026 — 15:26 (America/Chicago)");
|
||||||
|
expect(prompt).toContain("Time format: 24-hour");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows timezone when only timezone is provided", () => {
|
it("shows UTC fallback when only timezone is provided", () => {
|
||||||
const prompt = buildAgentSystemPrompt({
|
const prompt = buildAgentSystemPrompt({
|
||||||
workspaceDir: "/tmp/clawd",
|
workspaceDir: "/tmp/clawd",
|
||||||
userTimezone: "America/Chicago",
|
userTimezone: "America/Chicago",
|
||||||
@@ -156,7 +158,9 @@ describe("buildAgentSystemPrompt", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(prompt).toContain("## Current Date & Time");
|
expect(prompt).toContain("## Current Date & Time");
|
||||||
expect(prompt).toContain("Time zone: America/Chicago");
|
expect(prompt).toContain(
|
||||||
|
"Time zone: America/Chicago. Current time unknown; assume UTC for date/time references.",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes model alias guidance when aliases are provided", () => {
|
it("includes model alias guidance when aliases are provided", () => {
|
||||||
|
|||||||
@@ -49,9 +49,22 @@ function buildUserIdentitySection(ownerLine: string | undefined, isMinimal: bool
|
|||||||
return ["## User Identity", ownerLine, ""];
|
return ["## User Identity", ownerLine, ""];
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTimeSection(params: { userTimezone?: string }) {
|
function buildTimeSection(params: {
|
||||||
if (!params.userTimezone) return [];
|
userTimezone?: string;
|
||||||
return ["## Current Date & Time", `Time zone: ${params.userTimezone}`, ""];
|
userTime?: string;
|
||||||
|
userTimeFormat?: ResolvedTimeFormat;
|
||||||
|
}) {
|
||||||
|
if (!params.userTimezone && !params.userTime) return [];
|
||||||
|
return [
|
||||||
|
"## Current Date & Time",
|
||||||
|
params.userTime
|
||||||
|
? `${params.userTime} (${params.userTimezone ?? "unknown"})`
|
||||||
|
: `Time zone: ${params.userTimezone}. Current time unknown; assume UTC for date/time references.`,
|
||||||
|
params.userTimeFormat
|
||||||
|
? `Time format: ${params.userTimeFormat === "24" ? "24-hour" : "12-hour"}`
|
||||||
|
: "",
|
||||||
|
"",
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildReplyTagsSection(isMinimal: boolean) {
|
function buildReplyTagsSection(isMinimal: boolean) {
|
||||||
@@ -199,7 +212,7 @@ export function buildAgentSystemPrompt(params: {
|
|||||||
sessions_send: "Send a message to another session/sub-agent",
|
sessions_send: "Send a message to another session/sub-agent",
|
||||||
sessions_spawn: "Spawn a sub-agent session",
|
sessions_spawn: "Spawn a sub-agent session",
|
||||||
session_status:
|
session_status:
|
||||||
"Show a /status-equivalent status card (usage + time + Reasoning/Verbose/Elevated); use for model-use questions (📊 session_status); optional per-session model override",
|
"Show a /status-equivalent status card (usage + Reasoning/Verbose/Elevated); use for model-use questions (📊 session_status); optional per-session model override",
|
||||||
image: "Analyze an image with the configured image model",
|
image: "Analyze an image with the configured image model",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -289,6 +302,7 @@ export function buildAgentSystemPrompt(params: {
|
|||||||
: undefined;
|
: undefined;
|
||||||
const reasoningLevel = params.reasoningLevel ?? "off";
|
const reasoningLevel = params.reasoningLevel ?? "off";
|
||||||
const userTimezone = params.userTimezone?.trim();
|
const userTimezone = params.userTimezone?.trim();
|
||||||
|
const userTime = params.userTime?.trim();
|
||||||
const skillsPrompt = params.skillsPrompt?.trim();
|
const skillsPrompt = params.skillsPrompt?.trim();
|
||||||
const heartbeatPrompt = params.heartbeatPrompt?.trim();
|
const heartbeatPrompt = params.heartbeatPrompt?.trim();
|
||||||
const heartbeatPromptLine = heartbeatPrompt
|
const heartbeatPromptLine = heartbeatPrompt
|
||||||
@@ -451,6 +465,8 @@ export function buildAgentSystemPrompt(params: {
|
|||||||
...buildUserIdentitySection(ownerLine, isMinimal),
|
...buildUserIdentitySection(ownerLine, isMinimal),
|
||||||
...buildTimeSection({
|
...buildTimeSection({
|
||||||
userTimezone,
|
userTimezone,
|
||||||
|
userTime,
|
||||||
|
userTimeFormat: params.userTimeFormat,
|
||||||
}),
|
}),
|
||||||
"## Workspace Files (injected)",
|
"## Workspace Files (injected)",
|
||||||
"These user-editable files are loaded by Clawdbot and included below in Project Context.",
|
"These user-editable files are loaded by Clawdbot and included below in Project Context.",
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const BROWSER_TOOL_ACTIONS = [
|
|||||||
"act",
|
"act",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const BROWSER_TARGETS = ["sandbox", "host", "custom", "node"] as const;
|
const BROWSER_TARGETS = ["sandbox", "host", "custom"] as const;
|
||||||
|
|
||||||
const BROWSER_SNAPSHOT_FORMATS = ["aria", "ai"] as const;
|
const BROWSER_SNAPSHOT_FORMATS = ["aria", "ai"] as const;
|
||||||
const BROWSER_SNAPSHOT_MODES = ["efficient"] as const;
|
const BROWSER_SNAPSHOT_MODES = ["efficient"] as const;
|
||||||
@@ -84,7 +84,6 @@ const BrowserActSchema = Type.Object({
|
|||||||
export const BrowserToolSchema = Type.Object({
|
export const BrowserToolSchema = Type.Object({
|
||||||
action: stringEnum(BROWSER_TOOL_ACTIONS),
|
action: stringEnum(BROWSER_TOOL_ACTIONS),
|
||||||
target: optionalStringEnum(BROWSER_TARGETS),
|
target: optionalStringEnum(BROWSER_TARGETS),
|
||||||
node: Type.Optional(Type.String()),
|
|
||||||
profile: Type.Optional(Type.String()),
|
profile: Type.Optional(Type.String()),
|
||||||
controlUrl: Type.Optional(Type.String()),
|
controlUrl: Type.Optional(Type.String()),
|
||||||
targetUrl: Type.Optional(Type.String()),
|
targetUrl: Type.Optional(Type.String()),
|
||||||
|
|||||||
@@ -49,25 +49,6 @@ const browserConfigMocks = vi.hoisted(() => ({
|
|||||||
}));
|
}));
|
||||||
vi.mock("../../browser/config.js", () => browserConfigMocks);
|
vi.mock("../../browser/config.js", () => browserConfigMocks);
|
||||||
|
|
||||||
const nodesUtilsMocks = vi.hoisted(() => ({
|
|
||||||
listNodes: vi.fn(async () => []),
|
|
||||||
}));
|
|
||||||
vi.mock("./nodes-utils.js", async () => {
|
|
||||||
const actual = await vi.importActual<typeof import("./nodes-utils.js")>("./nodes-utils.js");
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
listNodes: nodesUtilsMocks.listNodes,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const gatewayMocks = vi.hoisted(() => ({
|
|
||||||
callGatewayTool: vi.fn(async () => ({
|
|
||||||
ok: true,
|
|
||||||
payload: { result: { ok: true, running: true } },
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
vi.mock("./gateway.js", () => gatewayMocks);
|
|
||||||
|
|
||||||
const configMocks = vi.hoisted(() => ({
|
const configMocks = vi.hoisted(() => ({
|
||||||
loadConfig: vi.fn(() => ({ browser: {} })),
|
loadConfig: vi.fn(() => ({ browser: {} })),
|
||||||
}));
|
}));
|
||||||
@@ -91,7 +72,6 @@ describe("browser tool snapshot maxChars", () => {
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
configMocks.loadConfig.mockReturnValue({ browser: {} });
|
configMocks.loadConfig.mockReturnValue({ browser: {} });
|
||||||
nodesUtilsMocks.listNodes.mockResolvedValue([]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("applies the default ai snapshot limit", async () => {
|
it("applies the default ai snapshot limit", async () => {
|
||||||
@@ -195,70 +175,6 @@ describe("browser tool snapshot maxChars", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("routes to node proxy when target=node", async () => {
|
|
||||||
nodesUtilsMocks.listNodes.mockResolvedValue([
|
|
||||||
{
|
|
||||||
nodeId: "node-1",
|
|
||||||
displayName: "Browser Node",
|
|
||||||
connected: true,
|
|
||||||
caps: ["browser"],
|
|
||||||
commands: ["browser.proxy"],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
const tool = createBrowserTool();
|
|
||||||
await tool.execute?.(null, { action: "status", target: "node" });
|
|
||||||
|
|
||||||
expect(gatewayMocks.callGatewayTool).toHaveBeenCalledWith(
|
|
||||||
"node.invoke",
|
|
||||||
{ timeoutMs: 20000 },
|
|
||||||
expect.objectContaining({
|
|
||||||
nodeId: "node-1",
|
|
||||||
command: "browser.proxy",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(browserClientMocks.browserStatus).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps sandbox control url when node proxy is available", async () => {
|
|
||||||
nodesUtilsMocks.listNodes.mockResolvedValue([
|
|
||||||
{
|
|
||||||
nodeId: "node-1",
|
|
||||||
displayName: "Browser Node",
|
|
||||||
connected: true,
|
|
||||||
caps: ["browser"],
|
|
||||||
commands: ["browser.proxy"],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
const tool = createBrowserTool({ defaultControlUrl: "http://127.0.0.1:9999" });
|
|
||||||
await tool.execute?.(null, { action: "status" });
|
|
||||||
|
|
||||||
expect(browserClientMocks.browserStatus).toHaveBeenCalledWith(
|
|
||||||
"http://127.0.0.1:9999",
|
|
||||||
expect.objectContaining({ profile: undefined }),
|
|
||||||
);
|
|
||||||
expect(gatewayMocks.callGatewayTool).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps chrome profile on host when node proxy is available", async () => {
|
|
||||||
nodesUtilsMocks.listNodes.mockResolvedValue([
|
|
||||||
{
|
|
||||||
nodeId: "node-1",
|
|
||||||
displayName: "Browser Node",
|
|
||||||
connected: true,
|
|
||||||
caps: ["browser"],
|
|
||||||
commands: ["browser.proxy"],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
const tool = createBrowserTool();
|
|
||||||
await tool.execute?.(null, { action: "status", profile: "chrome" });
|
|
||||||
|
|
||||||
expect(browserClientMocks.browserStatus).toHaveBeenCalledWith(
|
|
||||||
"http://127.0.0.1:18791",
|
|
||||||
expect.objectContaining({ profile: "chrome" }),
|
|
||||||
);
|
|
||||||
expect(gatewayMocks.callGatewayTool).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("browser tool snapshot labels", () => {
|
describe("browser tool snapshot labels", () => {
|
||||||
|
|||||||
@@ -18,173 +18,11 @@ import {
|
|||||||
browserPdfSave,
|
browserPdfSave,
|
||||||
browserScreenshotAction,
|
browserScreenshotAction,
|
||||||
} from "../../browser/client-actions.js";
|
} from "../../browser/client-actions.js";
|
||||||
import crypto from "node:crypto";
|
|
||||||
|
|
||||||
import { resolveBrowserConfig } from "../../browser/config.js";
|
import { resolveBrowserConfig } from "../../browser/config.js";
|
||||||
import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "../../browser/constants.js";
|
import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "../../browser/constants.js";
|
||||||
import { loadConfig } from "../../config/config.js";
|
import { loadConfig } from "../../config/config.js";
|
||||||
import { saveMediaBuffer } from "../../media/store.js";
|
|
||||||
import { listNodes, resolveNodeIdFromList, type NodeListNode } from "./nodes-utils.js";
|
|
||||||
import { BrowserToolSchema } from "./browser-tool.schema.js";
|
import { BrowserToolSchema } from "./browser-tool.schema.js";
|
||||||
import { type AnyAgentTool, imageResultFromFile, jsonResult, readStringParam } from "./common.js";
|
import { type AnyAgentTool, imageResultFromFile, jsonResult, readStringParam } from "./common.js";
|
||||||
import { callGatewayTool } from "./gateway.js";
|
|
||||||
|
|
||||||
type BrowserProxyFile = {
|
|
||||||
path: string;
|
|
||||||
base64: string;
|
|
||||||
mimeType?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type BrowserProxyResult = {
|
|
||||||
result: unknown;
|
|
||||||
files?: BrowserProxyFile[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const DEFAULT_BROWSER_PROXY_TIMEOUT_MS = 20_000;
|
|
||||||
|
|
||||||
type BrowserNodeTarget = {
|
|
||||||
nodeId: string;
|
|
||||||
label?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function isBrowserNode(node: NodeListNode) {
|
|
||||||
const caps = Array.isArray(node.caps) ? node.caps : [];
|
|
||||||
const commands = Array.isArray(node.commands) ? node.commands : [];
|
|
||||||
return caps.includes("browser") || commands.includes("browser.proxy");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resolveBrowserNodeTarget(params: {
|
|
||||||
requestedNode?: string;
|
|
||||||
target?: "sandbox" | "host" | "custom" | "node";
|
|
||||||
controlUrl?: string;
|
|
||||||
defaultControlUrl?: string;
|
|
||||||
}): Promise<BrowserNodeTarget | null> {
|
|
||||||
const cfg = loadConfig();
|
|
||||||
const policy = cfg.gateway?.nodes?.browser;
|
|
||||||
const mode = policy?.mode ?? "auto";
|
|
||||||
if (mode === "off") {
|
|
||||||
if (params.target === "node" || params.requestedNode) {
|
|
||||||
throw new Error("Node browser proxy is disabled (gateway.nodes.browser.mode=off).");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (params.defaultControlUrl?.trim() && params.target !== "node" && !params.requestedNode) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (params.controlUrl?.trim()) return null;
|
|
||||||
if (params.target && params.target !== "node") return null;
|
|
||||||
if (mode === "manual" && params.target !== "node" && !params.requestedNode) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodes = await listNodes({});
|
|
||||||
const browserNodes = nodes.filter((node) => node.connected && isBrowserNode(node));
|
|
||||||
if (browserNodes.length === 0) {
|
|
||||||
if (params.target === "node" || params.requestedNode) {
|
|
||||||
throw new Error("No connected browser-capable nodes.");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const requested = params.requestedNode?.trim() || policy?.node?.trim();
|
|
||||||
if (requested) {
|
|
||||||
const nodeId = resolveNodeIdFromList(browserNodes, requested, false);
|
|
||||||
const node = browserNodes.find((entry) => entry.nodeId === nodeId);
|
|
||||||
return { nodeId, label: node?.displayName ?? node?.remoteIp ?? nodeId };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.target === "node") {
|
|
||||||
if (browserNodes.length === 1) {
|
|
||||||
const node = browserNodes[0]!;
|
|
||||||
return { nodeId: node.nodeId, label: node.displayName ?? node.remoteIp ?? node.nodeId };
|
|
||||||
}
|
|
||||||
throw new Error(
|
|
||||||
`Multiple browser-capable nodes connected (${browserNodes.length}). Set gateway.nodes.browser.node or pass node=<id>.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === "manual") return null;
|
|
||||||
|
|
||||||
if (browserNodes.length === 1) {
|
|
||||||
const node = browserNodes[0]!;
|
|
||||||
return { nodeId: node.nodeId, label: node.displayName ?? node.remoteIp ?? node.nodeId };
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function callBrowserProxy(params: {
|
|
||||||
nodeId: string;
|
|
||||||
method: string;
|
|
||||||
path: string;
|
|
||||||
query?: Record<string, string | number | boolean | undefined>;
|
|
||||||
body?: unknown;
|
|
||||||
timeoutMs?: number;
|
|
||||||
profile?: string;
|
|
||||||
}): Promise<BrowserProxyResult> {
|
|
||||||
const gatewayTimeoutMs =
|
|
||||||
typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs)
|
|
||||||
? Math.max(1, Math.floor(params.timeoutMs))
|
|
||||||
: DEFAULT_BROWSER_PROXY_TIMEOUT_MS;
|
|
||||||
const payload = (await callGatewayTool(
|
|
||||||
"node.invoke",
|
|
||||||
{ timeoutMs: gatewayTimeoutMs },
|
|
||||||
{
|
|
||||||
nodeId: params.nodeId,
|
|
||||||
command: "browser.proxy",
|
|
||||||
params: {
|
|
||||||
method: params.method,
|
|
||||||
path: params.path,
|
|
||||||
query: params.query,
|
|
||||||
body: params.body,
|
|
||||||
timeoutMs: params.timeoutMs,
|
|
||||||
profile: params.profile,
|
|
||||||
},
|
|
||||||
idempotencyKey: crypto.randomUUID(),
|
|
||||||
},
|
|
||||||
)) as {
|
|
||||||
ok?: boolean;
|
|
||||||
payload?: BrowserProxyResult;
|
|
||||||
payloadJSON?: string | null;
|
|
||||||
};
|
|
||||||
const parsed =
|
|
||||||
payload?.payload ??
|
|
||||||
(typeof payload?.payloadJSON === "string" && payload.payloadJSON
|
|
||||||
? (JSON.parse(payload.payloadJSON) as BrowserProxyResult)
|
|
||||||
: null);
|
|
||||||
if (!parsed || typeof parsed !== "object") {
|
|
||||||
throw new Error("browser proxy failed");
|
|
||||||
}
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function persistProxyFiles(files: BrowserProxyFile[] | undefined) {
|
|
||||||
if (!files || files.length === 0) return new Map<string, string>();
|
|
||||||
const mapping = new Map<string, string>();
|
|
||||||
for (const file of files) {
|
|
||||||
const buffer = Buffer.from(file.base64, "base64");
|
|
||||||
const saved = await saveMediaBuffer(buffer, file.mimeType, "browser", buffer.byteLength);
|
|
||||||
mapping.set(file.path, saved.path);
|
|
||||||
}
|
|
||||||
return mapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyProxyPaths(result: unknown, mapping: Map<string, string>) {
|
|
||||||
if (!result || typeof result !== "object") return;
|
|
||||||
const obj = result as Record<string, unknown>;
|
|
||||||
if (typeof obj.path === "string" && mapping.has(obj.path)) {
|
|
||||||
obj.path = mapping.get(obj.path);
|
|
||||||
}
|
|
||||||
if (typeof obj.imagePath === "string" && mapping.has(obj.imagePath)) {
|
|
||||||
obj.imagePath = mapping.get(obj.imagePath);
|
|
||||||
}
|
|
||||||
const download = obj.download;
|
|
||||||
if (download && typeof download === "object") {
|
|
||||||
const d = download as Record<string, unknown>;
|
|
||||||
if (typeof d.path === "string" && mapping.has(d.path)) {
|
|
||||||
d.path = mapping.get(d.path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveBrowserBaseUrl(params: {
|
function resolveBrowserBaseUrl(params: {
|
||||||
target?: "sandbox" | "host" | "custom";
|
target?: "sandbox" | "host" | "custom";
|
||||||
@@ -289,12 +127,11 @@ export function createBrowserTool(opts?: {
|
|||||||
"Control the browser via Clawdbot's browser control server (status/start/stop/profiles/tabs/open/snapshot/screenshot/actions).",
|
"Control the browser via Clawdbot's browser control server (status/start/stop/profiles/tabs/open/snapshot/screenshot/actions).",
|
||||||
'Profiles: use profile="chrome" for Chrome extension relay takeover (your existing Chrome tabs). Use profile="clawd" for the isolated clawd-managed browser.',
|
'Profiles: use profile="chrome" for Chrome extension relay takeover (your existing Chrome tabs). Use profile="clawd" for the isolated clawd-managed browser.',
|
||||||
'If the user mentions the Chrome extension / Browser Relay / toolbar button / “attach tab”, ALWAYS use profile="chrome" (do not ask which profile).',
|
'If the user mentions the Chrome extension / Browser Relay / toolbar button / “attach tab”, ALWAYS use profile="chrome" (do not ask which profile).',
|
||||||
'When a node-hosted browser proxy is available, the tool may auto-route to it. Pin a node with node=<id|name> or target="node".',
|
|
||||||
"Chrome extension relay needs an attached tab: user must click the Clawdbot Browser Relay toolbar icon on the tab (badge ON). If no tab is connected, ask them to attach it.",
|
"Chrome extension relay needs an attached tab: user must click the Clawdbot Browser Relay toolbar icon on the tab (badge ON). If no tab is connected, ask them to attach it.",
|
||||||
"When using refs from snapshot (e.g. e12), keep the same tab: prefer passing targetId from the snapshot response into subsequent actions (act/click/type/etc).",
|
"When using refs from snapshot (e.g. e12), keep the same tab: prefer passing targetId from the snapshot response into subsequent actions (act/click/type/etc).",
|
||||||
'For stable, self-resolving refs across calls, use snapshot with refs="aria" (Playwright aria-ref ids). Default refs="role" are role+name-based.',
|
'For stable, self-resolving refs across calls, use snapshot with refs="aria" (Playwright aria-ref ids). Default refs="role" are role+name-based.',
|
||||||
"Use snapshot+act for UI automation. Avoid act:wait by default; use only in exceptional cases when no reliable UI state exists.",
|
"Use snapshot+act for UI automation. Avoid act:wait by default; use only in exceptional cases when no reliable UI state exists.",
|
||||||
`target selects browser location (sandbox|host|custom|node). Default: ${targetDefault}.`,
|
`target selects browser location (sandbox|host|custom). Default: ${targetDefault}.`,
|
||||||
"controlUrl implies target=custom (remote control server).",
|
"controlUrl implies target=custom (remote control server).",
|
||||||
hostHint,
|
hostHint,
|
||||||
allowlistHint,
|
allowlistHint,
|
||||||
@@ -305,184 +142,49 @@ export function createBrowserTool(opts?: {
|
|||||||
const action = readStringParam(params, "action", { required: true });
|
const action = readStringParam(params, "action", { required: true });
|
||||||
const controlUrl = readStringParam(params, "controlUrl");
|
const controlUrl = readStringParam(params, "controlUrl");
|
||||||
const profile = readStringParam(params, "profile");
|
const profile = readStringParam(params, "profile");
|
||||||
const requestedNode = readStringParam(params, "node");
|
let target = readStringParam(params, "target") as "sandbox" | "host" | "custom" | undefined;
|
||||||
let target = readStringParam(params, "target") as
|
if (profile === "chrome" && !target && !controlUrl?.trim()) {
|
||||||
| "sandbox"
|
// Chrome extension relay takeover is a host Chrome feature; default to host even in sandboxed sessions.
|
||||||
| "host"
|
|
||||||
| "custom"
|
|
||||||
| "node"
|
|
||||||
| undefined;
|
|
||||||
|
|
||||||
if (controlUrl?.trim() && (target === "node" || requestedNode)) {
|
|
||||||
throw new Error('controlUrl is not supported with target="node".');
|
|
||||||
}
|
|
||||||
if (target === "custom" && requestedNode) {
|
|
||||||
throw new Error('node is not supported with target="custom".');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!target && !controlUrl?.trim() && !requestedNode && profile === "chrome") {
|
|
||||||
// Chrome extension relay takeover is a host Chrome feature; prefer host unless explicitly targeting a node.
|
|
||||||
target = "host";
|
target = "host";
|
||||||
}
|
}
|
||||||
|
const baseUrl = resolveBrowserBaseUrl({
|
||||||
const nodeTarget = await resolveBrowserNodeTarget({
|
|
||||||
requestedNode: requestedNode ?? undefined,
|
|
||||||
target,
|
target,
|
||||||
controlUrl,
|
controlUrl,
|
||||||
defaultControlUrl: opts?.defaultControlUrl,
|
defaultControlUrl: opts?.defaultControlUrl,
|
||||||
|
allowHostControl: opts?.allowHostControl,
|
||||||
|
allowedControlUrls: opts?.allowedControlUrls,
|
||||||
|
allowedControlHosts: opts?.allowedControlHosts,
|
||||||
|
allowedControlPorts: opts?.allowedControlPorts,
|
||||||
});
|
});
|
||||||
|
|
||||||
const resolvedTarget = target === "node" ? undefined : target;
|
|
||||||
const baseUrl = nodeTarget
|
|
||||||
? ""
|
|
||||||
: resolveBrowserBaseUrl({
|
|
||||||
target: resolvedTarget,
|
|
||||||
controlUrl,
|
|
||||||
defaultControlUrl: opts?.defaultControlUrl,
|
|
||||||
allowHostControl: opts?.allowHostControl,
|
|
||||||
allowedControlUrls: opts?.allowedControlUrls,
|
|
||||||
allowedControlHosts: opts?.allowedControlHosts,
|
|
||||||
allowedControlPorts: opts?.allowedControlPorts,
|
|
||||||
});
|
|
||||||
|
|
||||||
const proxyRequest = nodeTarget
|
|
||||||
? async (opts: {
|
|
||||||
method: string;
|
|
||||||
path: string;
|
|
||||||
query?: Record<string, string | number | boolean | undefined>;
|
|
||||||
body?: unknown;
|
|
||||||
timeoutMs?: number;
|
|
||||||
profile?: string;
|
|
||||||
}) => {
|
|
||||||
const proxy = await callBrowserProxy({
|
|
||||||
nodeId: nodeTarget.nodeId,
|
|
||||||
method: opts.method,
|
|
||||||
path: opts.path,
|
|
||||||
query: opts.query,
|
|
||||||
body: opts.body,
|
|
||||||
timeoutMs: opts.timeoutMs,
|
|
||||||
profile: opts.profile,
|
|
||||||
});
|
|
||||||
const mapping = await persistProxyFiles(proxy.files);
|
|
||||||
applyProxyPaths(proxy.result, mapping);
|
|
||||||
return proxy.result;
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "status":
|
case "status":
|
||||||
if (proxyRequest) {
|
|
||||||
return jsonResult(
|
|
||||||
await proxyRequest({
|
|
||||||
method: "GET",
|
|
||||||
path: "/",
|
|
||||||
profile,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return jsonResult(await browserStatus(baseUrl, { profile }));
|
return jsonResult(await browserStatus(baseUrl, { profile }));
|
||||||
case "start":
|
case "start":
|
||||||
if (proxyRequest) {
|
|
||||||
await proxyRequest({
|
|
||||||
method: "POST",
|
|
||||||
path: "/start",
|
|
||||||
profile,
|
|
||||||
});
|
|
||||||
return jsonResult(
|
|
||||||
await proxyRequest({
|
|
||||||
method: "GET",
|
|
||||||
path: "/",
|
|
||||||
profile,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await browserStart(baseUrl, { profile });
|
await browserStart(baseUrl, { profile });
|
||||||
return jsonResult(await browserStatus(baseUrl, { profile }));
|
return jsonResult(await browserStatus(baseUrl, { profile }));
|
||||||
case "stop":
|
case "stop":
|
||||||
if (proxyRequest) {
|
|
||||||
await proxyRequest({
|
|
||||||
method: "POST",
|
|
||||||
path: "/stop",
|
|
||||||
profile,
|
|
||||||
});
|
|
||||||
return jsonResult(
|
|
||||||
await proxyRequest({
|
|
||||||
method: "GET",
|
|
||||||
path: "/",
|
|
||||||
profile,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await browserStop(baseUrl, { profile });
|
await browserStop(baseUrl, { profile });
|
||||||
return jsonResult(await browserStatus(baseUrl, { profile }));
|
return jsonResult(await browserStatus(baseUrl, { profile }));
|
||||||
case "profiles":
|
case "profiles":
|
||||||
if (proxyRequest) {
|
|
||||||
const result = await proxyRequest({
|
|
||||||
method: "GET",
|
|
||||||
path: "/profiles",
|
|
||||||
});
|
|
||||||
return jsonResult(result);
|
|
||||||
}
|
|
||||||
return jsonResult({ profiles: await browserProfiles(baseUrl) });
|
return jsonResult({ profiles: await browserProfiles(baseUrl) });
|
||||||
case "tabs":
|
case "tabs":
|
||||||
if (proxyRequest) {
|
|
||||||
const result = await proxyRequest({
|
|
||||||
method: "GET",
|
|
||||||
path: "/tabs",
|
|
||||||
profile,
|
|
||||||
});
|
|
||||||
const tabs = (result as { tabs?: unknown[] }).tabs ?? [];
|
|
||||||
return jsonResult({ tabs });
|
|
||||||
}
|
|
||||||
return jsonResult({ tabs: await browserTabs(baseUrl, { profile }) });
|
return jsonResult({ tabs: await browserTabs(baseUrl, { profile }) });
|
||||||
case "open": {
|
case "open": {
|
||||||
const targetUrl = readStringParam(params, "targetUrl", {
|
const targetUrl = readStringParam(params, "targetUrl", {
|
||||||
required: true,
|
required: true,
|
||||||
});
|
});
|
||||||
if (proxyRequest) {
|
|
||||||
const result = await proxyRequest({
|
|
||||||
method: "POST",
|
|
||||||
path: "/tabs/open",
|
|
||||||
profile,
|
|
||||||
body: { url: targetUrl },
|
|
||||||
});
|
|
||||||
return jsonResult(result);
|
|
||||||
}
|
|
||||||
return jsonResult(await browserOpenTab(baseUrl, targetUrl, { profile }));
|
return jsonResult(await browserOpenTab(baseUrl, targetUrl, { profile }));
|
||||||
}
|
}
|
||||||
case "focus": {
|
case "focus": {
|
||||||
const targetId = readStringParam(params, "targetId", {
|
const targetId = readStringParam(params, "targetId", {
|
||||||
required: true,
|
required: true,
|
||||||
});
|
});
|
||||||
if (proxyRequest) {
|
|
||||||
const result = await proxyRequest({
|
|
||||||
method: "POST",
|
|
||||||
path: "/tabs/focus",
|
|
||||||
profile,
|
|
||||||
body: { targetId },
|
|
||||||
});
|
|
||||||
return jsonResult(result);
|
|
||||||
}
|
|
||||||
await browserFocusTab(baseUrl, targetId, { profile });
|
await browserFocusTab(baseUrl, targetId, { profile });
|
||||||
return jsonResult({ ok: true });
|
return jsonResult({ ok: true });
|
||||||
}
|
}
|
||||||
case "close": {
|
case "close": {
|
||||||
const targetId = readStringParam(params, "targetId");
|
const targetId = readStringParam(params, "targetId");
|
||||||
if (proxyRequest) {
|
|
||||||
const result = targetId
|
|
||||||
? await proxyRequest({
|
|
||||||
method: "DELETE",
|
|
||||||
path: `/tabs/${encodeURIComponent(targetId)}`,
|
|
||||||
profile,
|
|
||||||
})
|
|
||||||
: await proxyRequest({
|
|
||||||
method: "POST",
|
|
||||||
path: "/act",
|
|
||||||
profile,
|
|
||||||
body: { kind: "close" },
|
|
||||||
});
|
|
||||||
return jsonResult(result);
|
|
||||||
}
|
|
||||||
if (targetId) await browserCloseTab(baseUrl, targetId, { profile });
|
if (targetId) await browserCloseTab(baseUrl, targetId, { profile });
|
||||||
else await browserAct(baseUrl, { kind: "close" }, { profile });
|
else await browserAct(baseUrl, { kind: "close" }, { profile });
|
||||||
return jsonResult({ ok: true });
|
return jsonResult({ ok: true });
|
||||||
@@ -530,41 +232,21 @@ export function createBrowserTool(opts?: {
|
|||||||
: undefined;
|
: undefined;
|
||||||
const selector = typeof params.selector === "string" ? params.selector.trim() : undefined;
|
const selector = typeof params.selector === "string" ? params.selector.trim() : undefined;
|
||||||
const frame = typeof params.frame === "string" ? params.frame.trim() : undefined;
|
const frame = typeof params.frame === "string" ? params.frame.trim() : undefined;
|
||||||
const snapshot = proxyRequest
|
const snapshot = await browserSnapshot(baseUrl, {
|
||||||
? ((await proxyRequest({
|
format,
|
||||||
method: "GET",
|
targetId,
|
||||||
path: "/snapshot",
|
limit,
|
||||||
profile,
|
...(typeof resolvedMaxChars === "number" ? { maxChars: resolvedMaxChars } : {}),
|
||||||
query: {
|
refs,
|
||||||
format,
|
interactive,
|
||||||
targetId,
|
compact,
|
||||||
limit,
|
depth,
|
||||||
...(typeof resolvedMaxChars === "number" ? { maxChars: resolvedMaxChars } : {}),
|
selector,
|
||||||
refs,
|
frame,
|
||||||
interactive,
|
labels,
|
||||||
compact,
|
mode,
|
||||||
depth,
|
profile,
|
||||||
selector,
|
});
|
||||||
frame,
|
|
||||||
labels,
|
|
||||||
mode,
|
|
||||||
},
|
|
||||||
})) as Awaited<ReturnType<typeof browserSnapshot>>)
|
|
||||||
: await browserSnapshot(baseUrl, {
|
|
||||||
format,
|
|
||||||
targetId,
|
|
||||||
limit,
|
|
||||||
...(typeof resolvedMaxChars === "number" ? { maxChars: resolvedMaxChars } : {}),
|
|
||||||
refs,
|
|
||||||
interactive,
|
|
||||||
compact,
|
|
||||||
depth,
|
|
||||||
selector,
|
|
||||||
frame,
|
|
||||||
labels,
|
|
||||||
mode,
|
|
||||||
profile,
|
|
||||||
});
|
|
||||||
if (snapshot.format === "ai") {
|
if (snapshot.format === "ai") {
|
||||||
if (labels && snapshot.imagePath) {
|
if (labels && snapshot.imagePath) {
|
||||||
return await imageResultFromFile({
|
return await imageResultFromFile({
|
||||||
@@ -587,27 +269,14 @@ export function createBrowserTool(opts?: {
|
|||||||
const ref = readStringParam(params, "ref");
|
const ref = readStringParam(params, "ref");
|
||||||
const element = readStringParam(params, "element");
|
const element = readStringParam(params, "element");
|
||||||
const type = params.type === "jpeg" ? "jpeg" : "png";
|
const type = params.type === "jpeg" ? "jpeg" : "png";
|
||||||
const result = proxyRequest
|
const result = await browserScreenshotAction(baseUrl, {
|
||||||
? ((await proxyRequest({
|
targetId,
|
||||||
method: "POST",
|
fullPage,
|
||||||
path: "/screenshot",
|
ref,
|
||||||
profile,
|
element,
|
||||||
body: {
|
type,
|
||||||
targetId,
|
profile,
|
||||||
fullPage,
|
});
|
||||||
ref,
|
|
||||||
element,
|
|
||||||
type,
|
|
||||||
},
|
|
||||||
})) as Awaited<ReturnType<typeof browserScreenshotAction>>)
|
|
||||||
: await browserScreenshotAction(baseUrl, {
|
|
||||||
targetId,
|
|
||||||
fullPage,
|
|
||||||
ref,
|
|
||||||
element,
|
|
||||||
type,
|
|
||||||
profile,
|
|
||||||
});
|
|
||||||
return await imageResultFromFile({
|
return await imageResultFromFile({
|
||||||
label: "browser:screenshot",
|
label: "browser:screenshot",
|
||||||
path: result.path,
|
path: result.path,
|
||||||
@@ -619,18 +288,6 @@ export function createBrowserTool(opts?: {
|
|||||||
required: true,
|
required: true,
|
||||||
});
|
});
|
||||||
const targetId = readStringParam(params, "targetId");
|
const targetId = readStringParam(params, "targetId");
|
||||||
if (proxyRequest) {
|
|
||||||
const result = await proxyRequest({
|
|
||||||
method: "POST",
|
|
||||||
path: "/navigate",
|
|
||||||
profile,
|
|
||||||
body: {
|
|
||||||
url: targetUrl,
|
|
||||||
targetId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return jsonResult(result);
|
|
||||||
}
|
|
||||||
return jsonResult(
|
return jsonResult(
|
||||||
await browserNavigate(baseUrl, {
|
await browserNavigate(baseUrl, {
|
||||||
url: targetUrl,
|
url: targetUrl,
|
||||||
@@ -642,30 +299,11 @@ export function createBrowserTool(opts?: {
|
|||||||
case "console": {
|
case "console": {
|
||||||
const level = typeof params.level === "string" ? params.level.trim() : undefined;
|
const level = typeof params.level === "string" ? params.level.trim() : undefined;
|
||||||
const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined;
|
const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined;
|
||||||
if (proxyRequest) {
|
|
||||||
const result = await proxyRequest({
|
|
||||||
method: "GET",
|
|
||||||
path: "/console",
|
|
||||||
profile,
|
|
||||||
query: {
|
|
||||||
level,
|
|
||||||
targetId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return jsonResult(result);
|
|
||||||
}
|
|
||||||
return jsonResult(await browserConsoleMessages(baseUrl, { level, targetId, profile }));
|
return jsonResult(await browserConsoleMessages(baseUrl, { level, targetId, profile }));
|
||||||
}
|
}
|
||||||
case "pdf": {
|
case "pdf": {
|
||||||
const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined;
|
const targetId = typeof params.targetId === "string" ? params.targetId.trim() : undefined;
|
||||||
const result = proxyRequest
|
const result = await browserPdfSave(baseUrl, { targetId, profile });
|
||||||
? ((await proxyRequest({
|
|
||||||
method: "POST",
|
|
||||||
path: "/pdf",
|
|
||||||
profile,
|
|
||||||
body: { targetId },
|
|
||||||
})) as Awaited<ReturnType<typeof browserPdfSave>>)
|
|
||||||
: await browserPdfSave(baseUrl, { targetId, profile });
|
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text", text: `FILE:${result.path}` }],
|
content: [{ type: "text", text: `FILE:${result.path}` }],
|
||||||
details: result,
|
details: result,
|
||||||
@@ -682,22 +320,6 @@ export function createBrowserTool(opts?: {
|
|||||||
typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs)
|
typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs)
|
||||||
? params.timeoutMs
|
? params.timeoutMs
|
||||||
: undefined;
|
: undefined;
|
||||||
if (proxyRequest) {
|
|
||||||
const result = await proxyRequest({
|
|
||||||
method: "POST",
|
|
||||||
path: "/hooks/file-chooser",
|
|
||||||
profile,
|
|
||||||
body: {
|
|
||||||
paths,
|
|
||||||
ref,
|
|
||||||
inputRef,
|
|
||||||
element,
|
|
||||||
targetId,
|
|
||||||
timeoutMs,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return jsonResult(result);
|
|
||||||
}
|
|
||||||
return jsonResult(
|
return jsonResult(
|
||||||
await browserArmFileChooser(baseUrl, {
|
await browserArmFileChooser(baseUrl, {
|
||||||
paths,
|
paths,
|
||||||
@@ -718,20 +340,6 @@ export function createBrowserTool(opts?: {
|
|||||||
typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs)
|
typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs)
|
||||||
? params.timeoutMs
|
? params.timeoutMs
|
||||||
: undefined;
|
: undefined;
|
||||||
if (proxyRequest) {
|
|
||||||
const result = await proxyRequest({
|
|
||||||
method: "POST",
|
|
||||||
path: "/hooks/dialog",
|
|
||||||
profile,
|
|
||||||
body: {
|
|
||||||
accept,
|
|
||||||
promptText,
|
|
||||||
targetId,
|
|
||||||
timeoutMs,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return jsonResult(result);
|
|
||||||
}
|
|
||||||
return jsonResult(
|
return jsonResult(
|
||||||
await browserArmDialog(baseUrl, {
|
await browserArmDialog(baseUrl, {
|
||||||
accept,
|
accept,
|
||||||
@@ -748,29 +356,14 @@ export function createBrowserTool(opts?: {
|
|||||||
throw new Error("request required");
|
throw new Error("request required");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = proxyRequest
|
const result = await browserAct(baseUrl, request as Parameters<typeof browserAct>[1], {
|
||||||
? await proxyRequest({
|
profile,
|
||||||
method: "POST",
|
});
|
||||||
path: "/act",
|
|
||||||
profile,
|
|
||||||
body: request,
|
|
||||||
})
|
|
||||||
: await browserAct(baseUrl, request as Parameters<typeof browserAct>[1], {
|
|
||||||
profile,
|
|
||||||
});
|
|
||||||
return jsonResult(result);
|
return jsonResult(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = String(err);
|
const msg = String(err);
|
||||||
if (msg.includes("404:") && msg.includes("tab not found") && profile === "chrome") {
|
if (msg.includes("404:") && msg.includes("tab not found") && profile === "chrome") {
|
||||||
const tabs = proxyRequest
|
const tabs = await browserTabs(baseUrl, { profile }).catch(() => []);
|
||||||
? ((
|
|
||||||
(await proxyRequest({
|
|
||||||
method: "GET",
|
|
||||||
path: "/tabs",
|
|
||||||
profile,
|
|
||||||
})) as { tabs?: unknown[] }
|
|
||||||
).tabs ?? [])
|
|
||||||
: await browserTabs(baseUrl, { profile }).catch(() => []);
|
|
||||||
if (!tabs.length) {
|
if (!tabs.length) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"No Chrome tabs are attached via the Clawdbot Browser Relay extension. Click the toolbar icon on the tab you want to control (badge ON), then retry.",
|
"No Chrome tabs are attached via the Clawdbot Browser Relay extension. Click the toolbar icon on the tab you want to control (badge ON), then retry.",
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
resolveDefaultModelForAgent,
|
resolveDefaultModelForAgent,
|
||||||
resolveModelRefFromString,
|
resolveModelRefFromString,
|
||||||
} from "../../agents/model-selection.js";
|
} from "../../agents/model-selection.js";
|
||||||
import { formatUserTime, resolveUserTimeFormat, resolveUserTimezone } from "../date-time.js";
|
|
||||||
import { normalizeGroupActivation } from "../../auto-reply/group-activation.js";
|
import { normalizeGroupActivation } from "../../auto-reply/group-activation.js";
|
||||||
import { getFollowupQueueDepth, resolveQueueSettings } from "../../auto-reply/reply/queue.js";
|
import { getFollowupQueueDepth, resolveQueueSettings } from "../../auto-reply/reply/queue.js";
|
||||||
import { buildStatusMessage } from "../../auto-reply/status.js";
|
import { buildStatusMessage } from "../../auto-reply/status.js";
|
||||||
@@ -216,7 +215,7 @@ export function createSessionStatusTool(opts?: {
|
|||||||
label: "Session Status",
|
label: "Session Status",
|
||||||
name: "session_status",
|
name: "session_status",
|
||||||
description:
|
description:
|
||||||
"Show a /status-equivalent session status card (usage + time + cost when available). Use for model-use questions (📊 session_status). Optional: set per-session model override (model=default resets overrides).",
|
"Show a /status-equivalent session status card (usage + cost when available). Use for model-use questions (📊 session_status). Optional: set per-session model override (model=default resets overrides).",
|
||||||
parameters: SessionStatusToolSchema,
|
parameters: SessionStatusToolSchema,
|
||||||
execute: async (_toolCallId, args) => {
|
execute: async (_toolCallId, args) => {
|
||||||
const params = args as Record<string, unknown>;
|
const params = args as Record<string, unknown>;
|
||||||
@@ -325,13 +324,6 @@ export function createSessionStatusTool(opts?: {
|
|||||||
resolved.entry.queueDebounceMs ?? resolved.entry.queueCap ?? resolved.entry.queueDrop,
|
resolved.entry.queueDebounceMs ?? resolved.entry.queueCap ?? resolved.entry.queueDrop,
|
||||||
);
|
);
|
||||||
|
|
||||||
const userTimezone = resolveUserTimezone(cfg.agents?.defaults?.userTimezone);
|
|
||||||
const userTimeFormat = resolveUserTimeFormat(cfg.agents?.defaults?.timeFormat);
|
|
||||||
const userTime = formatUserTime(new Date(), userTimezone, userTimeFormat);
|
|
||||||
const timeLine = userTime
|
|
||||||
? `🕒 Time: ${userTime} (${userTimezone})`
|
|
||||||
: `🕒 Time zone: ${userTimezone}`;
|
|
||||||
|
|
||||||
const agentDefaults = cfg.agents?.defaults ?? {};
|
const agentDefaults = cfg.agents?.defaults ?? {};
|
||||||
const defaultLabel = `${configured.provider}/${configured.model}`;
|
const defaultLabel = `${configured.provider}/${configured.model}`;
|
||||||
const agentModel =
|
const agentModel =
|
||||||
@@ -354,7 +346,6 @@ export function createSessionStatusTool(opts?: {
|
|||||||
agentDir,
|
agentDir,
|
||||||
}),
|
}),
|
||||||
usageLine,
|
usageLine,
|
||||||
timeLine,
|
|
||||||
queue: {
|
queue: {
|
||||||
mode: queueSettings.mode,
|
mode: queueSettings.mode,
|
||||||
depth: queueDepth,
|
depth: queueDepth,
|
||||||
|
|||||||
@@ -63,9 +63,6 @@ export function createSessionsSpawnTool(opts?: {
|
|||||||
agentAccountId?: string;
|
agentAccountId?: string;
|
||||||
agentTo?: string;
|
agentTo?: string;
|
||||||
agentThreadId?: string | number;
|
agentThreadId?: string | number;
|
||||||
agentGroupId?: string | null;
|
|
||||||
agentGroupChannel?: string | null;
|
|
||||||
agentGroupSpace?: string | null;
|
|
||||||
sandboxed?: boolean;
|
sandboxed?: boolean;
|
||||||
}): AnyAgentTool {
|
}): AnyAgentTool {
|
||||||
return {
|
return {
|
||||||
@@ -156,7 +153,7 @@ export function createSessionsSpawnTool(opts?: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const childSessionKey = `agent:${targetAgentId}:subagent:${crypto.randomUUID()}`;
|
const childSessionKey = `agent:${targetAgentId}:subagent:${crypto.randomUUID()}`;
|
||||||
const spawnedByKey = requesterInternalKey;
|
const shouldPatchSpawnedBy = opts?.sandboxed === true;
|
||||||
const targetAgentConfig = resolveAgentConfig(cfg, targetAgentId);
|
const targetAgentConfig = resolveAgentConfig(cfg, targetAgentId);
|
||||||
const resolvedModel =
|
const resolvedModel =
|
||||||
normalizeModelSelection(modelOverride) ??
|
normalizeModelSelection(modelOverride) ??
|
||||||
@@ -222,10 +219,7 @@ export function createSessionsSpawnTool(opts?: {
|
|||||||
thinking: thinkingOverride,
|
thinking: thinkingOverride,
|
||||||
timeout: runTimeoutSeconds > 0 ? runTimeoutSeconds : undefined,
|
timeout: runTimeoutSeconds > 0 ? runTimeoutSeconds : undefined,
|
||||||
label: label || undefined,
|
label: label || undefined,
|
||||||
spawnedBy: spawnedByKey,
|
spawnedBy: shouldPatchSpawnedBy ? requesterInternalKey : undefined,
|
||||||
groupId: opts?.agentGroupId ?? undefined,
|
|
||||||
groupChannel: opts?.agentGroupChannel ?? undefined,
|
|
||||||
groupSpace: opts?.agentGroupSpace ?? undefined,
|
|
||||||
},
|
},
|
||||||
timeoutMs: 10_000,
|
timeoutMs: 10_000,
|
||||||
})) as { runId?: string };
|
})) as { runId?: string };
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import {
|
import { DEFAULT_HEARTBEAT_ACK_MAX_CHARS, stripHeartbeatToken } from "./heartbeat.js";
|
||||||
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
|
||||||
isHeartbeatContentEffectivelyEmpty,
|
|
||||||
stripHeartbeatToken,
|
|
||||||
} from "./heartbeat.js";
|
|
||||||
import { HEARTBEAT_TOKEN } from "./tokens.js";
|
import { HEARTBEAT_TOKEN } from "./tokens.js";
|
||||||
|
|
||||||
describe("stripHeartbeatToken", () => {
|
describe("stripHeartbeatToken", () => {
|
||||||
@@ -109,76 +105,3 @@ describe("stripHeartbeatToken", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("isHeartbeatContentEffectivelyEmpty", () => {
|
|
||||||
it("returns false for undefined/null (missing file should not skip)", () => {
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty(undefined)).toBe(false);
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty(null)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns true for empty string", () => {
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty("")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns true for whitespace only", () => {
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty(" ")).toBe(true);
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty("\n\n\n")).toBe(true);
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty(" \n \n ")).toBe(true);
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty("\t\t")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns true for header-only content", () => {
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty("# HEARTBEAT.md")).toBe(true);
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty("# HEARTBEAT.md\n")).toBe(true);
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty("# HEARTBEAT.md\n\n")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns true for comments only", () => {
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty("# Header\n# Another comment")).toBe(true);
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty("## Subheader\n### Another")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns true for default template content (header + comment)", () => {
|
|
||||||
const defaultTemplate = `# HEARTBEAT.md
|
|
||||||
|
|
||||||
Keep this file empty unless you want a tiny checklist. Keep it small.
|
|
||||||
`;
|
|
||||||
// Note: The template has actual text content, so it's NOT effectively empty
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty(defaultTemplate)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns true for header with only empty lines", () => {
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty("# HEARTBEAT.md\n\n\n")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns false when actionable content exists", () => {
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty("- Check email")).toBe(false);
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty("# HEARTBEAT.md\n- Task 1")).toBe(false);
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty("Remind me to call mom")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns false for content with tasks after header", () => {
|
|
||||||
const content = `# HEARTBEAT.md
|
|
||||||
|
|
||||||
- Task 1
|
|
||||||
- Task 2
|
|
||||||
`;
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty(content)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns false for mixed content with non-comment text", () => {
|
|
||||||
const content = `# HEARTBEAT.md
|
|
||||||
## Tasks
|
|
||||||
Check the server logs
|
|
||||||
`;
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty(content)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("treats markdown headers as comments (effectively empty)", () => {
|
|
||||||
const content = `# HEARTBEAT.md
|
|
||||||
## Section 1
|
|
||||||
### Subsection
|
|
||||||
`;
|
|
||||||
expect(isHeartbeatContentEffectivelyEmpty(content)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -7,38 +7,6 @@ export const HEARTBEAT_PROMPT =
|
|||||||
export const DEFAULT_HEARTBEAT_EVERY = "30m";
|
export const DEFAULT_HEARTBEAT_EVERY = "30m";
|
||||||
export const DEFAULT_HEARTBEAT_ACK_MAX_CHARS = 300;
|
export const DEFAULT_HEARTBEAT_ACK_MAX_CHARS = 300;
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if HEARTBEAT.md content is "effectively empty" - meaning it has no actionable tasks.
|
|
||||||
* This allows skipping heartbeat API calls when no tasks are configured.
|
|
||||||
*
|
|
||||||
* A file is considered effectively empty if it contains only:
|
|
||||||
* - Whitespace
|
|
||||||
* - Comment lines (lines starting with #)
|
|
||||||
* - Empty lines
|
|
||||||
*
|
|
||||||
* Note: A missing file returns false (not effectively empty) so the LLM can still
|
|
||||||
* decide what to do. This function is only for when the file exists but has no content.
|
|
||||||
*/
|
|
||||||
export function isHeartbeatContentEffectivelyEmpty(content: string | undefined | null): boolean {
|
|
||||||
if (content === undefined || content === null) return false;
|
|
||||||
if (typeof content !== "string") return false;
|
|
||||||
|
|
||||||
const lines = content.split("\n");
|
|
||||||
for (const line of lines) {
|
|
||||||
const trimmed = line.trim();
|
|
||||||
// Skip empty lines
|
|
||||||
if (!trimmed) continue;
|
|
||||||
// Skip markdown header lines (# followed by space or EOL, ## etc)
|
|
||||||
// This intentionally does NOT skip lines like "#TODO" or "#hashtag" which might be content
|
|
||||||
// (Those aren't valid markdown headers - ATX headers require space after #)
|
|
||||||
if (/^#+(\s|$)/.test(trimmed)) continue;
|
|
||||||
// Found a non-empty, non-comment line - there's actionable content
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// All lines were either empty or comments
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveHeartbeatPrompt(raw?: string): string {
|
export function resolveHeartbeatPrompt(raw?: string): string {
|
||||||
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
||||||
return trimmed || HEARTBEAT_PROMPT;
|
return trimmed || HEARTBEAT_PROMPT;
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
} from "../../agents/pi-embedded-helpers.js";
|
} from "../../agents/pi-embedded-helpers.js";
|
||||||
import {
|
import {
|
||||||
resolveAgentIdFromSessionKey,
|
resolveAgentIdFromSessionKey,
|
||||||
resolveGroupSessionKey,
|
|
||||||
resolveSessionTranscriptPath,
|
resolveSessionTranscriptPath,
|
||||||
type SessionEntry,
|
type SessionEntry,
|
||||||
updateSessionStore,
|
updateSessionStore,
|
||||||
@@ -215,10 +214,6 @@ export async function runAgentTurnWithFallback(params: {
|
|||||||
agentAccountId: params.sessionCtx.AccountId,
|
agentAccountId: params.sessionCtx.AccountId,
|
||||||
messageTo: params.sessionCtx.OriginatingTo ?? params.sessionCtx.To,
|
messageTo: params.sessionCtx.OriginatingTo ?? params.sessionCtx.To,
|
||||||
messageThreadId: params.sessionCtx.MessageThreadId ?? undefined,
|
messageThreadId: params.sessionCtx.MessageThreadId ?? undefined,
|
||||||
groupId: resolveGroupSessionKey(params.sessionCtx)?.id,
|
|
||||||
groupChannel:
|
|
||||||
params.sessionCtx.GroupChannel?.trim() ?? params.sessionCtx.GroupSubject?.trim(),
|
|
||||||
groupSpace: params.sessionCtx.GroupSpace?.trim() ?? undefined,
|
|
||||||
// Provider threading context for tool auto-injection
|
// Provider threading context for tool auto-injection
|
||||||
...buildThreadingToolContext({
|
...buildThreadingToolContext({
|
||||||
sessionCtx: params.sessionCtx,
|
sessionCtx: params.sessionCtx,
|
||||||
|
|||||||
@@ -67,10 +67,6 @@ export const handleCompactCommand: CommandHandler = async (params) => {
|
|||||||
sessionId,
|
sessionId,
|
||||||
sessionKey: params.sessionKey,
|
sessionKey: params.sessionKey,
|
||||||
messageChannel: params.command.channel,
|
messageChannel: params.command.channel,
|
||||||
groupId: params.sessionEntry.groupId,
|
|
||||||
groupChannel: params.sessionEntry.groupChannel,
|
|
||||||
groupSpace: params.sessionEntry.space,
|
|
||||||
spawnedBy: params.sessionEntry.spawnedBy,
|
|
||||||
sessionFile: resolveSessionFilePath(sessionId, params.sessionEntry),
|
sessionFile: resolveSessionFilePath(sessionId, params.sessionEntry),
|
||||||
workspaceDir: params.workspaceDir,
|
workspaceDir: params.workspaceDir,
|
||||||
config: params.cfg,
|
config: params.cfg,
|
||||||
|
|||||||
@@ -81,10 +81,6 @@ async function resolveContextReport(
|
|||||||
workspaceDir,
|
workspaceDir,
|
||||||
sessionKey: params.sessionKey,
|
sessionKey: params.sessionKey,
|
||||||
messageProvider: params.command.channel,
|
messageProvider: params.command.channel,
|
||||||
groupId: params.sessionEntry?.groupId ?? undefined,
|
|
||||||
groupChannel: params.sessionEntry?.groupChannel ?? undefined,
|
|
||||||
groupSpace: params.sessionEntry?.space ?? undefined,
|
|
||||||
spawnedBy: params.sessionEntry?.spawnedBy ?? undefined,
|
|
||||||
modelProvider: params.provider,
|
modelProvider: params.provider,
|
||||||
modelId: params.model,
|
modelId: params.model,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -147,9 +147,6 @@ export function createFollowupRunner(params: {
|
|||||||
agentAccountId: queued.run.agentAccountId,
|
agentAccountId: queued.run.agentAccountId,
|
||||||
messageTo: queued.originatingTo,
|
messageTo: queued.originatingTo,
|
||||||
messageThreadId: queued.originatingThreadId,
|
messageThreadId: queued.originatingThreadId,
|
||||||
groupId: queued.run.groupId,
|
|
||||||
groupChannel: queued.run.groupChannel,
|
|
||||||
groupSpace: queued.run.groupSpace,
|
|
||||||
sessionFile: queued.run.sessionFile,
|
sessionFile: queued.run.sessionFile,
|
||||||
workspaceDir: queued.run.workspaceDir,
|
workspaceDir: queued.run.workspaceDir,
|
||||||
config: queued.run.config,
|
config: queued.run.config,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { resolveSessionAuthProfileOverride } from "../../agents/auth-profiles/se
|
|||||||
import type { ExecToolDefaults } from "../../agents/bash-tools.js";
|
import type { ExecToolDefaults } from "../../agents/bash-tools.js";
|
||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import {
|
import {
|
||||||
resolveGroupSessionKey,
|
|
||||||
resolveSessionFilePath,
|
resolveSessionFilePath,
|
||||||
type SessionEntry,
|
type SessionEntry,
|
||||||
updateSessionStore,
|
updateSessionStore,
|
||||||
@@ -367,9 +366,6 @@ export async function runPreparedReply(
|
|||||||
sessionKey,
|
sessionKey,
|
||||||
messageProvider: sessionCtx.Provider?.trim().toLowerCase() || undefined,
|
messageProvider: sessionCtx.Provider?.trim().toLowerCase() || undefined,
|
||||||
agentAccountId: sessionCtx.AccountId,
|
agentAccountId: sessionCtx.AccountId,
|
||||||
groupId: resolveGroupSessionKey(sessionCtx)?.id ?? undefined,
|
|
||||||
groupChannel: sessionCtx.GroupChannel?.trim() ?? sessionCtx.GroupSubject?.trim(),
|
|
||||||
groupSpace: sessionCtx.GroupSpace?.trim() ?? undefined,
|
|
||||||
sessionFile,
|
sessionFile,
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
config: cfg,
|
config: cfg,
|
||||||
|
|||||||
@@ -48,9 +48,6 @@ export type FollowupRun = {
|
|||||||
sessionKey?: string;
|
sessionKey?: string;
|
||||||
messageProvider?: string;
|
messageProvider?: string;
|
||||||
agentAccountId?: string;
|
agentAccountId?: string;
|
||||||
groupId?: string;
|
|
||||||
groupChannel?: string;
|
|
||||||
groupSpace?: string;
|
|
||||||
sessionFile: string;
|
sessionFile: string;
|
||||||
workspaceDir: string;
|
workspaceDir: string;
|
||||||
config: ClawdbotConfig;
|
config: ClawdbotConfig;
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ type StatusArgs = {
|
|||||||
resolvedElevated?: ElevatedLevel;
|
resolvedElevated?: ElevatedLevel;
|
||||||
modelAuth?: string;
|
modelAuth?: string;
|
||||||
usageLine?: string;
|
usageLine?: string;
|
||||||
timeLine?: string;
|
|
||||||
queue?: QueueStatus;
|
queue?: QueueStatus;
|
||||||
mediaDecisions?: MediaUnderstandingDecision[];
|
mediaDecisions?: MediaUnderstandingDecision[];
|
||||||
subagentsLine?: string;
|
subagentsLine?: string;
|
||||||
@@ -382,7 +381,6 @@ export function buildStatusMessage(args: StatusArgs): string {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
versionLine,
|
versionLine,
|
||||||
args.timeLine,
|
|
||||||
modelLine,
|
modelLine,
|
||||||
usageCostLine,
|
usageCostLine,
|
||||||
`📚 ${contextLine}`,
|
`📚 ${contextLine}`,
|
||||||
|
|||||||
@@ -247,71 +247,4 @@ describe("chrome extension relay server", () => {
|
|||||||
cdp.close();
|
cdp.close();
|
||||||
ext.close();
|
ext.close();
|
||||||
}, 15_000);
|
}, 15_000);
|
||||||
|
|
||||||
it("rebroadcasts attach when a session id is reused for a new target", async () => {
|
|
||||||
const port = await getFreePort();
|
|
||||||
cdpUrl = `http://127.0.0.1:${port}`;
|
|
||||||
await ensureChromeExtensionRelayServer({ cdpUrl });
|
|
||||||
|
|
||||||
const ext = new WebSocket(`ws://127.0.0.1:${port}/extension`);
|
|
||||||
await waitForOpen(ext);
|
|
||||||
|
|
||||||
const cdp = new WebSocket(`ws://127.0.0.1:${port}/cdp`);
|
|
||||||
await waitForOpen(cdp);
|
|
||||||
const q = createMessageQueue(cdp);
|
|
||||||
|
|
||||||
ext.send(
|
|
||||||
JSON.stringify({
|
|
||||||
method: "forwardCDPEvent",
|
|
||||||
params: {
|
|
||||||
method: "Target.attachedToTarget",
|
|
||||||
params: {
|
|
||||||
sessionId: "shared-session",
|
|
||||||
targetInfo: {
|
|
||||||
targetId: "t1",
|
|
||||||
type: "page",
|
|
||||||
title: "First",
|
|
||||||
url: "https://example.com",
|
|
||||||
},
|
|
||||||
waitingForDebugger: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const first = JSON.parse(await q.next()) as { method?: string; params?: unknown };
|
|
||||||
expect(first.method).toBe("Target.attachedToTarget");
|
|
||||||
expect(JSON.stringify(first.params ?? {})).toContain("t1");
|
|
||||||
|
|
||||||
ext.send(
|
|
||||||
JSON.stringify({
|
|
||||||
method: "forwardCDPEvent",
|
|
||||||
params: {
|
|
||||||
method: "Target.attachedToTarget",
|
|
||||||
params: {
|
|
||||||
sessionId: "shared-session",
|
|
||||||
targetInfo: {
|
|
||||||
targetId: "t2",
|
|
||||||
type: "page",
|
|
||||||
title: "Second",
|
|
||||||
url: "https://example.org",
|
|
||||||
},
|
|
||||||
waitingForDebugger: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const received: Array<{ method?: string; params?: unknown }> = [];
|
|
||||||
received.push(JSON.parse(await q.next()) as never);
|
|
||||||
received.push(JSON.parse(await q.next()) as never);
|
|
||||||
|
|
||||||
const detached = received.find((m) => m.method === "Target.detachedFromTarget");
|
|
||||||
const attached = received.find((m) => m.method === "Target.attachedToTarget");
|
|
||||||
expect(JSON.stringify(detached?.params ?? {})).toContain("t1");
|
|
||||||
expect(JSON.stringify(attached?.params ?? {})).toContain("t2");
|
|
||||||
|
|
||||||
cdp.close();
|
|
||||||
ext.close();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
@@ -477,23 +477,13 @@ export async function ensureChromeExtensionRelayServer(opts: {
|
|||||||
const targetType = attached?.targetInfo?.type ?? "page";
|
const targetType = attached?.targetInfo?.type ?? "page";
|
||||||
if (targetType !== "page") return;
|
if (targetType !== "page") return;
|
||||||
if (attached?.sessionId && attached?.targetInfo?.targetId) {
|
if (attached?.sessionId && attached?.targetInfo?.targetId) {
|
||||||
const prev = connectedTargets.get(attached.sessionId);
|
const already = connectedTargets.has(attached.sessionId);
|
||||||
const nextTargetId = attached.targetInfo.targetId;
|
|
||||||
const prevTargetId = prev?.targetId;
|
|
||||||
const changedTarget = Boolean(prev && prevTargetId && prevTargetId !== nextTargetId);
|
|
||||||
connectedTargets.set(attached.sessionId, {
|
connectedTargets.set(attached.sessionId, {
|
||||||
sessionId: attached.sessionId,
|
sessionId: attached.sessionId,
|
||||||
targetId: nextTargetId,
|
targetId: attached.targetInfo.targetId,
|
||||||
targetInfo: attached.targetInfo,
|
targetInfo: attached.targetInfo,
|
||||||
});
|
});
|
||||||
if (changedTarget && prevTargetId) {
|
if (!already) {
|
||||||
broadcastToCdpClients({
|
|
||||||
method: "Target.detachedFromTarget",
|
|
||||||
params: { sessionId: attached.sessionId, targetId: prevTargetId },
|
|
||||||
sessionId: attached.sessionId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!prev || changedTarget) {
|
|
||||||
broadcastToCdpClients({ method, params, sessionId });
|
broadcastToCdpClients({ method, params, sessionId });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -11,15 +11,10 @@ import { normalizeWhatsAppTarget } from "../whatsapp/normalize.js";
|
|||||||
import { requireActivePluginRegistry } from "../plugins/runtime.js";
|
import { requireActivePluginRegistry } from "../plugins/runtime.js";
|
||||||
import {
|
import {
|
||||||
resolveDiscordGroupRequireMention,
|
resolveDiscordGroupRequireMention,
|
||||||
resolveDiscordGroupToolPolicy,
|
|
||||||
resolveIMessageGroupRequireMention,
|
resolveIMessageGroupRequireMention,
|
||||||
resolveIMessageGroupToolPolicy,
|
|
||||||
resolveSlackGroupRequireMention,
|
resolveSlackGroupRequireMention,
|
||||||
resolveSlackGroupToolPolicy,
|
|
||||||
resolveTelegramGroupRequireMention,
|
resolveTelegramGroupRequireMention,
|
||||||
resolveTelegramGroupToolPolicy,
|
|
||||||
resolveWhatsAppGroupRequireMention,
|
resolveWhatsAppGroupRequireMention,
|
||||||
resolveWhatsAppGroupToolPolicy,
|
|
||||||
} from "./plugins/group-mentions.js";
|
} from "./plugins/group-mentions.js";
|
||||||
import type {
|
import type {
|
||||||
ChannelCapabilities,
|
ChannelCapabilities,
|
||||||
@@ -108,7 +103,6 @@ const DOCKS: Record<ChatChannelId, ChannelDock> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveTelegramGroupRequireMention,
|
resolveRequireMention: resolveTelegramGroupRequireMention,
|
||||||
resolveToolPolicy: resolveTelegramGroupToolPolicy,
|
|
||||||
},
|
},
|
||||||
threading: {
|
threading: {
|
||||||
resolveReplyToMode: ({ cfg }) => cfg.channels?.telegram?.replyToMode ?? "first",
|
resolveReplyToMode: ({ cfg }) => cfg.channels?.telegram?.replyToMode ?? "first",
|
||||||
@@ -147,7 +141,6 @@ const DOCKS: Record<ChatChannelId, ChannelDock> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveWhatsAppGroupRequireMention,
|
resolveRequireMention: resolveWhatsAppGroupRequireMention,
|
||||||
resolveToolPolicy: resolveWhatsAppGroupToolPolicy,
|
|
||||||
resolveGroupIntroHint: () =>
|
resolveGroupIntroHint: () =>
|
||||||
"WhatsApp IDs: SenderId is the participant JID; [message_id: ...] is the message id for reactions (use SenderId as participant).",
|
"WhatsApp IDs: SenderId is the participant JID; [message_id: ...] is the message id for reactions (use SenderId as participant).",
|
||||||
},
|
},
|
||||||
@@ -196,7 +189,6 @@ const DOCKS: Record<ChatChannelId, ChannelDock> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveDiscordGroupRequireMention,
|
resolveRequireMention: resolveDiscordGroupRequireMention,
|
||||||
resolveToolPolicy: resolveDiscordGroupToolPolicy,
|
|
||||||
},
|
},
|
||||||
mentions: {
|
mentions: {
|
||||||
stripPatterns: () => ["<@!?\\d+>"],
|
stripPatterns: () => ["<@!?\\d+>"],
|
||||||
@@ -230,7 +222,6 @@ const DOCKS: Record<ChatChannelId, ChannelDock> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveSlackGroupRequireMention,
|
resolveRequireMention: resolveSlackGroupRequireMention,
|
||||||
resolveToolPolicy: resolveSlackGroupToolPolicy,
|
|
||||||
},
|
},
|
||||||
threading: {
|
threading: {
|
||||||
resolveReplyToMode: ({ cfg, accountId, chatType }) =>
|
resolveReplyToMode: ({ cfg, accountId, chatType }) =>
|
||||||
@@ -293,7 +284,6 @@ const DOCKS: Record<ChatChannelId, ChannelDock> = {
|
|||||||
},
|
},
|
||||||
groups: {
|
groups: {
|
||||||
resolveRequireMention: resolveIMessageGroupRequireMention,
|
resolveRequireMention: resolveIMessageGroupRequireMention,
|
||||||
resolveToolPolicy: resolveIMessageGroupToolPolicy,
|
|
||||||
},
|
},
|
||||||
threading: {
|
threading: {
|
||||||
buildToolContext: ({ context, hasRepliedRef }) => {
|
buildToolContext: ({ context, hasRepliedRef }) => {
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import {
|
import { resolveChannelGroupRequireMention } from "../../config/group-policy.js";
|
||||||
resolveChannelGroupRequireMention,
|
|
||||||
resolveChannelGroupToolsPolicy,
|
|
||||||
} from "../../config/group-policy.js";
|
|
||||||
import type { DiscordConfig } from "../../config/types.js";
|
import type { DiscordConfig } from "../../config/types.js";
|
||||||
import type { GroupToolPolicyConfig } from "../../config/types.tools.js";
|
|
||||||
import { resolveSlackAccount } from "../../slack/accounts.js";
|
import { resolveSlackAccount } from "../../slack/accounts.js";
|
||||||
|
|
||||||
type GroupMentionParams = {
|
type GroupMentionParams = {
|
||||||
@@ -196,103 +192,3 @@ export function resolveBlueBubblesGroupRequireMention(params: GroupMentionParams
|
|||||||
accountId: params.accountId,
|
accountId: params.accountId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveTelegramGroupToolPolicy(
|
|
||||||
params: GroupMentionParams,
|
|
||||||
): GroupToolPolicyConfig | undefined {
|
|
||||||
const { chatId } = parseTelegramGroupId(params.groupId);
|
|
||||||
return resolveChannelGroupToolsPolicy({
|
|
||||||
cfg: params.cfg,
|
|
||||||
channel: "telegram",
|
|
||||||
groupId: chatId ?? params.groupId,
|
|
||||||
accountId: params.accountId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveWhatsAppGroupToolPolicy(
|
|
||||||
params: GroupMentionParams,
|
|
||||||
): GroupToolPolicyConfig | undefined {
|
|
||||||
return resolveChannelGroupToolsPolicy({
|
|
||||||
cfg: params.cfg,
|
|
||||||
channel: "whatsapp",
|
|
||||||
groupId: params.groupId,
|
|
||||||
accountId: params.accountId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveIMessageGroupToolPolicy(
|
|
||||||
params: GroupMentionParams,
|
|
||||||
): GroupToolPolicyConfig | undefined {
|
|
||||||
return resolveChannelGroupToolsPolicy({
|
|
||||||
cfg: params.cfg,
|
|
||||||
channel: "imessage",
|
|
||||||
groupId: params.groupId,
|
|
||||||
accountId: params.accountId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveDiscordGroupToolPolicy(
|
|
||||||
params: GroupMentionParams,
|
|
||||||
): GroupToolPolicyConfig | undefined {
|
|
||||||
const guildEntry = resolveDiscordGuildEntry(
|
|
||||||
params.cfg.channels?.discord?.guilds,
|
|
||||||
params.groupSpace,
|
|
||||||
);
|
|
||||||
const channelEntries = guildEntry?.channels;
|
|
||||||
if (channelEntries && Object.keys(channelEntries).length > 0) {
|
|
||||||
const groupChannel = params.groupChannel;
|
|
||||||
const channelSlug = normalizeDiscordSlug(groupChannel);
|
|
||||||
const entry =
|
|
||||||
(params.groupId ? channelEntries[params.groupId] : undefined) ??
|
|
||||||
(channelSlug
|
|
||||||
? (channelEntries[channelSlug] ?? channelEntries[`#${channelSlug}`])
|
|
||||||
: undefined) ??
|
|
||||||
(groupChannel ? channelEntries[normalizeDiscordSlug(groupChannel)] : undefined);
|
|
||||||
if (entry?.tools) return entry.tools;
|
|
||||||
}
|
|
||||||
if (guildEntry?.tools) return guildEntry.tools;
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveSlackGroupToolPolicy(
|
|
||||||
params: GroupMentionParams,
|
|
||||||
): GroupToolPolicyConfig | undefined {
|
|
||||||
const account = resolveSlackAccount({
|
|
||||||
cfg: params.cfg,
|
|
||||||
accountId: params.accountId,
|
|
||||||
});
|
|
||||||
const channels = account.channels ?? {};
|
|
||||||
const keys = Object.keys(channels);
|
|
||||||
if (keys.length === 0) return undefined;
|
|
||||||
const channelId = params.groupId?.trim();
|
|
||||||
const groupChannel = params.groupChannel;
|
|
||||||
const channelName = groupChannel?.replace(/^#/, "");
|
|
||||||
const normalizedName = normalizeSlackSlug(channelName);
|
|
||||||
const candidates = [
|
|
||||||
channelId ?? "",
|
|
||||||
channelName ? `#${channelName}` : "",
|
|
||||||
channelName ?? "",
|
|
||||||
normalizedName,
|
|
||||||
].filter(Boolean);
|
|
||||||
let matched: { tools?: GroupToolPolicyConfig } | undefined;
|
|
||||||
for (const candidate of candidates) {
|
|
||||||
if (candidate && channels[candidate]) {
|
|
||||||
matched = channels[candidate];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const resolved = matched ?? channels["*"];
|
|
||||||
if (resolved?.tools) return resolved.tools;
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveBlueBubblesGroupToolPolicy(
|
|
||||||
params: GroupMentionParams,
|
|
||||||
): GroupToolPolicyConfig | undefined {
|
|
||||||
return resolveChannelGroupToolsPolicy({
|
|
||||||
cfg: params.cfg,
|
|
||||||
channel: "bluebubbles",
|
|
||||||
groupId: params.groupId,
|
|
||||||
accountId: params.accountId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { ClawdbotConfig } from "../../config/config.js";
|
import type { ClawdbotConfig } from "../../config/config.js";
|
||||||
import type { GroupToolPolicyConfig } from "../../config/types.tools.js";
|
|
||||||
import type { OutboundDeliveryResult, OutboundSendDeps } from "../../infra/outbound/deliver.js";
|
import type { OutboundDeliveryResult, OutboundSendDeps } from "../../infra/outbound/deliver.js";
|
||||||
import type { RuntimeEnv } from "../../runtime.js";
|
import type { RuntimeEnv } from "../../runtime.js";
|
||||||
import type {
|
import type {
|
||||||
@@ -66,7 +65,6 @@ export type ChannelConfigAdapter<ResolvedAccount> = {
|
|||||||
export type ChannelGroupAdapter = {
|
export type ChannelGroupAdapter = {
|
||||||
resolveRequireMention?: (params: ChannelGroupContext) => boolean | undefined;
|
resolveRequireMention?: (params: ChannelGroupContext) => boolean | undefined;
|
||||||
resolveGroupIntroHint?: (params: ChannelGroupContext) => string | undefined;
|
resolveGroupIntroHint?: (params: ChannelGroupContext) => string | undefined;
|
||||||
resolveToolPolicy?: (params: ChannelGroupContext) => GroupToolPolicyConfig | undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChannelOutboundContext = {
|
export type ChannelOutboundContext = {
|
||||||
|
|||||||
@@ -8,8 +8,11 @@ import {
|
|||||||
} from "./register.cron-add.js";
|
} from "./register.cron-add.js";
|
||||||
import { registerCronEditCommand } from "./register.cron-edit.js";
|
import { registerCronEditCommand } from "./register.cron-edit.js";
|
||||||
import { registerCronSimpleCommands } from "./register.cron-simple.js";
|
import { registerCronSimpleCommands } from "./register.cron-simple.js";
|
||||||
|
import { registerWakeCommand } from "./register.wake.js";
|
||||||
|
|
||||||
export function registerCronCli(program: Command) {
|
export function registerCronCli(program: Command) {
|
||||||
|
registerWakeCommand(program);
|
||||||
|
|
||||||
const cron = program
|
const cron = program
|
||||||
.command("cron")
|
.command("cron")
|
||||||
.description("Manage cron jobs (via Gateway)")
|
.description("Manage cron jobs (via Gateway)")
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import type { Command } from "commander";
|
||||||
|
import { danger } from "../../globals.js";
|
||||||
|
import { defaultRuntime } from "../../runtime.js";
|
||||||
|
import { formatDocsLink } from "../../terminal/links.js";
|
||||||
|
import { theme } from "../../terminal/theme.js";
|
||||||
|
import type { GatewayRpcOpts } from "../gateway-rpc.js";
|
||||||
|
import { addGatewayClientOptions, callGatewayFromCli } from "../gateway-rpc.js";
|
||||||
|
|
||||||
|
export function registerWakeCommand(program: Command) {
|
||||||
|
addGatewayClientOptions(
|
||||||
|
program
|
||||||
|
.command("wake")
|
||||||
|
.description("Enqueue a system event and optionally trigger an immediate heartbeat")
|
||||||
|
.requiredOption("--text <text>", "System event text")
|
||||||
|
.option("--mode <mode>", "Wake mode (now|next-heartbeat)", "next-heartbeat")
|
||||||
|
.option("--json", "Output JSON", false),
|
||||||
|
)
|
||||||
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
() => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/wake", "docs.clawd.bot/cli/wake")}\n`,
|
||||||
|
)
|
||||||
|
.action(async (opts: GatewayRpcOpts & { text?: string; mode?: string }) => {
|
||||||
|
try {
|
||||||
|
const result = await callGatewayFromCli(
|
||||||
|
"wake",
|
||||||
|
opts,
|
||||||
|
{ mode: opts.mode, text: opts.text },
|
||||||
|
{ expectFinal: false },
|
||||||
|
);
|
||||||
|
if (opts.json) defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||||
|
else defaultRuntime.log("ok");
|
||||||
|
} catch (err) {
|
||||||
|
defaultRuntime.error(danger(String(err)));
|
||||||
|
defaultRuntime.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -248,7 +248,7 @@ export function registerNodesInvokeCommands(nodes: Command) {
|
|||||||
const approvals = resolveExecApprovalsFromFile({
|
const approvals = resolveExecApprovalsFromFile({
|
||||||
file: approvalsFile as ExecApprovalsFile,
|
file: approvalsFile as ExecApprovalsFile,
|
||||||
agentId,
|
agentId,
|
||||||
overrides: { security, ask },
|
overrides: { security: "allowlist" },
|
||||||
});
|
});
|
||||||
const hostSecurity = minSecurity(security, approvals.agent.security);
|
const hostSecurity = minSecurity(security, approvals.agent.security);
|
||||||
const hostAsk = maxAsk(ask, approvals.agent.ask);
|
const hostAsk = maxAsk(ask, approvals.agent.ask);
|
||||||
|
|||||||
@@ -60,14 +60,6 @@ const entries: SubCliEntry[] = [
|
|||||||
mod.registerLogsCli(program);
|
mod.registerLogsCli(program);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "system",
|
|
||||||
description: "System events, heartbeat, and presence",
|
|
||||||
register: async (program) => {
|
|
||||||
const mod = await import("../system-cli.js");
|
|
||||||
mod.registerSystemCli(program);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "models",
|
name: "models",
|
||||||
description: "Model configuration",
|
description: "Model configuration",
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
import type { Command } from "commander";
|
|
||||||
|
|
||||||
import { danger } from "../globals.js";
|
|
||||||
import { defaultRuntime } from "../runtime.js";
|
|
||||||
import { formatDocsLink } from "../terminal/links.js";
|
|
||||||
import { theme } from "../terminal/theme.js";
|
|
||||||
import type { GatewayRpcOpts } from "./gateway-rpc.js";
|
|
||||||
import { addGatewayClientOptions, callGatewayFromCli } from "./gateway-rpc.js";
|
|
||||||
|
|
||||||
type SystemEventOpts = GatewayRpcOpts & { text?: string; mode?: string; json?: boolean };
|
|
||||||
|
|
||||||
const normalizeWakeMode = (raw: unknown) => {
|
|
||||||
const mode = typeof raw === "string" ? raw.trim() : "";
|
|
||||||
if (!mode) return "next-heartbeat" as const;
|
|
||||||
if (mode === "now" || mode === "next-heartbeat") return mode;
|
|
||||||
throw new Error("--mode must be now or next-heartbeat");
|
|
||||||
};
|
|
||||||
|
|
||||||
export function registerSystemCli(program: Command) {
|
|
||||||
const system = program
|
|
||||||
.command("system")
|
|
||||||
.description("System tools (events, heartbeat, presence)")
|
|
||||||
.addHelpText(
|
|
||||||
"after",
|
|
||||||
() =>
|
|
||||||
`\n${theme.muted("Docs:")} ${formatDocsLink("/cli/system", "docs.clawd.bot/cli/system")}\n`,
|
|
||||||
);
|
|
||||||
|
|
||||||
addGatewayClientOptions(
|
|
||||||
system
|
|
||||||
.command("event")
|
|
||||||
.description("Enqueue a system event and optionally trigger a heartbeat")
|
|
||||||
.requiredOption("--text <text>", "System event text")
|
|
||||||
.option("--mode <mode>", "Wake mode (now|next-heartbeat)", "next-heartbeat")
|
|
||||||
.option("--json", "Output JSON", false),
|
|
||||||
).action(async (opts: SystemEventOpts) => {
|
|
||||||
try {
|
|
||||||
const text = typeof opts.text === "string" ? opts.text.trim() : "";
|
|
||||||
if (!text) throw new Error("--text is required");
|
|
||||||
const mode = normalizeWakeMode(opts.mode);
|
|
||||||
const result = await callGatewayFromCli("wake", opts, { mode, text }, { expectFinal: false });
|
|
||||||
if (opts.json) defaultRuntime.log(JSON.stringify(result, null, 2));
|
|
||||||
else defaultRuntime.log("ok");
|
|
||||||
} catch (err) {
|
|
||||||
defaultRuntime.error(danger(String(err)));
|
|
||||||
defaultRuntime.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const heartbeat = system.command("heartbeat").description("Heartbeat controls");
|
|
||||||
|
|
||||||
addGatewayClientOptions(
|
|
||||||
heartbeat
|
|
||||||
.command("last")
|
|
||||||
.description("Show the last heartbeat event")
|
|
||||||
.option("--json", "Output JSON", false),
|
|
||||||
).action(async (opts: GatewayRpcOpts & { json?: boolean }) => {
|
|
||||||
try {
|
|
||||||
const result = await callGatewayFromCli("last-heartbeat", opts, undefined, {
|
|
||||||
expectFinal: false,
|
|
||||||
});
|
|
||||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
|
||||||
} catch (err) {
|
|
||||||
defaultRuntime.error(danger(String(err)));
|
|
||||||
defaultRuntime.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
addGatewayClientOptions(
|
|
||||||
heartbeat
|
|
||||||
.command("enable")
|
|
||||||
.description("Enable heartbeats")
|
|
||||||
.option("--json", "Output JSON", false),
|
|
||||||
).action(async (opts: GatewayRpcOpts & { json?: boolean }) => {
|
|
||||||
try {
|
|
||||||
const result = await callGatewayFromCli(
|
|
||||||
"set-heartbeats",
|
|
||||||
opts,
|
|
||||||
{ enabled: true },
|
|
||||||
{ expectFinal: false },
|
|
||||||
);
|
|
||||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
|
||||||
} catch (err) {
|
|
||||||
defaultRuntime.error(danger(String(err)));
|
|
||||||
defaultRuntime.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
addGatewayClientOptions(
|
|
||||||
heartbeat
|
|
||||||
.command("disable")
|
|
||||||
.description("Disable heartbeats")
|
|
||||||
.option("--json", "Output JSON", false),
|
|
||||||
).action(async (opts: GatewayRpcOpts & { json?: boolean }) => {
|
|
||||||
try {
|
|
||||||
const result = await callGatewayFromCli(
|
|
||||||
"set-heartbeats",
|
|
||||||
opts,
|
|
||||||
{ enabled: false },
|
|
||||||
{ expectFinal: false },
|
|
||||||
);
|
|
||||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
|
||||||
} catch (err) {
|
|
||||||
defaultRuntime.error(danger(String(err)));
|
|
||||||
defaultRuntime.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
addGatewayClientOptions(
|
|
||||||
system
|
|
||||||
.command("presence")
|
|
||||||
.description("List system presence entries")
|
|
||||||
.option("--json", "Output JSON", false),
|
|
||||||
).action(async (opts: GatewayRpcOpts & { json?: boolean }) => {
|
|
||||||
try {
|
|
||||||
const result = await callGatewayFromCli("system-presence", opts, undefined, {
|
|
||||||
expectFinal: false,
|
|
||||||
});
|
|
||||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
|
||||||
} catch (err) {
|
|
||||||
defaultRuntime.error(danger(String(err)));
|
|
||||||
defaultRuntime.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -377,7 +377,6 @@ export async function agentCommand(
|
|||||||
runContext.messageChannel,
|
runContext.messageChannel,
|
||||||
opts.replyChannel ?? opts.channel,
|
opts.replyChannel ?? opts.channel,
|
||||||
);
|
);
|
||||||
const spawnedBy = opts.spawnedBy ?? sessionEntry?.spawnedBy;
|
|
||||||
const fallbackResult = await runWithModelFallback({
|
const fallbackResult = await runWithModelFallback({
|
||||||
cfg,
|
cfg,
|
||||||
provider,
|
provider,
|
||||||
@@ -413,10 +412,6 @@ export async function agentCommand(
|
|||||||
agentAccountId: runContext.accountId,
|
agentAccountId: runContext.accountId,
|
||||||
messageTo: opts.replyTo ?? opts.to,
|
messageTo: opts.replyTo ?? opts.to,
|
||||||
messageThreadId: opts.threadId,
|
messageThreadId: opts.threadId,
|
||||||
groupId: runContext.groupId,
|
|
||||||
groupChannel: runContext.groupChannel,
|
|
||||||
groupSpace: runContext.groupSpace,
|
|
||||||
spawnedBy,
|
|
||||||
currentChannelId: runContext.currentChannelId,
|
currentChannelId: runContext.currentChannelId,
|
||||||
currentThreadTs: runContext.currentThreadTs,
|
currentThreadTs: runContext.currentThreadTs,
|
||||||
replyToMode: runContext.replyToMode,
|
replyToMode: runContext.replyToMode,
|
||||||
|
|||||||
@@ -14,15 +14,6 @@ export function resolveAgentRunContext(opts: AgentCommandOpts): AgentRunContext
|
|||||||
const normalizedAccountId = normalizeAccountId(merged.accountId ?? opts.accountId);
|
const normalizedAccountId = normalizeAccountId(merged.accountId ?? opts.accountId);
|
||||||
if (normalizedAccountId) merged.accountId = normalizedAccountId;
|
if (normalizedAccountId) merged.accountId = normalizedAccountId;
|
||||||
|
|
||||||
const groupId = (merged.groupId ?? opts.groupId)?.toString().trim();
|
|
||||||
if (groupId) merged.groupId = groupId;
|
|
||||||
|
|
||||||
const groupChannel = (merged.groupChannel ?? opts.groupChannel)?.toString().trim();
|
|
||||||
if (groupChannel) merged.groupChannel = groupChannel;
|
|
||||||
|
|
||||||
const groupSpace = (merged.groupSpace ?? opts.groupSpace)?.toString().trim();
|
|
||||||
if (groupSpace) merged.groupSpace = groupSpace;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
merged.currentThreadTs == null &&
|
merged.currentThreadTs == null &&
|
||||||
opts.threadId != null &&
|
opts.threadId != null &&
|
||||||
|
|||||||
@@ -17,9 +17,6 @@ export type AgentStreamParams = {
|
|||||||
export type AgentRunContext = {
|
export type AgentRunContext = {
|
||||||
messageChannel?: string;
|
messageChannel?: string;
|
||||||
accountId?: string;
|
accountId?: string;
|
||||||
groupId?: string | null;
|
|
||||||
groupChannel?: string | null;
|
|
||||||
groupSpace?: string | null;
|
|
||||||
currentChannelId?: string;
|
currentChannelId?: string;
|
||||||
currentThreadTs?: string;
|
currentThreadTs?: string;
|
||||||
replyToMode?: "off" | "first" | "all";
|
replyToMode?: "off" | "first" | "all";
|
||||||
@@ -58,14 +55,6 @@ export type AgentCommandOpts = {
|
|||||||
accountId?: string;
|
accountId?: string;
|
||||||
/** Context for embedded run routing (channel/account/thread). */
|
/** Context for embedded run routing (channel/account/thread). */
|
||||||
runContext?: AgentRunContext;
|
runContext?: AgentRunContext;
|
||||||
/** Group id for channel-level tool policy resolution. */
|
|
||||||
groupId?: string | null;
|
|
||||||
/** Group channel label for channel-level tool policy resolution. */
|
|
||||||
groupChannel?: string | null;
|
|
||||||
/** Group space label for channel-level tool policy resolution. */
|
|
||||||
groupSpace?: string | null;
|
|
||||||
/** Parent session key for subagent policy inheritance. */
|
|
||||||
spawnedBy?: string | null;
|
|
||||||
deliveryTargetMode?: ChannelOutboundTargetMode;
|
deliveryTargetMode?: ChannelOutboundTargetMode;
|
||||||
bestEffortDeliver?: boolean;
|
bestEffortDeliver?: boolean;
|
||||||
abortSignal?: AbortSignal;
|
abortSignal?: AbortSignal;
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import type { ChannelId } from "../channels/plugins/types.js";
|
import type { ChannelId } from "../channels/plugins/types.js";
|
||||||
import { normalizeAccountId } from "../routing/session-key.js";
|
import { normalizeAccountId } from "../routing/session-key.js";
|
||||||
import type { ClawdbotConfig } from "./config.js";
|
import type { ClawdbotConfig } from "./config.js";
|
||||||
import type { GroupToolPolicyConfig } from "./types.tools.js";
|
|
||||||
|
|
||||||
export type GroupPolicyChannel = ChannelId;
|
export type GroupPolicyChannel = ChannelId;
|
||||||
|
|
||||||
export type ChannelGroupConfig = {
|
export type ChannelGroupConfig = {
|
||||||
requireMention?: boolean;
|
requireMention?: boolean;
|
||||||
tools?: GroupToolPolicyConfig;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChannelGroupPolicy = {
|
export type ChannelGroupPolicy = {
|
||||||
@@ -93,15 +91,3 @@ export function resolveChannelGroupRequireMention(params: {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveChannelGroupToolsPolicy(params: {
|
|
||||||
cfg: ClawdbotConfig;
|
|
||||||
channel: GroupPolicyChannel;
|
|
||||||
groupId?: string | null;
|
|
||||||
accountId?: string | null;
|
|
||||||
}): GroupToolPolicyConfig | undefined {
|
|
||||||
const { groupConfig, defaultConfig } = resolveChannelGroupPolicy(params);
|
|
||||||
if (groupConfig?.tools) return groupConfig.tools;
|
|
||||||
if (defaultConfig?.tools) return defaultConfig.tools;
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
@@ -50,7 +50,6 @@ const GROUP_LABELS: Record<string, string> = {
|
|||||||
diagnostics: "Diagnostics",
|
diagnostics: "Diagnostics",
|
||||||
logging: "Logging",
|
logging: "Logging",
|
||||||
gateway: "Gateway",
|
gateway: "Gateway",
|
||||||
nodeHost: "Node Host",
|
|
||||||
agents: "Agents",
|
agents: "Agents",
|
||||||
tools: "Tools",
|
tools: "Tools",
|
||||||
bindings: "Bindings",
|
bindings: "Bindings",
|
||||||
@@ -77,7 +76,6 @@ const GROUP_ORDER: Record<string, number> = {
|
|||||||
update: 25,
|
update: 25,
|
||||||
diagnostics: 27,
|
diagnostics: 27,
|
||||||
gateway: 30,
|
gateway: 30,
|
||||||
nodeHost: 35,
|
|
||||||
agents: 40,
|
agents: 40,
|
||||||
tools: 50,
|
tools: 50,
|
||||||
bindings: 55,
|
bindings: 55,
|
||||||
@@ -121,8 +119,6 @@ const FIELD_LABELS: Record<string, string> = {
|
|||||||
"diagnostics.cacheTrace.includeMessages": "Cache Trace Include Messages",
|
"diagnostics.cacheTrace.includeMessages": "Cache Trace Include Messages",
|
||||||
"diagnostics.cacheTrace.includePrompt": "Cache Trace Include Prompt",
|
"diagnostics.cacheTrace.includePrompt": "Cache Trace Include Prompt",
|
||||||
"diagnostics.cacheTrace.includeSystem": "Cache Trace Include System",
|
"diagnostics.cacheTrace.includeSystem": "Cache Trace Include System",
|
||||||
"diagnostics.anthropicPayloadLog.enabled": "Anthropic Payload Log Enabled",
|
|
||||||
"diagnostics.anthropicPayloadLog.filePath": "Anthropic Payload Log File Path",
|
|
||||||
"agents.list.*.identity.avatar": "Identity Avatar",
|
"agents.list.*.identity.avatar": "Identity Avatar",
|
||||||
"gateway.remote.url": "Remote Gateway URL",
|
"gateway.remote.url": "Remote Gateway URL",
|
||||||
"gateway.remote.sshTarget": "Remote Gateway SSH Target",
|
"gateway.remote.sshTarget": "Remote Gateway SSH Target",
|
||||||
@@ -197,12 +193,8 @@ const FIELD_LABELS: Record<string, string> = {
|
|||||||
"gateway.http.endpoints.chatCompletions.enabled": "OpenAI Chat Completions Endpoint",
|
"gateway.http.endpoints.chatCompletions.enabled": "OpenAI Chat Completions Endpoint",
|
||||||
"gateway.reload.mode": "Config Reload Mode",
|
"gateway.reload.mode": "Config Reload Mode",
|
||||||
"gateway.reload.debounceMs": "Config Reload Debounce (ms)",
|
"gateway.reload.debounceMs": "Config Reload Debounce (ms)",
|
||||||
"gateway.nodes.browser.mode": "Gateway Node Browser Mode",
|
|
||||||
"gateway.nodes.browser.node": "Gateway Node Browser Pin",
|
|
||||||
"gateway.nodes.allowCommands": "Gateway Node Allowlist (Extra Commands)",
|
"gateway.nodes.allowCommands": "Gateway Node Allowlist (Extra Commands)",
|
||||||
"gateway.nodes.denyCommands": "Gateway Node Denylist",
|
"gateway.nodes.denyCommands": "Gateway Node Denylist",
|
||||||
"nodeHost.browserProxy.enabled": "Node Browser Proxy Enabled",
|
|
||||||
"nodeHost.browserProxy.allowProfiles": "Node Browser Proxy Allowed Profiles",
|
|
||||||
"skills.load.watch": "Watch Skills",
|
"skills.load.watch": "Watch Skills",
|
||||||
"skills.load.watchDebounceMs": "Skills Watch Debounce (ms)",
|
"skills.load.watchDebounceMs": "Skills Watch Debounce (ms)",
|
||||||
"agents.defaults.workspace": "Workspace",
|
"agents.defaults.workspace": "Workspace",
|
||||||
@@ -374,16 +366,10 @@ const FIELD_HELP: Record<string, string> = {
|
|||||||
"Enable the OpenAI-compatible `POST /v1/chat/completions` endpoint (default: false).",
|
"Enable the OpenAI-compatible `POST /v1/chat/completions` endpoint (default: false).",
|
||||||
"gateway.reload.mode": 'Hot reload strategy for config changes ("hybrid" recommended).',
|
"gateway.reload.mode": 'Hot reload strategy for config changes ("hybrid" recommended).',
|
||||||
"gateway.reload.debounceMs": "Debounce window (ms) before applying config changes.",
|
"gateway.reload.debounceMs": "Debounce window (ms) before applying config changes.",
|
||||||
"gateway.nodes.browser.mode":
|
|
||||||
'Node browser routing ("auto" = pick single connected browser node, "manual" = require node param, "off" = disable).',
|
|
||||||
"gateway.nodes.browser.node": "Pin browser routing to a specific node id or name (optional).",
|
|
||||||
"gateway.nodes.allowCommands":
|
"gateway.nodes.allowCommands":
|
||||||
"Extra node.invoke commands to allow beyond the gateway defaults (array of command strings).",
|
"Extra node.invoke commands to allow beyond the gateway defaults (array of command strings).",
|
||||||
"gateway.nodes.denyCommands":
|
"gateway.nodes.denyCommands":
|
||||||
"Commands to block even if present in node claims or default allowlist.",
|
"Commands to block even if present in node claims or default allowlist.",
|
||||||
"nodeHost.browserProxy.enabled": "Expose the local browser control server via node proxy.",
|
|
||||||
"nodeHost.browserProxy.allowProfiles":
|
|
||||||
"Optional allowlist of browser profile names exposed via the node proxy.",
|
|
||||||
"diagnostics.cacheTrace.enabled":
|
"diagnostics.cacheTrace.enabled":
|
||||||
"Log cache trace snapshots for embedded agent runs (default: false).",
|
"Log cache trace snapshots for embedded agent runs (default: false).",
|
||||||
"diagnostics.cacheTrace.filePath":
|
"diagnostics.cacheTrace.filePath":
|
||||||
@@ -392,10 +378,6 @@ const FIELD_HELP: Record<string, string> = {
|
|||||||
"Include full message payloads in trace output (default: true).",
|
"Include full message payloads in trace output (default: true).",
|
||||||
"diagnostics.cacheTrace.includePrompt": "Include prompt text in trace output (default: true).",
|
"diagnostics.cacheTrace.includePrompt": "Include prompt text in trace output (default: true).",
|
||||||
"diagnostics.cacheTrace.includeSystem": "Include system prompt in trace output (default: true).",
|
"diagnostics.cacheTrace.includeSystem": "Include system prompt in trace output (default: true).",
|
||||||
"diagnostics.anthropicPayloadLog.enabled":
|
|
||||||
"Log Anthropic request payloads + usage for embedded runs (default: false).",
|
|
||||||
"diagnostics.anthropicPayloadLog.filePath":
|
|
||||||
"JSONL output path for Anthropic payload logs (default: $CLAWDBOT_STATE_DIR/logs/anthropic-payload.jsonl).",
|
|
||||||
"tools.exec.applyPatch.enabled":
|
"tools.exec.applyPatch.enabled":
|
||||||
"Experimental. Enables apply_patch for OpenAI models when allowed by tool policy.",
|
"Experimental. Enables apply_patch for OpenAI models when allowed by tool policy.",
|
||||||
"tools.exec.applyPatch.allowModels":
|
"tools.exec.applyPatch.allowModels":
|
||||||
|
|||||||
@@ -256,22 +256,6 @@ describe("sessions", () => {
|
|||||||
expect(store["agent:main:two"]?.sessionId).toBe("sess-2");
|
expect(store["agent:main:two"]?.sessionId).toBe("sess-2");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("recovers from array-backed session stores", async () => {
|
|
||||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-sessions-"));
|
|
||||||
const storePath = path.join(dir, "sessions.json");
|
|
||||||
await fs.writeFile(storePath, "[]", "utf-8");
|
|
||||||
|
|
||||||
await updateSessionStore(storePath, (store) => {
|
|
||||||
store["agent:main:main"] = { sessionId: "sess-1", updatedAt: 1 };
|
|
||||||
});
|
|
||||||
|
|
||||||
const store = loadSessionStore(storePath);
|
|
||||||
expect(store["agent:main:main"]?.sessionId).toBe("sess-1");
|
|
||||||
|
|
||||||
const raw = await fs.readFile(storePath, "utf-8");
|
|
||||||
expect(raw.trim().startsWith("{")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("normalizes last route fields on write", async () => {
|
it("normalizes last route fields on write", async () => {
|
||||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-sessions-"));
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-sessions-"));
|
||||||
const storePath = path.join(dir, "sessions.json");
|
const storePath = path.join(dir, "sessions.json");
|
||||||
|
|||||||
@@ -29,10 +29,6 @@ type SessionStoreCacheEntry = {
|
|||||||
const SESSION_STORE_CACHE = new Map<string, SessionStoreCacheEntry>();
|
const SESSION_STORE_CACHE = new Map<string, SessionStoreCacheEntry>();
|
||||||
const DEFAULT_SESSION_STORE_TTL_MS = 45_000; // 45 seconds (between 30-60s)
|
const DEFAULT_SESSION_STORE_TTL_MS = 45_000; // 45 seconds (between 30-60s)
|
||||||
|
|
||||||
function isSessionStoreRecord(value: unknown): value is Record<string, SessionEntry> {
|
|
||||||
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSessionStoreTtl(): number {
|
function getSessionStoreTtl(): number {
|
||||||
return resolveCacheTtlMs({
|
return resolveCacheTtlMs({
|
||||||
envValue: process.env.CLAWDBOT_SESSION_CACHE_TTL_MS,
|
envValue: process.env.CLAWDBOT_SESSION_CACHE_TTL_MS,
|
||||||
@@ -119,7 +115,7 @@ export function loadSessionStore(
|
|||||||
try {
|
try {
|
||||||
const raw = fs.readFileSync(storePath, "utf-8");
|
const raw = fs.readFileSync(storePath, "utf-8");
|
||||||
const parsed = JSON5.parse(raw);
|
const parsed = JSON5.parse(raw);
|
||||||
if (isSessionStoreRecord(parsed)) {
|
if (parsed && typeof parsed === "object") {
|
||||||
store = parsed as Record<string, SessionEntry>;
|
store = parsed as Record<string, SessionEntry>;
|
||||||
}
|
}
|
||||||
mtimeMs = getFileMtimeMs(storePath) ?? mtimeMs;
|
mtimeMs = getFileMtimeMs(storePath) ?? mtimeMs;
|
||||||
|
|||||||
@@ -133,16 +133,10 @@ export type DiagnosticsCacheTraceConfig = {
|
|||||||
includeSystem?: boolean;
|
includeSystem?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DiagnosticsAnthropicPayloadLogConfig = {
|
|
||||||
enabled?: boolean;
|
|
||||||
filePath?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DiagnosticsConfig = {
|
export type DiagnosticsConfig = {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
otel?: DiagnosticsOtelConfig;
|
otel?: DiagnosticsOtelConfig;
|
||||||
cacheTrace?: DiagnosticsCacheTraceConfig;
|
cacheTrace?: DiagnosticsCacheTraceConfig;
|
||||||
anthropicPayloadLog?: DiagnosticsAnthropicPayloadLogConfig;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WebReconnectConfig = {
|
export type WebReconnectConfig = {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import type {
|
|||||||
MessagesConfig,
|
MessagesConfig,
|
||||||
} from "./types.messages.js";
|
} from "./types.messages.js";
|
||||||
import type { ModelsConfig } from "./types.models.js";
|
import type { ModelsConfig } from "./types.models.js";
|
||||||
import type { NodeHostConfig } from "./types.node-host.js";
|
|
||||||
import type { PluginsConfig } from "./types.plugins.js";
|
import type { PluginsConfig } from "./types.plugins.js";
|
||||||
import type { SkillsConfig } from "./types.skills.js";
|
import type { SkillsConfig } from "./types.skills.js";
|
||||||
import type { ToolsConfig } from "./types.tools.js";
|
import type { ToolsConfig } from "./types.tools.js";
|
||||||
@@ -76,7 +75,6 @@ export type ClawdbotConfig = {
|
|||||||
skills?: SkillsConfig;
|
skills?: SkillsConfig;
|
||||||
plugins?: PluginsConfig;
|
plugins?: PluginsConfig;
|
||||||
models?: ModelsConfig;
|
models?: ModelsConfig;
|
||||||
nodeHost?: NodeHostConfig;
|
|
||||||
agents?: AgentsConfig;
|
agents?: AgentsConfig;
|
||||||
tools?: ToolsConfig;
|
tools?: ToolsConfig;
|
||||||
bindings?: AgentBinding[];
|
bindings?: AgentBinding[];
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import type {
|
|||||||
ReplyToMode,
|
ReplyToMode,
|
||||||
} from "./types.base.js";
|
} from "./types.base.js";
|
||||||
import type { DmConfig, ProviderCommandsConfig } from "./types.messages.js";
|
import type { DmConfig, ProviderCommandsConfig } from "./types.messages.js";
|
||||||
import type { GroupToolPolicyConfig } from "./types.tools.js";
|
|
||||||
|
|
||||||
export type DiscordDmConfig = {
|
export type DiscordDmConfig = {
|
||||||
/** If false, ignore all incoming Discord DMs. Default: true. */
|
/** If false, ignore all incoming Discord DMs. Default: true. */
|
||||||
@@ -25,8 +24,6 @@ export type DiscordDmConfig = {
|
|||||||
export type DiscordGuildChannelConfig = {
|
export type DiscordGuildChannelConfig = {
|
||||||
allow?: boolean;
|
allow?: boolean;
|
||||||
requireMention?: boolean;
|
requireMention?: boolean;
|
||||||
/** Optional tool policy overrides for this channel. */
|
|
||||||
tools?: GroupToolPolicyConfig;
|
|
||||||
/** If specified, only load these skills for this channel. Omit = all skills; empty = no skills. */
|
/** If specified, only load these skills for this channel. Omit = all skills; empty = no skills. */
|
||||||
skills?: string[];
|
skills?: string[];
|
||||||
/** If false, disable the bot for this channel. */
|
/** If false, disable the bot for this channel. */
|
||||||
@@ -42,8 +39,6 @@ export type DiscordReactionNotificationMode = "off" | "own" | "all" | "allowlist
|
|||||||
export type DiscordGuildEntry = {
|
export type DiscordGuildEntry = {
|
||||||
slug?: string;
|
slug?: string;
|
||||||
requireMention?: boolean;
|
requireMention?: boolean;
|
||||||
/** Optional tool policy overrides for this guild (used when channel override is missing). */
|
|
||||||
tools?: GroupToolPolicyConfig;
|
|
||||||
/** Reaction notification mode (off|own|all|allowlist). Default: own. */
|
/** Reaction notification mode (off|own|all|allowlist). Default: own. */
|
||||||
reactionNotifications?: DiscordReactionNotificationMode;
|
reactionNotifications?: DiscordReactionNotificationMode;
|
||||||
users?: Array<string | number>;
|
users?: Array<string | number>;
|
||||||
|
|||||||
@@ -175,13 +175,6 @@ export type GatewayHttpConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type GatewayNodesConfig = {
|
export type GatewayNodesConfig = {
|
||||||
/** Browser routing policy for node-hosted browser proxies. */
|
|
||||||
browser?: {
|
|
||||||
/** Routing mode (default: auto). */
|
|
||||||
mode?: "auto" | "manual" | "off";
|
|
||||||
/** Pin to a specific node id/name (optional). */
|
|
||||||
node?: string;
|
|
||||||
};
|
|
||||||
/** Additional node.invoke commands to allow on the gateway. */
|
/** Additional node.invoke commands to allow on the gateway. */
|
||||||
allowCommands?: string[];
|
allowCommands?: string[];
|
||||||
/** Commands to deny even if they appear in the defaults or node claims. */
|
/** Commands to deny even if they appear in the defaults or node claims. */
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import type {
|
|||||||
MarkdownConfig,
|
MarkdownConfig,
|
||||||
} from "./types.base.js";
|
} from "./types.base.js";
|
||||||
import type { DmConfig } from "./types.messages.js";
|
import type { DmConfig } from "./types.messages.js";
|
||||||
import type { GroupToolPolicyConfig } from "./types.tools.js";
|
|
||||||
|
|
||||||
export type IMessageAccountConfig = {
|
export type IMessageAccountConfig = {
|
||||||
/** Optional display name for this account (used in CLI/UI lists). */
|
/** Optional display name for this account (used in CLI/UI lists). */
|
||||||
@@ -60,7 +59,6 @@ export type IMessageAccountConfig = {
|
|||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
requireMention?: boolean;
|
requireMention?: boolean;
|
||||||
tools?: GroupToolPolicyConfig;
|
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import type {
|
|||||||
MarkdownConfig,
|
MarkdownConfig,
|
||||||
} from "./types.base.js";
|
} from "./types.base.js";
|
||||||
import type { DmConfig } from "./types.messages.js";
|
import type { DmConfig } from "./types.messages.js";
|
||||||
import type { GroupToolPolicyConfig } from "./types.tools.js";
|
|
||||||
|
|
||||||
export type MSTeamsWebhookConfig = {
|
export type MSTeamsWebhookConfig = {
|
||||||
/** Port for the webhook server. Default: 3978. */
|
/** Port for the webhook server. Default: 3978. */
|
||||||
@@ -21,8 +20,6 @@ export type MSTeamsReplyStyle = "thread" | "top-level";
|
|||||||
export type MSTeamsChannelConfig = {
|
export type MSTeamsChannelConfig = {
|
||||||
/** Require @mention to respond. Default: true. */
|
/** Require @mention to respond. Default: true. */
|
||||||
requireMention?: boolean;
|
requireMention?: boolean;
|
||||||
/** Optional tool policy overrides for this channel. */
|
|
||||||
tools?: GroupToolPolicyConfig;
|
|
||||||
/** Reply style: "thread" replies to the message, "top-level" posts a new message. */
|
/** Reply style: "thread" replies to the message, "top-level" posts a new message. */
|
||||||
replyStyle?: MSTeamsReplyStyle;
|
replyStyle?: MSTeamsReplyStyle;
|
||||||
};
|
};
|
||||||
@@ -31,8 +28,6 @@ export type MSTeamsChannelConfig = {
|
|||||||
export type MSTeamsTeamConfig = {
|
export type MSTeamsTeamConfig = {
|
||||||
/** Default requireMention for channels in this team. */
|
/** Default requireMention for channels in this team. */
|
||||||
requireMention?: boolean;
|
requireMention?: boolean;
|
||||||
/** Default tool policy for channels in this team. */
|
|
||||||
tools?: GroupToolPolicyConfig;
|
|
||||||
/** Default reply style for channels in this team. */
|
/** Default reply style for channels in this team. */
|
||||||
replyStyle?: MSTeamsReplyStyle;
|
replyStyle?: MSTeamsReplyStyle;
|
||||||
/** Per-channel overrides. Key is conversation ID (e.g., "19:...@thread.tacv2"). */
|
/** Per-channel overrides. Key is conversation ID (e.g., "19:...@thread.tacv2"). */
|
||||||
|
|||||||
Loaded 100 of 156 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user