Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ed1f230ff | ||
|
|
08d9d9f9e3 | ||
|
|
c291b7a992 | ||
|
|
f564fe5adf | ||
|
|
3ce5b642b3 |
No files matched your search
+1
-11
@@ -6,34 +6,25 @@
|
|||||||
- Node bridge: harden keepalive + heartbeat handling (TCP keepalive, better disconnects, and keepalive config tests). (#577) — thanks @steipete
|
- Node bridge: harden keepalive + heartbeat handling (TCP keepalive, better disconnects, and keepalive config tests). (#577) — thanks @steipete
|
||||||
- Control UI: improve mobile responsiveness. (#558) — thanks @carlulsoe
|
- Control UI: improve mobile responsiveness. (#558) — thanks @carlulsoe
|
||||||
- CLI: add `sandbox list` and `sandbox recreate` commands for managing Docker sandbox containers after image/config updates. (#563) — thanks @pasogott
|
- CLI: add `sandbox list` and `sandbox recreate` commands for managing Docker sandbox containers after image/config updates. (#563) — thanks @pasogott
|
||||||
- Docs: add Hetzner Docker VPS guide. (#556) — thanks @Iamadig
|
|
||||||
- Docs: link Hetzner guide from install + platforms docs. (#592) — thanks @steipete
|
|
||||||
- Providers: add Microsoft Teams provider with polling, attachments, and CLI send support. (#404) — thanks @onutc
|
- Providers: add Microsoft Teams provider with polling, attachments, and CLI send support. (#404) — thanks @onutc
|
||||||
- Slack: honor reply tags + replyToMode while keeping threaded replies in-thread. (#574) — thanks @bolismauro
|
- Slack: honor reply tags + replyToMode while keeping threaded replies in-thread. (#574) — thanks @bolismauro
|
||||||
- Discord: avoid category parent overrides for channel allowlists and refactor thread context helpers. (#588) — thanks @steipete
|
|
||||||
- Discord: fix forum thread starters and cache channel lookups for thread context. (#585) — thanks @thewilloftheshadow
|
|
||||||
- Commands: accept /models as an alias for /model.
|
- Commands: accept /models as an alias for /model.
|
||||||
- Commands: add `/usage` as an alias for `/status`. (#492) — thanks @lc0rp
|
- Commands: add `/usage` as an alias for `/status`. (#492) — thanks @lc0rp
|
||||||
- Models/Auth: add MiniMax Anthropic-compatible API onboarding (minimax-api). (#590) — thanks @mneves75
|
- Commands: add `/commands` list including text-only entries. (#497) — thanks @lc0rp
|
||||||
- Commands: harden slash command registry and list text-only commands in `/commands`.
|
|
||||||
- Models/Auth: show per-agent auth candidates in `/model status`, and add `clawdbot models auth order {get,set,clear}` (per-agent auth rotation overrides). — thanks @steipete
|
- Models/Auth: show per-agent auth candidates in `/model status`, and add `clawdbot models auth order {get,set,clear}` (per-agent auth rotation overrides). — thanks @steipete
|
||||||
- Debugging: add raw model stream logging flags and document gateway watch mode.
|
- Debugging: add raw model stream logging flags and document gateway watch mode.
|
||||||
- Gateway: decode dns-sd escaped UTF-8 in discovery output and show scan progress immediately. — thanks @steipete
|
- Gateway: decode dns-sd escaped UTF-8 in discovery output and show scan progress immediately. — thanks @steipete
|
||||||
- Agent: add claude-cli/opus-4.5 runner via Claude CLI with resume support (tools disabled).
|
- Agent: add claude-cli/opus-4.5 runner via Claude CLI with resume support (tools disabled).
|
||||||
- CLI: move `clawdbot message` to subcommands (`message send|poll|…`), fold Discord/Slack/Telegram/WhatsApp tools into `message`, and require `--provider` unless only one provider is configured.
|
- CLI: move `clawdbot message` to subcommands (`message send|poll|…`), fold Discord/Slack/Telegram/WhatsApp tools into `message`, and require `--provider` unless only one provider is configured.
|
||||||
- CLI: improve `logs` output (pretty/plain/JSONL), add gateway unreachable hint, and document logging.
|
- CLI: improve `logs` output (pretty/plain/JSONL), add gateway unreachable hint, and document logging.
|
||||||
- Hooks: default hook agent delivery to true. (#533) — thanks @mcinteerj
|
|
||||||
- Hooks: normalize hook agent providers (aliases + msteams support).
|
|
||||||
- WhatsApp: route queued replies to the original sender instead of the bot's own number. (#534) — thanks @mcinteerj
|
- WhatsApp: route queued replies to the original sender instead of the bot's own number. (#534) — thanks @mcinteerj
|
||||||
- Models: add OAuth expiry checks in doctor, expanded `models status` auth output (missing auth + `--check` exit codes). (#538) — thanks @latitudeki5223
|
- Models: add OAuth expiry checks in doctor, expanded `models status` auth output (missing auth + `--check` exit codes). (#538) — thanks @latitudeki5223
|
||||||
- Deps: bump Pi to 0.40.0 and drop pi-ai patch (upstream 429 fix). (#543) — thanks @mcinteerj
|
- Deps: bump Pi to 0.40.0 and drop pi-ai patch (upstream 429 fix). (#543) — thanks @mcinteerj
|
||||||
- Agent: skip empty error assistant messages when rebuilding session context to avoid tool-chain corruption. (#561) — thanks @mukhtharcm
|
|
||||||
- Security: per-agent mention patterns and group elevated directives now require explicit mention to avoid cross-agent toggles.
|
- Security: per-agent mention patterns and group elevated directives now require explicit mention to avoid cross-agent toggles.
|
||||||
- Config: support inline env vars in config (`env.*` / `env.vars`) and document env precedence.
|
- Config: support inline env vars in config (`env.*` / `env.vars`) and document env precedence.
|
||||||
- Agent: enable adaptive context pruning by default for tool-result trimming.
|
- Agent: enable adaptive context pruning by default for tool-result trimming.
|
||||||
- Doctor: check config/state permissions and offer to tighten them. — thanks @steipete
|
- Doctor: check config/state permissions and offer to tighten them. — thanks @steipete
|
||||||
- Doctor/Daemon: audit supervisor configs, add --repair/--force flows, surface service config audits in daemon status, and document user vs system services. — thanks @steipete
|
- Doctor/Daemon: audit supervisor configs, add --repair/--force flows, surface service config audits in daemon status, and document user vs system services. — thanks @steipete
|
||||||
- Doctor: repair gateway service entrypoint when switching between npm and git installs; add Docker e2e coverage. — thanks @steipete
|
|
||||||
- Daemon: align generated systemd unit with docs for network-online + restart delay. (#479) — thanks @azade-c
|
- Daemon: align generated systemd unit with docs for network-online + restart delay. (#479) — thanks @azade-c
|
||||||
- Daemon: add KillMode=process to systemd units to avoid podman restart hangs. (#541) — thanks @ogulcancelik
|
- Daemon: add KillMode=process to systemd units to avoid podman restart hangs. (#541) — thanks @ogulcancelik
|
||||||
- Doctor: run legacy state migrations in non-interactive mode without prompts.
|
- Doctor: run legacy state migrations in non-interactive mode without prompts.
|
||||||
@@ -124,7 +115,6 @@
|
|||||||
- Gateway/CLI: allow dev profile (`clawdbot --dev`) to auto-create the dev config + workspace. — thanks @steipete
|
- Gateway/CLI: allow dev profile (`clawdbot --dev`) to auto-create the dev config + workspace. — thanks @steipete
|
||||||
- Dev templates: ship C-3PO dev workspace defaults as docs templates and use them for dev bootstrap. — thanks @steipete
|
- Dev templates: ship C-3PO dev workspace defaults as docs templates and use them for dev bootstrap. — thanks @steipete
|
||||||
- Config: fix Minimax hosted onboarding to write `agents.defaults` and allow `msteams` as a heartbeat target. — thanks @steipete
|
- Config: fix Minimax hosted onboarding to write `agents.defaults` and allow `msteams` as a heartbeat target. — thanks @steipete
|
||||||
- Discord: add channel/category management actions (create/edit/move/delete + category removal). (#487) - thanks @NicholasSpisak
|
|
||||||
|
|
||||||
## 2026.1.8
|
## 2026.1.8
|
||||||
|
|
||||||
|
|||||||
@@ -331,8 +331,7 @@ actor MacNodeBridgeSession {
|
|||||||
let now = self.clock.now
|
let now = self.clock.now
|
||||||
if now > last.advanced(by: timeout) {
|
if now > last.advanced(by: timeout) {
|
||||||
let age = last.duration(to: now)
|
let age = last.duration(to: now)
|
||||||
self.logger.warning(
|
self.logger.warning("Node bridge heartbeat timed out; disconnecting (age: \(String(describing: age), privacy: .public)).")
|
||||||
"Node bridge heartbeat timed out; disconnecting (age: \(String(describing: age), privacy: .public)).")
|
|
||||||
await self.disconnect()
|
await self.disconnect()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -342,8 +341,7 @@ actor MacNodeBridgeSession {
|
|||||||
do {
|
do {
|
||||||
try await self.send(BridgePing(type: "ping", id: id))
|
try await self.send(BridgePing(type: "ping", id: id))
|
||||||
} catch {
|
} catch {
|
||||||
self.logger.warning(
|
self.logger.warning("Node bridge ping send failed; disconnecting (error: \(String(describing: error), privacy: .public)).")
|
||||||
"Node bridge ping send failed; disconnecting (error: \(String(describing: error), privacy: .public)).")
|
|
||||||
await self.disconnect()
|
await self.disconnect()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -358,8 +356,7 @@ actor MacNodeBridgeSession {
|
|||||||
private func handleConnectionState(_ state: NWConnection.State) async {
|
private func handleConnectionState(_ state: NWConnection.State) async {
|
||||||
switch state {
|
switch state {
|
||||||
case let .failed(error):
|
case let .failed(error):
|
||||||
self.logger.warning(
|
self.logger.warning("Node bridge connection failed; disconnecting (error: \(String(describing: error), privacy: .public)).")
|
||||||
"Node bridge connection failed; disconnecting (error: \(String(describing: error), privacy: .public)).")
|
|
||||||
await self.disconnect()
|
await self.disconnect()
|
||||||
case .cancelled:
|
case .cancelled:
|
||||||
self.logger.warning("Node bridge connection cancelled; disconnecting.")
|
self.logger.warning("Node bridge connection cancelled; disconnecting.")
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ Payload:
|
|||||||
"name": "Email",
|
"name": "Email",
|
||||||
"sessionKey": "hook:email:msg-123",
|
"sessionKey": "hook:email:msg-123",
|
||||||
"wakeMode": "now",
|
"wakeMode": "now",
|
||||||
"deliver": true,
|
"deliver": false,
|
||||||
"provider": "last",
|
"provider": "last",
|
||||||
"to": "+15551234567",
|
"to": "+15551234567",
|
||||||
"model": "openai/gpt-5.2-mini",
|
"model": "openai/gpt-5.2-mini",
|
||||||
@@ -70,8 +70,8 @@ Payload:
|
|||||||
- `name` optional (string): Human-readable name for the hook (e.g., "GitHub"), used as a prefix in session summaries.
|
- `name` optional (string): Human-readable name for the hook (e.g., "GitHub"), used as a prefix in session summaries.
|
||||||
- `sessionKey` optional (string): The key used to identify the agent's session. Defaults to a random `hook:<uuid>`. Using a consistent key allows for a multi-turn conversation within the hook context.
|
- `sessionKey` optional (string): The key used to identify the agent's session. Defaults to a random `hook:<uuid>`. Using a consistent key allows for a multi-turn conversation within the hook context.
|
||||||
- `wakeMode` optional (`now` | `next-heartbeat`): Whether to trigger an immediate heartbeat (default `now`) or wait for the next periodic check.
|
- `wakeMode` optional (`now` | `next-heartbeat`): Whether to trigger an immediate heartbeat (default `now`) or wait for the next periodic check.
|
||||||
- `deliver` optional (boolean): If `true`, the agent's response will be sent to the messaging provider. Defaults to `true`. Responses that are only heartbeat acknowledgments are automatically skipped.
|
- `deliver` optional (boolean): If `true`, the agent's response will be sent to the messaging provider. Defaults to `false`. Responses that are only heartbeat acknowledgments are automatically skipped.
|
||||||
- `provider` optional (string): The messaging service for delivery. One of: `last`, `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`, `msteams`. Defaults to `last`.
|
- `provider` optional (string): The messaging service for delivery. One of: `last`, `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`. Defaults to `last`.
|
||||||
- `to` optional (string): The recipient identifier for the provider (e.g., phone number for WhatsApp/Signal, chat ID for Telegram, channel ID for Discord/Slack). Defaults to the last recipient in the main session.
|
- `to` optional (string): The recipient identifier for the provider (e.g., phone number for WhatsApp/Signal, chat ID for Telegram, channel ID for Discord/Slack). Defaults to the last recipient in the main session.
|
||||||
- `model` optional (string): Model override (e.g., `anthropic/claude-3-5-sonnet` or an alias). Must be in the allowed model list if restricted.
|
- `model` optional (string): Model override (e.g., `anthropic/claude-3-5-sonnet` or an alias). Must be in the allowed model list if restricted.
|
||||||
- `thinking` optional (string): Thinking level override (e.g., `low`, `medium`, `high`).
|
- `thinking` optional (string): Thinking level override (e.g., `low`, `medium`, `high`).
|
||||||
|
|||||||
+1
-5
@@ -177,11 +177,7 @@ Options:
|
|||||||
- `--workspace <dir>`
|
- `--workspace <dir>`
|
||||||
- `--non-interactive`
|
- `--non-interactive`
|
||||||
- `--mode <local|remote>`
|
- `--mode <local|remote>`
|
||||||
- `--auth-choice <setup-token|claude-cli|token|openai-codex|openai-api-key|codex-cli|antigravity|gemini-api-key|apiKey|minimax-cloud|minimax|skip>`
|
- `--auth-choice <oauth|claude-cli|token|openai-codex|openai-api-key|codex-cli|antigravity|gemini-api-key|apiKey|minimax-cloud|minimax|skip>`
|
||||||
- `--token-provider <id>` (non-interactive; used with `--auth-choice token`)
|
|
||||||
- `--token <token>` (non-interactive; used with `--auth-choice token`)
|
|
||||||
- `--token-profile-id <id>` (non-interactive; default: `<provider>:manual`)
|
|
||||||
- `--token-expires-in <duration>` (non-interactive; e.g. `365d`, `12h`)
|
|
||||||
- `--anthropic-api-key <key>`
|
- `--anthropic-api-key <key>`
|
||||||
- `--openai-api-key <key>`
|
- `--openai-api-key <key>`
|
||||||
- `--gemini-api-key <key>`
|
- `--gemini-api-key <key>`
|
||||||
|
|||||||
@@ -679,7 +679,6 @@
|
|||||||
"platforms/android",
|
"platforms/android",
|
||||||
"platforms/windows",
|
"platforms/windows",
|
||||||
"platforms/linux",
|
"platforms/linux",
|
||||||
"platforms/hetzner",
|
|
||||||
"platforms/exe-dev"
|
"platforms/exe-dev"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,19 +29,6 @@ clawdbot models status
|
|||||||
clawdbot doctor
|
clawdbot doctor
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternative: run the wrapper (also updates Clawdbot config):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
clawdbot models auth setup-token --provider anthropic
|
|
||||||
```
|
|
||||||
|
|
||||||
Manual token entry (any provider; writes `auth-profiles.json` + updates config):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
clawdbot models auth paste-token --provider anthropic
|
|
||||||
clawdbot models auth paste-token --provider openrouter
|
|
||||||
```
|
|
||||||
|
|
||||||
## Recommended: long‑lived Claude Code token
|
## Recommended: long‑lived Claude Code token
|
||||||
|
|
||||||
Run this on the **gateway host** (the machine running the Gateway):
|
Run this on the **gateway host** (the machine running the Gateway):
|
||||||
@@ -105,15 +92,13 @@ Use `--agent <id>` to target a specific agent; omit it to use the configured def
|
|||||||
2. **Clawdbot** syncs those into
|
2. **Clawdbot** syncs those into
|
||||||
`~/.clawdbot/agents/<agentId>/agent/auth-profiles.json` when the auth store is
|
`~/.clawdbot/agents/<agentId>/agent/auth-profiles.json` when the auth store is
|
||||||
loaded.
|
loaded.
|
||||||
3. Refreshable OAuth profiles can be refreshed automatically on use. Static
|
3. OAuth refresh happens automatically on use if a token is expired.
|
||||||
token profiles (including Claude CLI setup-token) are not refreshable by
|
|
||||||
Clawdbot.
|
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### “No credentials found”
|
### “No credentials found”
|
||||||
|
|
||||||
If the Anthropic token profile is missing, run `claude setup-token` on the
|
If the Anthropic OAuth profile is missing, run `claude setup-token` on the
|
||||||
**gateway host**, then re-check:
|
**gateway host**, then re-check:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -1464,67 +1464,6 @@ Notes:
|
|||||||
- Responses API enables clean reasoning/output separation; WhatsApp sees only final text.
|
- Responses API enables clean reasoning/output separation; WhatsApp sees only final text.
|
||||||
- Adjust `contextWindow`/`maxTokens` if your LM Studio context length differs.
|
- Adjust `contextWindow`/`maxTokens` if your LM Studio context length differs.
|
||||||
|
|
||||||
### MiniMax API (platform.minimax.io)
|
|
||||||
|
|
||||||
Use MiniMax's Anthropic-compatible API directly without LM Studio:
|
|
||||||
|
|
||||||
```json5
|
|
||||||
{
|
|
||||||
agent: {
|
|
||||||
model: { primary: "minimax/MiniMax-M2.1" },
|
|
||||||
models: {
|
|
||||||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
|
||||||
"minimax/MiniMax-M2.1": { alias: "Minimax" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
models: {
|
|
||||||
mode: "merge",
|
|
||||||
providers: {
|
|
||||||
minimax: {
|
|
||||||
baseUrl: "https://api.minimax.io/anthropic",
|
|
||||||
apiKey: "${MINIMAX_API_KEY}",
|
|
||||||
api: "anthropic-messages",
|
|
||||||
models: [
|
|
||||||
{
|
|
||||||
id: "MiniMax-M2.1",
|
|
||||||
name: "MiniMax M2.1",
|
|
||||||
reasoning: false,
|
|
||||||
input: ["text"],
|
|
||||||
// Pricing: MiniMax doesn't publish public rates. Override in models.json for accurate costs.
|
|
||||||
cost: { input: 15, output: 60, cacheRead: 2, cacheWrite: 10 },
|
|
||||||
contextWindow: 200000,
|
|
||||||
maxTokens: 8192
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "MiniMax-M2.1-lightning",
|
|
||||||
name: "MiniMax M2.1 Lightning",
|
|
||||||
reasoning: false,
|
|
||||||
input: ["text"],
|
|
||||||
cost: { input: 15, output: 60, cacheRead: 2, cacheWrite: 10 },
|
|
||||||
contextWindow: 200000,
|
|
||||||
maxTokens: 8192
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "MiniMax-M2",
|
|
||||||
name: "MiniMax M2",
|
|
||||||
reasoning: true,
|
|
||||||
input: ["text"],
|
|
||||||
cost: { input: 15, output: 60, cacheRead: 2, cacheWrite: 10 },
|
|
||||||
contextWindow: 200000,
|
|
||||||
maxTokens: 8192
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- Set `MINIMAX_API_KEY` environment variable or use `clawdbot onboard --auth-choice minimax-api`
|
|
||||||
- Available models: `MiniMax-M2.1` (default), `MiniMax-M2.1-lightning` (~100 tps), `MiniMax-M2` (reasoning)
|
|
||||||
- Pricing is a placeholder; MiniMax doesn't publish public rates. Override in `models.json` for accurate cost tracking.
|
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Supported APIs: `openai-completions`, `openai-responses`, `anthropic-messages`,
|
- Supported APIs: `openai-completions`, `openai-responses`, `anthropic-messages`,
|
||||||
`google-generative-ai`
|
`google-generative-ai`
|
||||||
|
|||||||
@@ -51,8 +51,6 @@ It writes config/workspace on the host:
|
|||||||
- `~/.clawdbot/`
|
- `~/.clawdbot/`
|
||||||
- `~/clawd`
|
- `~/clawd`
|
||||||
|
|
||||||
Running on a VPS? See [Hetzner (Docker VPS)](/platforms/hetzner).
|
|
||||||
|
|
||||||
### Manual flow (compose)
|
### Manual flow (compose)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -1,296 +0,0 @@
|
|||||||
---
|
|
||||||
summary: "Run Clawdbot Gateway on Hetzner (Docker + VPS) with durable state and baked-in binaries"
|
|
||||||
read_when:
|
|
||||||
- You want a production-grade, always-on Gateway on your own VPS
|
|
||||||
- You want full control over persistence, binaries, and restart behavior
|
|
||||||
- You are running Clawdbot in Docker on Hetzner or a similar provider
|
|
||||||
---
|
|
||||||
|
|
||||||
# Clawdbot on Hetzner (Docker, Production VPS Guide)
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
Run a persistent Clawdbot Gateway on a Hetzner VPS using Docker, with durable state, baked-in binaries, and safe restart behavior.
|
|
||||||
|
|
||||||
The Gateway can be accessed via:
|
|
||||||
- SSH port forwarding from your laptop
|
|
||||||
- Direct port exposure if you manage firewalling and tokens yourself
|
|
||||||
|
|
||||||
This guide assumes Ubuntu or Debian on Hetzner.
|
|
||||||
If you are on another Linux VPS, map packages accordingly.
|
|
||||||
For the generic Docker flow, see [Docker](/install/docker).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Quick path (experienced operators)
|
|
||||||
|
|
||||||
1) Provision Hetzner VPS
|
|
||||||
2) Install Docker
|
|
||||||
3) Clone Clawdbot repository
|
|
||||||
4) Create persistent host directories
|
|
||||||
5) Configure `.env` and `docker-compose.yml`
|
|
||||||
6) Bake required binaries into the image
|
|
||||||
7) `docker compose up -d`
|
|
||||||
8) Verify persistence and Gateway access
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What you need
|
|
||||||
|
|
||||||
- Hetzner VPS with root access
|
|
||||||
- SSH access from your laptop
|
|
||||||
- Docker and Docker Compose
|
|
||||||
- Model auth credentials
|
|
||||||
- Optional provider credentials
|
|
||||||
- WhatsApp QR
|
|
||||||
- Telegram bot token
|
|
||||||
- Gmail OAuth
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1) Provision the VPS
|
|
||||||
|
|
||||||
Create an Ubuntu or Debian VPS in Hetzner.
|
|
||||||
|
|
||||||
Connect as root:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ssh root@YOUR_VPS_IP
|
|
||||||
```
|
|
||||||
|
|
||||||
This guide assumes the VPS is stateful.
|
|
||||||
Do not treat it as disposable infrastructure.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2) Install Docker (on the VPS)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y git curl ca-certificates
|
|
||||||
curl -fsSL https://get.docker.com | sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker --version
|
|
||||||
docker compose version
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3) Clone the Clawdbot repository
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/clawdbot/clawdbot.git
|
|
||||||
cd clawdbot
|
|
||||||
```
|
|
||||||
|
|
||||||
This guide assumes you will build a custom image to guarantee binary persistence.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4) Create persistent host directories
|
|
||||||
|
|
||||||
Docker containers are ephemeral.
|
|
||||||
All long-lived state must live on the host.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p /root/.clawdbot
|
|
||||||
mkdir -p /root/clawd
|
|
||||||
|
|
||||||
# Set ownership to the container user (uid 1000):
|
|
||||||
chown -R 1000:1000 /root/.clawdbot
|
|
||||||
chown -R 1000:1000 /root/clawd
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5) Configure environment variables
|
|
||||||
|
|
||||||
Create `.env` in the repository root.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
CLAWDBOT_IMAGE=clawdbot:latest
|
|
||||||
CLAWDBOT_GATEWAY_TOKEN=change-me-now
|
|
||||||
CLAWDBOT_GATEWAY_BIND=lan
|
|
||||||
CLAWDBOT_GATEWAY_PORT=18789
|
|
||||||
CLAWDBOT_BRIDGE_PORT=18790
|
|
||||||
|
|
||||||
CLAWDBOT_CONFIG_DIR=/root/.clawdbot
|
|
||||||
CLAWDBOT_WORKSPACE_DIR=/root/clawd
|
|
||||||
|
|
||||||
GOG_KEYRING_PASSWORD=change-me-now
|
|
||||||
XDG_CONFIG_HOME=/home/node/.clawdbot
|
|
||||||
```
|
|
||||||
|
|
||||||
**Do not commit this file.**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6) Docker Compose configuration
|
|
||||||
|
|
||||||
Create or update `docker-compose.yml`.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
clawdbot-gateway:
|
|
||||||
image: ${CLAWDBOT_IMAGE}
|
|
||||||
build: .
|
|
||||||
restart: unless-stopped
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
- HOME=/home/node
|
|
||||||
- NODE_ENV=production
|
|
||||||
- TERM=xterm-256color
|
|
||||||
- CLAWDBOT_GATEWAY_BIND=${CLAWDBOT_GATEWAY_BIND}
|
|
||||||
- CLAWDBOT_GATEWAY_PORT=${CLAWDBOT_GATEWAY_PORT}
|
|
||||||
- CLAWDBOT_BRIDGE_PORT=${CLAWDBOT_BRIDGE_PORT}
|
|
||||||
- CLAWDBOT_GATEWAY_TOKEN=${CLAWDBOT_GATEWAY_TOKEN}
|
|
||||||
- GOG_KEYRING_PASSWORD=${GOG_KEYRING_PASSWORD}
|
|
||||||
- XDG_CONFIG_HOME=${XDG_CONFIG_HOME}
|
|
||||||
- PATH=/home/linuxbrew/.linuxbrew/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
||||||
volumes:
|
|
||||||
- ${CLAWDBOT_CONFIG_DIR}:/home/node/.clawdbot
|
|
||||||
- ${CLAWDBOT_WORKSPACE_DIR}:/home/node/clawd
|
|
||||||
ports:
|
|
||||||
- "${CLAWDBOT_GATEWAY_PORT}:18789"
|
|
||||||
- "${CLAWDBOT_BRIDGE_PORT}:18790"
|
|
||||||
- "18793:18793"
|
|
||||||
command:
|
|
||||||
[
|
|
||||||
"node",
|
|
||||||
"dist/index.js",
|
|
||||||
"gateway-daemon",
|
|
||||||
"--bind",
|
|
||||||
"${CLAWDBOT_GATEWAY_BIND}",
|
|
||||||
"--port",
|
|
||||||
"${CLAWDBOT_GATEWAY_PORT}"
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7) Bake required binaries into the image (critical)
|
|
||||||
|
|
||||||
Installing binaries inside a running container is a trap.
|
|
||||||
Anything installed at runtime will be lost on restart.
|
|
||||||
|
|
||||||
All external binaries required by skills must be installed at image build time.
|
|
||||||
|
|
||||||
The examples below show three common binaries only:
|
|
||||||
- `gog` for Gmail access
|
|
||||||
- `goplaces` for Google Places
|
|
||||||
- `wacli` for WhatsApp
|
|
||||||
|
|
||||||
These are examples, not a complete list.
|
|
||||||
You may install as many binaries as needed using the same pattern.
|
|
||||||
|
|
||||||
If you add new skills later that depend on additional binaries, you must:
|
|
||||||
1. Update the Dockerfile
|
|
||||||
2. Rebuild the image
|
|
||||||
3. Restart the containers
|
|
||||||
|
|
||||||
**Example Dockerfile**
|
|
||||||
|
|
||||||
```dockerfile
|
|
||||||
FROM node:22-bookworm
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y socat && rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Example binary 1: Gmail CLI
|
|
||||||
RUN curl -L https://github.com/steipete/gog/releases/latest/download/gog_Linux_x86_64.tar.gz \
|
|
||||||
| tar -xz -C /usr/local/bin && chmod +x /usr/local/bin/gog
|
|
||||||
|
|
||||||
# Example binary 2: Google Places CLI
|
|
||||||
RUN curl -L https://github.com/steipete/goplaces/releases/latest/download/goplaces_Linux_x86_64.tar.gz \
|
|
||||||
| tar -xz -C /usr/local/bin && chmod +x /usr/local/bin/goplaces
|
|
||||||
|
|
||||||
# Example binary 3: WhatsApp CLI
|
|
||||||
RUN curl -L https://github.com/steipete/wacli/releases/latest/download/wacli_Linux_x86_64.tar.gz \
|
|
||||||
| tar -xz -C /usr/local/bin && chmod +x /usr/local/bin/wacli
|
|
||||||
|
|
||||||
# Add more binaries below using the same pattern
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
COPY . .
|
|
||||||
RUN corepack enable
|
|
||||||
RUN pnpm install --frozen-lockfile
|
|
||||||
RUN pnpm build
|
|
||||||
RUN pnpm ui:install
|
|
||||||
RUN pnpm ui:build
|
|
||||||
|
|
||||||
CMD ["node","dist/index.js"]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8) Build and launch
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose build
|
|
||||||
docker compose up -d clawdbot-gateway
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify binaries:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose exec clawdbot-gateway which gog
|
|
||||||
docker compose exec clawdbot-gateway which goplaces
|
|
||||||
docker compose exec clawdbot-gateway which wacli
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected output:
|
|
||||||
|
|
||||||
```
|
|
||||||
/usr/local/bin/gog
|
|
||||||
/usr/local/bin/goplaces
|
|
||||||
/usr/local/bin/wacli
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9) Verify Gateway
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose logs -f clawdbot-gateway
|
|
||||||
```
|
|
||||||
|
|
||||||
Success:
|
|
||||||
|
|
||||||
```
|
|
||||||
[gateway] listening on ws://0.0.0.0:18789
|
|
||||||
```
|
|
||||||
|
|
||||||
From your laptop:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ssh -N -L 18789:127.0.0.1:18789 root@YOUR_VPS_IP
|
|
||||||
```
|
|
||||||
|
|
||||||
Open:
|
|
||||||
|
|
||||||
`http://127.0.0.1:18789/`
|
|
||||||
|
|
||||||
Paste your gateway token.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What persists where (source of truth)
|
|
||||||
|
|
||||||
Clawdbot runs in Docker, but Docker is not the source of truth.
|
|
||||||
All long-lived state must survive restarts, rebuilds, and reboots.
|
|
||||||
|
|
||||||
| Component | Location | Persistence mechanism | Notes |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Gateway config | `/home/node/.clawdbot/` | Host volume mount | Includes `clawdbot.json`, tokens |
|
|
||||||
| Model auth profiles | `/home/node/.clawdbot/` | Host volume mount | OAuth tokens, API keys |
|
|
||||||
| Skill configs | `/home/node/.clawdbot/skills/` | Host volume mount | Skill-level state |
|
|
||||||
| Agent workspace | `/home/node/clawd/` | Host volume mount | Code and agent artifacts |
|
|
||||||
| WhatsApp session | `/home/node/.clawdbot/` | Host volume mount | Preserves QR login |
|
|
||||||
| Gmail keyring | `/home/node/.clawdbot/` | Host volume + password | Requires `GOG_KEYRING_PASSWORD` |
|
|
||||||
| External binaries | `/usr/local/bin/` | Docker image | Must be baked at build time |
|
|
||||||
| Node runtime | Container filesystem | Docker image | Rebuilt every image build |
|
|
||||||
| OS packages | Container filesystem | Docker image | Do not install at runtime |
|
|
||||||
| Docker container | Ephemeral | Restartable | Safe to destroy |
|
|
||||||
@@ -20,11 +20,6 @@ Native companion apps for Windows are also planned; the Gateway is recommended v
|
|||||||
- Windows: [Windows](/platforms/windows)
|
- Windows: [Windows](/platforms/windows)
|
||||||
- Linux: [Linux](/platforms/linux)
|
- Linux: [Linux](/platforms/linux)
|
||||||
|
|
||||||
## VPS & hosting
|
|
||||||
|
|
||||||
- Hetzner (Docker): [Hetzner](/platforms/hetzner)
|
|
||||||
- exe.dev (VM + HTTPS proxy): [exe.dev](/platforms/exe-dev)
|
|
||||||
|
|
||||||
## Common links
|
## Common links
|
||||||
|
|
||||||
- Install guide: [Getting Started](/start/getting-started)
|
- Install guide: [Getting Started](/start/getting-started)
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ Directives (`/think`, `/verbose`, `/reasoning`, `/elevated`) are parsed even whe
|
|||||||
Text + native (when enabled):
|
Text + native (when enabled):
|
||||||
- `/help`
|
- `/help`
|
||||||
- `/commands`
|
- `/commands`
|
||||||
- `/status`
|
|
||||||
- `/status` (show current status; includes a short usage line when available)
|
- `/status` (show current status; includes a short usage line when available)
|
||||||
- `/usage` (alias: `/status`)
|
- `/usage` (alias: `/status`)
|
||||||
- `/debug show|set|unset|reset` (runtime overrides, owner-only)
|
- `/debug show|set|unset|reset` (runtime overrides, owner-only)
|
||||||
|
|||||||
+2
-6
@@ -80,7 +80,6 @@
|
|||||||
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
||||||
"test:live": "vitest run --config vitest.live.config.ts",
|
"test:live": "vitest run --config vitest.live.config.ts",
|
||||||
"test:docker:qr": "bash scripts/e2e/qr-import-docker.sh",
|
"test:docker:qr": "bash scripts/e2e/qr-import-docker.sh",
|
||||||
"test:docker:doctor-switch": "bash scripts/e2e/doctor-install-switch-docker.sh",
|
|
||||||
"protocol:gen": "tsx scripts/protocol-gen.ts",
|
"protocol:gen": "tsx scripts/protocol-gen.ts",
|
||||||
"protocol:gen:swift": "tsx scripts/protocol-gen-swift.ts",
|
"protocol:gen:swift": "tsx scripts/protocol-gen-swift.ts",
|
||||||
"protocol:check": "pnpm protocol:gen && pnpm protocol:gen:swift && git diff --exit-code -- dist/protocol.schema.json apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift",
|
"protocol:check": "pnpm protocol:gen && pnpm protocol:gen:swift && git diff --exit-code -- dist/protocol.schema.json apps/macos/Sources/ClawdbotProtocol/GatewayModels.swift",
|
||||||
@@ -94,7 +93,7 @@
|
|||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.23.0",
|
"packageManager": "pnpm@10.23.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@buape/carbon": "0.0.0-beta-20260109165250",
|
"@buape/carbon": "0.13.0",
|
||||||
"@clack/prompts": "^0.11.0",
|
"@clack/prompts": "^0.11.0",
|
||||||
"@grammyjs/runner": "^2.0.3",
|
"@grammyjs/runner": "^2.0.3",
|
||||||
"@grammyjs/transformer-throttler": "^1.2.1",
|
"@grammyjs/transformer-throttler": "^1.2.1",
|
||||||
@@ -170,9 +169,7 @@
|
|||||||
"@sinclair/typebox": "0.34.47"
|
"@sinclair/typebox": "0.34.47"
|
||||||
},
|
},
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"@mariozechner/pi-agent-core": "patches/@mariozechner__pi-agent-core.patch",
|
"@mariozechner/pi-agent-core": "patches/@mariozechner__pi-agent-core.patch"
|
||||||
"@mariozechner/pi-coding-agent": "patches/@mariozechner__pi-coding-agent.patch",
|
|
||||||
"@mariozechner/pi-ai": "patches/@mariozechner__pi-ai.patch"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vitest": {
|
"vitest": {
|
||||||
@@ -210,7 +207,6 @@
|
|||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"@mariozechner/pi-agent-core": "patches/@mariozechner__pi-agent-core.patch",
|
"@mariozechner/pi-agent-core": "patches/@mariozechner__pi-agent-core.patch",
|
||||||
"@mariozechner/pi-coding-agent": "patches/@mariozechner__pi-coding-agent.patch",
|
"@mariozechner/pi-coding-agent": "patches/@mariozechner__pi-coding-agent.patch",
|
||||||
"@mariozechner/pi-ai": "patches/@mariozechner__pi-ai.patch",
|
|
||||||
"qrcode-terminal": "patches/qrcode-terminal.patch",
|
"qrcode-terminal": "patches/qrcode-terminal.patch",
|
||||||
"playwright-core@1.57.0": "patches/playwright-core@1.57.0.patch"
|
"playwright-core@1.57.0": "patches/playwright-core@1.57.0.patch"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
diff --git a/dist/providers/google-gemini-cli.js b/dist/providers/google-gemini-cli.js
|
|
||||||
index 0000000..1111111 100644
|
|
||||||
--- a/dist/providers/google-gemini-cli.js
|
|
||||||
+++ b/dist/providers/google-gemini-cli.js
|
|
||||||
@@ -248,6 +248,12 @@ async function* streamGeminiCli(model, context, credentials, options) {
|
|
||||||
break; // Success, exit retry loop
|
|
||||||
}
|
|
||||||
const errorText = await response.text();
|
|
||||||
+ // PATCH: Fail immediately on 429 for Antigravity to let caller rotate accounts.
|
|
||||||
+ // Antigravity rate limits can have very long retry delays (10+ minutes).
|
|
||||||
+ if (isAntigravity && response.status === 429) {
|
|
||||||
+ console.log(`[pi-ai] 429 rate limit - failing fast to rotate account`);
|
|
||||||
+ throw new Error(`Cloud Code Assist API error (${response.status}): ${errorText}`);
|
|
||||||
+ }
|
|
||||||
// Check if retryable
|
|
||||||
if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) {
|
|
||||||
// Use server-provided delay or exponential backoff
|
|
||||||
@@ -26,23 +26,3 @@ index 0000000..1111111 100644
|
|||||||
}
|
}
|
||||||
/** Result from createAgentSession */
|
/** Result from createAgentSession */
|
||||||
export interface CreateAgentSessionResult {
|
export interface CreateAgentSessionResult {
|
||||||
diff --git a/dist/core/session-manager.js b/dist/core/session-manager.js
|
|
||||||
index b2aba5280d002253b0938b75aedbb9e6e6c4dcf8..67464efff535dbd7a8e6ed825aab2b305ca2aee2 100644
|
|
||||||
--- a/dist/core/session-manager.js
|
|
||||||
+++ b/dist/core/session-manager.js
|
|
||||||
@@ -161,6 +161,15 @@ export function buildSessionContext(entries, leafId, byId) {
|
|
||||||
const messages = [];
|
|
||||||
const appendMessage = (entry) => {
|
|
||||||
if (entry.type === "message") {
|
|
||||||
+ // PATCH: Filter out empty error assistant messages to prevent session corruption
|
|
||||||
+ // When 429/500 errors occur during tool execution, empty error messages get persisted
|
|
||||||
+ // to the session file. These break the tool_use -> tool_result chain for Claude/Gemini.
|
|
||||||
+ const msg = entry.message;
|
|
||||||
+ if (msg.role === "assistant" &&
|
|
||||||
+ msg.stopReason === "error" &&
|
|
||||||
+ (!msg.content || msg.content.length === 0)) {
|
|
||||||
+ return; // Skip empty error messages
|
|
||||||
+ }
|
|
||||||
messages.push(entry.message);
|
|
||||||
}
|
|
||||||
else if (entry.type === "custom_message") {
|
|
||||||
Generated
+61
-87
@@ -11,20 +11,14 @@ patchedDependencies:
|
|||||||
'@mariozechner/pi-agent-core':
|
'@mariozechner/pi-agent-core':
|
||||||
hash: 01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4
|
hash: 01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4
|
||||||
path: patches/@mariozechner__pi-agent-core.patch
|
path: patches/@mariozechner__pi-agent-core.patch
|
||||||
'@mariozechner/pi-ai':
|
|
||||||
hash: 3f4c1f943c57dbe2980bf21b1768dc780355f9124eeffbc30b5d5e42d2ea4b7c
|
|
||||||
path: patches/@mariozechner__pi-ai.patch
|
|
||||||
'@mariozechner/pi-coding-agent':
|
|
||||||
hash: 58af7c712ebe270527c2ad9d3351fac39d6cd4b81cc475a258d87840b446b90e
|
|
||||||
path: patches/@mariozechner__pi-coding-agent.patch
|
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@buape/carbon':
|
'@buape/carbon':
|
||||||
specifier: 0.0.0-beta-20260109165250
|
specifier: 0.13.0
|
||||||
version: 0.0.0-beta-20260109165250(hono@4.11.3)
|
version: 0.13.0(@types/react@19.2.7)(hono@4.11.3)
|
||||||
'@clack/prompts':
|
'@clack/prompts':
|
||||||
specifier: ^0.11.0
|
specifier: ^0.11.0
|
||||||
version: 0.11.0
|
version: 0.11.0
|
||||||
@@ -42,10 +36,10 @@ importers:
|
|||||||
version: 0.41.0(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)
|
version: 0.41.0(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)
|
||||||
'@mariozechner/pi-ai':
|
'@mariozechner/pi-ai':
|
||||||
specifier: ^0.41.0
|
specifier: ^0.41.0
|
||||||
version: 0.41.0(patch_hash=3f4c1f943c57dbe2980bf21b1768dc780355f9124eeffbc30b5d5e42d2ea4b7c)(ws@8.19.0)(zod@4.3.5)
|
version: 0.41.0(ws@8.19.0)(zod@4.3.5)
|
||||||
'@mariozechner/pi-coding-agent':
|
'@mariozechner/pi-coding-agent':
|
||||||
specifier: ^0.41.0
|
specifier: ^0.41.0
|
||||||
version: 0.41.0(patch_hash=58af7c712ebe270527c2ad9d3351fac39d6cd4b81cc475a258d87840b446b90e)(ws@8.19.0)(zod@4.3.5)
|
version: 0.41.0(ws@8.19.0)(zod@4.3.5)
|
||||||
'@mariozechner/pi-tui':
|
'@mariozechner/pi-tui':
|
||||||
specifier: ^0.41.0
|
specifier: ^0.41.0
|
||||||
version: 0.41.0
|
version: 0.41.0
|
||||||
@@ -372,8 +366,8 @@ packages:
|
|||||||
'@borewit/text-codec@0.2.1':
|
'@borewit/text-codec@0.2.1':
|
||||||
resolution: {integrity: sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==}
|
resolution: {integrity: sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==}
|
||||||
|
|
||||||
'@buape/carbon@0.0.0-beta-20260109165250':
|
'@buape/carbon@0.13.0':
|
||||||
resolution: {integrity: sha512-Xt8W0RYZxaprp2YxMU1rZRfGkHQWEJ2KZUurMwK/G4WVTfJSWAUeaeOGhpO40fCm/g0iPMuSLgQ4eudGOQHigw==}
|
resolution: {integrity: sha512-N52sGIJj832IezL+JmekC4gE7cCORj8r8mCJ1vsHOZiyr3O2pvsUA930E1j+rjStkd67TLxURPRMrpyqAFveIg==}
|
||||||
|
|
||||||
'@cacheable/memory@2.0.7':
|
'@cacheable/memory@2.0.7':
|
||||||
resolution: {integrity: sha512-RbxnxAMf89Tp1dLhXMS7ceft/PGsDl1Ip7T20z5nZ+pwIAsQ1p2izPjVG69oCLv/jfQ7HDPHTWK0c9rcAWXN3A==}
|
resolution: {integrity: sha512-RbxnxAMf89Tp1dLhXMS7ceft/PGsDl1Ip7T20z5nZ+pwIAsQ1p2izPjVG69oCLv/jfQ7HDPHTWK0c9rcAWXN3A==}
|
||||||
@@ -391,12 +385,8 @@ packages:
|
|||||||
'@clack/prompts@0.11.0':
|
'@clack/prompts@0.11.0':
|
||||||
resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==}
|
resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==}
|
||||||
|
|
||||||
'@cloudflare/workers-types@4.20251205.0':
|
'@cloudflare/workers-types@4.20250513.0':
|
||||||
resolution: {integrity: sha512-7pup7fYkuQW5XD8RUS/vkxF9SXlrGyCXuZ4ro3uVQvca/GTeSa+8bZ8T4wbq1Aea5lmLIGSlKbhl2msME7bRBA==}
|
resolution: {integrity: sha512-TXaQyWLqhxEmi/DHx+VSaHZ4DHF/uJCPVv/hRyC7M/eWBo/I7mBtAkUEsrhqcKKO9oCeeRUHUHoeRLh5Gd96Gg==}
|
||||||
|
|
||||||
'@discordjs/voice@0.19.0':
|
|
||||||
resolution: {integrity: sha512-UyX6rGEXzVyPzb1yvjHtPfTlnLvB5jX/stAMdiytHhfoydX+98hfympdOwsnTktzr+IRvphxTbdErgYDJkEsvw==}
|
|
||||||
engines: {node: '>=22.12.0'}
|
|
||||||
|
|
||||||
'@emnapi/core@1.8.1':
|
'@emnapi/core@1.8.1':
|
||||||
resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
|
resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
|
||||||
@@ -603,8 +593,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-qK6ZgGx0wwOubq/MY6eTbhApQHBUQCvCOsTYpQE01uLvfA2/Prm6egySHlZouKaina1RPuDwfLhCmsRCxwHj3Q==}
|
resolution: {integrity: sha512-qK6ZgGx0wwOubq/MY6eTbhApQHBUQCvCOsTYpQE01uLvfA2/Prm6egySHlZouKaina1RPuDwfLhCmsRCxwHj3Q==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@hono/node-server@1.19.6':
|
'@hono/node-server@1.18.2':
|
||||||
resolution: {integrity: sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw==}
|
resolution: {integrity: sha512-icgNvC0vRYivzyuSSaUv9ttcwtN8fDyd1k3AOIBDJgYd84tXRZSS6na8X54CY/oYoFTNhEmZraW/Rb9XYwX4KA==}
|
||||||
engines: {node: '>=18.14.1'}
|
engines: {node: '>=18.14.1'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
hono: ^4
|
hono: ^4
|
||||||
@@ -1290,8 +1280,8 @@ packages:
|
|||||||
'@types/body-parser@1.19.6':
|
'@types/body-parser@1.19.6':
|
||||||
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
||||||
|
|
||||||
'@types/bun@1.3.3':
|
'@types/bun@1.2.23':
|
||||||
resolution: {integrity: sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g==}
|
resolution: {integrity: sha512-le8ueOY5b6VKYf19xT3McVbXqLqmxzPXHsQT/q9JHgikJ2X22wyTW3g3ohz2ZMnp7dod6aduIiq8A14Xyimm0A==}
|
||||||
|
|
||||||
'@types/chai@5.2.3':
|
'@types/chai@5.2.3':
|
||||||
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
||||||
@@ -1347,8 +1337,8 @@ packages:
|
|||||||
'@types/node@10.17.60':
|
'@types/node@10.17.60':
|
||||||
resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==}
|
resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==}
|
||||||
|
|
||||||
'@types/node@24.10.4':
|
'@types/node@22.19.3':
|
||||||
resolution: {integrity: sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==}
|
resolution: {integrity: sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==}
|
||||||
|
|
||||||
'@types/node@25.0.3':
|
'@types/node@25.0.3':
|
||||||
resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==}
|
resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==}
|
||||||
@@ -1365,6 +1355,9 @@ packages:
|
|||||||
'@types/range-parser@1.2.7':
|
'@types/range-parser@1.2.7':
|
||||||
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
|
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
|
||||||
|
|
||||||
|
'@types/react@19.2.7':
|
||||||
|
resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==}
|
||||||
|
|
||||||
'@types/retry@0.12.0':
|
'@types/retry@0.12.0':
|
||||||
resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
|
resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
|
||||||
|
|
||||||
@@ -1618,8 +1611,10 @@ packages:
|
|||||||
buffer@6.0.3:
|
buffer@6.0.3:
|
||||||
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
||||||
|
|
||||||
bun-types@1.3.3:
|
bun-types@1.2.23:
|
||||||
resolution: {integrity: sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ==}
|
resolution: {integrity: sha512-R9f0hKAZXgFU3mlrA0YpE/fiDvwV0FT9rORApt2aQVWSuJDzZOyB5QLc0N/4HF57CS8IXJ6+L5E4W1bW6NS2Aw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': ^19
|
||||||
|
|
||||||
bytes@3.1.2:
|
bytes@3.1.2:
|
||||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||||
@@ -1731,6 +1726,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
csstype@3.2.3:
|
||||||
|
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||||
|
|
||||||
curve25519-js@0.0.4:
|
curve25519-js@0.0.4:
|
||||||
resolution: {integrity: sha512-axn2UMEnkhyDUPWOwVKBMVIzSQy2ejH2xRGy1wq81dqRwApXfIzfbE3hIX0ZRFBIihf/KDqK158DLwESu4AK1w==}
|
resolution: {integrity: sha512-axn2UMEnkhyDUPWOwVKBMVIzSQy2ejH2xRGy1wq81dqRwApXfIzfbE3hIX0ZRFBIihf/KDqK158DLwESu4AK1w==}
|
||||||
|
|
||||||
@@ -1770,8 +1768,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==}
|
resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==}
|
||||||
engines: {node: '>=0.3.1'}
|
engines: {node: '>=0.3.1'}
|
||||||
|
|
||||||
discord-api-types@0.38.36:
|
discord-api-types@0.38.29:
|
||||||
resolution: {integrity: sha512-qrbUbjjwtyeBg5HsAlm1C859epfOyiLjPqAOzkdWlCNsZCWJrertnETF/NwM8H+waMFU58xGSc5eXUfXah+WTQ==}
|
resolution: {integrity: sha512-+5BfrjLJN1hrrcK0MxDQli6NSv5lQH7Y3/qaOfk9+k7itex8RkA/UcevVMMLe8B4IKIawr4ITBTb2fBB2vDORg==}
|
||||||
|
|
||||||
discord-api-types@0.38.37:
|
discord-api-types@0.38.37:
|
||||||
resolution: {integrity: sha512-Cv47jzY1jkGkh5sv0bfHYqGgKOWO1peOrGMkDFM4UmaGMOTgOW8QSexhvixa9sVOiz8MnVOBryWYyw/CEVhj7w==}
|
resolution: {integrity: sha512-Cv47jzY1jkGkh5sv0bfHYqGgKOWO1peOrGMkDFM4UmaGMOTgOW8QSexhvixa9sVOiz8MnVOBryWYyw/CEVhj7w==}
|
||||||
@@ -2674,23 +2672,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
|
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
|
||||||
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
||||||
|
|
||||||
prism-media@1.3.5:
|
|
||||||
resolution: {integrity: sha512-IQdl0Q01m4LrkN1EGIE9lphov5Hy7WWlH6ulf5QdGePLlPas9p2mhgddTEHrlaXYjjFToM1/rWuwF37VF4taaA==}
|
|
||||||
peerDependencies:
|
|
||||||
'@discordjs/opus': '>=0.8.0 <1.0.0'
|
|
||||||
ffmpeg-static: ^5.0.2 || ^4.2.7 || ^3.0.0 || ^2.4.0
|
|
||||||
node-opus: ^0.3.3
|
|
||||||
opusscript: ^0.0.8
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@discordjs/opus':
|
|
||||||
optional: true
|
|
||||||
ffmpeg-static:
|
|
||||||
optional: true
|
|
||||||
node-opus:
|
|
||||||
optional: true
|
|
||||||
opusscript:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
process-nextick-args@2.0.1:
|
process-nextick-args@2.0.1:
|
||||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||||
|
|
||||||
@@ -3068,6 +3049,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==}
|
resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
undici-types@6.21.0:
|
||||||
|
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||||
|
|
||||||
undici-types@7.16.0:
|
undici-types@7.16.0:
|
||||||
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
||||||
|
|
||||||
@@ -3374,24 +3358,20 @@ snapshots:
|
|||||||
|
|
||||||
'@borewit/text-codec@0.2.1': {}
|
'@borewit/text-codec@0.2.1': {}
|
||||||
|
|
||||||
'@buape/carbon@0.0.0-beta-20260109165250(hono@4.11.3)':
|
'@buape/carbon@0.13.0(@types/react@19.2.7)(hono@4.11.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 24.10.4
|
'@types/node': 22.19.3
|
||||||
discord-api-types: 0.38.36
|
discord-api-types: 0.38.29
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@cloudflare/workers-types': 4.20251205.0
|
'@cloudflare/workers-types': 4.20250513.0
|
||||||
'@discordjs/voice': 0.19.0
|
'@hono/node-server': 1.18.2(hono@4.11.3)
|
||||||
'@hono/node-server': 1.19.6(hono@4.11.3)
|
'@types/bun': 1.2.23(@types/react@19.2.7)
|
||||||
'@types/bun': 1.3.3
|
|
||||||
'@types/ws': 8.18.1
|
'@types/ws': 8.18.1
|
||||||
ws: 8.18.3
|
ws: 8.18.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@discordjs/opus'
|
- '@types/react'
|
||||||
- bufferutil
|
- bufferutil
|
||||||
- ffmpeg-static
|
|
||||||
- hono
|
- hono
|
||||||
- node-opus
|
|
||||||
- opusscript
|
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
'@cacheable/memory@2.0.7':
|
'@cacheable/memory@2.0.7':
|
||||||
@@ -3423,23 +3403,7 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
sisteransi: 1.0.5
|
sisteransi: 1.0.5
|
||||||
|
|
||||||
'@cloudflare/workers-types@4.20251205.0':
|
'@cloudflare/workers-types@4.20250513.0':
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@discordjs/voice@0.19.0':
|
|
||||||
dependencies:
|
|
||||||
'@types/ws': 8.18.1
|
|
||||||
discord-api-types: 0.38.37
|
|
||||||
prism-media: 1.3.5
|
|
||||||
tslib: 2.8.1
|
|
||||||
ws: 8.19.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@discordjs/opus'
|
|
||||||
- bufferutil
|
|
||||||
- ffmpeg-static
|
|
||||||
- node-opus
|
|
||||||
- opusscript
|
|
||||||
- utf-8-validate
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@emnapi/core@1.8.1':
|
'@emnapi/core@1.8.1':
|
||||||
@@ -3577,7 +3541,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@hono/node-server@1.19.6(hono@4.11.3)':
|
'@hono/node-server@1.18.2(hono@4.11.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
hono: 4.11.3
|
hono: 4.11.3
|
||||||
optional: true
|
optional: true
|
||||||
@@ -3783,7 +3747,7 @@ snapshots:
|
|||||||
|
|
||||||
'@mariozechner/pi-agent-core@0.41.0(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)':
|
'@mariozechner/pi-agent-core@0.41.0(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@mariozechner/pi-ai': 0.41.0(patch_hash=3f4c1f943c57dbe2980bf21b1768dc780355f9124eeffbc30b5d5e42d2ea4b7c)(ws@8.19.0)(zod@4.3.5)
|
'@mariozechner/pi-ai': 0.41.0(ws@8.19.0)(zod@4.3.5)
|
||||||
'@mariozechner/pi-tui': 0.41.0
|
'@mariozechner/pi-tui': 0.41.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@modelcontextprotocol/sdk'
|
- '@modelcontextprotocol/sdk'
|
||||||
@@ -3793,7 +3757,7 @@ snapshots:
|
|||||||
- ws
|
- ws
|
||||||
- zod
|
- zod
|
||||||
|
|
||||||
'@mariozechner/pi-ai@0.41.0(patch_hash=3f4c1f943c57dbe2980bf21b1768dc780355f9124eeffbc30b5d5e42d2ea4b7c)(ws@8.19.0)(zod@4.3.5)':
|
'@mariozechner/pi-ai@0.41.0(ws@8.19.0)(zod@4.3.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@anthropic-ai/sdk': 0.71.2(zod@4.3.5)
|
'@anthropic-ai/sdk': 0.71.2(zod@4.3.5)
|
||||||
'@google/genai': 1.34.0
|
'@google/genai': 1.34.0
|
||||||
@@ -3813,11 +3777,11 @@ snapshots:
|
|||||||
- ws
|
- ws
|
||||||
- zod
|
- zod
|
||||||
|
|
||||||
'@mariozechner/pi-coding-agent@0.41.0(patch_hash=58af7c712ebe270527c2ad9d3351fac39d6cd4b81cc475a258d87840b446b90e)(ws@8.19.0)(zod@4.3.5)':
|
'@mariozechner/pi-coding-agent@0.41.0(ws@8.19.0)(zod@4.3.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@mariozechner/clipboard': 0.3.0
|
'@mariozechner/clipboard': 0.3.0
|
||||||
'@mariozechner/pi-agent-core': 0.41.0(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)
|
'@mariozechner/pi-agent-core': 0.41.0(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)
|
||||||
'@mariozechner/pi-ai': 0.41.0(patch_hash=3f4c1f943c57dbe2980bf21b1768dc780355f9124eeffbc30b5d5e42d2ea4b7c)(ws@8.19.0)(zod@4.3.5)
|
'@mariozechner/pi-ai': 0.41.0(ws@8.19.0)(zod@4.3.5)
|
||||||
'@mariozechner/pi-tui': 0.41.0
|
'@mariozechner/pi-tui': 0.41.0
|
||||||
chalk: 5.6.2
|
chalk: 5.6.2
|
||||||
cli-highlight: 2.1.11
|
cli-highlight: 2.1.11
|
||||||
@@ -4215,9 +4179,11 @@ snapshots:
|
|||||||
'@types/connect': 3.4.38
|
'@types/connect': 3.4.38
|
||||||
'@types/node': 25.0.3
|
'@types/node': 25.0.3
|
||||||
|
|
||||||
'@types/bun@1.3.3':
|
'@types/bun@1.2.23(@types/react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
bun-types: 1.3.3
|
bun-types: 1.2.23(@types/react@19.2.7)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/react'
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@types/chai@5.2.3':
|
'@types/chai@5.2.3':
|
||||||
@@ -4286,9 +4252,9 @@ snapshots:
|
|||||||
|
|
||||||
'@types/node@10.17.60': {}
|
'@types/node@10.17.60': {}
|
||||||
|
|
||||||
'@types/node@24.10.4':
|
'@types/node@22.19.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.16.0
|
undici-types: 6.21.0
|
||||||
|
|
||||||
'@types/node@25.0.3':
|
'@types/node@25.0.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -4304,6 +4270,11 @@ snapshots:
|
|||||||
|
|
||||||
'@types/range-parser@1.2.7': {}
|
'@types/range-parser@1.2.7': {}
|
||||||
|
|
||||||
|
'@types/react@19.2.7':
|
||||||
|
dependencies:
|
||||||
|
csstype: 3.2.3
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@types/retry@0.12.0': {}
|
'@types/retry@0.12.0': {}
|
||||||
|
|
||||||
'@types/retry@0.12.5': {}
|
'@types/retry@0.12.5': {}
|
||||||
@@ -4637,9 +4608,10 @@ snapshots:
|
|||||||
base64-js: 1.5.1
|
base64-js: 1.5.1
|
||||||
ieee754: 1.2.1
|
ieee754: 1.2.1
|
||||||
|
|
||||||
bun-types@1.3.3:
|
bun-types@1.2.23(@types/react@19.2.7):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.0.3
|
'@types/node': 25.0.3
|
||||||
|
'@types/react': 19.2.7
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
bytes@3.1.2: {}
|
bytes@3.1.2: {}
|
||||||
@@ -4757,6 +4729,9 @@ snapshots:
|
|||||||
shebang-command: 2.0.0
|
shebang-command: 2.0.0
|
||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
|
|
||||||
|
csstype@3.2.3:
|
||||||
|
optional: true
|
||||||
|
|
||||||
curve25519-js@0.0.4: {}
|
curve25519-js@0.0.4: {}
|
||||||
|
|
||||||
data-uri-to-buffer@4.0.1: {}
|
data-uri-to-buffer@4.0.1: {}
|
||||||
@@ -4778,7 +4753,7 @@ snapshots:
|
|||||||
|
|
||||||
diff@8.0.2: {}
|
diff@8.0.2: {}
|
||||||
|
|
||||||
discord-api-types@0.38.36: {}
|
discord-api-types@0.38.29: {}
|
||||||
|
|
||||||
discord-api-types@0.38.37: {}
|
discord-api-types@0.38.37: {}
|
||||||
|
|
||||||
@@ -5705,9 +5680,6 @@ snapshots:
|
|||||||
react-is: 17.0.2
|
react-is: 17.0.2
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
prism-media@1.3.5:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
process-nextick-args@2.0.1: {}
|
process-nextick-args@2.0.1: {}
|
||||||
|
|
||||||
process-warning@5.0.0: {}
|
process-warning@5.0.0: {}
|
||||||
@@ -6182,6 +6154,8 @@ snapshots:
|
|||||||
|
|
||||||
uint8array-extras@1.5.0: {}
|
uint8array-extras@1.5.0: {}
|
||||||
|
|
||||||
|
undici-types@6.21.0: {}
|
||||||
|
|
||||||
undici-types@7.16.0: {}
|
undici-types@7.16.0: {}
|
||||||
|
|
||||||
undici@7.18.2: {}
|
undici@7.18.2: {}
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
||||||
IMAGE_NAME="clawdbot-doctor-install-switch-e2e"
|
|
||||||
|
|
||||||
echo "Building Docker image..."
|
|
||||||
docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR"
|
|
||||||
|
|
||||||
echo "Running doctor install switch E2E..."
|
|
||||||
docker run --rm -t "$IMAGE_NAME" bash -lc '
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Stub systemd/loginctl so doctor + daemon flows work in Docker.
|
|
||||||
export PATH="/tmp/clawdbot-bin:$PATH"
|
|
||||||
mkdir -p /tmp/clawdbot-bin
|
|
||||||
|
|
||||||
cat > /tmp/clawdbot-bin/systemctl <<"SYSTEMCTL"
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
args=("$@")
|
|
||||||
if [[ "${args[0]:-}" == "--user" ]]; then
|
|
||||||
args=("${args[@]:1}")
|
|
||||||
fi
|
|
||||||
cmd="${args[0]:-}"
|
|
||||||
case "$cmd" in
|
|
||||||
status)
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
is-enabled)
|
|
||||||
unit="${args[1]:-}"
|
|
||||||
unit_path="$HOME/.config/systemd/user/${unit}"
|
|
||||||
if [ -f "$unit_path" ]; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
show)
|
|
||||||
echo "ActiveState=inactive"
|
|
||||||
echo "SubState=dead"
|
|
||||||
echo "MainPID=0"
|
|
||||||
echo "ExecMainStatus=0"
|
|
||||||
echo "ExecMainCode=0"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
SYSTEMCTL
|
|
||||||
chmod +x /tmp/clawdbot-bin/systemctl
|
|
||||||
|
|
||||||
cat > /tmp/clawdbot-bin/loginctl <<"LOGINCTL"
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
if [[ "$*" == *"show-user"* ]]; then
|
|
||||||
echo "Linger=yes"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
if [[ "$*" == *"enable-linger"* ]]; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
exit 0
|
|
||||||
LOGINCTL
|
|
||||||
chmod +x /tmp/clawdbot-bin/loginctl
|
|
||||||
|
|
||||||
# Install the npm-global variant from the local /app source.
|
|
||||||
npm install -g --prefix /tmp/npm-prefix /app
|
|
||||||
|
|
||||||
npm_bin="/tmp/npm-prefix/bin/clawdbot"
|
|
||||||
npm_entry="/tmp/npm-prefix/lib/node_modules/clawdbot/dist/entry.js"
|
|
||||||
git_entry="/app/dist/entry.js"
|
|
||||||
|
|
||||||
assert_entrypoint() {
|
|
||||||
local unit_path="$1"
|
|
||||||
local expected="$2"
|
|
||||||
local exec_line=""
|
|
||||||
exec_line=$(grep -m1 "^ExecStart=" "$unit_path" || true)
|
|
||||||
if [ -z "$exec_line" ]; then
|
|
||||||
echo "Missing ExecStart in $unit_path"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
exec_line="${exec_line#ExecStart=}"
|
|
||||||
entrypoint=$(echo "$exec_line" | awk "{print \$2}")
|
|
||||||
if [ "$entrypoint" != "$expected" ]; then
|
|
||||||
echo "Expected entrypoint $expected, got $entrypoint"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Each flow: install service with one variant, run doctor from the other,
|
|
||||||
# and verify ExecStart entrypoint switches accordingly.
|
|
||||||
run_flow() {
|
|
||||||
local name="$1"
|
|
||||||
local install_cmd="$2"
|
|
||||||
local install_expected="$3"
|
|
||||||
local doctor_cmd="$4"
|
|
||||||
local doctor_expected="$5"
|
|
||||||
|
|
||||||
echo "== Flow: $name =="
|
|
||||||
home_dir=$(mktemp -d "/tmp/clawdbot-switch-${name}.XXXXXX")
|
|
||||||
export HOME="$home_dir"
|
|
||||||
export USER="testuser"
|
|
||||||
|
|
||||||
eval "$install_cmd"
|
|
||||||
|
|
||||||
unit_path="$HOME/.config/systemd/user/clawdbot-gateway.service"
|
|
||||||
if [ ! -f "$unit_path" ]; then
|
|
||||||
echo "Missing unit file: $unit_path"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
assert_entrypoint "$unit_path" "$install_expected"
|
|
||||||
|
|
||||||
eval "$doctor_cmd"
|
|
||||||
|
|
||||||
assert_entrypoint "$unit_path" "$doctor_expected"
|
|
||||||
}
|
|
||||||
|
|
||||||
run_flow \
|
|
||||||
"npm-to-git" \
|
|
||||||
"$npm_bin daemon install --force" \
|
|
||||||
"$npm_entry" \
|
|
||||||
"node $git_entry doctor --repair" \
|
|
||||||
"$git_entry"
|
|
||||||
|
|
||||||
run_flow \
|
|
||||||
"git-to-npm" \
|
|
||||||
"node $git_entry daemon install --force" \
|
|
||||||
"$git_entry" \
|
|
||||||
"$npm_bin doctor --repair" \
|
|
||||||
"$npm_entry"
|
|
||||||
'
|
|
||||||
@@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest";
|
|||||||
|
|
||||||
const MINIMAX_KEY = process.env.MINIMAX_API_KEY ?? "";
|
const MINIMAX_KEY = process.env.MINIMAX_API_KEY ?? "";
|
||||||
const MINIMAX_BASE_URL =
|
const MINIMAX_BASE_URL =
|
||||||
process.env.MINIMAX_BASE_URL?.trim() || "https://api.minimax.io/anthropic";
|
process.env.MINIMAX_BASE_URL?.trim() || "https://api.minimax.io/v1";
|
||||||
const MINIMAX_MODEL = process.env.MINIMAX_MODEL?.trim() || "MiniMax-M2.1";
|
const MINIMAX_MODEL = process.env.MINIMAX_MODEL?.trim() || "MiniMax-M2.1";
|
||||||
const LIVE = process.env.MINIMAX_LIVE_TEST === "1" || process.env.LIVE === "1";
|
const LIVE = process.env.MINIMAX_LIVE_TEST === "1" || process.env.LIVE === "1";
|
||||||
|
|
||||||
@@ -11,16 +11,15 @@ const describeLive = LIVE && MINIMAX_KEY ? describe : describe.skip;
|
|||||||
|
|
||||||
describeLive("minimax live", () => {
|
describeLive("minimax live", () => {
|
||||||
it("returns assistant text", async () => {
|
it("returns assistant text", async () => {
|
||||||
const model: Model<"anthropic-messages"> = {
|
const model: Model<"openai-completions"> = {
|
||||||
id: MINIMAX_MODEL,
|
id: MINIMAX_MODEL,
|
||||||
name: `MiniMax ${MINIMAX_MODEL}`,
|
name: `MiniMax ${MINIMAX_MODEL}`,
|
||||||
api: "anthropic-messages",
|
api: "openai-completions",
|
||||||
provider: "minimax",
|
provider: "minimax",
|
||||||
baseUrl: MINIMAX_BASE_URL,
|
baseUrl: MINIMAX_BASE_URL,
|
||||||
reasoning: MINIMAX_MODEL === "MiniMax-M2",
|
reasoning: false,
|
||||||
input: ["text"],
|
input: ["text"],
|
||||||
// Pricing: placeholder values (per 1M tokens, multiplied by 1000 for display)
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||||
cost: { input: 15, output: 60, cacheRead: 2, cacheWrite: 10 },
|
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
maxTokens: 8192,
|
maxTokens: 8192,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|||||||
import type { DiscordActionConfig } from "../../config/config.js";
|
import type { DiscordActionConfig } from "../../config/config.js";
|
||||||
import {
|
import {
|
||||||
addRoleDiscord,
|
addRoleDiscord,
|
||||||
createChannelDiscord,
|
|
||||||
createScheduledEventDiscord,
|
createScheduledEventDiscord,
|
||||||
deleteChannelDiscord,
|
|
||||||
editChannelDiscord,
|
|
||||||
fetchChannelInfoDiscord,
|
fetchChannelInfoDiscord,
|
||||||
fetchMemberInfoDiscord,
|
fetchMemberInfoDiscord,
|
||||||
fetchRoleInfoDiscord,
|
fetchRoleInfoDiscord,
|
||||||
@@ -13,28 +10,17 @@ import {
|
|||||||
listGuildChannelsDiscord,
|
listGuildChannelsDiscord,
|
||||||
listGuildEmojisDiscord,
|
listGuildEmojisDiscord,
|
||||||
listScheduledEventsDiscord,
|
listScheduledEventsDiscord,
|
||||||
moveChannelDiscord,
|
|
||||||
removeChannelPermissionDiscord,
|
|
||||||
removeRoleDiscord,
|
removeRoleDiscord,
|
||||||
setChannelPermissionDiscord,
|
|
||||||
uploadEmojiDiscord,
|
uploadEmojiDiscord,
|
||||||
uploadStickerDiscord,
|
uploadStickerDiscord,
|
||||||
} from "../../discord/send.js";
|
} from "../../discord/send.js";
|
||||||
import {
|
import {
|
||||||
type ActionGate,
|
type ActionGate,
|
||||||
jsonResult,
|
jsonResult,
|
||||||
readNumberParam,
|
|
||||||
readStringArrayParam,
|
readStringArrayParam,
|
||||||
readStringParam,
|
readStringParam,
|
||||||
} from "./common.js";
|
} from "./common.js";
|
||||||
|
|
||||||
function readParentIdParam(
|
|
||||||
params: Record<string, unknown>,
|
|
||||||
): string | null | undefined {
|
|
||||||
if (params.parentId === null) return null;
|
|
||||||
return readStringParam(params, "parentId");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function handleDiscordGuildAction(
|
export async function handleDiscordGuildAction(
|
||||||
action: string,
|
action: string,
|
||||||
params: Record<string, unknown>,
|
params: Record<string, unknown>,
|
||||||
@@ -221,157 +207,6 @@ export async function handleDiscordGuildAction(
|
|||||||
const event = await createScheduledEventDiscord(guildId, payload);
|
const event = await createScheduledEventDiscord(guildId, payload);
|
||||||
return jsonResult({ ok: true, event });
|
return jsonResult({ ok: true, event });
|
||||||
}
|
}
|
||||||
case "channelCreate": {
|
|
||||||
if (!isActionEnabled("channels", false)) {
|
|
||||||
throw new Error("Discord channel management is disabled.");
|
|
||||||
}
|
|
||||||
const guildId = readStringParam(params, "guildId", { required: true });
|
|
||||||
const name = readStringParam(params, "name", { required: true });
|
|
||||||
const type = readNumberParam(params, "type", { integer: true });
|
|
||||||
const parentId = readParentIdParam(params);
|
|
||||||
const topic = readStringParam(params, "topic");
|
|
||||||
const position = readNumberParam(params, "position", { integer: true });
|
|
||||||
const nsfw = params.nsfw as boolean | undefined;
|
|
||||||
const channel = await createChannelDiscord({
|
|
||||||
guildId,
|
|
||||||
name,
|
|
||||||
type: type ?? undefined,
|
|
||||||
parentId: parentId ?? undefined,
|
|
||||||
topic: topic ?? undefined,
|
|
||||||
position: position ?? undefined,
|
|
||||||
nsfw,
|
|
||||||
});
|
|
||||||
return jsonResult({ ok: true, channel });
|
|
||||||
}
|
|
||||||
case "channelEdit": {
|
|
||||||
if (!isActionEnabled("channels", false)) {
|
|
||||||
throw new Error("Discord channel management is disabled.");
|
|
||||||
}
|
|
||||||
const channelId = readStringParam(params, "channelId", {
|
|
||||||
required: true,
|
|
||||||
});
|
|
||||||
const name = readStringParam(params, "name");
|
|
||||||
const topic = readStringParam(params, "topic");
|
|
||||||
const position = readNumberParam(params, "position", { integer: true });
|
|
||||||
const parentId = readParentIdParam(params);
|
|
||||||
const nsfw = params.nsfw as boolean | undefined;
|
|
||||||
const rateLimitPerUser = readNumberParam(params, "rateLimitPerUser", {
|
|
||||||
integer: true,
|
|
||||||
});
|
|
||||||
const channel = await editChannelDiscord({
|
|
||||||
channelId,
|
|
||||||
name: name ?? undefined,
|
|
||||||
topic: topic ?? undefined,
|
|
||||||
position: position ?? undefined,
|
|
||||||
parentId: parentId === undefined ? undefined : parentId,
|
|
||||||
nsfw,
|
|
||||||
rateLimitPerUser: rateLimitPerUser ?? undefined,
|
|
||||||
});
|
|
||||||
return jsonResult({ ok: true, channel });
|
|
||||||
}
|
|
||||||
case "channelDelete": {
|
|
||||||
if (!isActionEnabled("channels", false)) {
|
|
||||||
throw new Error("Discord channel management is disabled.");
|
|
||||||
}
|
|
||||||
const channelId = readStringParam(params, "channelId", {
|
|
||||||
required: true,
|
|
||||||
});
|
|
||||||
const result = await deleteChannelDiscord(channelId);
|
|
||||||
return jsonResult(result);
|
|
||||||
}
|
|
||||||
case "channelMove": {
|
|
||||||
if (!isActionEnabled("channels", false)) {
|
|
||||||
throw new Error("Discord channel management is disabled.");
|
|
||||||
}
|
|
||||||
const guildId = readStringParam(params, "guildId", { required: true });
|
|
||||||
const channelId = readStringParam(params, "channelId", {
|
|
||||||
required: true,
|
|
||||||
});
|
|
||||||
const parentId = readParentIdParam(params);
|
|
||||||
const position = readNumberParam(params, "position", { integer: true });
|
|
||||||
await moveChannelDiscord({
|
|
||||||
guildId,
|
|
||||||
channelId,
|
|
||||||
parentId: parentId === undefined ? undefined : parentId,
|
|
||||||
position: position ?? undefined,
|
|
||||||
});
|
|
||||||
return jsonResult({ ok: true });
|
|
||||||
}
|
|
||||||
case "categoryCreate": {
|
|
||||||
if (!isActionEnabled("channels", false)) {
|
|
||||||
throw new Error("Discord channel management is disabled.");
|
|
||||||
}
|
|
||||||
const guildId = readStringParam(params, "guildId", { required: true });
|
|
||||||
const name = readStringParam(params, "name", { required: true });
|
|
||||||
const position = readNumberParam(params, "position", { integer: true });
|
|
||||||
const channel = await createChannelDiscord({
|
|
||||||
guildId,
|
|
||||||
name,
|
|
||||||
type: 4,
|
|
||||||
position: position ?? undefined,
|
|
||||||
});
|
|
||||||
return jsonResult({ ok: true, category: channel });
|
|
||||||
}
|
|
||||||
case "categoryEdit": {
|
|
||||||
if (!isActionEnabled("channels", false)) {
|
|
||||||
throw new Error("Discord channel management is disabled.");
|
|
||||||
}
|
|
||||||
const categoryId = readStringParam(params, "categoryId", {
|
|
||||||
required: true,
|
|
||||||
});
|
|
||||||
const name = readStringParam(params, "name");
|
|
||||||
const position = readNumberParam(params, "position", { integer: true });
|
|
||||||
const channel = await editChannelDiscord({
|
|
||||||
channelId: categoryId,
|
|
||||||
name: name ?? undefined,
|
|
||||||
position: position ?? undefined,
|
|
||||||
});
|
|
||||||
return jsonResult({ ok: true, category: channel });
|
|
||||||
}
|
|
||||||
case "categoryDelete": {
|
|
||||||
if (!isActionEnabled("channels", false)) {
|
|
||||||
throw new Error("Discord channel management is disabled.");
|
|
||||||
}
|
|
||||||
const categoryId = readStringParam(params, "categoryId", {
|
|
||||||
required: true,
|
|
||||||
});
|
|
||||||
const result = await deleteChannelDiscord(categoryId);
|
|
||||||
return jsonResult(result);
|
|
||||||
}
|
|
||||||
case "channelPermissionSet": {
|
|
||||||
if (!isActionEnabled("channels", false)) {
|
|
||||||
throw new Error("Discord channel management is disabled.");
|
|
||||||
}
|
|
||||||
const channelId = readStringParam(params, "channelId", {
|
|
||||||
required: true,
|
|
||||||
});
|
|
||||||
const targetId = readStringParam(params, "targetId", { required: true });
|
|
||||||
const targetTypeRaw = readStringParam(params, "targetType", {
|
|
||||||
required: true,
|
|
||||||
});
|
|
||||||
const targetType = targetTypeRaw === "member" ? 1 : 0;
|
|
||||||
const allow = readStringParam(params, "allow");
|
|
||||||
const deny = readStringParam(params, "deny");
|
|
||||||
await setChannelPermissionDiscord({
|
|
||||||
channelId,
|
|
||||||
targetId,
|
|
||||||
targetType,
|
|
||||||
allow: allow ?? undefined,
|
|
||||||
deny: deny ?? undefined,
|
|
||||||
});
|
|
||||||
return jsonResult({ ok: true });
|
|
||||||
}
|
|
||||||
case "channelPermissionRemove": {
|
|
||||||
if (!isActionEnabled("channels", false)) {
|
|
||||||
throw new Error("Discord channel management is disabled.");
|
|
||||||
}
|
|
||||||
const channelId = readStringParam(params, "channelId", {
|
|
||||||
required: true,
|
|
||||||
});
|
|
||||||
const targetId = readStringParam(params, "targetId", { required: true });
|
|
||||||
await removeChannelPermissionDiscord(channelId, targetId);
|
|
||||||
return jsonResult({ ok: true });
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown action: ${action}`);
|
throw new Error(`Unknown action: ${action}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +1,38 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import type { DiscordActionConfig } from "../../config/config.js";
|
import type { DiscordActionConfig } from "../../config/config.js";
|
||||||
import { handleDiscordGuildAction } from "./discord-actions-guild.js";
|
|
||||||
import { handleDiscordMessagingAction } from "./discord-actions-messaging.js";
|
import { handleDiscordMessagingAction } from "./discord-actions-messaging.js";
|
||||||
|
|
||||||
const createChannelDiscord = vi.fn(async () => ({
|
|
||||||
id: "new-channel",
|
|
||||||
name: "test",
|
|
||||||
type: 0,
|
|
||||||
}));
|
|
||||||
const createThreadDiscord = vi.fn(async () => ({}));
|
const createThreadDiscord = vi.fn(async () => ({}));
|
||||||
const deleteChannelDiscord = vi.fn(async () => ({ ok: true, channelId: "C1" }));
|
|
||||||
const deleteMessageDiscord = vi.fn(async () => ({}));
|
const deleteMessageDiscord = vi.fn(async () => ({}));
|
||||||
const editChannelDiscord = vi.fn(async () => ({
|
|
||||||
id: "C1",
|
|
||||||
name: "edited",
|
|
||||||
}));
|
|
||||||
const editMessageDiscord = vi.fn(async () => ({}));
|
const editMessageDiscord = vi.fn(async () => ({}));
|
||||||
const fetchChannelPermissionsDiscord = vi.fn(async () => ({}));
|
const fetchChannelPermissionsDiscord = vi.fn(async () => ({}));
|
||||||
const fetchReactionsDiscord = vi.fn(async () => ({}));
|
const fetchReactionsDiscord = vi.fn(async () => ({}));
|
||||||
const listPinsDiscord = vi.fn(async () => ({}));
|
const listPinsDiscord = vi.fn(async () => ({}));
|
||||||
const listThreadsDiscord = vi.fn(async () => ({}));
|
const listThreadsDiscord = vi.fn(async () => ({}));
|
||||||
const moveChannelDiscord = vi.fn(async () => ({ ok: true }));
|
|
||||||
const pinMessageDiscord = vi.fn(async () => ({}));
|
const pinMessageDiscord = vi.fn(async () => ({}));
|
||||||
const reactMessageDiscord = vi.fn(async () => ({}));
|
const reactMessageDiscord = vi.fn(async () => ({}));
|
||||||
const readMessagesDiscord = vi.fn(async () => []);
|
const readMessagesDiscord = vi.fn(async () => []);
|
||||||
const removeChannelPermissionDiscord = vi.fn(async () => ({ ok: true }));
|
|
||||||
const removeOwnReactionsDiscord = vi.fn(async () => ({ removed: ["👍"] }));
|
const removeOwnReactionsDiscord = vi.fn(async () => ({ removed: ["👍"] }));
|
||||||
const removeReactionDiscord = vi.fn(async () => ({}));
|
const removeReactionDiscord = vi.fn(async () => ({}));
|
||||||
const searchMessagesDiscord = vi.fn(async () => ({}));
|
const searchMessagesDiscord = vi.fn(async () => ({}));
|
||||||
const sendMessageDiscord = vi.fn(async () => ({}));
|
const sendMessageDiscord = vi.fn(async () => ({}));
|
||||||
const sendPollDiscord = vi.fn(async () => ({}));
|
const sendPollDiscord = vi.fn(async () => ({}));
|
||||||
const sendStickerDiscord = vi.fn(async () => ({}));
|
const sendStickerDiscord = vi.fn(async () => ({}));
|
||||||
const setChannelPermissionDiscord = vi.fn(async () => ({ ok: true }));
|
|
||||||
const unpinMessageDiscord = vi.fn(async () => ({}));
|
const unpinMessageDiscord = vi.fn(async () => ({}));
|
||||||
|
|
||||||
vi.mock("../../discord/send.js", () => ({
|
vi.mock("../../discord/send.js", () => ({
|
||||||
createChannelDiscord: (...args: unknown[]) => createChannelDiscord(...args),
|
|
||||||
createThreadDiscord: (...args: unknown[]) => createThreadDiscord(...args),
|
createThreadDiscord: (...args: unknown[]) => createThreadDiscord(...args),
|
||||||
deleteChannelDiscord: (...args: unknown[]) => deleteChannelDiscord(...args),
|
|
||||||
deleteMessageDiscord: (...args: unknown[]) => deleteMessageDiscord(...args),
|
deleteMessageDiscord: (...args: unknown[]) => deleteMessageDiscord(...args),
|
||||||
editChannelDiscord: (...args: unknown[]) => editChannelDiscord(...args),
|
|
||||||
editMessageDiscord: (...args: unknown[]) => editMessageDiscord(...args),
|
editMessageDiscord: (...args: unknown[]) => editMessageDiscord(...args),
|
||||||
fetchChannelPermissionsDiscord: (...args: unknown[]) =>
|
fetchChannelPermissionsDiscord: (...args: unknown[]) =>
|
||||||
fetchChannelPermissionsDiscord(...args),
|
fetchChannelPermissionsDiscord(...args),
|
||||||
fetchReactionsDiscord: (...args: unknown[]) => fetchReactionsDiscord(...args),
|
fetchReactionsDiscord: (...args: unknown[]) => fetchReactionsDiscord(...args),
|
||||||
listPinsDiscord: (...args: unknown[]) => listPinsDiscord(...args),
|
listPinsDiscord: (...args: unknown[]) => listPinsDiscord(...args),
|
||||||
listThreadsDiscord: (...args: unknown[]) => listThreadsDiscord(...args),
|
listThreadsDiscord: (...args: unknown[]) => listThreadsDiscord(...args),
|
||||||
moveChannelDiscord: (...args: unknown[]) => moveChannelDiscord(...args),
|
|
||||||
pinMessageDiscord: (...args: unknown[]) => pinMessageDiscord(...args),
|
pinMessageDiscord: (...args: unknown[]) => pinMessageDiscord(...args),
|
||||||
reactMessageDiscord: (...args: unknown[]) => reactMessageDiscord(...args),
|
reactMessageDiscord: (...args: unknown[]) => reactMessageDiscord(...args),
|
||||||
readMessagesDiscord: (...args: unknown[]) => readMessagesDiscord(...args),
|
readMessagesDiscord: (...args: unknown[]) => readMessagesDiscord(...args),
|
||||||
removeChannelPermissionDiscord: (...args: unknown[]) =>
|
|
||||||
removeChannelPermissionDiscord(...args),
|
|
||||||
removeOwnReactionsDiscord: (...args: unknown[]) =>
|
removeOwnReactionsDiscord: (...args: unknown[]) =>
|
||||||
removeOwnReactionsDiscord(...args),
|
removeOwnReactionsDiscord(...args),
|
||||||
removeReactionDiscord: (...args: unknown[]) => removeReactionDiscord(...args),
|
removeReactionDiscord: (...args: unknown[]) => removeReactionDiscord(...args),
|
||||||
@@ -60,8 +40,6 @@ vi.mock("../../discord/send.js", () => ({
|
|||||||
sendMessageDiscord: (...args: unknown[]) => sendMessageDiscord(...args),
|
sendMessageDiscord: (...args: unknown[]) => sendMessageDiscord(...args),
|
||||||
sendPollDiscord: (...args: unknown[]) => sendPollDiscord(...args),
|
sendPollDiscord: (...args: unknown[]) => sendPollDiscord(...args),
|
||||||
sendStickerDiscord: (...args: unknown[]) => sendStickerDiscord(...args),
|
sendStickerDiscord: (...args: unknown[]) => sendStickerDiscord(...args),
|
||||||
setChannelPermissionDiscord: (...args: unknown[]) =>
|
|
||||||
setChannelPermissionDiscord(...args),
|
|
||||||
unpinMessageDiscord: (...args: unknown[]) => unpinMessageDiscord(...args),
|
unpinMessageDiscord: (...args: unknown[]) => unpinMessageDiscord(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -139,214 +117,3 @@ describe("handleDiscordMessagingAction", () => {
|
|||||||
).rejects.toThrow(/Discord reactions are disabled/);
|
).rejects.toThrow(/Discord reactions are disabled/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const channelsEnabled = (key: keyof DiscordActionConfig) => key === "channels";
|
|
||||||
const channelsDisabled = () => false;
|
|
||||||
|
|
||||||
describe("handleDiscordGuildAction - channel management", () => {
|
|
||||||
it("creates a channel", async () => {
|
|
||||||
const result = await handleDiscordGuildAction(
|
|
||||||
"channelCreate",
|
|
||||||
{
|
|
||||||
guildId: "G1",
|
|
||||||
name: "test-channel",
|
|
||||||
type: 0,
|
|
||||||
topic: "Test topic",
|
|
||||||
},
|
|
||||||
channelsEnabled,
|
|
||||||
);
|
|
||||||
expect(createChannelDiscord).toHaveBeenCalledWith({
|
|
||||||
guildId: "G1",
|
|
||||||
name: "test-channel",
|
|
||||||
type: 0,
|
|
||||||
parentId: undefined,
|
|
||||||
topic: "Test topic",
|
|
||||||
position: undefined,
|
|
||||||
nsfw: undefined,
|
|
||||||
});
|
|
||||||
expect(result.details).toMatchObject({ ok: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("respects channel gating for channelCreate", async () => {
|
|
||||||
await expect(
|
|
||||||
handleDiscordGuildAction(
|
|
||||||
"channelCreate",
|
|
||||||
{ guildId: "G1", name: "test" },
|
|
||||||
channelsDisabled,
|
|
||||||
),
|
|
||||||
).rejects.toThrow(/Discord channel management is disabled/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("edits a channel", async () => {
|
|
||||||
await handleDiscordGuildAction(
|
|
||||||
"channelEdit",
|
|
||||||
{
|
|
||||||
channelId: "C1",
|
|
||||||
name: "new-name",
|
|
||||||
topic: "new topic",
|
|
||||||
},
|
|
||||||
channelsEnabled,
|
|
||||||
);
|
|
||||||
expect(editChannelDiscord).toHaveBeenCalledWith({
|
|
||||||
channelId: "C1",
|
|
||||||
name: "new-name",
|
|
||||||
topic: "new topic",
|
|
||||||
position: undefined,
|
|
||||||
parentId: undefined,
|
|
||||||
nsfw: undefined,
|
|
||||||
rateLimitPerUser: undefined,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("clears the channel parent when parentId is null", async () => {
|
|
||||||
await handleDiscordGuildAction(
|
|
||||||
"channelEdit",
|
|
||||||
{
|
|
||||||
channelId: "C1",
|
|
||||||
parentId: null,
|
|
||||||
},
|
|
||||||
channelsEnabled,
|
|
||||||
);
|
|
||||||
expect(editChannelDiscord).toHaveBeenCalledWith({
|
|
||||||
channelId: "C1",
|
|
||||||
name: undefined,
|
|
||||||
topic: undefined,
|
|
||||||
position: undefined,
|
|
||||||
parentId: null,
|
|
||||||
nsfw: undefined,
|
|
||||||
rateLimitPerUser: undefined,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("deletes a channel", async () => {
|
|
||||||
await handleDiscordGuildAction(
|
|
||||||
"channelDelete",
|
|
||||||
{ channelId: "C1" },
|
|
||||||
channelsEnabled,
|
|
||||||
);
|
|
||||||
expect(deleteChannelDiscord).toHaveBeenCalledWith("C1");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("moves a channel", async () => {
|
|
||||||
await handleDiscordGuildAction(
|
|
||||||
"channelMove",
|
|
||||||
{
|
|
||||||
guildId: "G1",
|
|
||||||
channelId: "C1",
|
|
||||||
parentId: "P1",
|
|
||||||
position: 5,
|
|
||||||
},
|
|
||||||
channelsEnabled,
|
|
||||||
);
|
|
||||||
expect(moveChannelDiscord).toHaveBeenCalledWith({
|
|
||||||
guildId: "G1",
|
|
||||||
channelId: "C1",
|
|
||||||
parentId: "P1",
|
|
||||||
position: 5,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("clears the channel parent on move when parentId is null", async () => {
|
|
||||||
await handleDiscordGuildAction(
|
|
||||||
"channelMove",
|
|
||||||
{
|
|
||||||
guildId: "G1",
|
|
||||||
channelId: "C1",
|
|
||||||
parentId: null,
|
|
||||||
},
|
|
||||||
channelsEnabled,
|
|
||||||
);
|
|
||||||
expect(moveChannelDiscord).toHaveBeenCalledWith({
|
|
||||||
guildId: "G1",
|
|
||||||
channelId: "C1",
|
|
||||||
parentId: null,
|
|
||||||
position: undefined,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("creates a category with type=4", async () => {
|
|
||||||
await handleDiscordGuildAction(
|
|
||||||
"categoryCreate",
|
|
||||||
{ guildId: "G1", name: "My Category" },
|
|
||||||
channelsEnabled,
|
|
||||||
);
|
|
||||||
expect(createChannelDiscord).toHaveBeenCalledWith({
|
|
||||||
guildId: "G1",
|
|
||||||
name: "My Category",
|
|
||||||
type: 4,
|
|
||||||
position: undefined,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("edits a category", async () => {
|
|
||||||
await handleDiscordGuildAction(
|
|
||||||
"categoryEdit",
|
|
||||||
{ categoryId: "CAT1", name: "Renamed Category" },
|
|
||||||
channelsEnabled,
|
|
||||||
);
|
|
||||||
expect(editChannelDiscord).toHaveBeenCalledWith({
|
|
||||||
channelId: "CAT1",
|
|
||||||
name: "Renamed Category",
|
|
||||||
position: undefined,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("deletes a category", async () => {
|
|
||||||
await handleDiscordGuildAction(
|
|
||||||
"categoryDelete",
|
|
||||||
{ categoryId: "CAT1" },
|
|
||||||
channelsEnabled,
|
|
||||||
);
|
|
||||||
expect(deleteChannelDiscord).toHaveBeenCalledWith("CAT1");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sets channel permissions for role", async () => {
|
|
||||||
await handleDiscordGuildAction(
|
|
||||||
"channelPermissionSet",
|
|
||||||
{
|
|
||||||
channelId: "C1",
|
|
||||||
targetId: "R1",
|
|
||||||
targetType: "role",
|
|
||||||
allow: "1024",
|
|
||||||
deny: "2048",
|
|
||||||
},
|
|
||||||
channelsEnabled,
|
|
||||||
);
|
|
||||||
expect(setChannelPermissionDiscord).toHaveBeenCalledWith({
|
|
||||||
channelId: "C1",
|
|
||||||
targetId: "R1",
|
|
||||||
targetType: 0,
|
|
||||||
allow: "1024",
|
|
||||||
deny: "2048",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sets channel permissions for member", async () => {
|
|
||||||
await handleDiscordGuildAction(
|
|
||||||
"channelPermissionSet",
|
|
||||||
{
|
|
||||||
channelId: "C1",
|
|
||||||
targetId: "U1",
|
|
||||||
targetType: "member",
|
|
||||||
allow: "1024",
|
|
||||||
},
|
|
||||||
channelsEnabled,
|
|
||||||
);
|
|
||||||
expect(setChannelPermissionDiscord).toHaveBeenCalledWith({
|
|
||||||
channelId: "C1",
|
|
||||||
targetId: "U1",
|
|
||||||
targetType: 1,
|
|
||||||
allow: "1024",
|
|
||||||
deny: undefined,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("removes channel permissions", async () => {
|
|
||||||
await handleDiscordGuildAction(
|
|
||||||
"channelPermissionRemove",
|
|
||||||
{ channelId: "C1", targetId: "R1" },
|
|
||||||
channelsEnabled,
|
|
||||||
);
|
|
||||||
expect(removeChannelPermissionDiscord).toHaveBeenCalledWith("C1", "R1");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -37,15 +37,6 @@ const guildActions = new Set([
|
|||||||
"voiceStatus",
|
"voiceStatus",
|
||||||
"eventList",
|
"eventList",
|
||||||
"eventCreate",
|
"eventCreate",
|
||||||
"channelCreate",
|
|
||||||
"channelEdit",
|
|
||||||
"channelDelete",
|
|
||||||
"channelMove",
|
|
||||||
"categoryCreate",
|
|
||||||
"categoryEdit",
|
|
||||||
"categoryDelete",
|
|
||||||
"channelPermissionSet",
|
|
||||||
"channelPermissionRemove",
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const moderationActions = new Set(["timeout", "kick", "ban"]);
|
const moderationActions = new Set(["timeout", "kick", "ban"]);
|
||||||
|
|||||||
@@ -202,67 +202,4 @@ export const DiscordToolSchema = Type.Union([
|
|||||||
reason: Type.Optional(Type.String()),
|
reason: Type.Optional(Type.String()),
|
||||||
deleteMessageDays: Type.Optional(Type.Number()),
|
deleteMessageDays: Type.Optional(Type.Number()),
|
||||||
}),
|
}),
|
||||||
// Channel management actions
|
|
||||||
Type.Object({
|
|
||||||
action: Type.Literal("channelCreate"),
|
|
||||||
guildId: Type.String(),
|
|
||||||
name: Type.String(),
|
|
||||||
type: Type.Optional(Type.Number()),
|
|
||||||
parentId: Type.Optional(Type.String()),
|
|
||||||
topic: Type.Optional(Type.String()),
|
|
||||||
position: Type.Optional(Type.Number()),
|
|
||||||
nsfw: Type.Optional(Type.Boolean()),
|
|
||||||
}),
|
|
||||||
Type.Object({
|
|
||||||
action: Type.Literal("channelEdit"),
|
|
||||||
channelId: Type.String(),
|
|
||||||
name: Type.Optional(Type.String()),
|
|
||||||
topic: Type.Optional(Type.String()),
|
|
||||||
position: Type.Optional(Type.Number()),
|
|
||||||
parentId: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
||||||
nsfw: Type.Optional(Type.Boolean()),
|
|
||||||
rateLimitPerUser: Type.Optional(Type.Number()),
|
|
||||||
}),
|
|
||||||
Type.Object({
|
|
||||||
action: Type.Literal("channelDelete"),
|
|
||||||
channelId: Type.String(),
|
|
||||||
}),
|
|
||||||
Type.Object({
|
|
||||||
action: Type.Literal("channelMove"),
|
|
||||||
guildId: Type.String(),
|
|
||||||
channelId: Type.String(),
|
|
||||||
parentId: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
||||||
position: Type.Optional(Type.Number()),
|
|
||||||
}),
|
|
||||||
// Category management actions (convenience aliases)
|
|
||||||
Type.Object({
|
|
||||||
action: Type.Literal("categoryCreate"),
|
|
||||||
guildId: Type.String(),
|
|
||||||
name: Type.String(),
|
|
||||||
position: Type.Optional(Type.Number()),
|
|
||||||
}),
|
|
||||||
Type.Object({
|
|
||||||
action: Type.Literal("categoryEdit"),
|
|
||||||
categoryId: Type.String(),
|
|
||||||
name: Type.Optional(Type.String()),
|
|
||||||
position: Type.Optional(Type.Number()),
|
|
||||||
}),
|
|
||||||
Type.Object({
|
|
||||||
action: Type.Literal("categoryDelete"),
|
|
||||||
categoryId: Type.String(),
|
|
||||||
}),
|
|
||||||
// Permission overwrite actions
|
|
||||||
Type.Object({
|
|
||||||
action: Type.Literal("channelPermissionSet"),
|
|
||||||
channelId: Type.String(),
|
|
||||||
targetId: Type.String(),
|
|
||||||
targetType: Type.Union([Type.Literal("role"), Type.Literal("member")]),
|
|
||||||
allow: Type.Optional(Type.String()),
|
|
||||||
deny: Type.Optional(Type.String()),
|
|
||||||
}),
|
|
||||||
Type.Object({
|
|
||||||
action: Type.Literal("channelPermissionRemove"),
|
|
||||||
channelId: Type.String(),
|
|
||||||
targetId: Type.String(),
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
@@ -43,11 +43,13 @@ describe("control command parsing", () => {
|
|||||||
expect(hasControlCommand("/commands")).toBe(true);
|
expect(hasControlCommand("/commands")).toBe(true);
|
||||||
expect(hasControlCommand("/commands:")).toBe(true);
|
expect(hasControlCommand("/commands:")).toBe(true);
|
||||||
expect(hasControlCommand("commands")).toBe(false);
|
expect(hasControlCommand("commands")).toBe(false);
|
||||||
|
expect(hasControlCommand("/status")).toBe(true);
|
||||||
|
expect(hasControlCommand("/status:")).toBe(true);
|
||||||
|
expect(hasControlCommand("status")).toBe(false);
|
||||||
|
expect(hasControlCommand("usage")).toBe(false);
|
||||||
expect(hasControlCommand("/compact")).toBe(true);
|
expect(hasControlCommand("/compact")).toBe(true);
|
||||||
expect(hasControlCommand("/compact:")).toBe(true);
|
expect(hasControlCommand("/compact:")).toBe(true);
|
||||||
expect(hasControlCommand("compact")).toBe(false);
|
expect(hasControlCommand("compact")).toBe(false);
|
||||||
expect(hasControlCommand("status")).toBe(false);
|
|
||||||
expect(hasControlCommand("usage")).toBe(false);
|
|
||||||
|
|
||||||
for (const command of listChatCommands()) {
|
for (const command of listChatCommands()) {
|
||||||
for (const alias of command.textAliases) {
|
for (const alias of command.textAliases) {
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ describe("commands registry", () => {
|
|||||||
const specs = listNativeCommandSpecs();
|
const specs = listNativeCommandSpecs();
|
||||||
expect(specs.find((spec) => spec.name === "help")).toBeTruthy();
|
expect(specs.find((spec) => spec.name === "help")).toBeTruthy();
|
||||||
expect(specs.find((spec) => spec.name === "stop")).toBeTruthy();
|
expect(specs.find((spec) => spec.name === "stop")).toBeTruthy();
|
||||||
expect(specs.find((spec) => spec.name === "compact")).toBeFalsy();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("detects known text commands", () => {
|
it("detects known text commands", () => {
|
||||||
const detection = getCommandDetection();
|
const detection = getCommandDetection();
|
||||||
|
expect(detection.exact.has("/help")).toBe(true);
|
||||||
expect(detection.exact.has("/commands")).toBe(true);
|
expect(detection.exact.has("/commands")).toBe(true);
|
||||||
expect(detection.exact.has("/compact")).toBe(true);
|
expect(detection.exact.has("/compact")).toBe(true);
|
||||||
for (const command of listChatCommands()) {
|
for (const command of listChatCommands()) {
|
||||||
|
|||||||
@@ -17,11 +17,6 @@ export type NativeCommandSpec = {
|
|||||||
acceptsArgs: boolean;
|
acceptsArgs: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TextAliasSpec = {
|
|
||||||
canonical: string;
|
|
||||||
acceptsArgs: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
function defineChatCommand(command: {
|
function defineChatCommand(command: {
|
||||||
key: string;
|
key: string;
|
||||||
nativeName?: string;
|
nativeName?: string;
|
||||||
@@ -31,11 +26,8 @@ function defineChatCommand(command: {
|
|||||||
textAliases?: string[];
|
textAliases?: string[];
|
||||||
scope?: CommandScope;
|
scope?: CommandScope;
|
||||||
}): ChatCommandDefinition {
|
}): ChatCommandDefinition {
|
||||||
const aliases = (
|
const aliases =
|
||||||
command.textAliases ?? (command.textAlias ? [command.textAlias] : [])
|
command.textAliases ?? (command.textAlias ? [command.textAlias] : []);
|
||||||
)
|
|
||||||
.map((alias) => alias.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
const scope =
|
const scope =
|
||||||
command.scope ??
|
command.scope ??
|
||||||
(command.nativeName ? (aliases.length ? "both" : "native") : "text");
|
(command.nativeName ? (aliases.length ? "both" : "native") : "text");
|
||||||
@@ -58,64 +50,16 @@ function registerAlias(
|
|||||||
if (!command) {
|
if (!command) {
|
||||||
throw new Error(`registerAlias: unknown command key: ${key}`);
|
throw new Error(`registerAlias: unknown command key: ${key}`);
|
||||||
}
|
}
|
||||||
const existing = new Set(
|
const existing = new Set(command.textAliases.map((alias) => alias.trim()));
|
||||||
command.textAliases.map((alias) => alias.trim().toLowerCase()),
|
|
||||||
);
|
|
||||||
for (const alias of aliases) {
|
for (const alias of aliases) {
|
||||||
const trimmed = alias.trim();
|
const trimmed = alias.trim();
|
||||||
if (!trimmed) continue;
|
if (!trimmed) continue;
|
||||||
const lowered = trimmed.toLowerCase();
|
if (existing.has(trimmed)) continue;
|
||||||
if (existing.has(lowered)) continue;
|
existing.add(trimmed);
|
||||||
existing.add(lowered);
|
|
||||||
command.textAliases.push(trimmed);
|
command.textAliases.push(trimmed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertCommandRegistry(commands: ChatCommandDefinition[]): void {
|
|
||||||
const keys = new Set<string>();
|
|
||||||
const nativeNames = new Set<string>();
|
|
||||||
const textAliases = new Set<string>();
|
|
||||||
for (const command of commands) {
|
|
||||||
if (keys.has(command.key)) {
|
|
||||||
throw new Error(`Duplicate command key: ${command.key}`);
|
|
||||||
}
|
|
||||||
keys.add(command.key);
|
|
||||||
|
|
||||||
const nativeName = command.nativeName?.trim();
|
|
||||||
if (command.scope === "text") {
|
|
||||||
if (nativeName) {
|
|
||||||
throw new Error(`Text-only command has native name: ${command.key}`);
|
|
||||||
}
|
|
||||||
if (command.textAliases.length === 0) {
|
|
||||||
throw new Error(`Text-only command missing text alias: ${command.key}`);
|
|
||||||
}
|
|
||||||
} else if (!nativeName) {
|
|
||||||
throw new Error(`Native command missing native name: ${command.key}`);
|
|
||||||
} else {
|
|
||||||
const nativeKey = nativeName.toLowerCase();
|
|
||||||
if (nativeNames.has(nativeKey)) {
|
|
||||||
throw new Error(`Duplicate native command: ${nativeName}`);
|
|
||||||
}
|
|
||||||
nativeNames.add(nativeKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command.scope === "native" && command.textAliases.length > 0) {
|
|
||||||
throw new Error(`Native-only command has text aliases: ${command.key}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const alias of command.textAliases) {
|
|
||||||
if (!alias.startsWith("/")) {
|
|
||||||
throw new Error(`Command alias missing leading '/': ${alias}`);
|
|
||||||
}
|
|
||||||
const aliasKey = alias.toLowerCase();
|
|
||||||
if (textAliases.has(aliasKey)) {
|
|
||||||
throw new Error(`Duplicate command alias: ${alias}`);
|
|
||||||
}
|
|
||||||
textAliases.add(aliasKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CHAT_COMMANDS: ChatCommandDefinition[] = (() => {
|
export const CHAT_COMMANDS: ChatCommandDefinition[] = (() => {
|
||||||
const commands: ChatCommandDefinition[] = [
|
const commands: ChatCommandDefinition[] = [
|
||||||
defineChatCommand({
|
defineChatCommand({
|
||||||
@@ -246,28 +190,11 @@ export const CHAT_COMMANDS: ChatCommandDefinition[] = (() => {
|
|||||||
registerAlias(commands, "elevated", "/elev");
|
registerAlias(commands, "elevated", "/elev");
|
||||||
registerAlias(commands, "model", "/models");
|
registerAlias(commands, "model", "/models");
|
||||||
|
|
||||||
assertCommandRegistry(commands);
|
|
||||||
return commands;
|
return commands;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const NATIVE_COMMAND_SURFACES = new Set(["discord", "slack", "telegram"]);
|
const NATIVE_COMMAND_SURFACES = new Set(["discord", "slack", "telegram"]);
|
||||||
|
|
||||||
const TEXT_ALIAS_MAP: Map<string, TextAliasSpec> = (() => {
|
|
||||||
const map = new Map<string, TextAliasSpec>();
|
|
||||||
for (const command of CHAT_COMMANDS) {
|
|
||||||
const canonical = `/${command.key}`;
|
|
||||||
const acceptsArgs = Boolean(command.acceptsArgs);
|
|
||||||
for (const alias of command.textAliases) {
|
|
||||||
const normalized = alias.trim().toLowerCase();
|
|
||||||
if (!normalized) continue;
|
|
||||||
if (!map.has(normalized)) {
|
|
||||||
map.set(normalized, { canonical, acceptsArgs });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
})();
|
|
||||||
|
|
||||||
let cachedDetection:
|
let cachedDetection:
|
||||||
| {
|
| {
|
||||||
exact: Set<string>;
|
exact: Set<string>;
|
||||||
@@ -299,8 +226,8 @@ export function findCommandByNativeName(
|
|||||||
const normalized = name.trim().toLowerCase();
|
const normalized = name.trim().toLowerCase();
|
||||||
return CHAT_COMMANDS.find(
|
return CHAT_COMMANDS.find(
|
||||||
(command) =>
|
(command) =>
|
||||||
command.scope !== "text" &&
|
command.nativeName?.toLowerCase() === normalized &&
|
||||||
command.nativeName?.toLowerCase() === normalized,
|
command.scope !== "text",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,31 +239,11 @@ export function buildCommandText(commandName: string, args?: string): string {
|
|||||||
export function normalizeCommandBody(raw: string): string {
|
export function normalizeCommandBody(raw: string): string {
|
||||||
const trimmed = raw.trim();
|
const trimmed = raw.trim();
|
||||||
if (!trimmed.startsWith("/")) return trimmed;
|
if (!trimmed.startsWith("/")) return trimmed;
|
||||||
|
const match = trimmed.match(/^\/([^\s:]+)\s*:(.*)$/);
|
||||||
const colonMatch = trimmed.match(/^\/([^\s:]+)\s*:(.*)$/);
|
if (!match) return trimmed;
|
||||||
const normalized = colonMatch
|
const [, command, rest] = match;
|
||||||
? (() => {
|
const normalizedRest = rest.trimStart();
|
||||||
const [, command, rest] = colonMatch;
|
return normalizedRest ? `/${command} ${normalizedRest}` : `/${command}`;
|
||||||
const normalizedRest = rest.trimStart();
|
|
||||||
return normalizedRest ? `/${command} ${normalizedRest}` : `/${command}`;
|
|
||||||
})()
|
|
||||||
: trimmed;
|
|
||||||
|
|
||||||
const lowered = normalized.toLowerCase();
|
|
||||||
const exact = TEXT_ALIAS_MAP.get(lowered);
|
|
||||||
if (exact) return exact.canonical;
|
|
||||||
|
|
||||||
const tokenMatch = normalized.match(/^\/([^\s]+)(?:\s+([\s\S]+))?$/);
|
|
||||||
if (!tokenMatch) return normalized;
|
|
||||||
const [, token, rest] = tokenMatch;
|
|
||||||
const tokenKey = `/${token.toLowerCase()}`;
|
|
||||||
const tokenSpec = TEXT_ALIAS_MAP.get(tokenKey);
|
|
||||||
if (!tokenSpec) return normalized;
|
|
||||||
if (rest && !tokenSpec.acceptsArgs) return normalized;
|
|
||||||
const normalizedRest = rest?.trimStart();
|
|
||||||
return normalizedRest
|
|
||||||
? `${tokenSpec.canonical} ${normalizedRest}`
|
|
||||||
: tokenSpec.canonical;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCommandDetection(): { exact: Set<string>; regex: RegExp } {
|
export function getCommandDetection(): { exact: Set<string>; regex: RegExp } {
|
||||||
|
|||||||
+1
-22
@@ -240,23 +240,7 @@ export function buildProgram() {
|
|||||||
.option("--mode <mode>", "Wizard mode: local|remote")
|
.option("--mode <mode>", "Wizard mode: local|remote")
|
||||||
.option(
|
.option(
|
||||||
"--auth-choice <choice>",
|
"--auth-choice <choice>",
|
||||||
"Auth: setup-token|claude-cli|token|openai-codex|openai-api-key|codex-cli|antigravity|gemini-api-key|apiKey|minimax-cloud|minimax|skip",
|
"Auth: oauth|claude-cli|token|openai-codex|openai-api-key|codex-cli|antigravity|gemini-api-key|apiKey|minimax-cloud|minimax|skip",
|
||||||
)
|
|
||||||
.option(
|
|
||||||
"--token-provider <id>",
|
|
||||||
"Token provider id (non-interactive; used with --auth-choice token)",
|
|
||||||
)
|
|
||||||
.option(
|
|
||||||
"--token <token>",
|
|
||||||
"Token value (non-interactive; used with --auth-choice token)",
|
|
||||||
)
|
|
||||||
.option(
|
|
||||||
"--token-profile-id <id>",
|
|
||||||
"Auth profile id (non-interactive; default: <provider>:manual)",
|
|
||||||
)
|
|
||||||
.option(
|
|
||||||
"--token-expires-in <duration>",
|
|
||||||
"Optional token expiry duration (e.g. 365d, 12h)",
|
|
||||||
)
|
)
|
||||||
.option("--anthropic-api-key <key>", "Anthropic API key")
|
.option("--anthropic-api-key <key>", "Anthropic API key")
|
||||||
.option("--openai-api-key <key>", "OpenAI API key")
|
.option("--openai-api-key <key>", "OpenAI API key")
|
||||||
@@ -286,7 +270,6 @@ export function buildProgram() {
|
|||||||
mode: opts.mode as "local" | "remote" | undefined,
|
mode: opts.mode as "local" | "remote" | undefined,
|
||||||
authChoice: opts.authChoice as
|
authChoice: opts.authChoice as
|
||||||
| "oauth"
|
| "oauth"
|
||||||
| "setup-token"
|
|
||||||
| "claude-cli"
|
| "claude-cli"
|
||||||
| "token"
|
| "token"
|
||||||
| "openai-codex"
|
| "openai-codex"
|
||||||
@@ -299,10 +282,6 @@ export function buildProgram() {
|
|||||||
| "minimax"
|
| "minimax"
|
||||||
| "skip"
|
| "skip"
|
||||||
| undefined,
|
| undefined,
|
||||||
tokenProvider: opts.tokenProvider as string | undefined,
|
|
||||||
token: opts.token as string | undefined,
|
|
||||||
tokenProfileId: opts.tokenProfileId as string | undefined,
|
|
||||||
tokenExpiresIn: opts.tokenExpiresIn as string | undefined,
|
|
||||||
anthropicApiKey: opts.anthropicApiKey as string | undefined,
|
anthropicApiKey: opts.anthropicApiKey as string | undefined,
|
||||||
openaiApiKey: opts.openaiApiKey as string | undefined,
|
openaiApiKey: opts.openaiApiKey as string | undefined,
|
||||||
geminiApiKey: opts.geminiApiKey as string | undefined,
|
geminiApiKey: opts.geminiApiKey as string | undefined,
|
||||||
|
|||||||
@@ -64,23 +64,17 @@ export function buildAuthChoiceOptions(params: {
|
|||||||
if (claudeCli?.type === "oauth" || claudeCli?.type === "token") {
|
if (claudeCli?.type === "oauth" || claudeCli?.type === "token") {
|
||||||
options.push({
|
options.push({
|
||||||
value: "claude-cli",
|
value: "claude-cli",
|
||||||
label: "Anthropic token (Claude CLI)",
|
label: "Anthropic OAuth (Claude CLI)",
|
||||||
hint: formatOAuthHint(claudeCli.expires),
|
hint: formatOAuthHint(claudeCli.expires),
|
||||||
});
|
});
|
||||||
} else if (params.includeClaudeCliIfMissing && platform === "darwin") {
|
} else if (params.includeClaudeCliIfMissing && platform === "darwin") {
|
||||||
options.push({
|
options.push({
|
||||||
value: "claude-cli",
|
value: "claude-cli",
|
||||||
label: "Anthropic token (Claude CLI)",
|
label: "Anthropic OAuth (Claude CLI)",
|
||||||
hint: "requires Keychain access",
|
hint: "requires Keychain access",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
options.push({
|
|
||||||
value: "setup-token",
|
|
||||||
label: "Anthropic token (run setup-token)",
|
|
||||||
hint: "Runs `claude setup-token`",
|
|
||||||
});
|
|
||||||
|
|
||||||
options.push({
|
options.push({
|
||||||
value: "token",
|
value: "token",
|
||||||
label: "Anthropic token (paste setup-token)",
|
label: "Anthropic token (paste setup-token)",
|
||||||
@@ -101,10 +95,6 @@ export function buildAuthChoiceOptions(params: {
|
|||||||
// Token flow is currently Anthropic-only; use CLI for advanced providers.
|
// Token flow is currently Anthropic-only; use CLI for advanced providers.
|
||||||
options.push({ value: "minimax-cloud", label: "MiniMax M2.1 (minimax.io)" });
|
options.push({ value: "minimax-cloud", label: "MiniMax M2.1 (minimax.io)" });
|
||||||
options.push({ value: "minimax", label: "Minimax M2.1 (LM Studio)" });
|
options.push({ value: "minimax", label: "Minimax M2.1 (LM Studio)" });
|
||||||
options.push({
|
|
||||||
value: "minimax-api",
|
|
||||||
label: "MiniMax API (platform.minimax.io)",
|
|
||||||
});
|
|
||||||
if (params.includeSkip) {
|
if (params.includeSkip) {
|
||||||
options.push({ value: "skip", label: "Skip for now" });
|
options.push({ value: "skip", label: "Skip for now" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,8 +36,6 @@ import {
|
|||||||
} from "./google-gemini-model-default.js";
|
} from "./google-gemini-model-default.js";
|
||||||
import {
|
import {
|
||||||
applyAuthProfileConfig,
|
applyAuthProfileConfig,
|
||||||
applyMinimaxApiConfig,
|
|
||||||
applyMinimaxApiProviderConfig,
|
|
||||||
applyMinimaxConfig,
|
applyMinimaxConfig,
|
||||||
applyMinimaxHostedConfig,
|
applyMinimaxHostedConfig,
|
||||||
applyMinimaxHostedProviderConfig,
|
applyMinimaxHostedProviderConfig,
|
||||||
@@ -218,68 +216,7 @@ export async function applyAuthChoice(params: {
|
|||||||
provider: "anthropic",
|
provider: "anthropic",
|
||||||
mode: "token",
|
mode: "token",
|
||||||
});
|
});
|
||||||
} else if (
|
} else if (params.authChoice === "token" || params.authChoice === "oauth") {
|
||||||
params.authChoice === "setup-token" ||
|
|
||||||
params.authChoice === "oauth"
|
|
||||||
) {
|
|
||||||
await params.prompter.note(
|
|
||||||
[
|
|
||||||
"This will run `claude setup-token` to create a long-lived Anthropic token.",
|
|
||||||
"Requires an interactive TTY and a Claude Pro/Max subscription.",
|
|
||||||
].join("\n"),
|
|
||||||
"Anthropic setup-token",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!process.stdin.isTTY) {
|
|
||||||
await params.prompter.note(
|
|
||||||
"`claude setup-token` requires an interactive TTY.",
|
|
||||||
"Anthropic setup-token",
|
|
||||||
);
|
|
||||||
return { config: nextConfig, agentModelOverride };
|
|
||||||
}
|
|
||||||
|
|
||||||
const proceed = await params.prompter.confirm({
|
|
||||||
message: "Run `claude setup-token` now?",
|
|
||||||
initialValue: true,
|
|
||||||
});
|
|
||||||
if (!proceed) return { config: nextConfig, agentModelOverride };
|
|
||||||
|
|
||||||
const res = await (async () => {
|
|
||||||
const { spawnSync } = await import("node:child_process");
|
|
||||||
return spawnSync("claude", ["setup-token"], { stdio: "inherit" });
|
|
||||||
})();
|
|
||||||
if (res.error) {
|
|
||||||
await params.prompter.note(
|
|
||||||
`Failed to run claude: ${String(res.error)}`,
|
|
||||||
"Anthropic setup-token",
|
|
||||||
);
|
|
||||||
return { config: nextConfig, agentModelOverride };
|
|
||||||
}
|
|
||||||
if (typeof res.status === "number" && res.status !== 0) {
|
|
||||||
await params.prompter.note(
|
|
||||||
`claude setup-token failed (exit ${res.status})`,
|
|
||||||
"Anthropic setup-token",
|
|
||||||
);
|
|
||||||
return { config: nextConfig, agentModelOverride };
|
|
||||||
}
|
|
||||||
|
|
||||||
const store = ensureAuthProfileStore(params.agentDir, {
|
|
||||||
allowKeychainPrompt: true,
|
|
||||||
});
|
|
||||||
if (!store.profiles[CLAUDE_CLI_PROFILE_ID]) {
|
|
||||||
await params.prompter.note(
|
|
||||||
`No Claude CLI credentials found after setup-token. Expected ${CLAUDE_CLI_PROFILE_ID}.`,
|
|
||||||
"Anthropic setup-token",
|
|
||||||
);
|
|
||||||
return { config: nextConfig, agentModelOverride };
|
|
||||||
}
|
|
||||||
|
|
||||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
|
||||||
profileId: CLAUDE_CLI_PROFILE_ID,
|
|
||||||
provider: "anthropic",
|
|
||||||
mode: "token",
|
|
||||||
});
|
|
||||||
} else if (params.authChoice === "token") {
|
|
||||||
const provider = (await params.prompter.select({
|
const provider = (await params.prompter.select({
|
||||||
message: "Token provider",
|
message: "Token provider",
|
||||||
options: [{ value: "anthropic", label: "Anthropic (only supported)" }],
|
options: [{ value: "anthropic", label: "Anthropic (only supported)" }],
|
||||||
@@ -631,24 +568,6 @@ export async function applyAuthChoice(params: {
|
|||||||
agentModelOverride = "lmstudio/minimax-m2.1-gs32";
|
agentModelOverride = "lmstudio/minimax-m2.1-gs32";
|
||||||
await noteAgentModel("lmstudio/minimax-m2.1-gs32");
|
await noteAgentModel("lmstudio/minimax-m2.1-gs32");
|
||||||
}
|
}
|
||||||
} else if (params.authChoice === "minimax-api") {
|
|
||||||
const key = await params.prompter.text({
|
|
||||||
message: "Enter MiniMax API key",
|
|
||||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
||||||
});
|
|
||||||
await setMinimaxApiKey(String(key).trim(), params.agentDir);
|
|
||||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
|
||||||
profileId: "minimax:default",
|
|
||||||
provider: "minimax",
|
|
||||||
mode: "api_key",
|
|
||||||
});
|
|
||||||
if (params.setDefaultModel) {
|
|
||||||
nextConfig = applyMinimaxApiConfig(nextConfig);
|
|
||||||
} else {
|
|
||||||
nextConfig = applyMinimaxApiProviderConfig(nextConfig);
|
|
||||||
agentModelOverride = "minimax/MiniMax-M2.1";
|
|
||||||
await noteAgentModel("minimax/MiniMax-M2.1");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { config: nextConfig, agentModelOverride };
|
return { config: nextConfig, agentModelOverride };
|
||||||
|
|||||||
@@ -352,7 +352,6 @@ async function promptAuthConfig(
|
|||||||
runtime,
|
runtime,
|
||||||
) as
|
) as
|
||||||
| "oauth"
|
| "oauth"
|
||||||
| "setup-token"
|
|
||||||
| "claude-cli"
|
| "claude-cli"
|
||||||
| "token"
|
| "token"
|
||||||
| "openai-codex"
|
| "openai-codex"
|
||||||
@@ -404,68 +403,7 @@ async function promptAuthConfig(
|
|||||||
provider: "anthropic",
|
provider: "anthropic",
|
||||||
mode: "token",
|
mode: "token",
|
||||||
});
|
});
|
||||||
} else if (authChoice === "setup-token" || authChoice === "oauth") {
|
} else if (authChoice === "token" || authChoice === "oauth") {
|
||||||
note(
|
|
||||||
[
|
|
||||||
"This will run `claude setup-token` to create a long-lived Anthropic token.",
|
|
||||||
"Requires an interactive TTY and a Claude Pro/Max subscription.",
|
|
||||||
].join("\n"),
|
|
||||||
"Anthropic setup-token",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!process.stdin.isTTY) {
|
|
||||||
note(
|
|
||||||
"`claude setup-token` requires an interactive TTY.",
|
|
||||||
"Anthropic setup-token",
|
|
||||||
);
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
const runNow = guardCancel(
|
|
||||||
await confirm({
|
|
||||||
message: "Run `claude setup-token` now?",
|
|
||||||
initialValue: true,
|
|
||||||
}),
|
|
||||||
runtime,
|
|
||||||
);
|
|
||||||
if (!runNow) return next;
|
|
||||||
|
|
||||||
const res = await (async () => {
|
|
||||||
const { spawnSync } = await import("node:child_process");
|
|
||||||
return spawnSync("claude", ["setup-token"], { stdio: "inherit" });
|
|
||||||
})();
|
|
||||||
if (res.error) {
|
|
||||||
note(
|
|
||||||
`Failed to run claude: ${String(res.error)}`,
|
|
||||||
"Anthropic setup-token",
|
|
||||||
);
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
if (typeof res.status === "number" && res.status !== 0) {
|
|
||||||
note(
|
|
||||||
`claude setup-token failed (exit ${res.status})`,
|
|
||||||
"Anthropic setup-token",
|
|
||||||
);
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
const store = ensureAuthProfileStore(undefined, {
|
|
||||||
allowKeychainPrompt: true,
|
|
||||||
});
|
|
||||||
if (!store.profiles[CLAUDE_CLI_PROFILE_ID]) {
|
|
||||||
note(
|
|
||||||
`No Claude CLI credentials found after setup-token. Expected ${CLAUDE_CLI_PROFILE_ID}.`,
|
|
||||||
"Anthropic setup-token",
|
|
||||||
);
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
next = applyAuthProfileConfig(next, {
|
|
||||||
profileId: CLAUDE_CLI_PROFILE_ID,
|
|
||||||
provider: "anthropic",
|
|
||||||
mode: "token",
|
|
||||||
});
|
|
||||||
} else if (authChoice === "token") {
|
|
||||||
const provider = guardCancel(
|
const provider = guardCancel(
|
||||||
await select({
|
await select({
|
||||||
message: "Token provider",
|
message: "Token provider",
|
||||||
@@ -788,7 +726,6 @@ async function promptAuthConfig(
|
|||||||
: (next.agents?.defaults?.model?.primary ?? "");
|
: (next.agents?.defaults?.model?.primary ?? "");
|
||||||
const preferAnthropic =
|
const preferAnthropic =
|
||||||
authChoice === "claude-cli" ||
|
authChoice === "claude-cli" ||
|
||||||
authChoice === "setup-token" ||
|
|
||||||
authChoice === "token" ||
|
authChoice === "token" ||
|
||||||
authChoice === "oauth" ||
|
authChoice === "oauth" ||
|
||||||
authChoice === "apiKey";
|
authChoice === "apiKey";
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import { resolveGatewayService } from "../daemon/service.js";
|
|||||||
import {
|
import {
|
||||||
auditGatewayServiceConfig,
|
auditGatewayServiceConfig,
|
||||||
needsNodeRuntimeMigration,
|
needsNodeRuntimeMigration,
|
||||||
SERVICE_AUDIT_CODES,
|
|
||||||
} from "../daemon/service-audit.js";
|
} from "../daemon/service-audit.js";
|
||||||
import { buildServiceEnvironment } from "../daemon/service-env.js";
|
import { buildServiceEnvironment } from "../daemon/service-env.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
@@ -49,17 +48,6 @@ function detectGatewayRuntime(
|
|||||||
return DEFAULT_GATEWAY_DAEMON_RUNTIME;
|
return DEFAULT_GATEWAY_DAEMON_RUNTIME;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findGatewayEntrypoint(programArguments?: string[]): string | null {
|
|
||||||
if (!programArguments || programArguments.length === 0) return null;
|
|
||||||
const gatewayIndex = programArguments.indexOf("gateway");
|
|
||||||
if (gatewayIndex <= 0) return null;
|
|
||||||
return programArguments[gatewayIndex - 1] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeExecutablePath(value: string): string {
|
|
||||||
return path.resolve(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function maybeMigrateLegacyGatewayService(
|
export async function maybeMigrateLegacyGatewayService(
|
||||||
cfg: ClawdbotConfig,
|
cfg: ClawdbotConfig,
|
||||||
mode: "local" | "remote",
|
mode: "local" | "remote",
|
||||||
@@ -183,45 +171,6 @@ export async function maybeRepairGatewayServiceConfig(
|
|||||||
env: process.env,
|
env: process.env,
|
||||||
command,
|
command,
|
||||||
});
|
});
|
||||||
const needsNodeRuntime = needsNodeRuntimeMigration(audit.issues);
|
|
||||||
const systemNodePath = needsNodeRuntime
|
|
||||||
? await resolveSystemNodePath(process.env)
|
|
||||||
: null;
|
|
||||||
if (needsNodeRuntime && !systemNodePath) {
|
|
||||||
note(
|
|
||||||
"System Node 22+ not found. Install via Homebrew/apt/choco and rerun doctor to migrate off Bun/version managers.",
|
|
||||||
"Gateway runtime",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const devMode =
|
|
||||||
process.argv[1]?.includes(`${path.sep}src${path.sep}`) &&
|
|
||||||
process.argv[1]?.endsWith(".ts");
|
|
||||||
const port = resolveGatewayPort(cfg, process.env);
|
|
||||||
const runtimeChoice = detectGatewayRuntime(command.programArguments);
|
|
||||||
const { programArguments, workingDirectory } =
|
|
||||||
await resolveGatewayProgramArguments({
|
|
||||||
port,
|
|
||||||
dev: devMode,
|
|
||||||
runtime: needsNodeRuntime && systemNodePath ? "node" : runtimeChoice,
|
|
||||||
nodePath: systemNodePath ?? undefined,
|
|
||||||
});
|
|
||||||
const expectedEntrypoint = findGatewayEntrypoint(programArguments);
|
|
||||||
const currentEntrypoint = findGatewayEntrypoint(command.programArguments);
|
|
||||||
if (
|
|
||||||
expectedEntrypoint &&
|
|
||||||
currentEntrypoint &&
|
|
||||||
normalizeExecutablePath(expectedEntrypoint) !==
|
|
||||||
normalizeExecutablePath(currentEntrypoint)
|
|
||||||
) {
|
|
||||||
audit.issues.push({
|
|
||||||
code: SERVICE_AUDIT_CODES.gatewayEntrypointMismatch,
|
|
||||||
message: "Gateway service entrypoint does not match the current install.",
|
|
||||||
detail: `${currentEntrypoint} -> ${expectedEntrypoint}`,
|
|
||||||
level: "recommended",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (audit.issues.length === 0) return;
|
if (audit.issues.length === 0) return;
|
||||||
|
|
||||||
note(
|
note(
|
||||||
@@ -258,6 +207,30 @@ export async function maybeRepairGatewayServiceConfig(
|
|||||||
initialValue: true,
|
initialValue: true,
|
||||||
});
|
});
|
||||||
if (!repair) return;
|
if (!repair) return;
|
||||||
|
|
||||||
|
const needsNodeRuntime = needsNodeRuntimeMigration(audit.issues);
|
||||||
|
const systemNodePath = needsNodeRuntime
|
||||||
|
? await resolveSystemNodePath(process.env)
|
||||||
|
: null;
|
||||||
|
if (needsNodeRuntime && !systemNodePath) {
|
||||||
|
note(
|
||||||
|
"System Node 22+ not found. Install via Homebrew/apt/choco and rerun doctor to migrate off Bun/version managers.",
|
||||||
|
"Gateway runtime",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const devMode =
|
||||||
|
process.argv[1]?.includes(`${path.sep}src${path.sep}`) &&
|
||||||
|
process.argv[1]?.endsWith(".ts");
|
||||||
|
const port = resolveGatewayPort(cfg, process.env);
|
||||||
|
const runtimeChoice = detectGatewayRuntime(command.programArguments);
|
||||||
|
const { programArguments, workingDirectory } =
|
||||||
|
await resolveGatewayProgramArguments({
|
||||||
|
port,
|
||||||
|
dev: devMode,
|
||||||
|
runtime: needsNodeRuntime && systemNodePath ? "node" : runtimeChoice,
|
||||||
|
nodePath: systemNodePath ?? undefined,
|
||||||
|
});
|
||||||
const environment = buildServiceEnvironment({
|
const environment = buildServiceEnvironment({
|
||||||
env: process.env,
|
env: process.env,
|
||||||
port,
|
port,
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import { afterEach, describe, expect, it } from "vitest";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
applyAuthProfileConfig,
|
applyAuthProfileConfig,
|
||||||
applyMinimaxApiConfig,
|
|
||||||
applyMinimaxApiProviderConfig,
|
|
||||||
writeOAuthCredentials,
|
writeOAuthCredentials,
|
||||||
} from "./onboard-auth.js";
|
} from "./onboard-auth.js";
|
||||||
|
|
||||||
@@ -107,146 +105,3 @@ describe("applyAuthProfileConfig", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("applyMinimaxApiConfig", () => {
|
|
||||||
it("adds minimax provider with correct settings", () => {
|
|
||||||
const cfg = applyMinimaxApiConfig({});
|
|
||||||
expect(cfg.models?.providers?.minimax).toMatchObject({
|
|
||||||
baseUrl: "https://api.minimax.io/anthropic",
|
|
||||||
api: "anthropic-messages",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sets correct primary model", () => {
|
|
||||||
const cfg = applyMinimaxApiConfig({}, "MiniMax-M2.1-lightning");
|
|
||||||
expect(cfg.agents?.defaults?.model?.primary).toBe(
|
|
||||||
"minimax/MiniMax-M2.1-lightning",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sets reasoning flag for MiniMax-M2 model", () => {
|
|
||||||
const cfg = applyMinimaxApiConfig({}, "MiniMax-M2");
|
|
||||||
expect(cfg.models?.providers?.minimax?.models[0]?.reasoning).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not set reasoning for non-M2 models", () => {
|
|
||||||
const cfg = applyMinimaxApiConfig({}, "MiniMax-M2.1");
|
|
||||||
expect(cfg.models?.providers?.minimax?.models[0]?.reasoning).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("preserves existing model fallbacks", () => {
|
|
||||||
const cfg = applyMinimaxApiConfig({
|
|
||||||
agents: {
|
|
||||||
defaults: {
|
|
||||||
model: { fallbacks: ["anthropic/claude-opus-4-5"] },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(cfg.agents?.defaults?.model?.fallbacks).toEqual([
|
|
||||||
"anthropic/claude-opus-4-5",
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("adds model alias", () => {
|
|
||||||
const cfg = applyMinimaxApiConfig({}, "MiniMax-M2.1");
|
|
||||||
expect(cfg.agents?.defaults?.models?.["minimax/MiniMax-M2.1"]?.alias).toBe(
|
|
||||||
"Minimax",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("preserves existing model params when adding alias", () => {
|
|
||||||
const cfg = applyMinimaxApiConfig(
|
|
||||||
{
|
|
||||||
agents: {
|
|
||||||
defaults: {
|
|
||||||
models: {
|
|
||||||
"minimax/MiniMax-M2.1": {
|
|
||||||
alias: "MiniMax",
|
|
||||||
params: { custom: "value" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"MiniMax-M2.1",
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
cfg.agents?.defaults?.models?.["minimax/MiniMax-M2.1"],
|
|
||||||
).toMatchObject({ alias: "Minimax", params: { custom: "value" } });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("replaces existing minimax provider entirely", () => {
|
|
||||||
const cfg = applyMinimaxApiConfig({
|
|
||||||
models: {
|
|
||||||
providers: {
|
|
||||||
minimax: {
|
|
||||||
baseUrl: "https://old.example.com",
|
|
||||||
apiKey: "old-key",
|
|
||||||
api: "openai-completions",
|
|
||||||
models: [
|
|
||||||
{
|
|
||||||
id: "old-model",
|
|
||||||
name: "Old",
|
|
||||||
reasoning: false,
|
|
||||||
input: ["text"],
|
|
||||||
cost: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 },
|
|
||||||
contextWindow: 1000,
|
|
||||||
maxTokens: 100,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(cfg.models?.providers?.minimax?.baseUrl).toBe(
|
|
||||||
"https://api.minimax.io/anthropic",
|
|
||||||
);
|
|
||||||
expect(cfg.models?.providers?.minimax?.api).toBe("anthropic-messages");
|
|
||||||
expect(cfg.models?.providers?.minimax?.models[0]?.id).toBe("MiniMax-M2.1");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("preserves other providers when adding minimax", () => {
|
|
||||||
const cfg = applyMinimaxApiConfig({
|
|
||||||
models: {
|
|
||||||
providers: {
|
|
||||||
anthropic: {
|
|
||||||
baseUrl: "https://api.anthropic.com",
|
|
||||||
apiKey: "anthropic-key",
|
|
||||||
api: "anthropic-messages",
|
|
||||||
models: [
|
|
||||||
{
|
|
||||||
id: "claude-opus-4-5",
|
|
||||||
name: "Claude Opus 4.5",
|
|
||||||
reasoning: false,
|
|
||||||
input: ["text"],
|
|
||||||
cost: { input: 15, output: 75, cacheRead: 0, cacheWrite: 0 },
|
|
||||||
contextWindow: 200000,
|
|
||||||
maxTokens: 8192,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(cfg.models?.providers?.anthropic).toBeDefined();
|
|
||||||
expect(cfg.models?.providers?.minimax).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("preserves existing models mode", () => {
|
|
||||||
const cfg = applyMinimaxApiConfig({
|
|
||||||
models: { mode: "replace", providers: {} },
|
|
||||||
});
|
|
||||||
expect(cfg.models?.mode).toBe("replace");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("applyMinimaxApiProviderConfig", () => {
|
|
||||||
it("does not overwrite existing primary model", () => {
|
|
||||||
const cfg = applyMinimaxApiProviderConfig({
|
|
||||||
agents: { defaults: { model: { primary: "anthropic/claude-opus-4-5" } } },
|
|
||||||
});
|
|
||||||
expect(cfg.agents?.defaults?.model?.primary).toBe(
|
|
||||||
"anthropic/claude-opus-4-5",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -263,73 +263,3 @@ export function applyMinimaxHostedConfig(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// MiniMax Anthropic-compatible API (platform.minimax.io/anthropic)
|
|
||||||
export function applyMinimaxApiProviderConfig(
|
|
||||||
cfg: ClawdbotConfig,
|
|
||||||
modelId: string = "MiniMax-M2.1",
|
|
||||||
): ClawdbotConfig {
|
|
||||||
const providers = { ...cfg.models?.providers };
|
|
||||||
providers.minimax = {
|
|
||||||
baseUrl: "https://api.minimax.io/anthropic",
|
|
||||||
apiKey: "", // Resolved via MINIMAX_API_KEY env var or auth profile
|
|
||||||
api: "anthropic-messages",
|
|
||||||
models: [
|
|
||||||
{
|
|
||||||
id: modelId,
|
|
||||||
name: `MiniMax ${modelId}`,
|
|
||||||
reasoning: modelId === "MiniMax-M2",
|
|
||||||
input: ["text"],
|
|
||||||
// Pricing: MiniMax doesn't publish public rates. Override in models.json for accurate costs.
|
|
||||||
cost: { input: 15, output: 60, cacheRead: 2, cacheWrite: 10 },
|
|
||||||
contextWindow: 200000,
|
|
||||||
maxTokens: 8192,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const models = { ...cfg.agents?.defaults?.models };
|
|
||||||
models[`minimax/${modelId}`] = {
|
|
||||||
...models[`minimax/${modelId}`],
|
|
||||||
alias: "Minimax",
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
...cfg,
|
|
||||||
agents: {
|
|
||||||
...cfg.agents,
|
|
||||||
defaults: {
|
|
||||||
...cfg.agents?.defaults,
|
|
||||||
models,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
models: { mode: cfg.models?.mode ?? "merge", providers },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function applyMinimaxApiConfig(
|
|
||||||
cfg: ClawdbotConfig,
|
|
||||||
modelId: string = "MiniMax-M2.1",
|
|
||||||
): ClawdbotConfig {
|
|
||||||
const next = applyMinimaxApiProviderConfig(cfg, modelId);
|
|
||||||
return {
|
|
||||||
...next,
|
|
||||||
agents: {
|
|
||||||
...next.agents,
|
|
||||||
defaults: {
|
|
||||||
...next.agents?.defaults,
|
|
||||||
model: {
|
|
||||||
...(next.agents?.defaults?.model &&
|
|
||||||
"fallbacks" in (next.agents.defaults.model as Record<string, unknown>)
|
|
||||||
? {
|
|
||||||
fallbacks: (
|
|
||||||
next.agents.defaults.model as { fallbacks?: string[] }
|
|
||||||
).fallbacks,
|
|
||||||
}
|
|
||||||
: undefined),
|
|
||||||
primary: `minimax/${modelId}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -29,7 +29,6 @@ import { applyGoogleGeminiModelDefault } from "./google-gemini-model-default.js"
|
|||||||
import { healthCommand } from "./health.js";
|
import { healthCommand } from "./health.js";
|
||||||
import {
|
import {
|
||||||
applyAuthProfileConfig,
|
applyAuthProfileConfig,
|
||||||
applyMinimaxApiConfig,
|
|
||||||
applyMinimaxConfig,
|
applyMinimaxConfig,
|
||||||
applyMinimaxHostedConfig,
|
applyMinimaxHostedConfig,
|
||||||
setAnthropicApiKey,
|
setAnthropicApiKey,
|
||||||
@@ -174,21 +173,6 @@ export async function runNonInteractiveOnboarding(
|
|||||||
mode: "api_key",
|
mode: "api_key",
|
||||||
});
|
});
|
||||||
nextConfig = applyMinimaxHostedConfig(nextConfig);
|
nextConfig = applyMinimaxHostedConfig(nextConfig);
|
||||||
} else if (authChoice === "minimax-api") {
|
|
||||||
const key =
|
|
||||||
opts.minimaxApiKey?.trim() || resolveEnvApiKey("minimax")?.apiKey;
|
|
||||||
if (!key) {
|
|
||||||
runtime.error("Missing --minimax-api-key (or MINIMAX_API_KEY in env).");
|
|
||||||
runtime.exit(1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await setMinimaxApiKey(key);
|
|
||||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
|
||||||
profileId: "minimax:default",
|
|
||||||
provider: "minimax",
|
|
||||||
mode: "api_key",
|
|
||||||
});
|
|
||||||
nextConfig = applyMinimaxApiConfig(nextConfig);
|
|
||||||
} else if (authChoice === "claude-cli") {
|
} else if (authChoice === "claude-cli") {
|
||||||
const store = ensureAuthProfileStore(undefined, {
|
const store = ensureAuthProfileStore(undefined, {
|
||||||
allowKeychainPrompt: false,
|
allowKeychainPrompt: false,
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ import type { GatewayDaemonRuntime } from "./daemon-runtime.js";
|
|||||||
|
|
||||||
export type OnboardMode = "local" | "remote";
|
export type OnboardMode = "local" | "remote";
|
||||||
export type AuthChoice =
|
export type AuthChoice =
|
||||||
// Legacy alias for `setup-token` (kept for backwards CLI compatibility).
|
|
||||||
| "oauth"
|
| "oauth"
|
||||||
| "setup-token"
|
|
||||||
| "claude-cli"
|
| "claude-cli"
|
||||||
| "token"
|
| "token"
|
||||||
| "openai-codex"
|
| "openai-codex"
|
||||||
@@ -16,7 +14,6 @@ export type AuthChoice =
|
|||||||
| "gemini-api-key"
|
| "gemini-api-key"
|
||||||
| "minimax-cloud"
|
| "minimax-cloud"
|
||||||
| "minimax"
|
| "minimax"
|
||||||
| "minimax-api"
|
|
||||||
| "skip";
|
| "skip";
|
||||||
export type GatewayAuthChoice = "off" | "token" | "password";
|
export type GatewayAuthChoice = "off" | "token" | "password";
|
||||||
export type ResetScope = "config" | "config+creds+sessions" | "full";
|
export type ResetScope = "config" | "config+creds+sessions" | "full";
|
||||||
@@ -30,14 +27,6 @@ export type OnboardOptions = {
|
|||||||
workspace?: string;
|
workspace?: string;
|
||||||
nonInteractive?: boolean;
|
nonInteractive?: boolean;
|
||||||
authChoice?: AuthChoice;
|
authChoice?: AuthChoice;
|
||||||
/** Used when `authChoice=token` in non-interactive mode. */
|
|
||||||
tokenProvider?: string;
|
|
||||||
/** Used when `authChoice=token` in non-interactive mode. */
|
|
||||||
token?: string;
|
|
||||||
/** Used when `authChoice=token` in non-interactive mode. */
|
|
||||||
tokenProfileId?: string;
|
|
||||||
/** Used when `authChoice=token` in non-interactive mode. */
|
|
||||||
tokenExpiresIn?: string;
|
|
||||||
anthropicApiKey?: string;
|
anthropicApiKey?: string;
|
||||||
openaiApiKey?: string;
|
openaiApiKey?: string;
|
||||||
geminiApiKey?: string;
|
geminiApiKey?: string;
|
||||||
|
|||||||
@@ -10,11 +10,6 @@ export async function onboardCommand(
|
|||||||
runtime: RuntimeEnv = defaultRuntime,
|
runtime: RuntimeEnv = defaultRuntime,
|
||||||
) {
|
) {
|
||||||
assertSupportedRuntime(runtime);
|
assertSupportedRuntime(runtime);
|
||||||
const authChoice =
|
|
||||||
opts.authChoice === "oauth" ? ("setup-token" as const) : opts.authChoice;
|
|
||||||
const normalizedOpts =
|
|
||||||
authChoice === opts.authChoice ? opts : { ...opts, authChoice };
|
|
||||||
|
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
runtime.log(
|
runtime.log(
|
||||||
[
|
[
|
||||||
@@ -25,12 +20,12 @@ export async function onboardCommand(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalizedOpts.nonInteractive) {
|
if (opts.nonInteractive) {
|
||||||
await runNonInteractiveOnboarding(normalizedOpts, runtime);
|
await runNonInteractiveOnboarding(opts, runtime);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await runInteractiveOnboarding(normalizedOpts, runtime);
|
await runInteractiveOnboarding(opts, runtime);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { OnboardOptions } from "./onboard-types.js";
|
export type { OnboardOptions } from "./onboard-types.js";
|
||||||
@@ -404,7 +404,6 @@ export type DiscordActionConfig = {
|
|||||||
moderation?: boolean;
|
moderation?: boolean;
|
||||||
emojiUploads?: boolean;
|
emojiUploads?: boolean;
|
||||||
stickerUploads?: boolean;
|
stickerUploads?: boolean;
|
||||||
channels?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DiscordAccountConfig = {
|
export type DiscordAccountConfig = {
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ export type ServiceConfigAudit = {
|
|||||||
|
|
||||||
export const SERVICE_AUDIT_CODES = {
|
export const SERVICE_AUDIT_CODES = {
|
||||||
gatewayCommandMissing: "gateway-command-missing",
|
gatewayCommandMissing: "gateway-command-missing",
|
||||||
gatewayEntrypointMismatch: "gateway-entrypoint-mismatch",
|
|
||||||
gatewayPathMissing: "gateway-path-missing",
|
gatewayPathMissing: "gateway-path-missing",
|
||||||
gatewayPathMissingDirs: "gateway-path-missing-dirs",
|
gatewayPathMissingDirs: "gateway-path-missing-dirs",
|
||||||
gatewayPathNonMinimal: "gateway-path-nonminimal",
|
gatewayPathNonMinimal: "gateway-path-nonminimal",
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { Client } from "@buape/carbon";
|
import type { Client } from "@buape/carbon";
|
||||||
import { ChannelType, MessageType } from "@buape/carbon";
|
import { ChannelType, MessageType } from "@buape/carbon";
|
||||||
import { Routes } from "discord-api-types/v10";
|
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
const sendMock = vi.fn();
|
const sendMock = vi.fn();
|
||||||
@@ -120,168 +119,6 @@ describe("discord tool result dispatch", () => {
|
|||||||
expect(sendMock.mock.calls[0]?.[1]).toMatch(/^PFX /);
|
expect(sendMock.mock.calls[0]?.[1]).toMatch(/^PFX /);
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
it("caches channel info lookups between messages", async () => {
|
|
||||||
const { createDiscordMessageHandler } = await import("./monitor.js");
|
|
||||||
const cfg = {
|
|
||||||
agents: {
|
|
||||||
defaults: {
|
|
||||||
model: "anthropic/claude-opus-4-5",
|
|
||||||
workspace: "/tmp/clawd",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
session: { store: "/tmp/clawdbot-sessions.json" },
|
|
||||||
discord: { dm: { enabled: true, policy: "open" } },
|
|
||||||
} as ReturnType<typeof import("../config/config.js").loadConfig>;
|
|
||||||
|
|
||||||
const handler = createDiscordMessageHandler({
|
|
||||||
cfg,
|
|
||||||
discordConfig: cfg.discord,
|
|
||||||
accountId: "default",
|
|
||||||
token: "token",
|
|
||||||
runtime: {
|
|
||||||
log: vi.fn(),
|
|
||||||
error: vi.fn(),
|
|
||||||
exit: (code: number): never => {
|
|
||||||
throw new Error(`exit ${code}`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
botUserId: "bot-id",
|
|
||||||
guildHistories: new Map(),
|
|
||||||
historyLimit: 0,
|
|
||||||
mediaMaxBytes: 10_000,
|
|
||||||
textLimit: 2000,
|
|
||||||
replyToMode: "off",
|
|
||||||
dmEnabled: true,
|
|
||||||
groupDmEnabled: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchChannel = vi.fn().mockResolvedValue({
|
|
||||||
type: ChannelType.DM,
|
|
||||||
name: "dm",
|
|
||||||
});
|
|
||||||
const client = { fetchChannel } as unknown as Client;
|
|
||||||
const baseMessage = {
|
|
||||||
content: "hello",
|
|
||||||
channelId: "cache-channel-1",
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
type: MessageType.Default,
|
|
||||||
attachments: [],
|
|
||||||
embeds: [],
|
|
||||||
mentionedEveryone: false,
|
|
||||||
mentionedUsers: [],
|
|
||||||
mentionedRoles: [],
|
|
||||||
author: { id: "u-cache", bot: false, username: "Ada" },
|
|
||||||
};
|
|
||||||
|
|
||||||
await handler(
|
|
||||||
{
|
|
||||||
message: { ...baseMessage, id: "m-cache-1" },
|
|
||||||
author: baseMessage.author,
|
|
||||||
guild_id: null,
|
|
||||||
},
|
|
||||||
client,
|
|
||||||
);
|
|
||||||
await handler(
|
|
||||||
{
|
|
||||||
message: { ...baseMessage, id: "m-cache-2" },
|
|
||||||
author: baseMessage.author,
|
|
||||||
guild_id: null,
|
|
||||||
},
|
|
||||||
client,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(fetchChannel).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses channel id allowlists for non-thread channels with categories", async () => {
|
|
||||||
const { createDiscordMessageHandler } = await import("./monitor.js");
|
|
||||||
let capturedCtx: { SessionKey?: string } | undefined;
|
|
||||||
dispatchMock.mockImplementationOnce(async ({ ctx, dispatcher }) => {
|
|
||||||
capturedCtx = ctx;
|
|
||||||
dispatcher.sendFinalReply({ text: "hi" });
|
|
||||||
return { queuedFinal: true, counts: { final: 1 } };
|
|
||||||
});
|
|
||||||
|
|
||||||
const cfg = {
|
|
||||||
agents: {
|
|
||||||
defaults: {
|
|
||||||
model: "anthropic/claude-opus-4-5",
|
|
||||||
workspace: "/tmp/clawd",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
session: { store: "/tmp/clawdbot-sessions.json" },
|
|
||||||
discord: {
|
|
||||||
dm: { enabled: true, policy: "open" },
|
|
||||||
guilds: {
|
|
||||||
"*": {
|
|
||||||
requireMention: false,
|
|
||||||
channels: { c1: { allow: true } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
routing: { allowFrom: [] },
|
|
||||||
} as ReturnType<typeof import("../config/config.js").loadConfig>;
|
|
||||||
|
|
||||||
const handler = createDiscordMessageHandler({
|
|
||||||
cfg,
|
|
||||||
discordConfig: cfg.discord,
|
|
||||||
accountId: "default",
|
|
||||||
token: "token",
|
|
||||||
runtime: {
|
|
||||||
log: vi.fn(),
|
|
||||||
error: vi.fn(),
|
|
||||||
exit: (code: number): never => {
|
|
||||||
throw new Error(`exit ${code}`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
botUserId: "bot-id",
|
|
||||||
guildHistories: new Map(),
|
|
||||||
historyLimit: 0,
|
|
||||||
mediaMaxBytes: 10_000,
|
|
||||||
textLimit: 2000,
|
|
||||||
replyToMode: "off",
|
|
||||||
dmEnabled: true,
|
|
||||||
groupDmEnabled: false,
|
|
||||||
guildEntries: {
|
|
||||||
"*": { requireMention: false, channels: { c1: { allow: true } } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const client = {
|
|
||||||
fetchChannel: vi.fn().mockResolvedValue({
|
|
||||||
type: ChannelType.GuildText,
|
|
||||||
name: "general",
|
|
||||||
parentId: "category-1",
|
|
||||||
}),
|
|
||||||
rest: { get: vi.fn() },
|
|
||||||
} as unknown as Client;
|
|
||||||
|
|
||||||
await handler(
|
|
||||||
{
|
|
||||||
message: {
|
|
||||||
id: "m-category",
|
|
||||||
content: "hello",
|
|
||||||
channelId: "c1",
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
type: MessageType.Default,
|
|
||||||
attachments: [],
|
|
||||||
embeds: [],
|
|
||||||
mentionedEveryone: false,
|
|
||||||
mentionedUsers: [],
|
|
||||||
mentionedRoles: [],
|
|
||||||
author: { id: "u1", bot: false, username: "Ada", tag: "Ada#1" },
|
|
||||||
},
|
|
||||||
author: { id: "u1", bot: false, username: "Ada", tag: "Ada#1" },
|
|
||||||
member: { displayName: "Ada" },
|
|
||||||
guild: { id: "g1", name: "Guild" },
|
|
||||||
guild_id: "g1",
|
|
||||||
},
|
|
||||||
client,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(capturedCtx?.SessionKey).toBe("agent:main:discord:channel:c1");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("replies with pairing code and sender id when dmPolicy is pairing", async () => {
|
it("replies with pairing code and sender id when dmPolicy is pairing", async () => {
|
||||||
const { createDiscordMessageHandler } = await import("./monitor.js");
|
const { createDiscordMessageHandler } = await import("./monitor.js");
|
||||||
const cfg = {
|
const cfg = {
|
||||||
@@ -544,110 +381,6 @@ describe("discord tool result dispatch", () => {
|
|||||||
expect(capturedCtx?.ThreadLabel).toContain("Discord thread #general");
|
expect(capturedCtx?.ThreadLabel).toContain("Discord thread #general");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("treats forum threads as distinct sessions without channel payloads", async () => {
|
|
||||||
const { createDiscordMessageHandler } = await import("./monitor.js");
|
|
||||||
let capturedCtx:
|
|
||||||
| {
|
|
||||||
SessionKey?: string;
|
|
||||||
ParentSessionKey?: string;
|
|
||||||
ThreadStarterBody?: string;
|
|
||||||
ThreadLabel?: string;
|
|
||||||
}
|
|
||||||
| undefined;
|
|
||||||
dispatchMock.mockImplementationOnce(async ({ ctx, dispatcher }) => {
|
|
||||||
capturedCtx = ctx;
|
|
||||||
dispatcher.sendFinalReply({ text: "hi" });
|
|
||||||
return { queuedFinal: true, counts: { final: 1 } };
|
|
||||||
});
|
|
||||||
|
|
||||||
const cfg = {
|
|
||||||
agent: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/clawd" },
|
|
||||||
session: { store: "/tmp/clawdbot-sessions.json" },
|
|
||||||
discord: {
|
|
||||||
dm: { enabled: true, policy: "open" },
|
|
||||||
guilds: { "*": { requireMention: false } },
|
|
||||||
},
|
|
||||||
routing: { allowFrom: [] },
|
|
||||||
} as ReturnType<typeof import("../config/config.js").loadConfig>;
|
|
||||||
|
|
||||||
const handler = createDiscordMessageHandler({
|
|
||||||
cfg,
|
|
||||||
discordConfig: cfg.discord,
|
|
||||||
accountId: "default",
|
|
||||||
token: "token",
|
|
||||||
runtime: {
|
|
||||||
log: vi.fn(),
|
|
||||||
error: vi.fn(),
|
|
||||||
exit: (code: number): never => {
|
|
||||||
throw new Error(`exit ${code}`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
botUserId: "bot-id",
|
|
||||||
guildHistories: new Map(),
|
|
||||||
historyLimit: 0,
|
|
||||||
mediaMaxBytes: 10_000,
|
|
||||||
textLimit: 2000,
|
|
||||||
replyToMode: "off",
|
|
||||||
dmEnabled: true,
|
|
||||||
groupDmEnabled: false,
|
|
||||||
guildEntries: { "*": { requireMention: false } },
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchChannel = vi
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValueOnce({
|
|
||||||
type: ChannelType.PublicThread,
|
|
||||||
name: "topic-1",
|
|
||||||
parentId: "forum-1",
|
|
||||||
})
|
|
||||||
.mockResolvedValueOnce({
|
|
||||||
type: ChannelType.GuildForum,
|
|
||||||
name: "support",
|
|
||||||
});
|
|
||||||
const restGet = vi.fn().mockResolvedValue({
|
|
||||||
content: "starter message",
|
|
||||||
author: { id: "u1", username: "Alice", discriminator: "0001" },
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
const client = {
|
|
||||||
fetchChannel,
|
|
||||||
rest: {
|
|
||||||
get: restGet,
|
|
||||||
},
|
|
||||||
} as unknown as Client;
|
|
||||||
|
|
||||||
await handler(
|
|
||||||
{
|
|
||||||
message: {
|
|
||||||
id: "m6",
|
|
||||||
content: "thread reply",
|
|
||||||
channelId: "t1",
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
type: MessageType.Default,
|
|
||||||
attachments: [],
|
|
||||||
embeds: [],
|
|
||||||
mentionedEveryone: false,
|
|
||||||
mentionedUsers: [],
|
|
||||||
mentionedRoles: [],
|
|
||||||
author: { id: "u2", bot: false, username: "Bob", tag: "Bob#2" },
|
|
||||||
},
|
|
||||||
author: { id: "u2", bot: false, username: "Bob", tag: "Bob#2" },
|
|
||||||
member: { displayName: "Bob" },
|
|
||||||
guild: { id: "g1", name: "Guild" },
|
|
||||||
guild_id: "g1",
|
|
||||||
},
|
|
||||||
client,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(capturedCtx?.SessionKey).toBe("agent:main:discord:channel:t1");
|
|
||||||
expect(capturedCtx?.ParentSessionKey).toBe(
|
|
||||||
"agent:main:discord:channel:forum-1",
|
|
||||||
);
|
|
||||||
expect(capturedCtx?.ThreadStarterBody).toContain("starter message");
|
|
||||||
expect(capturedCtx?.ThreadLabel).toContain("Discord thread #support");
|
|
||||||
expect(restGet).toHaveBeenCalledWith(Routes.channelMessage("t1", "t1"));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("scopes thread sessions to the routed agent", async () => {
|
it("scopes thread sessions to the routed agent", async () => {
|
||||||
const { createDiscordMessageHandler } = await import("./monitor.js");
|
const { createDiscordMessageHandler } = await import("./monitor.js");
|
||||||
|
|
||||||
|
|||||||
+16
-132
@@ -113,20 +113,7 @@ type DiscordThreadStarter = {
|
|||||||
timestamp?: number;
|
timestamp?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DiscordChannelInfo = {
|
|
||||||
type: ChannelType;
|
|
||||||
name?: string;
|
|
||||||
topic?: string;
|
|
||||||
parentId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const DISCORD_THREAD_STARTER_CACHE = new Map<string, DiscordThreadStarter>();
|
const DISCORD_THREAD_STARTER_CACHE = new Map<string, DiscordThreadStarter>();
|
||||||
const DISCORD_CHANNEL_INFO_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
||||||
const DISCORD_CHANNEL_INFO_NEGATIVE_CACHE_TTL_MS = 30 * 1000;
|
|
||||||
const DISCORD_CHANNEL_INFO_CACHE = new Map<
|
|
||||||
string,
|
|
||||||
{ value: DiscordChannelInfo | null; expiresAt: number }
|
|
||||||
>();
|
|
||||||
const DISCORD_SLOW_LISTENER_THRESHOLD_MS = 1000;
|
const DISCORD_SLOW_LISTENER_THRESHOLD_MS = 1000;
|
||||||
|
|
||||||
function logSlowDiscordListener(params: {
|
function logSlowDiscordListener(params: {
|
||||||
@@ -152,22 +139,14 @@ async function resolveDiscordThreadStarter(params: {
|
|||||||
channel: DiscordThreadChannel;
|
channel: DiscordThreadChannel;
|
||||||
client: Client;
|
client: Client;
|
||||||
parentId?: string;
|
parentId?: string;
|
||||||
parentType?: ChannelType;
|
|
||||||
}): Promise<DiscordThreadStarter | null> {
|
}): Promise<DiscordThreadStarter | null> {
|
||||||
const cacheKey = params.channel.id;
|
const cacheKey = params.channel.id;
|
||||||
const cached = DISCORD_THREAD_STARTER_CACHE.get(cacheKey);
|
const cached = DISCORD_THREAD_STARTER_CACHE.get(cacheKey);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
try {
|
try {
|
||||||
const parentType = params.parentType;
|
if (!params.parentId) return null;
|
||||||
const isForumParent =
|
|
||||||
parentType === ChannelType.GuildForum ||
|
|
||||||
parentType === ChannelType.GuildMedia;
|
|
||||||
const messageChannelId = isForumParent
|
|
||||||
? params.channel.id
|
|
||||||
: params.parentId;
|
|
||||||
if (!messageChannelId) return null;
|
|
||||||
const starter = (await params.client.rest.get(
|
const starter = (await params.client.rest.get(
|
||||||
Routes.channelMessage(messageChannelId, params.channel.id),
|
Routes.channelMessage(params.parentId, params.channel.id),
|
||||||
)) as {
|
)) as {
|
||||||
content?: string | null;
|
content?: string | null;
|
||||||
embeds?: Array<{ description?: string | null }>;
|
embeds?: Array<{ description?: string | null }>;
|
||||||
@@ -247,66 +226,6 @@ export type DiscordMessageHandler = (
|
|||||||
client: Client,
|
client: Client,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
|
||||||
function isDiscordThreadType(type: ChannelType | undefined): boolean {
|
|
||||||
return (
|
|
||||||
type === ChannelType.PublicThread ||
|
|
||||||
type === ChannelType.PrivateThread ||
|
|
||||||
type === ChannelType.AnnouncementThread
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type DiscordThreadParentInfo = {
|
|
||||||
id?: string;
|
|
||||||
name?: string;
|
|
||||||
type?: ChannelType;
|
|
||||||
};
|
|
||||||
|
|
||||||
function resolveDiscordThreadChannel(params: {
|
|
||||||
isGuildMessage: boolean;
|
|
||||||
message: DiscordMessageEvent["message"];
|
|
||||||
channelInfo: DiscordChannelInfo | null;
|
|
||||||
}): DiscordThreadChannel | null {
|
|
||||||
if (!params.isGuildMessage) return null;
|
|
||||||
const { message, channelInfo } = params;
|
|
||||||
const channel =
|
|
||||||
"channel" in message
|
|
||||||
? (message as { channel?: unknown }).channel
|
|
||||||
: undefined;
|
|
||||||
const isThreadChannel =
|
|
||||||
channel &&
|
|
||||||
typeof channel === "object" &&
|
|
||||||
"isThread" in channel &&
|
|
||||||
typeof (channel as { isThread?: unknown }).isThread === "function" &&
|
|
||||||
(channel as { isThread: () => boolean }).isThread();
|
|
||||||
if (isThreadChannel) return channel as unknown as DiscordThreadChannel;
|
|
||||||
if (!isDiscordThreadType(channelInfo?.type)) return null;
|
|
||||||
return {
|
|
||||||
id: message.channelId,
|
|
||||||
name: channelInfo?.name ?? undefined,
|
|
||||||
parentId: channelInfo?.parentId ?? undefined,
|
|
||||||
parent: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resolveDiscordThreadParentInfo(params: {
|
|
||||||
client: Client;
|
|
||||||
threadChannel: DiscordThreadChannel;
|
|
||||||
channelInfo: DiscordChannelInfo | null;
|
|
||||||
}): Promise<DiscordThreadParentInfo> {
|
|
||||||
const { threadChannel, channelInfo, client } = params;
|
|
||||||
const parentId =
|
|
||||||
threadChannel.parentId ??
|
|
||||||
threadChannel.parent?.id ??
|
|
||||||
channelInfo?.parentId ??
|
|
||||||
undefined;
|
|
||||||
if (!parentId) return {};
|
|
||||||
let parentName = threadChannel.parent?.name;
|
|
||||||
const parentInfo = await resolveDiscordChannelInfo(client, parentId);
|
|
||||||
parentName = parentName ?? parentInfo?.name;
|
|
||||||
const parentType = parentInfo?.type;
|
|
||||||
return { id: parentId, name: parentName, type: parentType };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveDiscordReplyTarget(opts: {
|
export function resolveDiscordReplyTarget(opts: {
|
||||||
replyToMode: ReplyToMode;
|
replyToMode: ReplyToMode;
|
||||||
replyToId?: string;
|
replyToId?: string;
|
||||||
@@ -742,24 +661,17 @@ export function createDiscordMessageHandler(params: {
|
|||||||
"name" in message.channel
|
"name" in message.channel
|
||||||
? message.channel.name
|
? message.channel.name
|
||||||
: undefined);
|
: undefined);
|
||||||
const threadChannel = resolveDiscordThreadChannel({
|
const isThreadChannel =
|
||||||
isGuildMessage,
|
isGuildMessage &&
|
||||||
message,
|
message.channel &&
|
||||||
channelInfo,
|
"isThread" in message.channel &&
|
||||||
});
|
message.channel.isThread();
|
||||||
let threadParentId: string | undefined;
|
const threadChannel = isThreadChannel
|
||||||
let threadParentName: string | undefined;
|
? (message.channel as DiscordThreadChannel)
|
||||||
let threadParentType: ChannelType | undefined;
|
: null;
|
||||||
if (threadChannel) {
|
const threadParentId =
|
||||||
const parentInfo = await resolveDiscordThreadParentInfo({
|
threadChannel?.parentId ?? threadChannel?.parent?.id ?? undefined;
|
||||||
client,
|
const threadParentName = threadChannel?.parent?.name;
|
||||||
threadChannel,
|
|
||||||
channelInfo,
|
|
||||||
});
|
|
||||||
threadParentId = parentInfo.id;
|
|
||||||
threadParentName = parentInfo.name;
|
|
||||||
threadParentType = parentInfo.type;
|
|
||||||
}
|
|
||||||
const threadName = threadChannel?.name;
|
const threadName = threadChannel?.name;
|
||||||
const configChannelName = threadParentName ?? channelName;
|
const configChannelName = threadParentName ?? channelName;
|
||||||
const configChannelSlug = configChannelName
|
const configChannelSlug = configChannelName
|
||||||
@@ -1023,7 +935,6 @@ export function createDiscordMessageHandler(params: {
|
|||||||
channel: threadChannel,
|
channel: threadChannel,
|
||||||
client,
|
client,
|
||||||
parentId: threadParentId,
|
parentId: threadParentId,
|
||||||
parentType: threadParentType,
|
|
||||||
});
|
});
|
||||||
if (starter?.text) {
|
if (starter?.text) {
|
||||||
const starterEnvelope = formatThreadStarterEnvelope({
|
const starterEnvelope = formatThreadStarterEnvelope({
|
||||||
@@ -1773,42 +1684,15 @@ async function deliverDiscordReply(params: {
|
|||||||
async function resolveDiscordChannelInfo(
|
async function resolveDiscordChannelInfo(
|
||||||
client: Client,
|
client: Client,
|
||||||
channelId: string,
|
channelId: string,
|
||||||
): Promise<DiscordChannelInfo | null> {
|
): Promise<{ type: ChannelType; name?: string; topic?: string } | null> {
|
||||||
const cached = DISCORD_CHANNEL_INFO_CACHE.get(channelId);
|
|
||||||
if (cached) {
|
|
||||||
if (cached.expiresAt > Date.now()) return cached.value;
|
|
||||||
DISCORD_CHANNEL_INFO_CACHE.delete(channelId);
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const channel = await client.fetchChannel(channelId);
|
const channel = await client.fetchChannel(channelId);
|
||||||
if (!channel) {
|
if (!channel) return null;
|
||||||
DISCORD_CHANNEL_INFO_CACHE.set(channelId, {
|
|
||||||
value: null,
|
|
||||||
expiresAt: Date.now() + DISCORD_CHANNEL_INFO_NEGATIVE_CACHE_TTL_MS,
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const name = "name" in channel ? (channel.name ?? undefined) : undefined;
|
const name = "name" in channel ? (channel.name ?? undefined) : undefined;
|
||||||
const topic = "topic" in channel ? (channel.topic ?? undefined) : undefined;
|
const topic = "topic" in channel ? (channel.topic ?? undefined) : undefined;
|
||||||
const parentId =
|
return { type: channel.type, name, topic };
|
||||||
"parentId" in channel ? (channel.parentId ?? undefined) : undefined;
|
|
||||||
const payload: DiscordChannelInfo = {
|
|
||||||
type: channel.type,
|
|
||||||
name,
|
|
||||||
topic,
|
|
||||||
parentId,
|
|
||||||
};
|
|
||||||
DISCORD_CHANNEL_INFO_CACHE.set(channelId, {
|
|
||||||
value: payload,
|
|
||||||
expiresAt: Date.now() + DISCORD_CHANNEL_INFO_CACHE_TTL_MS,
|
|
||||||
});
|
|
||||||
return payload;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logVerbose(`discord: failed to fetch channel ${channelId}: ${String(err)}`);
|
logVerbose(`discord: failed to fetch channel ${channelId}: ${String(err)}`);
|
||||||
DISCORD_CHANNEL_INFO_CACHE.set(channelId, {
|
|
||||||
value: null,
|
|
||||||
expiresAt: Date.now() + DISCORD_CHANNEL_INFO_NEGATIVE_CACHE_TTL_MS,
|
|
||||||
});
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,41 +183,6 @@ export type DiscordStickerUpload = {
|
|||||||
mediaUrl: string;
|
mediaUrl: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DiscordChannelCreate = {
|
|
||||||
guildId: string;
|
|
||||||
name: string;
|
|
||||||
type?: number;
|
|
||||||
parentId?: string;
|
|
||||||
topic?: string;
|
|
||||||
position?: number;
|
|
||||||
nsfw?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DiscordChannelEdit = {
|
|
||||||
channelId: string;
|
|
||||||
name?: string;
|
|
||||||
topic?: string;
|
|
||||||
position?: number;
|
|
||||||
parentId?: string | null;
|
|
||||||
nsfw?: boolean;
|
|
||||||
rateLimitPerUser?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DiscordChannelMove = {
|
|
||||||
guildId: string;
|
|
||||||
channelId: string;
|
|
||||||
parentId?: string | null;
|
|
||||||
position?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DiscordChannelPermissionSet = {
|
|
||||||
channelId: string;
|
|
||||||
targetId: string;
|
|
||||||
targetType: 0 | 1;
|
|
||||||
allow?: string;
|
|
||||||
deny?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function resolveToken(params: {
|
function resolveToken(params: {
|
||||||
explicit?: string;
|
explicit?: string;
|
||||||
accountId: string;
|
accountId: string;
|
||||||
@@ -1226,93 +1191,3 @@ export async function banMemberDiscord(
|
|||||||
});
|
});
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Channel management functions
|
|
||||||
|
|
||||||
export async function createChannelDiscord(
|
|
||||||
payload: DiscordChannelCreate,
|
|
||||||
opts: DiscordReactOpts = {},
|
|
||||||
): Promise<APIChannel> {
|
|
||||||
const rest = resolveDiscordRest(opts);
|
|
||||||
const body: Record<string, unknown> = {
|
|
||||||
name: payload.name,
|
|
||||||
};
|
|
||||||
if (payload.type !== undefined) body.type = payload.type;
|
|
||||||
if (payload.parentId) body.parent_id = payload.parentId;
|
|
||||||
if (payload.topic) body.topic = payload.topic;
|
|
||||||
if (payload.position !== undefined) body.position = payload.position;
|
|
||||||
if (payload.nsfw !== undefined) body.nsfw = payload.nsfw;
|
|
||||||
return (await rest.post(Routes.guildChannels(payload.guildId), {
|
|
||||||
body,
|
|
||||||
})) as APIChannel;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function editChannelDiscord(
|
|
||||||
payload: DiscordChannelEdit,
|
|
||||||
opts: DiscordReactOpts = {},
|
|
||||||
): Promise<APIChannel> {
|
|
||||||
const rest = resolveDiscordRest(opts);
|
|
||||||
const body: Record<string, unknown> = {};
|
|
||||||
if (payload.name !== undefined) body.name = payload.name;
|
|
||||||
if (payload.topic !== undefined) body.topic = payload.topic;
|
|
||||||
if (payload.position !== undefined) body.position = payload.position;
|
|
||||||
if (payload.parentId !== undefined) body.parent_id = payload.parentId;
|
|
||||||
if (payload.nsfw !== undefined) body.nsfw = payload.nsfw;
|
|
||||||
if (payload.rateLimitPerUser !== undefined)
|
|
||||||
body.rate_limit_per_user = payload.rateLimitPerUser;
|
|
||||||
return (await rest.patch(Routes.channel(payload.channelId), {
|
|
||||||
body,
|
|
||||||
})) as APIChannel;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteChannelDiscord(
|
|
||||||
channelId: string,
|
|
||||||
opts: DiscordReactOpts = {},
|
|
||||||
) {
|
|
||||||
const rest = resolveDiscordRest(opts);
|
|
||||||
await rest.delete(Routes.channel(channelId));
|
|
||||||
return { ok: true, channelId };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function moveChannelDiscord(
|
|
||||||
payload: DiscordChannelMove,
|
|
||||||
opts: DiscordReactOpts = {},
|
|
||||||
) {
|
|
||||||
const rest = resolveDiscordRest(opts);
|
|
||||||
const body: Array<Record<string, unknown>> = [
|
|
||||||
{
|
|
||||||
id: payload.channelId,
|
|
||||||
...(payload.parentId !== undefined && { parent_id: payload.parentId }),
|
|
||||||
...(payload.position !== undefined && { position: payload.position }),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
await rest.patch(Routes.guildChannels(payload.guildId), { body });
|
|
||||||
return { ok: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function setChannelPermissionDiscord(
|
|
||||||
payload: DiscordChannelPermissionSet,
|
|
||||||
opts: DiscordReactOpts = {},
|
|
||||||
) {
|
|
||||||
const rest = resolveDiscordRest(opts);
|
|
||||||
const body: Record<string, unknown> = {
|
|
||||||
type: payload.targetType,
|
|
||||||
};
|
|
||||||
if (payload.allow !== undefined) body.allow = payload.allow;
|
|
||||||
if (payload.deny !== undefined) body.deny = payload.deny;
|
|
||||||
await rest.put(
|
|
||||||
`/channels/${payload.channelId}/permissions/${payload.targetId}`,
|
|
||||||
{ body },
|
|
||||||
);
|
|
||||||
return { ok: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function removeChannelPermissionDiscord(
|
|
||||||
channelId: string,
|
|
||||||
targetId: string,
|
|
||||||
opts: DiscordReactOpts = {},
|
|
||||||
) {
|
|
||||||
const rest = resolveDiscordRest(opts);
|
|
||||||
await rest.delete(`/channels/${channelId}/permissions/${targetId}`);
|
|
||||||
return { ok: true };
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
type HookMappingConfig,
|
type HookMappingConfig,
|
||||||
type HooksConfig,
|
type HooksConfig,
|
||||||
} from "../config/config.js";
|
} from "../config/config.js";
|
||||||
import type { HookMessageProvider } from "./hooks.js";
|
|
||||||
|
|
||||||
export type HookMappingResolved = {
|
export type HookMappingResolved = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -19,7 +18,15 @@ export type HookMappingResolved = {
|
|||||||
messageTemplate?: string;
|
messageTemplate?: string;
|
||||||
textTemplate?: string;
|
textTemplate?: string;
|
||||||
deliver?: boolean;
|
deliver?: boolean;
|
||||||
provider?: HookMessageProvider;
|
provider?:
|
||||||
|
| "last"
|
||||||
|
| "whatsapp"
|
||||||
|
| "telegram"
|
||||||
|
| "discord"
|
||||||
|
| "slack"
|
||||||
|
| "signal"
|
||||||
|
| "imessage"
|
||||||
|
| "msteams";
|
||||||
to?: string;
|
to?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
thinking?: string;
|
thinking?: string;
|
||||||
@@ -52,7 +59,15 @@ export type HookAction =
|
|||||||
wakeMode: "now" | "next-heartbeat";
|
wakeMode: "now" | "next-heartbeat";
|
||||||
sessionKey?: string;
|
sessionKey?: string;
|
||||||
deliver?: boolean;
|
deliver?: boolean;
|
||||||
provider?: HookMessageProvider;
|
provider?:
|
||||||
|
| "last"
|
||||||
|
| "whatsapp"
|
||||||
|
| "telegram"
|
||||||
|
| "discord"
|
||||||
|
| "slack"
|
||||||
|
| "signal"
|
||||||
|
| "imessage"
|
||||||
|
| "msteams";
|
||||||
to?: string;
|
to?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
thinking?: string;
|
thinking?: string;
|
||||||
@@ -90,7 +105,14 @@ type HookTransformResult = Partial<{
|
|||||||
name: string;
|
name: string;
|
||||||
sessionKey: string;
|
sessionKey: string;
|
||||||
deliver: boolean;
|
deliver: boolean;
|
||||||
provider: HookMessageProvider;
|
provider:
|
||||||
|
| "last"
|
||||||
|
| "whatsapp"
|
||||||
|
| "telegram"
|
||||||
|
| "discord"
|
||||||
|
| "slack"
|
||||||
|
| "signal"
|
||||||
|
| "imessage";
|
||||||
to: string;
|
to: string;
|
||||||
model: string;
|
model: string;
|
||||||
thinking: string;
|
thinking: string;
|
||||||
|
|||||||
@@ -66,16 +66,6 @@ describe("gateway hooks helpers", () => {
|
|||||||
expect(ok.value.sessionKey).toBe("hook:fixed");
|
expect(ok.value.sessionKey).toBe("hook:fixed");
|
||||||
expect(ok.value.provider).toBe("last");
|
expect(ok.value.provider).toBe("last");
|
||||||
expect(ok.value.name).toBe("Hook");
|
expect(ok.value.name).toBe("Hook");
|
||||||
expect(ok.value.deliver).toBe(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const explicitNoDeliver = normalizeAgentPayload(
|
|
||||||
{ message: "hello", deliver: false },
|
|
||||||
{ idFactory: () => "fixed" },
|
|
||||||
);
|
|
||||||
expect(explicitNoDeliver.ok).toBe(true);
|
|
||||||
if (explicitNoDeliver.ok) {
|
|
||||||
expect(explicitNoDeliver.value.deliver).toBe(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const imsg = normalizeAgentPayload(
|
const imsg = normalizeAgentPayload(
|
||||||
@@ -87,15 +77,6 @@ describe("gateway hooks helpers", () => {
|
|||||||
expect(imsg.value.provider).toBe("imessage");
|
expect(imsg.value.provider).toBe("imessage");
|
||||||
}
|
}
|
||||||
|
|
||||||
const teams = normalizeAgentPayload(
|
|
||||||
{ message: "yo", provider: "teams" },
|
|
||||||
{ idFactory: () => "x" },
|
|
||||||
);
|
|
||||||
expect(teams.ok).toBe(true);
|
|
||||||
if (teams.ok) {
|
|
||||||
expect(teams.value.provider).toBe("msteams");
|
|
||||||
}
|
|
||||||
|
|
||||||
const bad = normalizeAgentPayload({ message: "yo", provider: "sms" });
|
const bad = normalizeAgentPayload({ message: "yo", provider: "sms" });
|
||||||
expect(bad.ok).toBe(false);
|
expect(bad.ok).toBe(false);
|
||||||
});
|
});
|
||||||
|
|||||||
+33
-32
@@ -138,41 +138,20 @@ export type HookAgentPayload = {
|
|||||||
wakeMode: "now" | "next-heartbeat";
|
wakeMode: "now" | "next-heartbeat";
|
||||||
sessionKey: string;
|
sessionKey: string;
|
||||||
deliver: boolean;
|
deliver: boolean;
|
||||||
provider: HookMessageProvider;
|
provider:
|
||||||
|
| "last"
|
||||||
|
| "whatsapp"
|
||||||
|
| "telegram"
|
||||||
|
| "discord"
|
||||||
|
| "slack"
|
||||||
|
| "signal"
|
||||||
|
| "imessage";
|
||||||
to?: string;
|
to?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
thinking?: string;
|
thinking?: string;
|
||||||
timeoutSeconds?: number;
|
timeoutSeconds?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const HOOK_PROVIDER_VALUES = [
|
|
||||||
"last",
|
|
||||||
"whatsapp",
|
|
||||||
"telegram",
|
|
||||||
"discord",
|
|
||||||
"slack",
|
|
||||||
"signal",
|
|
||||||
"imessage",
|
|
||||||
"msteams",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export type HookMessageProvider = (typeof HOOK_PROVIDER_VALUES)[number];
|
|
||||||
|
|
||||||
const hookProviderSet = new Set<string>(HOOK_PROVIDER_VALUES);
|
|
||||||
export const HOOK_PROVIDER_ERROR = `provider must be ${HOOK_PROVIDER_VALUES.join("|")}`;
|
|
||||||
|
|
||||||
export function resolveHookProvider(raw: unknown): HookMessageProvider | null {
|
|
||||||
if (raw === undefined) return "last";
|
|
||||||
if (typeof raw !== "string") return null;
|
|
||||||
const normalized = normalizeMessageProvider(raw);
|
|
||||||
if (!normalized || !hookProviderSet.has(normalized)) return null;
|
|
||||||
return normalized as HookMessageProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveHookDeliver(raw: unknown): boolean {
|
|
||||||
return raw !== false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeAgentPayload(
|
export function normalizeAgentPayload(
|
||||||
payload: Record<string, unknown>,
|
payload: Record<string, unknown>,
|
||||||
opts?: { idFactory?: () => string },
|
opts?: { idFactory?: () => string },
|
||||||
@@ -196,8 +175,30 @@ export function normalizeAgentPayload(
|
|||||||
typeof sessionKeyRaw === "string" && sessionKeyRaw.trim()
|
typeof sessionKeyRaw === "string" && sessionKeyRaw.trim()
|
||||||
? sessionKeyRaw.trim()
|
? sessionKeyRaw.trim()
|
||||||
: `hook:${idFactory()}`;
|
: `hook:${idFactory()}`;
|
||||||
const provider = resolveHookProvider(payload.provider);
|
const providerRaw = payload.provider;
|
||||||
if (!provider) return { ok: false, error: HOOK_PROVIDER_ERROR };
|
const providerNormalized =
|
||||||
|
typeof providerRaw === "string"
|
||||||
|
? normalizeMessageProvider(providerRaw)
|
||||||
|
: undefined;
|
||||||
|
const provider =
|
||||||
|
providerNormalized === "whatsapp" ||
|
||||||
|
providerNormalized === "telegram" ||
|
||||||
|
providerNormalized === "discord" ||
|
||||||
|
providerNormalized === "slack" ||
|
||||||
|
providerNormalized === "signal" ||
|
||||||
|
providerNormalized === "imessage" ||
|
||||||
|
providerNormalized === "last"
|
||||||
|
? providerNormalized
|
||||||
|
: providerRaw === undefined
|
||||||
|
? "last"
|
||||||
|
: null;
|
||||||
|
if (provider === null) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error:
|
||||||
|
"provider must be last|whatsapp|telegram|discord|slack|signal|imessage",
|
||||||
|
};
|
||||||
|
}
|
||||||
const toRaw = payload.to;
|
const toRaw = payload.to;
|
||||||
const to =
|
const to =
|
||||||
typeof toRaw === "string" && toRaw.trim() ? toRaw.trim() : undefined;
|
typeof toRaw === "string" && toRaw.trim() ? toRaw.trim() : undefined;
|
||||||
@@ -209,7 +210,7 @@ export function normalizeAgentPayload(
|
|||||||
if (modelRaw !== undefined && !model) {
|
if (modelRaw !== undefined && !model) {
|
||||||
return { ok: false, error: "model required" };
|
return { ok: false, error: "model required" };
|
||||||
}
|
}
|
||||||
const deliver = resolveHookDeliver(payload.deliver);
|
const deliver = payload.deliver === true;
|
||||||
const thinkingRaw = payload.thinking;
|
const thinkingRaw = payload.thinking;
|
||||||
const thinking =
|
const thinking =
|
||||||
typeof thinkingRaw === "string" && thinkingRaw.trim()
|
typeof thinkingRaw === "string" && thinkingRaw.trim()
|
||||||
|
|||||||
+11
-12
@@ -11,15 +11,11 @@ import type { createSubsystemLogger } from "../logging.js";
|
|||||||
import { handleControlUiHttpRequest } from "./control-ui.js";
|
import { handleControlUiHttpRequest } from "./control-ui.js";
|
||||||
import {
|
import {
|
||||||
extractHookToken,
|
extractHookToken,
|
||||||
HOOK_PROVIDER_ERROR,
|
|
||||||
type HookMessageProvider,
|
|
||||||
type HooksConfigResolved,
|
type HooksConfigResolved,
|
||||||
normalizeAgentPayload,
|
normalizeAgentPayload,
|
||||||
normalizeHookHeaders,
|
normalizeHookHeaders,
|
||||||
normalizeWakePayload,
|
normalizeWakePayload,
|
||||||
readJsonBody,
|
readJsonBody,
|
||||||
resolveHookDeliver,
|
|
||||||
resolveHookProvider,
|
|
||||||
} from "./hooks.js";
|
} from "./hooks.js";
|
||||||
import { applyHookMappings } from "./hooks-mapping.js";
|
import { applyHookMappings } from "./hooks-mapping.js";
|
||||||
|
|
||||||
@@ -36,7 +32,15 @@ type HookDispatchers = {
|
|||||||
wakeMode: "now" | "next-heartbeat";
|
wakeMode: "now" | "next-heartbeat";
|
||||||
sessionKey: string;
|
sessionKey: string;
|
||||||
deliver: boolean;
|
deliver: boolean;
|
||||||
provider: HookMessageProvider;
|
provider:
|
||||||
|
| "last"
|
||||||
|
| "whatsapp"
|
||||||
|
| "telegram"
|
||||||
|
| "discord"
|
||||||
|
| "slack"
|
||||||
|
| "signal"
|
||||||
|
| "imessage"
|
||||||
|
| "msteams";
|
||||||
to?: string;
|
to?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
thinking?: string;
|
thinking?: string;
|
||||||
@@ -167,18 +171,13 @@ export function createHooksRequestHandler(
|
|||||||
sendJson(res, 200, { ok: true, mode: mapped.action.mode });
|
sendJson(res, 200, { ok: true, mode: mapped.action.mode });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const provider = resolveHookProvider(mapped.action.provider);
|
|
||||||
if (!provider) {
|
|
||||||
sendJson(res, 400, { ok: false, error: HOOK_PROVIDER_ERROR });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const runId = dispatchAgentHook({
|
const runId = dispatchAgentHook({
|
||||||
message: mapped.action.message,
|
message: mapped.action.message,
|
||||||
name: mapped.action.name ?? "Hook",
|
name: mapped.action.name ?? "Hook",
|
||||||
wakeMode: mapped.action.wakeMode,
|
wakeMode: mapped.action.wakeMode,
|
||||||
sessionKey: mapped.action.sessionKey ?? "",
|
sessionKey: mapped.action.sessionKey ?? "",
|
||||||
deliver: resolveHookDeliver(mapped.action.deliver),
|
deliver: mapped.action.deliver === true,
|
||||||
provider,
|
provider: mapped.action.provider ?? "last",
|
||||||
to: mapped.action.to,
|
to: mapped.action.to,
|
||||||
model: mapped.action.model,
|
model: mapped.action.model,
|
||||||
thinking: mapped.action.thinking,
|
thinking: mapped.action.thinking,
|
||||||
|
|||||||
+10
-2
@@ -111,7 +111,7 @@ import {
|
|||||||
startGatewayConfigReloader,
|
startGatewayConfigReloader,
|
||||||
} from "./config-reload.js";
|
} from "./config-reload.js";
|
||||||
import { normalizeControlUiBasePath } from "./control-ui.js";
|
import { normalizeControlUiBasePath } from "./control-ui.js";
|
||||||
import { type HookMessageProvider, resolveHooksConfig } from "./hooks.js";
|
import { resolveHooksConfig } from "./hooks.js";
|
||||||
import {
|
import {
|
||||||
isLoopbackAddress,
|
isLoopbackAddress,
|
||||||
isLoopbackHost,
|
isLoopbackHost,
|
||||||
@@ -496,7 +496,15 @@ export async function startGatewayServer(
|
|||||||
wakeMode: "now" | "next-heartbeat";
|
wakeMode: "now" | "next-heartbeat";
|
||||||
sessionKey: string;
|
sessionKey: string;
|
||||||
deliver: boolean;
|
deliver: boolean;
|
||||||
provider: HookMessageProvider;
|
provider:
|
||||||
|
| "last"
|
||||||
|
| "whatsapp"
|
||||||
|
| "telegram"
|
||||||
|
| "discord"
|
||||||
|
| "slack"
|
||||||
|
| "signal"
|
||||||
|
| "imessage"
|
||||||
|
| "msteams";
|
||||||
to?: string;
|
to?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
thinking?: string;
|
thinking?: string;
|
||||||
|
|||||||
+1
-12
@@ -6,10 +6,6 @@ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|||||||
|
|
||||||
const realOs = await vi.importActual<typeof import("node:os")>("node:os");
|
const realOs = await vi.importActual<typeof import("node:os")>("node:os");
|
||||||
const HOME = path.join(realOs.tmpdir(), "clawdbot-home-test");
|
const HOME = path.join(realOs.tmpdir(), "clawdbot-home-test");
|
||||||
const PREV_CLAWDBOT_STATE_DIR = process.env.CLAWDBOT_STATE_DIR;
|
|
||||||
const PREV_CLAWDIS_STATE_DIR = process.env.CLAWDIS_STATE_DIR;
|
|
||||||
delete process.env.CLAWDBOT_STATE_DIR;
|
|
||||||
delete process.env.CLAWDIS_STATE_DIR;
|
|
||||||
|
|
||||||
vi.mock("node:os", () => ({
|
vi.mock("node:os", () => ({
|
||||||
default: { homedir: () => HOME, tmpdir: () => realOs.tmpdir() },
|
default: { homedir: () => HOME, tmpdir: () => realOs.tmpdir() },
|
||||||
@@ -26,18 +22,11 @@ describe("media store", () => {
|
|||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await fs.rm(HOME, { recursive: true, force: true });
|
await fs.rm(HOME, { recursive: true, force: true });
|
||||||
if (PREV_CLAWDBOT_STATE_DIR !== undefined)
|
|
||||||
process.env.CLAWDBOT_STATE_DIR = PREV_CLAWDBOT_STATE_DIR;
|
|
||||||
else delete process.env.CLAWDBOT_STATE_DIR;
|
|
||||||
|
|
||||||
if (PREV_CLAWDIS_STATE_DIR !== undefined)
|
|
||||||
process.env.CLAWDIS_STATE_DIR = PREV_CLAWDIS_STATE_DIR;
|
|
||||||
else delete process.env.CLAWDIS_STATE_DIR;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates and returns media directory", async () => {
|
it("creates and returns media directory", async () => {
|
||||||
const dir = await store.ensureMediaDir();
|
const dir = await store.ensureMediaDir();
|
||||||
expect(path.basename(dir)).toBe("media");
|
expect(dir).toContain("clawdbot-home-test");
|
||||||
const stat = await fs.stat(dir);
|
const stat = await fs.stat(dir);
|
||||||
expect(stat.isDirectory()).toBe(true);
|
expect(stat.isDirectory()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-17
@@ -14,16 +14,9 @@ const runtime = {
|
|||||||
describe("web logout", () => {
|
describe("web logout", () => {
|
||||||
const origHomedir = os.homedir;
|
const origHomedir = os.homedir;
|
||||||
let tmpDir: string;
|
let tmpDir: string;
|
||||||
let prevStateDir: string | undefined;
|
|
||||||
let prevClawdisStateDir: string | undefined;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
prevStateDir = process.env.CLAWDBOT_STATE_DIR;
|
|
||||||
prevClawdisStateDir = process.env.CLAWDIS_STATE_DIR;
|
|
||||||
delete process.env.CLAWDBOT_STATE_DIR;
|
|
||||||
delete process.env.CLAWDIS_STATE_DIR;
|
|
||||||
|
|
||||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-logout-"));
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-logout-"));
|
||||||
vi.spyOn(os, "homedir").mockReturnValue(tmpDir);
|
vi.spyOn(os, "homedir").mockReturnValue(tmpDir);
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
@@ -43,14 +36,6 @@ describe("web logout", () => {
|
|||||||
await fsPromises
|
await fsPromises
|
||||||
.rm(tmpDir, { recursive: true, force: true })
|
.rm(tmpDir, { recursive: true, force: true })
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
|
||||||
if (prevStateDir !== undefined) process.env.CLAWDBOT_STATE_DIR = prevStateDir;
|
|
||||||
else delete process.env.CLAWDBOT_STATE_DIR;
|
|
||||||
|
|
||||||
if (prevClawdisStateDir !== undefined)
|
|
||||||
process.env.CLAWDIS_STATE_DIR = prevClawdisStateDir;
|
|
||||||
else delete process.env.CLAWDIS_STATE_DIR;
|
|
||||||
|
|
||||||
// restore for safety
|
// restore for safety
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
(os.homedir as unknown as typeof origHomedir) = origHomedir;
|
(os.homedir as unknown as typeof origHomedir) = origHomedir;
|
||||||
@@ -62,8 +47,7 @@ describe("web logout", () => {
|
|||||||
async () => {
|
async () => {
|
||||||
const { logoutWeb, WA_WEB_AUTH_DIR } = await import("./session.js");
|
const { logoutWeb, WA_WEB_AUTH_DIR } = await import("./session.js");
|
||||||
|
|
||||||
const rel = path.relative(tmpDir, WA_WEB_AUTH_DIR);
|
expect(WA_WEB_AUTH_DIR.startsWith(tmpDir)).toBe(true);
|
||||||
expect(rel && !rel.startsWith("..") && !path.isAbsolute(rel)).toBe(true);
|
|
||||||
fs.mkdirSync(WA_WEB_AUTH_DIR, { recursive: true });
|
fs.mkdirSync(WA_WEB_AUTH_DIR, { recursive: true });
|
||||||
fs.writeFileSync(path.join(WA_WEB_AUTH_DIR, "creds.json"), "{}");
|
fs.writeFileSync(path.join(WA_WEB_AUTH_DIR, "creds.json"), "{}");
|
||||||
const result = await logoutWeb({ runtime: runtime as never });
|
const result = await logoutWeb({ runtime: runtime as never });
|
||||||
|
|||||||
Reference in new issue
Block a user