Compare commits

..
Author SHA1 Message Date
Peter Steinberger 6b3f9a5934 docs: note android sms capability 2026-01-04 13:57:41 +01:00
Peter Steinberger 0372457ba8 fix(android): refresh hello on sms permission grant 2026-01-04 13:56:20 +01:00
Peter Steinberger f28a03c407 fix(android): add sms permission flow and tests 2026-01-04 13:27:30 +01:00
Vasanth Rao Naik Sabavat f5abc8e9c9 feat(android): add SMS sending capability to Android node
Add sms.send command to allow sending text messages via the paired Android device.

Changes:
- Add SmsManager class to handle SMS sending via Android SmsManager API
- Add ClawdisSmsCommand enum and Sms capability to protocol constants
- Wire sms.send command into NodeRuntime invoke handler
- Add SEND_SMS permission to AndroidManifest.xml
- Advertise sms capability when SEND_SMS permission is granted

The SMS capability is only advertised when the user has granted SEND_SMS
permission. Messages longer than 160 chars are automatically split into
multipart messages.
2026-01-03 22:16:46 -08:00
67 changed files with 114 additions and 4888 deletions

No files matched your search

+8 -4
View File
@@ -9,28 +9,31 @@
- Sessions: primary session key is fixed to `main` (or `global` for global scope); `session.mainKey` is ignored. - Sessions: primary session key is fixed to `main` (or `global` for global scope); `session.mainKey` is ignored.
### Features ### Features
- Highlight: agent-to-agent ping-pong (reply-back loop) with `REPLY_SKIP` plus target announce step with `ANNOUNCE_SKIP` (max turns configurable, 0–5).
- Gateway: support `gateway.port` + `CLAWDIS_GATEWAY_PORT` across CLI, TUI, and macOS app. - Gateway: support `gateway.port` + `CLAWDIS_GATEWAY_PORT` across CLI, TUI, and macOS app.
- Gateway: add config hot reload with hybrid restart strategy (`gateway.reload`) and per-section reload handling. - Gateway: add config hot reload with hybrid restart strategy (`gateway.reload`) and per-section reload handling.
- UI: centralize tool display metadata and show action/detail summaries across Web Chat, SwiftUI, Android, and the TUI. - UI: centralize tool display metadata and show action/detail summaries across Web Chat, SwiftUI, Android, and the TUI.
- Control UI: support configurable base paths (`gateway.controlUi.basePath`, default unchanged) for hosting under URL prefixes. - Control UI: support configurable base paths (`gateway.controlUi.basePath`, default unchanged) for hosting under URL prefixes.
- Onboarding: shared wizard engine powering CLI + macOS via gateway wizard RPC. - Onboarding: shared wizard engine powering CLI + macOS via gateway wizard RPC.
- Config: expose schema + UI hints for generic config forms (Web UI + future clients). - Config: expose schema + UI hints for generic config forms (Web UI + future clients).
- Browser: add multi-profile browser control with per-profile remote CDP URLs — thanks @jamesgroat.
- Skills: add blogwatcher skill for RSS/Atom monitoring — thanks @Hyaxia. - Skills: add blogwatcher skill for RSS/Atom monitoring — thanks @Hyaxia.
- Skills: add Notion API skill — thanks @scald.
- Discord: emit system events for reaction add/remove with per-guild reaction notifications (off|own|all|allowlist) (#140) — thanks @thewilloftheshadow. - Discord: emit system events for reaction add/remove with per-guild reaction notifications (off|own|all|allowlist) (#140) — thanks @thewilloftheshadow.
- Agent: add optional per-session Docker sandbox for tool execution (`agent.sandbox`) with allow/deny policy and auto-pruning. - Agent: add optional per-session Docker sandbox for tool execution (`agent.sandbox`) with allow/deny policy and auto-pruning.
- Agent: add sandboxed Chromium browser (CDP + optional noVNC observer) for sandboxed sessions. - Agent: add sandboxed Chromium browser (CDP + optional noVNC observer) for sandboxed sessions.
- Nodes: add `location.get` with Always/Precise settings on macOS/iOS/Android plus CLI/tool support. - Nodes: add `location.get` with Always/Precise settings on macOS/iOS/Android plus CLI/tool support.
- Sessions: add agent‑to‑agent post step with `ANNOUNCE_SKIP` to suppress channel announcements. - Android nodes: add `sms.send` with permission-gated capability refresh (#172) — thanks @vsabavat.
### Fixes ### Fixes
- Gateway/macOS: keep node presence fresh with periodic beacons + show presence status in Instances (#168) — thanks @mbelinky. - macOS: improve Swift 6 strict concurrency compatibility (#166) — thanks @Nachx639.
- CI: fix lint ordering after merge cleanup (#156) — thanks @steipete. - CI: fix lint ordering after merge cleanup (#156) — thanks @steipete.
- CI: consolidate checks to avoid redundant installs (#144) — thanks @thewilloftheshadow. - CI: consolidate checks to avoid redundant installs (#144) — thanks @thewilloftheshadow.
- WhatsApp: support `gifPlayback` for MP4 GIF sends via CLI/gateway. - WhatsApp: support `gifPlayback` for MP4 GIF sends via CLI/gateway.
- Gateway: log config hot reloads for dynamic-read changes without restarts.
- Sessions: prevent `sessions_send` timeouts by running nested agent turns on a separate lane. - Sessions: prevent `sessions_send` timeouts by running nested agent turns on a separate lane.
- Sessions: use per-send run IDs for gateway agent calls to avoid wait collisions. - Sessions: use per-send run IDs for gateway agent calls to avoid wait collisions.
- Auto-reply: drop final payloads when block streaming to avoid duplicate Discord sends. - Auto-reply: drop final payloads when block streaming to avoid duplicate Discord sends.
- Auto-reply: default elevated to off when not allowed (#183) — thanks @cash-echo-bot.
- Auto-reply: fix typing TTL to 2 minutes and log TTL with s/m units. - Auto-reply: fix typing TTL to 2 minutes and log TTL with s/m units.
- Bash tool: default auto-background delay to 10s. - Bash tool: default auto-background delay to 10s.
- Telegram: chunk block-stream replies to avoid “message is too long” errors (#124) — thanks @mukhtharcm. - Telegram: chunk block-stream replies to avoid “message is too long” errors (#124) — thanks @mukhtharcm.
@@ -74,7 +77,8 @@
- Queue: clarify steer-backlog behavior with inline commands and update examples for streaming surfaces. - Queue: clarify steer-backlog behavior with inline commands and update examples for streaming surfaces.
- Sandbox: document per-session agent sandbox setup, browser image, and Docker build. - Sandbox: document per-session agent sandbox setup, browser image, and Docker build.
- macOS: clarify menu bar uses sessionKey from agent events. - macOS: clarify menu bar uses sessionKey from agent events.
- Sessions: document agent-to-agent post step and `ANNOUNCE_SKIP`. - Sessions: document agent-to-agent reply loop (`REPLY_SKIP`) and announce step (`ANNOUNCE_SKIP`).
- Skills: clarify wacli third-party messaging scope and JID format examples.
## 2.0.0-beta5 — 2026-01-03 ## 2.0.0-beta5 — 2026-01-03
+1 -1
View File
@@ -12,7 +12,7 @@ Welcome to the lobster tank! 🦞
- **Peter Steinberger** - Benevolent Dictator - **Peter Steinberger** - Benevolent Dictator
- GitHub: [@steipete](https://github.com/steipete) · X: [@steipete](https://x.com/steipete) - GitHub: [@steipete](https://github.com/steipete) · X: [@steipete](https://x.com/steipete)
- **Shadow** - Discord + Slack subsystem - **Shadow** - Discord subsystem
- GitHub: [@4shadowed](https://github.com/4shadowed) · X: [@4shad0wed](https://x.com/4shad0wed) - GitHub: [@4shadowed](https://github.com/4shadowed) · X: [@4shad0wed](https://x.com/4shad0wed)
- **Jos** - Telegram, API, Nix mode - **Jos** - Telegram, API, Nix mode
Submodule Peekaboo updated: 5c8eedd642...b69e4e8dc0.
@@ -73,7 +73,6 @@ struct InstancesSettings: View {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 8) { HStack(spacing: 8) {
Text(inst.host ?? "unknown host").font(.subheadline.bold()) Text(inst.host ?? "unknown host").font(.subheadline.bold())
self.presenceIndicator(inst)
if let ip = inst.ip { Text("(") + Text(ip).monospaced() + Text(")") } if let ip = inst.ip { Text("(") + Text(ip).monospaced() + Text(")") }
} }
@@ -147,29 +146,6 @@ struct InstancesSettings: View {
.font(.footnote) .font(.footnote)
} }
private func presenceIndicator(_ inst: InstanceInfo) -> some View {
let status = self.presenceStatus(for: inst)
return HStack(spacing: 4) {
Circle()
.fill(status.color)
.frame(width: 6, height: 6)
.accessibilityHidden(true)
Text(status.label)
.foregroundStyle(.secondary)
}
.font(.caption)
.help("Presence updated \(inst.ageDescription).")
.accessibilityLabel("\(status.label) presence")
}
private func presenceStatus(for inst: InstanceInfo) -> (label: String, color: Color) {
let nowMs = Date().timeIntervalSince1970 * 1000
let ageSeconds = max(0, Int((nowMs - inst.ts) / 1000))
if ageSeconds <= 120 { return ("Active", .green) }
if ageSeconds <= 300 { return ("Idle", .yellow) }
return ("Stale", .gray)
}
@ViewBuilder @ViewBuilder
private func leadingDeviceIcon(_ inst: InstanceInfo, device: DevicePresentation?) -> some View { private func leadingDeviceIcon(_ inst: InstanceInfo, device: DevicePresentation?) -> some View {
let symbol = self.leadingDeviceSymbol(inst, device: device) let symbol = self.leadingDeviceSymbol(inst, device: device)
@@ -331,10 +307,6 @@ struct InstancesSettings: View {
return "Connect" return "Connect"
case "disconnect": case "disconnect":
return "Disconnect" return "Disconnect"
case "node-connected":
return "Node connect"
case "node-disconnected":
return "Node disconnect"
case "launch": case "launch":
return "Launch" return "Launch"
case "periodic": case "periodic":
-57
View File
@@ -292,63 +292,6 @@ Reaction notification modes:
- `all`: all reactions on all messages. - `all`: all reactions on all messages.
- `allowlist`: reactions from `guilds.<id>.users` on all messages (empty list disables). - `allowlist`: reactions from `guilds.<id>.users` on all messages (empty list disables).
### `slack` (socket mode)
Slack runs in Socket Mode and requires both a bot token and app token:
```json5
{
slack: {
enabled: true,
botToken: "xoxb-...",
appToken: "xapp-...",
dm: {
enabled: true,
allowFrom: ["U123", "U456", "*"],
groupEnabled: false,
groupChannels: ["G123"]
},
channels: {
C123: { allow: true, requireMention: true },
"#general": { allow: true, requireMention: false }
},
reactionNotifications: "own", // off | own | all | allowlist
reactionAllowlist: ["U123"],
actions: {
reactions: true,
messages: true,
pins: true,
memberInfo: true,
emojiList: true
},
slashCommand: {
enabled: true,
name: "clawd",
sessionPrefix: "slack:slash",
ephemeral: true
},
textChunkLimit: 4000,
mediaMaxMb: 20
}
}
```
Clawdis starts Slack when the provider is enabled and both tokens are set (via config or `SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN`). Use `user:<id>` (DM) or `channel:<id>` when specifying delivery targets for cron/CLI commands.
Reaction notification modes:
- `off`: no reaction events.
- `own`: reactions on the bot's own messages (default).
- `all`: all reactions on all messages.
- `allowlist`: reactions from `slack.reactionAllowlist` on all messages (empty list disables).
Slack action groups (gate `slack` tool actions):
| Action group | Default | Notes |
| --- | --- | --- |
| reactions | enabled | React + list reactions |
| messages | enabled | Read/send/edit/delete |
| pins | enabled | Pin/unpin/list |
| memberInfo | enabled | Member info |
| emojiList | enabled | Custom emoji list |
### `imessage` (imsg CLI) ### `imessage` (imsg CLI)
Clawdis spawns `imsg rpc` (JSON-RPC over stdio). No daemon or port required. Clawdis spawns `imsg rpc` (JSON-RPC over stdio). No daemon or port required.
+1 -1
View File
@@ -100,7 +100,7 @@ Pairing details: `docs/gateway/pairing.md`.
## 5) Verify the node is connected ## 5) Verify the node is connected
- In the macOS app: **Instances** tab should show something like `iOS Node (...)` with a green “Active” presence dot shortly after connect. - In the macOS app: **Instances** tab should show something like `iOS Node (...)`.
- Via nodes status (paired + connected): - Via nodes status (paired + connected):
```bash ```bash
clawdis nodes status clawdis nodes status
+1 -14
View File
@@ -24,7 +24,7 @@ Presence entries are structured objects with (some) fields:
- `modelIdentifier` (optional): hardware model identifier like `iPad16,6` or `Mac16,6` - `modelIdentifier` (optional): hardware model identifier like `iPad16,6` or `Mac16,6`
- `mode`: e.g. `gateway`, `app`, `webchat`, `cli` - `mode`: e.g. `gateway`, `app`, `webchat`, `cli`
- `lastInputSeconds` (optional): “seconds since last user input” for that client machine - `lastInputSeconds` (optional): “seconds since last user input” for that client machine
- `reason`: a short marker like `self`, `connect`, `node-connected`, `node-disconnected`, `periodic`, `instances-refresh` - `reason`: a short marker like `self`, `connect`, `periodic`, `instances-refresh`
- `text`: legacy/debug summary string (kept for backwards compatibility and UI display) - `text`: legacy/debug summary string (kept for backwards compatibility and UI display)
- `ts`: last update timestamp (ms since epoch) - `ts`: last update timestamp (ms since epoch)
@@ -61,16 +61,6 @@ Implementation:
- Gateway: `src/gateway/server.ts` handles method `system-event` by calling `updateSystemPresence(...)`. - Gateway: `src/gateway/server.ts` handles method `system-event` by calling `updateSystemPresence(...)`.
- mac app beaconing: `apps/macos/Sources/Clawdis/PresenceReporter.swift`. - mac app beaconing: `apps/macos/Sources/Clawdis/PresenceReporter.swift`.
### 4) Node bridge beacons (gateway-owned presence)
When a node bridge connection authenticates, the Gateway emits a presence entry
for that node and starts periodic refresh beacons so it does not expire.
- Connect/disconnect markers: `node-connected`, `node-disconnected`
- Periodic heartbeat: every 3 minutes (`reason: periodic`)
Implementation: `src/gateway/server.ts` (node bridge handlers + timer beacons).
## Merge + dedupe rules (why `instanceId` matters) ## Merge + dedupe rules (why `instanceId` matters)
All producers write into a single in-memory presence map. All producers write into a single in-memory presence map.
@@ -119,9 +109,6 @@ Implementation:
- View: `apps/macos/Sources/Clawdis/InstancesSettings.swift` - View: `apps/macos/Sources/Clawdis/InstancesSettings.swift`
- Store: `apps/macos/Sources/Clawdis/InstancesStore.swift` - Store: `apps/macos/Sources/Clawdis/InstancesStore.swift`
The Instances rows show a small presence indicator (Active/Idle/Stale) based on
the last beacon age. The label is derived from the entry timestamp (`ts`).
The store refreshes periodically and also applies `presence` WS events. The store refreshes periodically and also applies `presence` WS events.
## Debugging tips ## Debugging tips
-148
View File
@@ -1,148 +0,0 @@
# Running Clawdis.app with a Remote Gateway
Clawdis.app uses SSH tunneling to connect to a remote gateway. This guide shows you how to set it up.
## Overview
```
┌─────────────────────────────────────────────────────────────┐
│ MacBook │
│ │
│ Clawdis.app ──► ws://127.0.0.1:18789 (local port) │
│ │ │
│ ▼ │
│ SSH Tunnel ────────────────────────────────────────────────│
│ │ │
└─────────────────────┼──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Remote Machine │
│ │
│ Gateway WebSocket ──► ws://127.0.0.1:18789 ──► │
│ │
└─────────────────────────────────────────────────────────────┘
```
## Quick Setup
### Step 1: Add SSH Config
Edit `~/.ssh/config` and add:
```ssh
Host remote-gateway
HostName <REMOTE_IP> # e.g., 172.27.187.184
User <REMOTE_USER> # e.g., jefferson
LocalForward 18789 127.0.0.1:18789
IdentityFile ~/.ssh/id_rsa
```
Replace `<REMOTE_IP>` and `<REMOTE_USER>` with your values.
### Step 2: Copy SSH Key
Copy your public key to the remote machine (enter password once):
```bash
ssh-copy-id -i ~/.ssh/id_rsa <REMOTE_USER>@<REMOTE_IP>
```
### Step 3: Set Gateway Token
```bash
launchctl setenv CLAWDIS_GATEWAY_TOKEN "<your-token>"
```
### Step 4: Start SSH Tunnel
```bash
ssh -N remote-gateway &
```
### Step 5: Restart Clawdis.app
```bash
killall Clawdis
open /path/to/Clawdis.app
```
The app will now connect to the remote gateway through the SSH tunnel.
---
## Auto-Start Tunnel on Login
To have the SSH tunnel start automatically when you log in, create a Launch Agent.
### Create the PLIST file
Save this as `~/Library/LaunchAgents/com.clawdis.ssh-tunnel.plist`:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.clawdis.ssh-tunnel</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/ssh</string>
<string>-N</string>
<string>remote-gateway</string>
</array>
<key>KeepAlive</key>
<true/>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
```
### Load the Launch Agent
```bash
launchctl load ~/Library/LaunchAgents/com.clawdis.ssh-tunnel.plist
```
The tunnel will now:
- Start automatically when you log in
- Restart if it crashes
- Keep running in the background
---
## Troubleshooting
**Check if tunnel is running:**
```bash
ps aux | grep "ssh -N remote-gateway" | grep -v grep
lsof -i :18789
```
**Restart the tunnel:**
```bash
launchctl restart com.clawdis.ssh-tunnel
```
**Stop the tunnel:**
```bash
launchctl unload ~/Library/LaunchAgents/com.clawdis.ssh-tunnel.plist
```
---
## How It Works
| Component | What It Does |
|-----------|--------------|
| `LocalForward 18789 127.0.0.1:18789` | Forwards local port 18789 to remote port 18789 |
| `ssh -N` | SSH without executing remote commands (just port forwarding) |
| `KeepAlive` | Automatically restarts tunnel if it crashes |
| `RunAtLoad` | Starts tunnel when the agent loads |
Clawdis.app connects to `ws://127.0.0.1:18789` on your MacBook. The SSH tunnel forwards that connection to port 18789 on the remote machine where the Gateway is running.
-158
View File
@@ -1,158 +0,0 @@
# Slack (socket mode)
## Setup
1) Create a Slack app (From scratch) in https://api.slack.com/apps.
2) **Socket Mode** → toggle on. Then go to **Basic Information** → **App-Level Tokens** → **Generate Token and Scopes** with scope `connections:write`. Copy the **App Token** (`xapp-...`).
3) **OAuth & Permissions** → add bot token scopes (use the manifest below). Click **Install to Workspace**. Copy the **Bot User OAuth Token** (`xoxb-...`).
4) **Event Subscriptions** → enable events and subscribe to:
- `message.*` (includes edits/deletes/thread broadcasts)
- `app_mention`
- `reaction_added`, `reaction_removed`
- `member_joined_channel`, `member_left_channel`
- `channel_rename`
- `pin_added`, `pin_removed`
5) Invite the bot to channels you want it to read.
6) Slash Commands → create the `/clawd` command (or your preferred name).
7) App Home → enable the **Messages Tab** so users can DM the bot.
Use the manifest below so scopes and events stay in sync.
## Manifest (optional)
Use this Slack app manifest to create the app quickly (adjust the name/command if you want).
```json
{
"display_information": {
"name": "Clawdbot",
"description": "Slack connector for Clawdbot"
},
"features": {
"bot_user": {
"display_name": "Clawdbot",
"always_online": false
},
"app_home": {
"messages_tab_enabled": true,
"messages_tab_read_only_enabled": false
},
"slash_commands": [
{
"command": "/clawd",
"description": "Send a message to Clawdbot",
"should_escape": false
}
]
},
"oauth_config": {
"scopes": {
"bot": [
"chat:write",
"channels:history",
"channels:read",
"groups:history",
"im:history",
"mpim:history",
"users:read",
"app_mentions:read",
"reactions:read",
"reactions:write",
"pins:read",
"pins:write",
"emoji:read",
"commands",
"files:read",
"files:write"
]
}
},
"settings": {
"socket_mode_enabled": true,
"event_subscriptions": {
"bot_events": [
"app_mention",
"message.channels",
"message.groups",
"message.im",
"message.mpim",
"reaction_added",
"reaction_removed",
"member_joined_channel",
"member_left_channel",
"channel_rename",
"pin_added",
"pin_removed"
]
}
}
}
```
## Config
Slack uses Socket Mode only (no HTTP webhook server). Provide both tokens:
```json
{
"slack": {
"enabled": true,
"botToken": "xoxb-...",
"appToken": "xapp-...",
"dm": {
"enabled": true,
"allowFrom": ["U123", "U456", "*"],
"groupEnabled": false,
"groupChannels": ["G123"]
},
"channels": {
"C123": { "allow": true, "requireMention": true },
"#general": { "allow": true, "requireMention": false }
},
"reactionNotifications": "own",
"reactionAllowlist": ["U123"],
"actions": {
"reactions": true,
"messages": true,
"pins": true,
"memberInfo": true,
"emojiList": true
},
"slashCommand": {
"enabled": true,
"name": "clawd",
"sessionPrefix": "slack:slash",
"ephemeral": true
},
"textChunkLimit": 4000,
"mediaMaxMb": 20
}
}
```
Tokens can also be supplied via env vars:
- `SLACK_BOT_TOKEN`
- `SLACK_APP_TOKEN`
## Sessions + routing
- DMs share the `main` session (like WhatsApp/Telegram).
- Channels map to `slack:channel:<channelId>` sessions.
- Slash commands use `slack:slash:<userId>` sessions.
## Delivery targets
Use these with cron/CLI sends:
- `user:<id>` for DMs
- `channel:<id>` for channels
## Tool actions
Slack tool actions can be gated with `slack.actions.*`:
| Action group | Default | Notes |
| --- | --- | --- |
| reactions | enabled | React + list reactions |
| messages | enabled | Read/send/edit/delete |
| pins | enabled | Pin/unpin/list |
| memberInfo | enabled | Member info |
| emojiList | enabled | Custom emoji list |
## Notes
- Mention gating is controlled via `slack.channels` (set `requireMention` to `true`).
- Reaction notifications follow `slack.reactionNotifications` (use `reactionAllowlist` with mode `allowlist`).
- Attachments are downloaded to the media store when permitted and under the size limit.
+2 -3
View File
@@ -79,8 +79,6 @@
"@mariozechner/pi-ai": "^0.32.3", "@mariozechner/pi-ai": "^0.32.3",
"@mariozechner/pi-coding-agent": "^0.32.3", "@mariozechner/pi-coding-agent": "^0.32.3",
"@mariozechner/pi-tui": "^0.32.3", "@mariozechner/pi-tui": "^0.32.3",
"@slack/bolt": "^4.5.0",
"@slack/web-api": "^7.11.1",
"@sinclair/typebox": "0.34.46", "@sinclair/typebox": "0.34.46",
"@whiskeysockets/baileys": "7.0.0-rc.9", "@whiskeysockets/baileys": "7.0.0-rc.9",
"ajv": "^8.17.1", "ajv": "^8.17.1",
@@ -140,7 +138,8 @@
"@sinclair/typebox": "0.34.46" "@sinclair/typebox": "0.34.46"
}, },
"patchedDependencies": { "patchedDependencies": {
"@mariozechner/pi-ai": "patches/@mariozechner__pi-ai.patch" "@mariozechner/pi-ai": "patches/@mariozechner__pi-ai.patch",
"@mariozechner/pi-coding-agent@0.32.3": "patches/@mariozechner__pi-coding-agent@0.32.3.patch"
} }
}, },
"vitest": { "vitest": {
+3 -39
View File
@@ -1,39 +1,8 @@
diff --git a/dist/providers/google-shared.js b/dist/providers/google-shared.js diff --git a/dist/providers/google-shared.js b/dist/providers/google-shared.js
index 7bc0a9f5d6241f191cd607ecb37b3acac8d58267..76166a34784cbc0718d4b9bd1fa6336a6dd394ec 100644 index ff9cbcfebfac6b4370d85dc838f5cacf2a60ed64..42096c82aec925b412258348a36ba4a7025b283b 100644
--- a/dist/providers/google-shared.js --- a/dist/providers/google-shared.js
+++ b/dist/providers/google-shared.js +++ b/dist/providers/google-shared.js
@@ -51,9 +51,19 @@ export function convertMessages(model, context) { @@ -140,6 +140,72 @@ export function convertMessages(model, context) {
parts.push({ text: sanitizeSurrogates(block.text) });
}
else if (block.type === "thinking") {
- // Thinking blocks require signatures for Claude via Antigravity.
- // If signature is missing (e.g. from GPT-OSS), convert to regular text with delimiters.
- if (block.thinkingSignature) {
+ // Thinking blocks handling varies by model:
+ // - Claude via Antigravity: requires thinkingSignature
+ // - Gemini: skip entirely (doesn't understand thoughtSignature, and mimics <thinking> tags)
+ // - Other models: convert to text with delimiters
+ const isGemini = model.id.toLowerCase().includes("gemini");
+ const isClaude = model.id.toLowerCase().includes("claude");
+ if (isGemini) {
+ // Skip thinking blocks entirely for Gemini - it doesn't support them
+ // and will mimic <thinking> tags if we convert to text
+ continue;
+ }
+ else if (block.thinkingSignature && isClaude) {
+ // Claude via Antigravity requires the signature
parts.push({
thought: true,
text: sanitizeSurrogates(block.thinking),
@@ -61,6 +71,7 @@ export function convertMessages(model, context) {
});
}
else {
+ // Other models: convert to text with delimiters
parts.push({
text: `<thinking>\n${sanitizeSurrogates(block.thinking)}\n</thinking>`,
});
@@ -146,6 +157,77 @@ export function convertMessages(model, context) {
} }
return contents; return contents;
} }
@@ -101,17 +70,12 @@ index 7bc0a9f5d6241f191cd607ecb37b3acac8d58267..76166a34784cbc0718d4b9bd1fa6336a
+ sanitized[key] = value; + sanitized[key] = value;
+ } + }
+ } + }
+ // Ensure type: "object" is present when properties or required exist
+ // Google API requires type to be set when these fields are present
+ if (('properties' in sanitized || 'required' in sanitized) && !('type' in sanitized)) {
+ sanitized.type = 'object';
+ }
+ return sanitized; + return sanitized;
+} +}
/** /**
* Convert tools to Gemini function declarations format. * Convert tools to Gemini function declarations format.
*/ */
@@ -157,7 +239,7 @@ export function convertTools(tools) { @@ -151,7 +216,7 @@ export function convertTools(tools) {
functionDeclarations: tools.map((tool) => ({ functionDeclarations: tools.map((tool) => ({
name: tool.name, name: tool.name,
description: tool.description, description: tool.description,
+15 -325
View File
@@ -9,8 +9,11 @@ overrides:
patchedDependencies: patchedDependencies:
'@mariozechner/pi-ai': '@mariozechner/pi-ai':
hash: 628fb051b6f4886984a846a5ee7aa0a571c3360d35b8d114e4684e5edcd100c5 hash: 969db6f3f4cc91fec48124e1f5e515b386b1f1bed807769d0a80c28abadbaaae
path: patches/@mariozechner__pi-ai.patch path: patches/@mariozechner__pi-ai.patch
'@mariozechner/pi-coding-agent@0.32.3':
hash: d0d5ffa1bfda8a0f9d14a5e73a074014346d3edbdb2ffc91444d3be5119f5745
path: patches/@mariozechner__pi-coding-agent@0.32.3.patch
importers: importers:
@@ -30,22 +33,16 @@ importers:
version: 0.32.3(ws@8.18.3)(zod@4.3.4) version: 0.32.3(ws@8.18.3)(zod@4.3.4)
'@mariozechner/pi-ai': '@mariozechner/pi-ai':
specifier: ^0.32.3 specifier: ^0.32.3
version: 0.32.3(patch_hash=628fb051b6f4886984a846a5ee7aa0a571c3360d35b8d114e4684e5edcd100c5)(ws@8.18.3)(zod@4.3.4) version: 0.32.3(patch_hash=969db6f3f4cc91fec48124e1f5e515b386b1f1bed807769d0a80c28abadbaaae)(ws@8.18.3)(zod@4.3.4)
'@mariozechner/pi-coding-agent': '@mariozechner/pi-coding-agent':
specifier: ^0.32.3 specifier: ^0.32.3
version: 0.32.3(ws@8.18.3)(zod@4.3.4) version: 0.32.3(patch_hash=d0d5ffa1bfda8a0f9d14a5e73a074014346d3edbdb2ffc91444d3be5119f5745)(ws@8.18.3)(zod@4.3.4)
'@mariozechner/pi-tui': '@mariozechner/pi-tui':
specifier: ^0.32.3 specifier: ^0.32.3
version: 0.32.3 version: 0.32.3
'@sinclair/typebox': '@sinclair/typebox':
specifier: 0.34.46 specifier: 0.34.46
version: 0.34.46 version: 0.34.46
'@slack/bolt':
specifier: ^4.5.0
version: 4.6.0(@types/express@5.0.6)
'@slack/web-api':
specifier: ^7.11.1
version: 7.13.0
'@whiskeysockets/baileys': '@whiskeysockets/baileys':
specifier: 7.0.0-rc.9 specifier: 7.0.0-rc.9
version: 7.0.0-rc.9(audio-decode@2.2.3)(sharp@0.34.5) version: 7.0.0-rc.9(audio-decode@2.2.3)(sharp@0.34.5)
@@ -1101,32 +1098,6 @@ packages:
'@sinclair/typebox@0.34.46': '@sinclair/typebox@0.34.46':
resolution: {integrity: sha512-kiW7CtS/NkdvTUjkjUJo7d5JsFfbJ14YjdhDk9KoEgK6nFjKNXZPrX0jfLA8ZlET4cFLHxOZ/0vFKOP+bOxIOQ==} resolution: {integrity: sha512-kiW7CtS/NkdvTUjkjUJo7d5JsFfbJ14YjdhDk9KoEgK6nFjKNXZPrX0jfLA8ZlET4cFLHxOZ/0vFKOP+bOxIOQ==}
'@slack/bolt@4.6.0':
resolution: {integrity: sha512-xPgfUs2+OXSugz54Ky07pA890+Qydk22SYToi8uGpXeHSt1JWwFJkRyd/9Vlg5I1AdfdpGXExDpwnbuN9Q/2dQ==}
engines: {node: '>=18', npm: '>=8.6.0'}
peerDependencies:
'@types/express': ^5.0.0
'@slack/logger@4.0.0':
resolution: {integrity: sha512-Wz7QYfPAlG/DR+DfABddUZeNgoeY7d1J39OCR2jR+v7VBsB8ezulDK5szTnDDPDwLH5IWhLvXIHlCFZV7MSKgA==}
engines: {node: '>= 18', npm: '>= 8.6.0'}
'@slack/oauth@3.0.4':
resolution: {integrity: sha512-+8H0g7mbrHndEUbYCP7uYyBCbwqmm3E6Mo3nfsDvZZW74zKk1ochfH/fWSvGInYNCVvaBUbg3RZBbTp0j8yJCg==}
engines: {node: '>=18', npm: '>=8.6.0'}
'@slack/socket-mode@2.0.5':
resolution: {integrity: sha512-VaapvmrAifeFLAFaDPfGhEwwunTKsI6bQhYzxRXw7BSujZUae5sANO76WqlVsLXuhVtCVrBWPiS2snAQR2RHJQ==}
engines: {node: '>= 18', npm: '>= 8.6.0'}
'@slack/types@2.19.0':
resolution: {integrity: sha512-7+QZ38HGcNh/b/7MpvPG6jnw7mliV6UmrquJLqgdxkzJgQEYUcEztvFWRU49z0x4vthF0ixL5lTK601AXrS8IA==}
engines: {node: '>= 12.13.0', npm: '>= 6.12.0'}
'@slack/web-api@7.13.0':
resolution: {integrity: sha512-ERcExbWrnkDN8ovoWWe6Wgt/usanj1dWUd18dJLpctUI4mlPS0nKt81Joh8VI+OPbNnY1lIilVt9gdMBD9U2ig==}
engines: {node: '>= 18', npm: '>= 8.6.0'}
'@standard-schema/spec@1.1.0': '@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
@@ -1185,9 +1156,6 @@ packages:
'@types/http-errors@2.0.5': '@types/http-errors@2.0.5':
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
'@types/jsonwebtoken@9.0.10':
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
'@types/linkify-it@5.0.0': '@types/linkify-it@5.0.0':
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
@@ -1203,9 +1171,6 @@ packages:
'@types/mime-types@2.1.4': '@types/mime-types@2.1.4':
resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==} resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==}
'@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
'@types/node@10.17.60': '@types/node@10.17.60':
resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==} resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==}
@@ -1221,9 +1186,6 @@ 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/retry@0.12.0':
resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
'@types/send@1.2.1': '@types/send@1.2.1':
resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==}
@@ -1398,9 +1360,6 @@ packages:
async-mutex@0.5.0: async-mutex@0.5.0:
resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
atomic-sleep@1.0.0: atomic-sleep@1.0.0:
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
engines: {node: '>=8.0.0'} engines: {node: '>=8.0.0'}
@@ -1415,9 +1374,6 @@ packages:
resolution: {integrity: sha512-En9AY6EG1qYqEy5L/quryzbA4akBpJrnBZNxeKTqGHC2xT9Qc4aZ8b7CcbOMFTTc/MGdoNyp+SN4zInZNKxMYA==} resolution: {integrity: sha512-En9AY6EG1qYqEy5L/quryzbA4akBpJrnBZNxeKTqGHC2xT9Qc4aZ8b7CcbOMFTTc/MGdoNyp+SN4zInZNKxMYA==}
engines: {node: '>=14'} engines: {node: '>=14'}
axios@1.13.2:
resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
balanced-match@1.0.2: balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@@ -1533,10 +1489,6 @@ packages:
color-name@1.1.4: color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
commander@14.0.2: commander@14.0.2:
resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==}
engines: {node: '>=20'} engines: {node: '>=20'}
@@ -1591,10 +1543,6 @@ packages:
supports-color: supports-color:
optional: true optional: true
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
depd@2.0.0: depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@@ -1673,10 +1621,6 @@ packages:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
es-set-tostringtag@2.1.0:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
esbuild@0.27.2: esbuild@0.27.2:
resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -1700,9 +1644,6 @@ packages:
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
eventemitter3@4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
eventemitter3@5.0.1: eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
@@ -1759,23 +1700,10 @@ packages:
resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==}
engines: {node: '>= 18.0.0'} engines: {node: '>= 18.0.0'}
follow-redirects@1.15.11:
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
foreground-child@3.3.1: foreground-child@3.3.1:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'} engines: {node: '>=14'}
form-data@4.0.5:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'}
formdata-polyfill@4.0.10: formdata-polyfill@4.0.10:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
engines: {node: '>=12.20.0'} engines: {node: '>=12.20.0'}
@@ -1872,10 +1800,6 @@ packages:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
has-tostringtag@1.0.2:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
hashery@1.4.0: hashery@1.4.0:
resolution: {integrity: sha512-Wn2i1In6XFxl8Az55kkgnFRiAlIAushzh26PTjL2AKtQcEfXrcLa7Hn5QOWGZEf3LU057P9TwwZjFyxfS1VuvQ==} resolution: {integrity: sha512-Wn2i1In6XFxl8Az55kkgnFRiAlIAushzh26PTjL2AKtQcEfXrcLa7Hn5QOWGZEf3LU057P9TwwZjFyxfS1VuvQ==}
engines: {node: '>=20'} engines: {node: '>=20'}
@@ -1929,9 +1853,6 @@ packages:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'} engines: {node: '>=8'}
is-electron@2.2.2:
resolution: {integrity: sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==}
is-extglob@2.1.1: is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -1951,10 +1872,6 @@ packages:
is-promise@4.0.0: is-promise@4.0.0:
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
is-stream@2.0.1:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'}
is-url@1.2.4: is-url@1.2.4:
resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
@@ -2018,10 +1935,6 @@ packages:
jsonc-parser@3.3.1: jsonc-parser@3.3.1:
resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==}
jsonwebtoken@9.0.3:
resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==}
engines: {node: '>=12', npm: '>=6'}
jszip@3.10.1: jszip@3.10.1:
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
@@ -2123,27 +2036,6 @@ packages:
lit@3.3.2: lit@3.3.2:
resolution: {integrity: sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==} resolution: {integrity: sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==}
lodash.includes@4.3.0:
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
lodash.isboolean@3.0.3:
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
lodash.isinteger@4.0.4:
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
lodash.isnumber@3.0.3:
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
lodash.isplainobject@4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
lodash.isstring@4.0.1:
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
lodash.once@4.1.1:
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
lodash.snakecase@4.1.1: lodash.snakecase@4.1.1:
resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==}
@@ -2223,18 +2115,10 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'} engines: {node: '>=8.6'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-db@1.54.0: mime-db@1.54.0:
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
mime-types@3.0.2: mime-types@3.0.2:
resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -2280,6 +2164,9 @@ packages:
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
node-addon-api@7.1.1:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
node-domexception@1.0.0: node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'} engines: {node: '>=10.5.0'}
@@ -2363,26 +2250,10 @@ packages:
oxlint-tsgolint: oxlint-tsgolint:
optional: true optional: true
p-finally@1.0.0:
resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==}
engines: {node: '>=4'}
p-queue@6.6.2:
resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==}
engines: {node: '>=8'}
p-queue@9.0.1: p-queue@9.0.1:
resolution: {integrity: sha512-RhBdVhSwJb7Ocn3e8ULk4NMwBEuOxe+1zcgphUy9c2e5aR/xbEsdVXxHJ3lynw6Qiqu7OINEyHlZkiblEpaq7w==} resolution: {integrity: sha512-RhBdVhSwJb7Ocn3e8ULk4NMwBEuOxe+1zcgphUy9c2e5aR/xbEsdVXxHJ3lynw6Qiqu7OINEyHlZkiblEpaq7w==}
engines: {node: '>=20'} engines: {node: '>=20'}
p-retry@4.6.2:
resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==}
engines: {node: '>=8'}
p-timeout@3.2.0:
resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==}
engines: {node: '>=8'}
p-timeout@7.0.1: p-timeout@7.0.1:
resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==} resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==}
engines: {node: '>=20'} engines: {node: '>=20'}
@@ -2506,9 +2377,6 @@ packages:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
punycode.js@2.3.1: punycode.js@2.3.1:
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -2582,10 +2450,6 @@ packages:
resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
retry@0.13.1:
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
engines: {node: '>= 4'}
reusify@1.1.0: reusify@1.1.0:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'} engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
@@ -2833,10 +2697,6 @@ packages:
resolution: {integrity: sha512-XuELoRpMR+sq8fuWwX7P0bcj+PRNiicOKDEb3fGNURhxWVyykCi9BNq7c4uVz7h7P0sj8qgBsr5SWS6yBClq3g==} resolution: {integrity: sha512-XuELoRpMR+sq8fuWwX7P0bcj+PRNiicOKDEb3fGNURhxWVyykCi9BNq7c4uVz7h7P0sj8qgBsr5SWS6yBClq3g==}
engines: {node: '>=16'} engines: {node: '>=16'}
tsscmp@1.0.6:
resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==}
engines: {node: '>=0.6.x'}
tsx@4.21.0: tsx@4.21.0:
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@@ -3487,7 +3347,7 @@ snapshots:
'@mariozechner/pi-agent-core@0.32.3(ws@8.18.3)(zod@4.3.4)': '@mariozechner/pi-agent-core@0.32.3(ws@8.18.3)(zod@4.3.4)':
dependencies: dependencies:
'@mariozechner/pi-ai': 0.32.3(patch_hash=628fb051b6f4886984a846a5ee7aa0a571c3360d35b8d114e4684e5edcd100c5)(ws@8.18.3)(zod@4.3.4) '@mariozechner/pi-ai': 0.32.3(patch_hash=969db6f3f4cc91fec48124e1f5e515b386b1f1bed807769d0a80c28abadbaaae)(ws@8.18.3)(zod@4.3.4)
'@mariozechner/pi-tui': 0.32.3 '@mariozechner/pi-tui': 0.32.3
transitivePeerDependencies: transitivePeerDependencies:
- '@modelcontextprotocol/sdk' - '@modelcontextprotocol/sdk'
@@ -3497,7 +3357,7 @@ snapshots:
- ws - ws
- zod - zod
'@mariozechner/pi-ai@0.32.3(patch_hash=628fb051b6f4886984a846a5ee7aa0a571c3360d35b8d114e4684e5edcd100c5)(ws@8.18.3)(zod@4.3.4)': '@mariozechner/pi-ai@0.32.3(patch_hash=969db6f3f4cc91fec48124e1f5e515b386b1f1bed807769d0a80c28abadbaaae)(ws@8.18.3)(zod@4.3.4)':
dependencies: dependencies:
'@anthropic-ai/sdk': 0.71.2(zod@4.3.4) '@anthropic-ai/sdk': 0.71.2(zod@4.3.4)
'@google/genai': 1.34.0 '@google/genai': 1.34.0
@@ -3517,10 +3377,10 @@ snapshots:
- ws - ws
- zod - zod
'@mariozechner/pi-coding-agent@0.32.3(ws@8.18.3)(zod@4.3.4)': '@mariozechner/pi-coding-agent@0.32.3(patch_hash=d0d5ffa1bfda8a0f9d14a5e73a074014346d3edbdb2ffc91444d3be5119f5745)(ws@8.18.3)(zod@4.3.4)':
dependencies: dependencies:
'@mariozechner/pi-agent-core': 0.32.3(ws@8.18.3)(zod@4.3.4) '@mariozechner/pi-agent-core': 0.32.3(ws@8.18.3)(zod@4.3.4)
'@mariozechner/pi-ai': 0.32.3(patch_hash=628fb051b6f4886984a846a5ee7aa0a571c3360d35b8d114e4684e5edcd100c5)(ws@8.18.3)(zod@4.3.4) '@mariozechner/pi-ai': 0.32.3(patch_hash=969db6f3f4cc91fec48124e1f5e515b386b1f1bed807769d0a80c28abadbaaae)(ws@8.18.3)(zod@4.3.4)
'@mariozechner/pi-tui': 0.32.3 '@mariozechner/pi-tui': 0.32.3
chalk: 5.6.2 chalk: 5.6.2
cli-highlight: 2.1.11 cli-highlight: 2.1.11
@@ -3766,71 +3626,6 @@ snapshots:
'@sinclair/typebox@0.34.46': {} '@sinclair/typebox@0.34.46': {}
'@slack/bolt@4.6.0(@types/express@5.0.6)':
dependencies:
'@slack/logger': 4.0.0
'@slack/oauth': 3.0.4
'@slack/socket-mode': 2.0.5
'@slack/types': 2.19.0
'@slack/web-api': 7.13.0
'@types/express': 5.0.6
axios: 1.13.2
express: 5.2.1
path-to-regexp: 8.3.0
raw-body: 3.0.2
tsscmp: 1.0.6
transitivePeerDependencies:
- bufferutil
- debug
- supports-color
- utf-8-validate
'@slack/logger@4.0.0':
dependencies:
'@types/node': 25.0.3
'@slack/oauth@3.0.4':
dependencies:
'@slack/logger': 4.0.0
'@slack/web-api': 7.13.0
'@types/jsonwebtoken': 9.0.10
'@types/node': 25.0.3
jsonwebtoken: 9.0.3
transitivePeerDependencies:
- debug
'@slack/socket-mode@2.0.5':
dependencies:
'@slack/logger': 4.0.0
'@slack/web-api': 7.13.0
'@types/node': 25.0.3
'@types/ws': 8.18.1
eventemitter3: 5.0.1
ws: 8.18.3
transitivePeerDependencies:
- bufferutil
- debug
- utf-8-validate
'@slack/types@2.19.0': {}
'@slack/web-api@7.13.0':
dependencies:
'@slack/logger': 4.0.0
'@slack/types': 2.19.0
'@types/node': 25.0.3
'@types/retry': 0.12.0
axios: 1.13.2
eventemitter3: 5.0.1
form-data: 4.0.5
is-electron: 2.2.2
is-stream: 2.0.1
p-queue: 6.6.2
p-retry: 4.6.2
retry: 0.13.1
transitivePeerDependencies:
- debug
'@standard-schema/spec@1.1.0': {} '@standard-schema/spec@1.1.0': {}
'@testing-library/dom@10.4.1': '@testing-library/dom@10.4.1':
@@ -3908,11 +3703,6 @@ snapshots:
'@types/http-errors@2.0.5': {} '@types/http-errors@2.0.5': {}
'@types/jsonwebtoken@9.0.10':
dependencies:
'@types/ms': 2.1.0
'@types/node': 25.0.3
'@types/linkify-it@5.0.0': {} '@types/linkify-it@5.0.0': {}
'@types/long@4.0.2': {} '@types/long@4.0.2': {}
@@ -3926,8 +3716,6 @@ snapshots:
'@types/mime-types@2.1.4': {} '@types/mime-types@2.1.4': {}
'@types/ms@2.1.0': {}
'@types/node@10.17.60': {} '@types/node@10.17.60': {}
'@types/node@25.0.3': '@types/node@25.0.3':
@@ -3940,8 +3728,6 @@ snapshots:
'@types/range-parser@1.2.7': {} '@types/range-parser@1.2.7': {}
'@types/retry@0.12.0': {}
'@types/send@1.2.1': '@types/send@1.2.1':
dependencies: dependencies:
'@types/node': 25.0.3 '@types/node': 25.0.3
@@ -4175,8 +3961,6 @@ snapshots:
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
asynckit@0.4.0: {}
atomic-sleep@1.0.0: {} atomic-sleep@1.0.0: {}
audio-buffer@5.0.0: audio-buffer@5.0.0:
@@ -4197,14 +3981,6 @@ snapshots:
audio-type@2.2.1: audio-type@2.2.1:
optional: true optional: true
axios@1.13.2:
dependencies:
follow-redirects: 1.15.11
form-data: 4.0.5
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
balanced-match@1.0.2: {} balanced-match@1.0.2: {}
balanced-match@3.0.1: {} balanced-match@3.0.1: {}
@@ -4337,10 +4113,6 @@ snapshots:
color-name@1.1.4: {} color-name@1.1.4: {}
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
commander@14.0.2: {} commander@14.0.2: {}
commander@8.3.0: {} commander@8.3.0: {}
@@ -4377,8 +4149,6 @@ snapshots:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
delayed-stream@1.0.0: {}
depd@2.0.0: {} depd@2.0.0: {}
dequal@2.0.3: dequal@2.0.3:
@@ -4452,13 +4222,6 @@ snapshots:
dependencies: dependencies:
es-errors: 1.3.0 es-errors: 1.3.0
es-set-tostringtag@2.1.0:
dependencies:
es-errors: 1.3.0
get-intrinsic: 1.3.0
has-tostringtag: 1.0.2
hasown: 2.0.2
esbuild@0.27.2: esbuild@0.27.2:
optionalDependencies: optionalDependencies:
'@esbuild/aix-ppc64': 0.27.2 '@esbuild/aix-ppc64': 0.27.2
@@ -4500,8 +4263,6 @@ snapshots:
event-target-shim@5.0.1: {} event-target-shim@5.0.1: {}
eventemitter3@4.0.7: {}
eventemitter3@5.0.1: {} eventemitter3@5.0.1: {}
events@3.3.0: {} events@3.3.0: {}
@@ -4592,21 +4353,11 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
follow-redirects@1.15.11: {}
foreground-child@3.3.1: foreground-child@3.3.1:
dependencies: dependencies:
cross-spawn: 7.0.6 cross-spawn: 7.0.6
signal-exit: 4.1.0 signal-exit: 4.1.0
form-data@4.0.5:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
hasown: 2.0.2
mime-types: 2.1.35
formdata-polyfill@4.0.10: formdata-polyfill@4.0.10:
dependencies: dependencies:
fetch-blob: 3.2.0 fetch-blob: 3.2.0
@@ -4727,10 +4478,6 @@ snapshots:
has-symbols@1.1.0: {} has-symbols@1.1.0: {}
has-tostringtag@1.0.2:
dependencies:
has-symbols: 1.1.0
hashery@1.4.0: hashery@1.4.0:
dependencies: dependencies:
hookified: 1.14.0 hookified: 1.14.0
@@ -4780,8 +4527,6 @@ snapshots:
dependencies: dependencies:
binary-extensions: 2.3.0 binary-extensions: 2.3.0
is-electron@2.2.2: {}
is-extglob@2.1.1: {} is-extglob@2.1.1: {}
is-fullwidth-code-point@3.0.0: {} is-fullwidth-code-point@3.0.0: {}
@@ -4794,8 +4539,6 @@ snapshots:
is-promise@4.0.0: {} is-promise@4.0.0: {}
is-stream@2.0.1: {}
is-url@1.2.4: {} is-url@1.2.4: {}
isarray@1.0.0: {} isarray@1.0.0: {}
@@ -4857,19 +4600,6 @@ snapshots:
jsonc-parser@3.3.1: {} jsonc-parser@3.3.1: {}
jsonwebtoken@9.0.3:
dependencies:
jws: 4.0.1
lodash.includes: 4.3.0
lodash.isboolean: 3.0.3
lodash.isinteger: 4.0.4
lodash.isnumber: 3.0.3
lodash.isplainobject: 4.0.6
lodash.isstring: 4.0.1
lodash.once: 4.1.1
ms: 2.1.3
semver: 7.7.3
jszip@3.10.1: jszip@3.10.1:
dependencies: dependencies:
lie: 3.3.0 lie: 3.3.0
@@ -4970,20 +4700,6 @@ snapshots:
lit-element: 4.2.2 lit-element: 4.2.2
lit-html: 3.3.2 lit-html: 3.3.2
lodash.includes@4.3.0: {}
lodash.isboolean@3.0.3: {}
lodash.isinteger@4.0.4: {}
lodash.isnumber@3.0.3: {}
lodash.isplainobject@4.0.6: {}
lodash.isstring@4.0.1: {}
lodash.once@4.1.1: {}
lodash.snakecase@4.1.1: {} lodash.snakecase@4.1.1: {}
lodash@4.17.21: {} lodash@4.17.21: {}
@@ -5047,14 +4763,8 @@ snapshots:
braces: 3.0.3 braces: 3.0.3
picomatch: 2.3.1 picomatch: 2.3.1
mime-db@1.52.0: {}
mime-db@1.54.0: {} mime-db@1.54.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
mime-types@3.0.2: mime-types@3.0.2:
dependencies: dependencies:
mime-db: 1.54.0 mime-db: 1.54.0
@@ -5104,6 +4814,8 @@ snapshots:
negotiator@1.0.0: {} negotiator@1.0.0: {}
node-addon-api@7.1.1: {}
node-domexception@1.0.0: {} node-domexception@1.0.0: {}
node-fetch@2.7.0: node-fetch@2.7.0:
@@ -5180,27 +4892,11 @@ snapshots:
'@oxlint/win32-x64': 1.36.0 '@oxlint/win32-x64': 1.36.0
oxlint-tsgolint: 0.10.1 oxlint-tsgolint: 0.10.1
p-finally@1.0.0: {}
p-queue@6.6.2:
dependencies:
eventemitter3: 4.0.7
p-timeout: 3.2.0
p-queue@9.0.1: p-queue@9.0.1:
dependencies: dependencies:
eventemitter3: 5.0.1 eventemitter3: 5.0.1
p-timeout: 7.0.1 p-timeout: 7.0.1
p-retry@4.6.2:
dependencies:
'@types/retry': 0.12.0
retry: 0.13.1
p-timeout@3.2.0:
dependencies:
p-finally: 1.0.0
p-timeout@7.0.1: {} p-timeout@7.0.1: {}
package-json-from-dist@1.0.1: {} package-json-from-dist@1.0.1: {}
@@ -5340,8 +5036,6 @@ snapshots:
forwarded: 0.2.0 forwarded: 0.2.0
ipaddr.js: 1.9.1 ipaddr.js: 1.9.1
proxy-from-env@1.1.0: {}
punycode.js@2.3.1: {} punycode.js@2.3.1: {}
qified@0.5.3: qified@0.5.3:
@@ -5428,8 +5122,6 @@ snapshots:
retry@0.12.0: {} retry@0.12.0: {}
retry@0.13.1: {}
reusify@1.1.0: {} reusify@1.1.0: {}
rimraf@5.0.10: rimraf@5.0.10:
@@ -5740,8 +5432,6 @@ snapshots:
tslog@4.10.2: {} tslog@4.10.2: {}
tsscmp@1.0.6: {}
tsx@4.21.0: tsx@4.21.0:
dependencies: dependencies:
esbuild: 0.27.2 esbuild: 0.27.2
-68
View File
@@ -205,71 +205,3 @@ git worktree remove /tmp/issue-99
6. **Parallel is OK** — run many Codex processes at once for batch work 6. **Parallel is OK** — run many Codex processes at once for batch work
7. **NEVER start Codex in ~/clawd/** — it'll read your soul docs and get weird ideas about the org chart! Use the target project dir or /tmp for blank slate chats 7. **NEVER start Codex in ~/clawd/** — it'll read your soul docs and get weird ideas about the org chart! Use the target project dir or /tmp for blank slate chats
8. **NEVER checkout branches in ~/Projects/clawdis/** — that's the LIVE Clawdis instance! Clone to /tmp or use git worktree for PR reviews 8. **NEVER checkout branches in ~/Projects/clawdis/** — that's the LIVE Clawdis instance! Clone to /tmp or use git worktree for PR reviews
---
## PR Template (The Razor Standard)
When submitting PRs to external repos, use this format for quality & maintainer-friendliness:
````markdown
## Original Prompt
[Exact request/problem statement]
## What this does
[High-level description]
**Features:**
- [Key feature 1]
- [Key feature 2]
**Example usage:**
```bash
# Example
command example
```
````
## Feature intent (maintainer-friendly)
[Why useful, how it fits, workflows it enables]
## Prompt history (timestamped)
- YYYY-MM-DD HH:MM UTC: [Step 1]
- YYYY-MM-DD HH:MM UTC: [Step 2]
## How I tested
**Manual verification:**
1. [Test step] - Output: `[result]`
2. [Test step] - Result: [result]
**Files tested:**
- [Detail]
- [Edge cases]
## Session logs (implementation)
- [What was researched]
- [What was discovered]
- [Time spent]
## Implementation details
**New files:**
- `path/file.ts` - [description]
**Modified files:**
- `path/file.ts` - [change]
**Technical notes:**
- [Detail 1]
- [Detail 2]
---
*Submitted by Razor 🥷 - Mariano's AI agent*
```
**Key principles:**
1. Human-written description (no AI slop)
2. Feature intent for maintainers
3. Timestamped prompt history
4. Session logs if using Codex/agent
**Example:** https://github.com/steipete/bird/pull/22
+11 -22
View File
@@ -1,38 +1,27 @@
--- ---
name: mcporter name: mcporter
description: Use the mcporter CLI to list, configure, auth, and call MCP servers/tools directly (HTTP or stdio), including ad-hoc servers, config edits, and CLI/type generation. description: Manage and call MCP servers (list, call, auth, daemon).
homepage: http://mcporter.dev homepage: http://mcporter.dev
metadata: {"clawdis":{"emoji":"📦","requires":{"bins":["mcporter"]},"install":[{"id":"node","kind":"node","package":"mcporter","bins":["mcporter"],"label":"Install mcporter (node)"}]}} metadata: {"clawdis":{"emoji":"📦","requires":{"bins":["mcporter"]},"install":[{"id":"node","kind":"node","package":"mcporter","bins":["mcporter"],"label":"Install mcporter (node)"}]}}
--- ---
# mcporter # mcporter
Use `mcporter` to work with MCP servers directly. Use `mcporter` to list MCP servers and call tools.
Quick start Quick start
- `mcporter list` - `mcporter list`
- `mcporter list <server> --schema` - `mcporter list <server> --schema`
- `mcporter call <server.tool> key=value` - `mcporter call <server.tool> arg=value`
Call tools Auth + lifecycle
- Selector: `mcporter call linear.list_issues team=ENG limit:5` - OAuth: `mcporter auth <server>`
- Function syntax: `mcporter call "linear.create_issue(title: \"Bug\")"` - Daemon: `mcporter daemon status|start|stop`
- Full URL: `mcporter call https://api.example.com/mcp.fetch url:https://example.com`
- Stdio: `mcporter call --stdio "bun run ./server.ts" scrape url=https://example.com`
- JSON payload: `mcporter call <server.tool> --args '{"limit":5}'`
Auth + config Ad-hoc servers
- OAuth: `mcporter auth <server | url> [--reset]` - HTTP: `mcporter list --http-url https://host/mcp --name <name>`
- Config: `mcporter config list|get|add|remove|import|login|logout` - STDIO: `mcporter call --stdio "bun run ./server.ts" --name <name>`
Daemon
- `mcporter daemon start|status|stop|restart`
Codegen
- CLI: `mcporter generate-cli --server <name>` or `--command <url>`
- Inspect: `mcporter inspect-cli <path> [--json]`
- TS: `mcporter emit-ts <server> --mode client|types`
Notes Notes
- Config default: `./config/mcporter.json` (override with `--config`). - Config sources: `~/.mcporter/mcporter.json[c]` and `config/mcporter.json`.
- Prefer `--output json` for machine-readable results. - Prefer `--json` when you need machine-readable output.
-143
View File
@@ -1,143 +0,0 @@
---
name: slack
description: Use when you need to control Slack from Clawdis via the slack tool, including reacting to messages or pinning/unpinning items in Slack channels or DMs.
---
# Slack Actions
## Overview
Use `slack` to react, manage pins, send/edit/delete messages, and fetch member info. The tool uses the bot token configured for Clawdis.
## Inputs to collect
- `channelId` and `messageId` (Slack message timestamp, e.g. `1712023032.1234`).
- For reactions, an `emoji` (Unicode or `:name:`).
- For message sends, a `to` target (`channel:<id>` or `user:<id>`) and `content`.
Message context lines include `slack message id` and `channel` fields you can reuse directly.
## Actions
### Action groups
| Action group | Default | Notes |
| --- | --- | --- |
| reactions | enabled | React + list reactions |
| messages | enabled | Read/send/edit/delete |
| pins | enabled | Pin/unpin/list |
| memberInfo | enabled | Member info |
| emojiList | enabled | Custom emoji list |
### React to a message
```json
{
"action": "react",
"channelId": "C123",
"messageId": "1712023032.1234",
"emoji": "✅"
}
```
### List reactions
```json
{
"action": "reactions",
"channelId": "C123",
"messageId": "1712023032.1234"
}
```
### Send a message
```json
{
"action": "sendMessage",
"to": "channel:C123",
"content": "Hello from Clawdis"
}
```
### Edit a message
```json
{
"action": "editMessage",
"channelId": "C123",
"messageId": "1712023032.1234",
"content": "Updated text"
}
```
### Delete a message
```json
{
"action": "deleteMessage",
"channelId": "C123",
"messageId": "1712023032.1234"
}
```
### Read recent messages
```json
{
"action": "readMessages",
"channelId": "C123",
"limit": 20
}
```
### Pin a message
```json
{
"action": "pinMessage",
"channelId": "C123",
"messageId": "1712023032.1234"
}
```
### Unpin a message
```json
{
"action": "unpinMessage",
"channelId": "C123",
"messageId": "1712023032.1234"
}
```
### List pinned items
```json
{
"action": "listPins",
"channelId": "C123"
}
```
### Member info
```json
{
"action": "memberInfo",
"userId": "U123"
}
```
### Emoji list
```json
{
"action": "emojiList"
}
```
## Ideas to try
- React with ✅ to mark completed tasks.
- Pin key decisions or weekly status updates.
-2
View File
@@ -8,7 +8,6 @@ import { createNodesTool } from "./tools/nodes-tool.js";
import { createSessionsHistoryTool } from "./tools/sessions-history-tool.js"; import { createSessionsHistoryTool } from "./tools/sessions-history-tool.js";
import { createSessionsListTool } from "./tools/sessions-list-tool.js"; import { createSessionsListTool } from "./tools/sessions-list-tool.js";
import { createSessionsSendTool } from "./tools/sessions-send-tool.js"; import { createSessionsSendTool } from "./tools/sessions-send-tool.js";
import { createSlackTool } from "./tools/slack-tool.js";
export function createClawdisTools(options?: { export function createClawdisTools(options?: {
browserControlUrl?: string; browserControlUrl?: string;
@@ -21,7 +20,6 @@ export function createClawdisTools(options?: {
createNodesTool(), createNodesTool(),
createCronTool(), createCronTool(),
createDiscordTool(), createDiscordTool(),
createSlackTool(),
createGatewayTool(), createGatewayTool(),
createSessionsListTool(), createSessionsListTool(),
createSessionsHistoryTool(), createSessionsHistoryTool(),
+1 -3
View File
@@ -248,9 +248,7 @@ export function subscribeEmbeddedPiSession(params: {
}; };
const emitBlockChunk = (text: string) => { const emitBlockChunk = (text: string) => {
// Strip any <thinking> tags that may have leaked into the output (e.g., from Gemini mimicking history) const chunk = text.trimEnd();
const strippedText = stripThinkingSegments(stripUnpairedThinkingTags(text));
const chunk = strippedText.trimEnd();
if (!chunk) return; if (!chunk) return;
if (chunk === lastBlockReplyText) return; if (chunk === lastBlockReplyText) return;
lastBlockReplyText = chunk; lastBlockReplyText = chunk;
-8
View File
@@ -88,14 +88,6 @@ describe("createClawdisCodingTools", () => {
expect(discord.some((tool) => tool.name === "discord")).toBe(true); expect(discord.some((tool) => tool.name === "discord")).toBe(true);
}); });
it("scopes slack tool to slack surface", () => {
const other = createClawdisCodingTools({ surface: "whatsapp" });
expect(other.some((tool) => tool.name === "slack")).toBe(false);
const slack = createClawdisCodingTools({ surface: "slack" });
expect(slack.some((tool) => tool.name === "slack")).toBe(true);
});
it("keeps read tool image metadata intact", async () => { it("keeps read tool image metadata intact", async () => {
const tools = createClawdisCodingTools(); const tools = createClawdisCodingTools();
const readTool = tools.find((tool) => tool.name === "read"); const readTool = tools.find((tool) => tool.name === "read");
+3 -12
View File
@@ -441,12 +441,6 @@ function shouldIncludeDiscordTool(surface?: string): boolean {
return normalized === "discord" || normalized.startsWith("discord:"); return normalized === "discord" || normalized.startsWith("discord:");
} }
function shouldIncludeSlackTool(surface?: string): boolean {
const normalized = normalizeSurface(surface);
if (!normalized) return false;
return normalized === "slack" || normalized.startsWith("slack:");
}
export function createClawdisCodingTools(options?: { export function createClawdisCodingTools(options?: {
bash?: BashToolDefaults & ProcessToolDefaults; bash?: BashToolDefaults & ProcessToolDefaults;
surface?: string; surface?: string;
@@ -500,12 +494,9 @@ export function createClawdisCodingTools(options?: {
}), }),
]; ];
const allowDiscord = shouldIncludeDiscordTool(options?.surface); const allowDiscord = shouldIncludeDiscordTool(options?.surface);
const allowSlack = shouldIncludeSlackTool(options?.surface); const filtered = allowDiscord
const filtered = tools.filter((tool) => { ? tools
if (tool.name === "discord") return allowDiscord; : tools.filter((tool) => tool.name !== "discord");
if (tool.name === "slack") return allowSlack;
return true;
});
const sandboxed = sandbox const sandboxed = sandbox
? filterToolsByPolicy(filtered, sandbox.tools) ? filterToolsByPolicy(filtered, sandbox.tools)
: filtered; : filtered;
-18
View File
@@ -209,24 +209,6 @@
"kick": { "label": "kick", "detailKeys": ["guildId", "userId"] }, "kick": { "label": "kick", "detailKeys": ["guildId", "userId"] },
"ban": { "label": "ban", "detailKeys": ["guildId", "userId"] } "ban": { "label": "ban", "detailKeys": ["guildId", "userId"] }
} }
},
"slack": {
"emoji": "💬",
"title": "Slack",
"actions": {
"react": { "label": "react", "detailKeys": ["channelId", "messageId", "emoji"] },
"reactions": { "label": "reactions", "detailKeys": ["channelId", "messageId"] },
"sendMessage": { "label": "send", "detailKeys": ["to", "content"] },
"editMessage": { "label": "edit", "detailKeys": ["channelId", "messageId"] },
"deleteMessage": { "label": "delete", "detailKeys": ["channelId", "messageId"] },
"readMessages": { "label": "read messages", "detailKeys": ["channelId", "limit"] },
"pinMessage": { "label": "pin", "detailKeys": ["channelId", "messageId"] },
"unpinMessage": { "label": "unpin", "detailKeys": ["channelId", "messageId"] },
"listPins": { "label": "list pins", "detailKeys": ["channelId"] },
"memberInfo": { "label": "member", "detailKeys": ["userId"] },
"emojiList": { "label": "emoji list" }
} }
} }
} }
}
-161
View File
@@ -1,161 +0,0 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { ClawdisConfig, SlackActionConfig } from "../../config/config.js";
import {
deleteSlackMessage,
editSlackMessage,
getSlackMemberInfo,
listSlackEmojis,
listSlackPins,
listSlackReactions,
pinSlackMessage,
reactSlackMessage,
readSlackMessages,
sendSlackMessage,
unpinSlackMessage,
} from "../../slack/actions.js";
import { jsonResult, readStringParam } from "./common.js";
const messagingActions = new Set([
"sendMessage",
"editMessage",
"deleteMessage",
"readMessages",
]);
const reactionsActions = new Set(["react", "reactions"]);
const pinActions = new Set(["pinMessage", "unpinMessage", "listPins"]);
type ActionGate = (
key: keyof SlackActionConfig,
defaultValue?: boolean,
) => boolean;
export async function handleSlackAction(
params: Record<string, unknown>,
cfg: ClawdisConfig,
): Promise<AgentToolResult<unknown>> {
const action = readStringParam(params, "action", { required: true });
const isActionEnabled: ActionGate = (key, defaultValue = true) => {
const value = cfg.slack?.actions?.[key];
if (value === undefined) return defaultValue;
return value !== false;
};
if (reactionsActions.has(action)) {
if (!isActionEnabled("reactions")) {
throw new Error("Slack reactions are disabled.");
}
const channelId = readStringParam(params, "channelId", { required: true });
const messageId = readStringParam(params, "messageId", { required: true });
if (action === "react") {
const emoji = readStringParam(params, "emoji", { required: true });
await reactSlackMessage(channelId, messageId, emoji);
return jsonResult({ ok: true });
}
const reactions = await listSlackReactions(channelId, messageId);
return jsonResult({ ok: true, reactions });
}
if (messagingActions.has(action)) {
if (!isActionEnabled("messages")) {
throw new Error("Slack messages are disabled.");
}
switch (action) {
case "sendMessage": {
const to = readStringParam(params, "to", { required: true });
const content = readStringParam(params, "content", { required: true });
const mediaUrl = readStringParam(params, "mediaUrl");
const result = await sendSlackMessage(to, content, {
mediaUrl: mediaUrl ?? undefined,
});
return jsonResult({ ok: true, result });
}
case "editMessage": {
const channelId = readStringParam(params, "channelId", {
required: true,
});
const messageId = readStringParam(params, "messageId", {
required: true,
});
const content = readStringParam(params, "content", {
required: true,
});
await editSlackMessage(channelId, messageId, content);
return jsonResult({ ok: true });
}
case "deleteMessage": {
const channelId = readStringParam(params, "channelId", {
required: true,
});
const messageId = readStringParam(params, "messageId", {
required: true,
});
await deleteSlackMessage(channelId, messageId);
return jsonResult({ ok: true });
}
case "readMessages": {
const channelId = readStringParam(params, "channelId", {
required: true,
});
const limitRaw = params.limit;
const limit =
typeof limitRaw === "number" && Number.isFinite(limitRaw)
? limitRaw
: undefined;
const before = readStringParam(params, "before");
const after = readStringParam(params, "after");
const result = await readSlackMessages(channelId, {
limit,
before: before ?? undefined,
after: after ?? undefined,
});
return jsonResult({ ok: true, ...result });
}
default:
break;
}
}
if (pinActions.has(action)) {
if (!isActionEnabled("pins")) {
throw new Error("Slack pins are disabled.");
}
const channelId = readStringParam(params, "channelId", { required: true });
if (action === "pinMessage") {
const messageId = readStringParam(params, "messageId", {
required: true,
});
await pinSlackMessage(channelId, messageId);
return jsonResult({ ok: true });
}
if (action === "unpinMessage") {
const messageId = readStringParam(params, "messageId", {
required: true,
});
await unpinSlackMessage(channelId, messageId);
return jsonResult({ ok: true });
}
const pins = await listSlackPins(channelId);
return jsonResult({ ok: true, pins });
}
if (action === "memberInfo") {
if (!isActionEnabled("memberInfo")) {
throw new Error("Slack member info is disabled.");
}
const userId = readStringParam(params, "userId", { required: true });
const info = await getSlackMemberInfo(userId);
return jsonResult({ ok: true, info });
}
if (action === "emojiList") {
if (!isActionEnabled("emojiList")) {
throw new Error("Slack emoji list is disabled.");
}
const emojis = await listSlackEmojis();
return jsonResult({ ok: true, emojis });
}
throw new Error(`Unknown action: ${action}`);
}
-60
View File
@@ -1,60 +0,0 @@
import { Type } from "@sinclair/typebox";
export const SlackToolSchema = Type.Union([
Type.Object({
action: Type.Literal("react"),
channelId: Type.String(),
messageId: Type.String(),
emoji: Type.String(),
}),
Type.Object({
action: Type.Literal("reactions"),
channelId: Type.String(),
messageId: Type.String(),
}),
Type.Object({
action: Type.Literal("sendMessage"),
to: Type.String(),
content: Type.String(),
mediaUrl: Type.Optional(Type.String()),
}),
Type.Object({
action: Type.Literal("editMessage"),
channelId: Type.String(),
messageId: Type.String(),
content: Type.String(),
}),
Type.Object({
action: Type.Literal("deleteMessage"),
channelId: Type.String(),
messageId: Type.String(),
}),
Type.Object({
action: Type.Literal("readMessages"),
channelId: Type.String(),
limit: Type.Optional(Type.Number()),
before: Type.Optional(Type.String()),
after: Type.Optional(Type.String()),
}),
Type.Object({
action: Type.Literal("pinMessage"),
channelId: Type.String(),
messageId: Type.String(),
}),
Type.Object({
action: Type.Literal("unpinMessage"),
channelId: Type.String(),
messageId: Type.String(),
}),
Type.Object({
action: Type.Literal("listPins"),
channelId: Type.String(),
}),
Type.Object({
action: Type.Literal("memberInfo"),
userId: Type.String(),
}),
Type.Object({
action: Type.Literal("emojiList"),
}),
]);
-18
View File
@@ -1,18 +0,0 @@
import { loadConfig } from "../../config/config.js";
import type { AnyAgentTool } from "./common.js";
import { handleSlackAction } from "./slack-actions.js";
import { SlackToolSchema } from "./slack-schema.js";
export function createSlackTool(): AnyAgentTool {
return {
label: "Slack",
name: "slack",
description: "Manage Slack messages, reactions, and pins.",
parameters: SlackToolSchema,
execute: async (_toolCallId, args) => {
const params = args as Record<string, unknown>;
const cfg = loadConfig();
return await handleSlackAction(params, cfg);
},
};
}
+1 -6
View File
@@ -50,7 +50,6 @@ describe("resolveTextChunkLimit", () => {
it("uses per-surface defaults", () => { it("uses per-surface defaults", () => {
expect(resolveTextChunkLimit(undefined, "whatsapp")).toBe(4000); expect(resolveTextChunkLimit(undefined, "whatsapp")).toBe(4000);
expect(resolveTextChunkLimit(undefined, "telegram")).toBe(4000); expect(resolveTextChunkLimit(undefined, "telegram")).toBe(4000);
expect(resolveTextChunkLimit(undefined, "slack")).toBe(4000);
expect(resolveTextChunkLimit(undefined, "signal")).toBe(4000); expect(resolveTextChunkLimit(undefined, "signal")).toBe(4000);
expect(resolveTextChunkLimit(undefined, "imessage")).toBe(4000); expect(resolveTextChunkLimit(undefined, "imessage")).toBe(4000);
expect(resolveTextChunkLimit(undefined, "discord")).toBe(2000); expect(resolveTextChunkLimit(undefined, "discord")).toBe(2000);
@@ -63,12 +62,8 @@ describe("resolveTextChunkLimit", () => {
}); });
it("uses the matching provider override", () => { it("uses the matching provider override", () => {
const cfg = { const cfg = { discord: { textChunkLimit: 111 } };
discord: { textChunkLimit: 111 },
slack: { textChunkLimit: 222 },
};
expect(resolveTextChunkLimit(cfg, "discord")).toBe(111); expect(resolveTextChunkLimit(cfg, "discord")).toBe(111);
expect(resolveTextChunkLimit(cfg, "slack")).toBe(222);
expect(resolveTextChunkLimit(cfg, "telegram")).toBe(4000); expect(resolveTextChunkLimit(cfg, "telegram")).toBe(4000);
}); });
}); });
-3
View File
@@ -8,7 +8,6 @@ export type TextChunkSurface =
| "whatsapp" | "whatsapp"
| "telegram" | "telegram"
| "discord" | "discord"
| "slack"
| "signal" | "signal"
| "imessage" | "imessage"
| "webchat"; | "webchat";
@@ -17,7 +16,6 @@ const DEFAULT_CHUNK_LIMIT_BY_SURFACE: Record<TextChunkSurface, number> = {
whatsapp: 4000, whatsapp: 4000,
telegram: 4000, telegram: 4000,
discord: 2000, discord: 2000,
slack: 4000,
signal: 4000, signal: 4000,
imessage: 4000, imessage: 4000,
webchat: 4000, webchat: 4000,
@@ -32,7 +30,6 @@ export function resolveTextChunkLimit(
if (surface === "whatsapp") return cfg?.whatsapp?.textChunkLimit; if (surface === "whatsapp") return cfg?.whatsapp?.textChunkLimit;
if (surface === "telegram") return cfg?.telegram?.textChunkLimit; if (surface === "telegram") return cfg?.telegram?.textChunkLimit;
if (surface === "discord") return cfg?.discord?.textChunkLimit; if (surface === "discord") return cfg?.discord?.textChunkLimit;
if (surface === "slack") return cfg?.slack?.textChunkLimit;
if (surface === "signal") return cfg?.signal?.textChunkLimit; if (surface === "signal") return cfg?.signal?.textChunkLimit;
if (surface === "imessage") return cfg?.imessage?.textChunkLimit; if (surface === "imessage") return cfg?.imessage?.textChunkLimit;
return undefined; return undefined;
-45
View File
@@ -828,49 +828,4 @@ describe("directive parsing", () => {
}); });
}); });
}); });
it("defaults elevated to off when sender is not approved", async () => {
await withTempHome(async (home) => {
const storePath = path.join(home, "sessions.json");
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
payloads: [{ text: "done" }],
meta: {
durationMs: 5,
agentMeta: { sessionId: "s", provider: "p", model: "m" },
},
});
await getReplyFromConfig(
{
Body: "hello",
From: "+1004",
To: "+2000",
Surface: "whatsapp",
SenderE164: "+1004",
},
{},
{
agent: {
model: "anthropic/claude-opus-4-5",
workspace: path.join(home, "clawd"),
elevatedDefault: "on",
elevated: {
allowFrom: { whatsapp: ["+1999"] },
},
},
whatsapp: {
allowFrom: ["*"],
},
session: { store: storePath },
},
);
expect(runEmbeddedPiAgent).toHaveBeenCalledOnce();
const call = vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0];
expect(call?.bashElevated).toEqual({
enabled: true,
allowed: false,
defaultLevel: "off",
});
});
});
}); });
+1 -1
View File
@@ -305,7 +305,7 @@ export async function getReplyFromConfig(
(sessionEntry?.elevatedLevel as ElevatedLevel | undefined) ?? (sessionEntry?.elevatedLevel as ElevatedLevel | undefined) ??
(agentCfg?.elevatedDefault as ElevatedLevel | undefined) ?? (agentCfg?.elevatedDefault as ElevatedLevel | undefined) ??
"on") "on")
: "off"; : "on";
const resolvedBlockStreaming = const resolvedBlockStreaming =
agentCfg?.blockStreamingDefault === "off" ? "off" : "on"; agentCfg?.blockStreamingDefault === "off" ? "off" : "on";
const resolvedBlockStreamingBreak: "text_end" | "message_end" = const resolvedBlockStreamingBreak: "text_end" | "message_end" =
-1
View File
@@ -8,7 +8,6 @@ const BLOCK_CHUNK_SURFACES = new Set<TextChunkSurface>([
"whatsapp", "whatsapp",
"telegram", "telegram",
"discord", "discord",
"slack",
"signal", "signal",
"imessage", "imessage",
"webchat", "webchat",
-2
View File
@@ -8,7 +8,6 @@ export type CliDeps = {
sendMessageWhatsApp: typeof sendMessageWhatsApp; sendMessageWhatsApp: typeof sendMessageWhatsApp;
sendMessageTelegram: typeof sendMessageTelegram; sendMessageTelegram: typeof sendMessageTelegram;
sendMessageDiscord: typeof sendMessageDiscord; sendMessageDiscord: typeof sendMessageDiscord;
sendMessageSlack: typeof sendMessageSlack;
sendMessageSignal: typeof sendMessageSignal; sendMessageSignal: typeof sendMessageSignal;
sendMessageIMessage: typeof sendMessageIMessage; sendMessageIMessage: typeof sendMessageIMessage;
}; };
@@ -18,7 +17,6 @@ export function createDefaultDeps(): CliDeps {
sendMessageWhatsApp, sendMessageWhatsApp,
sendMessageTelegram, sendMessageTelegram,
sendMessageDiscord, sendMessageDiscord,
sendMessageSlack,
sendMessageSignal, sendMessageSignal,
sendMessageIMessage, sendMessageIMessage,
}; };
+5 -5
View File
@@ -300,7 +300,7 @@ export function buildProgram() {
program program
.command("send") .command("send")
.description( .description(
"Send a message (WhatsApp Web, Telegram bot, Discord, Slack, Signal, iMessage)", "Send a message (WhatsApp Web, Telegram bot, Discord, Signal, iMessage)",
) )
.requiredOption( .requiredOption(
"-t, --to <number>", "-t, --to <number>",
@@ -318,7 +318,7 @@ export function buildProgram() {
) )
.option( .option(
"--provider <provider>", "--provider <provider>",
"Delivery provider: whatsapp|telegram|discord|slack|signal|imessage (default: whatsapp)", "Delivery provider: whatsapp|telegram|discord|signal|imessage (default: whatsapp)",
) )
.option("--dry-run", "Print payload and skip sending", false) .option("--dry-run", "Print payload and skip sending", false)
.option("--json", "Output result as JSON", false) .option("--json", "Output result as JSON", false)
@@ -361,7 +361,7 @@ Examples:
.option("--verbose <on|off>", "Persist agent verbose level for the session") .option("--verbose <on|off>", "Persist agent verbose level for the session")
.option( .option(
"--provider <provider>", "--provider <provider>",
"Delivery provider: whatsapp|telegram|discord|slack|signal|imessage (default: whatsapp)", "Delivery provider: whatsapp|telegram|discord|signal|imessage (default: whatsapp)",
) )
.option( .option(
"--deliver", "--deliver",
@@ -411,7 +411,7 @@ Examples:
.option("--json", "Output JSON instead of text", false) .option("--json", "Output JSON instead of text", false)
.option( .option(
"--deep", "--deep",
"Probe providers (WhatsApp Web + Telegram + Discord + Slack + Signal)", "Probe providers (WhatsApp Web + Telegram + Discord + Signal)",
false, false,
) )
.option("--timeout <ms>", "Probe timeout in milliseconds", "10000") .option("--timeout <ms>", "Probe timeout in milliseconds", "10000")
@@ -422,7 +422,7 @@ Examples:
Examples: Examples:
clawdis status # show linked account + session store summary clawdis status # show linked account + session store summary
clawdis status --json # machine-readable output clawdis status --json # machine-readable output
clawdis status --deep # run provider probes (WA + Telegram + Discord + Slack + Signal) clawdis status --deep # run provider probes (WA + Telegram + Discord + Signal)
clawdis status --deep --timeout 5000 # tighten probe timeout`, clawdis status --deep --timeout 5000 # tighten probe timeout`,
) )
.action(async (opts) => { .action(async (opts) => {
-32
View File
@@ -473,7 +473,6 @@ export async function agentCommand(
const whatsappTarget = opts.to ? normalizeE164(opts.to) : allowFrom[0]; const whatsappTarget = opts.to ? normalizeE164(opts.to) : allowFrom[0];
const telegramTarget = opts.to?.trim() || undefined; const telegramTarget = opts.to?.trim() || undefined;
const discordTarget = opts.to?.trim() || undefined; const discordTarget = opts.to?.trim() || undefined;
const slackTarget = opts.to?.trim() || undefined;
const signalTarget = opts.to?.trim() || undefined; const signalTarget = opts.to?.trim() || undefined;
const imessageTarget = opts.to?.trim() || undefined; const imessageTarget = opts.to?.trim() || undefined;
@@ -485,8 +484,6 @@ export async function agentCommand(
? whatsappTarget ? whatsappTarget
: deliveryProvider === "discord" : deliveryProvider === "discord"
? discordTarget ? discordTarget
: deliveryProvider === "slack"
? slackTarget
: deliveryProvider === "signal" : deliveryProvider === "signal"
? signalTarget ? signalTarget
: deliveryProvider === "imessage" : deliveryProvider === "imessage"
@@ -517,13 +514,6 @@ export async function agentCommand(
if (!bestEffortDeliver) throw err; if (!bestEffortDeliver) throw err;
logDeliveryError(err); logDeliveryError(err);
} }
if (deliveryProvider === "slack" && !slackTarget) {
const err = new Error(
"Delivering to Slack requires --to <channelId|user:ID|channel:ID>",
);
if (!bestEffortDeliver) throw err;
logDeliveryError(err);
}
if (deliveryProvider === "signal" && !signalTarget) { if (deliveryProvider === "signal" && !signalTarget) {
const err = new Error( const err = new Error(
"Delivering to Signal requires --to <E.164|group:ID|signal:group:ID|signal:+E.164>", "Delivering to Signal requires --to <E.164|group:ID|signal:group:ID|signal:+E.164>",
@@ -549,7 +539,6 @@ export async function agentCommand(
deliveryProvider !== "whatsapp" && deliveryProvider !== "whatsapp" &&
deliveryProvider !== "telegram" && deliveryProvider !== "telegram" &&
deliveryProvider !== "discord" && deliveryProvider !== "discord" &&
deliveryProvider !== "slack" &&
deliveryProvider !== "signal" && deliveryProvider !== "signal" &&
deliveryProvider !== "imessage" && deliveryProvider !== "imessage" &&
deliveryProvider !== "webchat" deliveryProvider !== "webchat"
@@ -585,7 +574,6 @@ export async function agentCommand(
deliveryProvider === "whatsapp" || deliveryProvider === "whatsapp" ||
deliveryProvider === "telegram" || deliveryProvider === "telegram" ||
deliveryProvider === "discord" || deliveryProvider === "discord" ||
deliveryProvider === "slack" ||
deliveryProvider === "signal" || deliveryProvider === "signal" ||
deliveryProvider === "imessage" deliveryProvider === "imessage"
? resolveTextChunkLimit(cfg, deliveryProvider) ? resolveTextChunkLimit(cfg, deliveryProvider)
@@ -678,26 +666,6 @@ export async function agentCommand(
} }
} }
if (deliveryProvider === "slack" && slackTarget) {
try {
if (media.length === 0) {
await deps.sendMessageSlack(slackTarget, text);
} else {
let first = true;
for (const url of media) {
const caption = first ? text : "";
first = false;
await deps.sendMessageSlack(slackTarget, caption, {
mediaUrl: url,
});
}
}
} catch (err) {
if (!bestEffortDeliver) throw err;
logDeliveryError(err);
}
}
if (deliveryProvider === "signal" && signalTarget) { if (deliveryProvider === "signal" && signalTarget) {
try { try {
if (media.length === 0) { if (media.length === 0) {
-206
View File
@@ -31,7 +31,6 @@ async function noteProviderPrimer(prompter: WizardPrompter): Promise<void> {
"WhatsApp: dedicated second number recommended; primary number OK (self-chat).", "WhatsApp: dedicated second number recommended; primary number OK (self-chat).",
"Telegram: Bot API (token from @BotFather), replies via your bot.", "Telegram: Bot API (token from @BotFather), replies via your bot.",
"Discord: Bot token from Discord Developer Portal; invite bot to your server.", "Discord: Bot token from Discord Developer Portal; invite bot to your server.",
"Slack: Socket Mode app token + bot token, DMs via App Home Messages tab.",
"Signal: signal-cli as a linked device; separate number recommended.", "Signal: signal-cli as a linked device; separate number recommended.",
"iMessage: local imsg CLI; separate Apple ID recommended only on a separate Mac.", "iMessage: local imsg CLI; separate Apple ID recommended only on a separate Mac.",
].join("\n"), ].join("\n"),
@@ -63,96 +62,6 @@ async function noteDiscordTokenHelp(prompter: WizardPrompter): Promise<void> {
); );
} }
function buildSlackManifest(botName: string) {
const safeName = botName.trim() || "Clawdis";
const manifest = {
display_information: {
name: safeName,
description: `${safeName} connector for Clawdis`,
},
features: {
bot_user: {
display_name: safeName,
always_online: false,
},
app_home: {
messages_tab_enabled: true,
messages_tab_read_only_enabled: false,
},
slash_commands: [
{
command: "/clawd",
description: "Send a message to Clawdis",
should_escape: false,
},
],
},
oauth_config: {
scopes: {
bot: [
"chat:write",
"channels:history",
"channels:read",
"groups:history",
"im:history",
"mpim:history",
"users:read",
"app_mentions:read",
"reactions:read",
"reactions:write",
"pins:read",
"pins:write",
"emoji:read",
"commands",
"files:read",
"files:write",
],
},
},
settings: {
socket_mode_enabled: true,
event_subscriptions: {
bot_events: [
"app_mention",
"message.channels",
"message.groups",
"message.im",
"message.mpim",
"reaction_added",
"reaction_removed",
"member_joined_channel",
"member_left_channel",
"channel_rename",
"pin_added",
"pin_removed",
],
},
},
};
return JSON.stringify(manifest, null, 2);
}
async function noteSlackTokenHelp(
prompter: WizardPrompter,
botName: string,
): Promise<void> {
const manifest = buildSlackManifest(botName);
await prompter.note(
[
"1) Slack API → Create App → From scratch",
"2) Add Socket Mode + enable it to get the app-level token (xapp-...)",
"3) OAuth & Permissions → install app to workspace (xoxb- bot token)",
"4) Enable Event Subscriptions (socket) for message events",
"5) App Home → enable the Messages tab for DMs",
"Tip: set SLACK_BOT_TOKEN + SLACK_APP_TOKEN in your env.",
"",
"Manifest (JSON):",
manifest,
].join("\n"),
"Slack socket mode tokens",
);
}
function setWhatsAppAllowFrom(cfg: ClawdisConfig, allowFrom?: string[]) { function setWhatsAppAllowFrom(cfg: ClawdisConfig, allowFrom?: string[]) {
return { return {
...cfg, ...cfg,
@@ -244,16 +153,10 @@ export async function setupProviders(
const whatsappLinked = await detectWhatsAppLinked(); const whatsappLinked = await detectWhatsAppLinked();
const telegramEnv = Boolean(process.env.TELEGRAM_BOT_TOKEN?.trim()); const telegramEnv = Boolean(process.env.TELEGRAM_BOT_TOKEN?.trim());
const discordEnv = Boolean(process.env.DISCORD_BOT_TOKEN?.trim()); const discordEnv = Boolean(process.env.DISCORD_BOT_TOKEN?.trim());
const slackBotEnv = Boolean(process.env.SLACK_BOT_TOKEN?.trim());
const slackAppEnv = Boolean(process.env.SLACK_APP_TOKEN?.trim());
const telegramConfigured = Boolean( const telegramConfigured = Boolean(
telegramEnv || cfg.telegram?.botToken || cfg.telegram?.tokenFile, telegramEnv || cfg.telegram?.botToken || cfg.telegram?.tokenFile,
); );
const discordConfigured = Boolean(discordEnv || cfg.discord?.token); const discordConfigured = Boolean(discordEnv || cfg.discord?.token);
const slackConfigured = Boolean(
(slackBotEnv && slackAppEnv) ||
(cfg.slack?.botToken && cfg.slack?.appToken),
);
const signalConfigured = Boolean( const signalConfigured = Boolean(
cfg.signal?.account || cfg.signal?.httpUrl || cfg.signal?.httpPort, cfg.signal?.account || cfg.signal?.httpUrl || cfg.signal?.httpPort,
); );
@@ -270,7 +173,6 @@ export async function setupProviders(
`WhatsApp: ${whatsappLinked ? "linked" : "not linked"}`, `WhatsApp: ${whatsappLinked ? "linked" : "not linked"}`,
`Telegram: ${telegramConfigured ? "configured" : "needs token"}`, `Telegram: ${telegramConfigured ? "configured" : "needs token"}`,
`Discord: ${discordConfigured ? "configured" : "needs token"}`, `Discord: ${discordConfigured ? "configured" : "needs token"}`,
`Slack: ${slackConfigured ? "configured" : "needs tokens"}`,
`Signal: ${signalConfigured ? "configured" : "needs setup"}`, `Signal: ${signalConfigured ? "configured" : "needs setup"}`,
`iMessage: ${imessageConfigured ? "configured" : "needs setup"}`, `iMessage: ${imessageConfigured ? "configured" : "needs setup"}`,
`signal-cli: ${signalCliDetected ? "found" : "missing"} (${signalCliPath})`, `signal-cli: ${signalCliDetected ? "found" : "missing"} (${signalCliPath})`,
@@ -305,11 +207,6 @@ export async function setupProviders(
label: "Discord (Bot API)", label: "Discord (Bot API)",
hint: discordConfigured ? "configured" : "needs token", hint: discordConfigured ? "configured" : "needs token",
}, },
{
value: "slack",
label: "Slack (Socket Mode)",
hint: slackConfigured ? "configured" : "needs tokens",
},
{ {
value: "signal", value: "signal",
label: "Signal (signal-cli)", label: "Signal (signal-cli)",
@@ -477,96 +374,6 @@ export async function setupProviders(
} }
} }
if (selection.includes("slack")) {
let botToken: string | null = null;
let appToken: string | null = null;
const slackBotName = String(
await prompter.text({
message: "Slack bot display name (used for manifest)",
initialValue: "Clawdis",
}),
).trim();
if (!slackConfigured) {
await noteSlackTokenHelp(prompter, slackBotName);
}
if (
slackBotEnv &&
slackAppEnv &&
(!cfg.slack?.botToken || !cfg.slack?.appToken)
) {
const keepEnv = await prompter.confirm({
message: "SLACK_BOT_TOKEN + SLACK_APP_TOKEN detected. Use env vars?",
initialValue: true,
});
if (keepEnv) {
next = {
...next,
slack: {
...next.slack,
enabled: true,
},
};
} else {
botToken = String(
await prompter.text({
message: "Enter Slack bot token (xoxb-...)",
validate: (value) => (value?.trim() ? undefined : "Required"),
}),
).trim();
appToken = String(
await prompter.text({
message: "Enter Slack app token (xapp-...)",
validate: (value) => (value?.trim() ? undefined : "Required"),
}),
).trim();
}
} else if (cfg.slack?.botToken && cfg.slack?.appToken) {
const keep = await prompter.confirm({
message: "Slack tokens already configured. Keep them?",
initialValue: true,
});
if (!keep) {
botToken = String(
await prompter.text({
message: "Enter Slack bot token (xoxb-...)",
validate: (value) => (value?.trim() ? undefined : "Required"),
}),
).trim();
appToken = String(
await prompter.text({
message: "Enter Slack app token (xapp-...)",
validate: (value) => (value?.trim() ? undefined : "Required"),
}),
).trim();
}
} else {
botToken = String(
await prompter.text({
message: "Enter Slack bot token (xoxb-...)",
validate: (value) => (value?.trim() ? undefined : "Required"),
}),
).trim();
appToken = String(
await prompter.text({
message: "Enter Slack app token (xapp-...)",
validate: (value) => (value?.trim() ? undefined : "Required"),
}),
).trim();
}
if (botToken && appToken) {
next = {
...next,
slack: {
...next.slack,
enabled: true,
botToken,
appToken,
},
};
}
}
if (selection.includes("signal")) { if (selection.includes("signal")) {
let resolvedCliPath = signalCliPath; let resolvedCliPath = signalCliPath;
let cliDetected = signalCliDetected; let cliDetected = signalCliDetected;
@@ -714,19 +521,6 @@ export async function setupProviders(
} }
} }
if (!selection.includes("slack") && slackConfigured) {
const disable = await prompter.confirm({
message: "Disable Slack provider?",
initialValue: false,
});
if (disable) {
next = {
...next,
slack: { ...next.slack, enabled: false },
};
}
}
if (!selection.includes("signal") && signalConfigured) { if (!selection.includes("signal") && signalConfigured) {
const disable = await prompter.confirm({ const disable = await prompter.confirm({
message: "Disable Signal provider?", message: "Disable Signal provider?",
-1
View File
@@ -14,7 +14,6 @@ export type ProviderChoice =
| "whatsapp" | "whatsapp"
| "telegram" | "telegram"
| "discord" | "discord"
| "slack"
| "signal" | "signal"
| "imessage"; | "imessage";
-20
View File
@@ -41,7 +41,6 @@ const makeDeps = (overrides: Partial<CliDeps> = {}): CliDeps => ({
sendMessageWhatsApp: vi.fn(), sendMessageWhatsApp: vi.fn(),
sendMessageTelegram: vi.fn(), sendMessageTelegram: vi.fn(),
sendMessageDiscord: vi.fn(), sendMessageDiscord: vi.fn(),
sendMessageSlack: vi.fn(),
sendMessageSignal: vi.fn(), sendMessageSignal: vi.fn(),
sendMessageIMessage: vi.fn(), sendMessageIMessage: vi.fn(),
...overrides, ...overrides,
@@ -174,25 +173,6 @@ describe("sendCommand", () => {
expect(deps.sendMessageWhatsApp).not.toHaveBeenCalled(); expect(deps.sendMessageWhatsApp).not.toHaveBeenCalled();
}); });
it("routes to slack provider", async () => {
const deps = makeDeps({
sendMessageSlack: vi
.fn()
.mockResolvedValue({ messageId: "s1", channelId: "C123" }),
});
await sendCommand(
{ to: "channel:C123", message: "hi", provider: "slack" },
deps,
runtime,
);
expect(deps.sendMessageSlack).toHaveBeenCalledWith(
"channel:C123",
"hi",
expect.objectContaining({ mediaUrl: undefined }),
);
expect(deps.sendMessageWhatsApp).not.toHaveBeenCalled();
});
it("routes to imessage provider", async () => { it("routes to imessage provider", async () => {
const deps = makeDeps({ const deps = makeDeps({
sendMessageIMessage: vi.fn().mockResolvedValue({ messageId: "i1" }), sendMessageIMessage: vi.fn().mockResolvedValue({ messageId: "i1" }),
-28
View File
@@ -86,34 +86,6 @@ export async function sendCommand(
return; return;
} }
if (provider === "slack") {
const result = await deps.sendMessageSlack(opts.to, opts.message, {
mediaUrl: opts.media,
});
runtime.log(
success(
`✅ Sent via slack. Message ID: ${result.messageId} (channel ${result.channelId})`,
),
);
if (opts.json) {
runtime.log(
JSON.stringify(
{
provider: "slack",
via: "direct",
to: opts.to,
channelId: result.channelId,
messageId: result.messageId,
mediaUrl: opts.media ?? null,
},
null,
2,
),
);
}
return;
}
if (provider === "signal") { if (provider === "signal") {
const result = await deps.sendMessageSignal(opts.to, opts.message, { const result = await deps.sendMessageSignal(opts.to, opts.message, {
mediaUrl: opts.media, mediaUrl: opts.media,
-4
View File
@@ -40,7 +40,6 @@ const GROUP_LABELS: Record<string, string> = {
talk: "Talk", talk: "Talk",
telegram: "Telegram", telegram: "Telegram",
discord: "Discord", discord: "Discord",
slack: "Slack",
signal: "Signal", signal: "Signal",
imessage: "iMessage", imessage: "iMessage",
whatsapp: "WhatsApp", whatsapp: "WhatsApp",
@@ -66,7 +65,6 @@ const GROUP_ORDER: Record<string, number> = {
talk: 130, talk: 130,
telegram: 140, telegram: 140,
discord: 150, discord: 150,
slack: 155,
signal: 160, signal: 160,
imessage: 170, imessage: 170,
whatsapp: 180, whatsapp: 180,
@@ -94,8 +92,6 @@ const FIELD_LABELS: Record<string, string> = {
"talk.apiKey": "Talk API Key", "talk.apiKey": "Talk API Key",
"telegram.botToken": "Telegram Bot Token", "telegram.botToken": "Telegram Bot Token",
"discord.token": "Discord Bot Token", "discord.token": "Discord Bot Token",
"slack.botToken": "Slack Bot Token",
"slack.appToken": "Slack App Token",
"signal.account": "Signal Account", "signal.account": "Signal Account",
"imessage.cliPath": "iMessage CLI Path", "imessage.cliPath": "iMessage CLI Path",
}; };
-1
View File
@@ -61,7 +61,6 @@ export type SessionEntry = {
| "whatsapp" | "whatsapp"
| "telegram" | "telegram"
| "discord" | "discord"
| "slack"
| "signal" | "signal"
| "imessage" | "imessage"
| "webchat"; | "webchat";
-62
View File
@@ -65,7 +65,6 @@ export type AgentElevatedAllowFromConfig = {
whatsapp?: string[]; whatsapp?: string[];
telegram?: Array<string | number>; telegram?: Array<string | number>;
discord?: Array<string | number>; discord?: Array<string | number>;
slack?: Array<string | number>;
signal?: Array<string | number>; signal?: Array<string | number>;
imessage?: Array<string | number>; imessage?: Array<string | number>;
webchat?: Array<string | number>; webchat?: Array<string | number>;
@@ -145,7 +144,6 @@ export type HookMappingConfig = {
| "whatsapp" | "whatsapp"
| "telegram" | "telegram"
| "discord" | "discord"
| "slack"
| "signal" | "signal"
| "imessage"; | "imessage";
to?: string; to?: string;
@@ -292,63 +290,6 @@ export type DiscordConfig = {
guilds?: Record<string, DiscordGuildEntry>; guilds?: Record<string, DiscordGuildEntry>;
}; };
export type SlackDmConfig = {
/** If false, ignore all incoming Slack DMs. Default: true. */
enabled?: boolean;
/** Allowlist for DM senders (ids). */
allowFrom?: Array<string | number>;
/** If true, allow group DMs (default: false). */
groupEnabled?: boolean;
/** Optional allowlist for group DM channels (ids or slugs). */
groupChannels?: Array<string | number>;
};
export type SlackChannelConfig = {
allow?: boolean;
requireMention?: boolean;
};
export type SlackReactionNotificationMode = "off" | "own" | "all" | "allowlist";
export type SlackActionConfig = {
reactions?: boolean;
messages?: boolean;
pins?: boolean;
search?: boolean;
permissions?: boolean;
memberInfo?: boolean;
channelInfo?: boolean;
emojiList?: boolean;
};
export type SlackSlashCommandConfig = {
/** Enable handling for the configured slash command (default: false). */
enabled?: boolean;
/** Slash command name (default: "clawd"). */
name?: string;
/** Session key prefix for slash commands (default: "slack:slash"). */
sessionPrefix?: string;
/** Reply ephemerally (default: true). */
ephemeral?: boolean;
};
export type SlackConfig = {
/** If false, do not start the Slack provider. Default: true. */
enabled?: boolean;
botToken?: string;
appToken?: string;
textChunkLimit?: number;
mediaMaxMb?: number;
/** Reaction notification mode (off|own|all|allowlist). Default: own. */
reactionNotifications?: SlackReactionNotificationMode;
/** Allowlist for reaction notifications when mode is allowlist. */
reactionAllowlist?: Array<string | number>;
actions?: SlackActionConfig;
slashCommand?: SlackSlashCommandConfig;
dm?: SlackDmConfig;
channels?: Record<string, SlackChannelConfig>;
};
export type SignalConfig = { export type SignalConfig = {
/** If false, do not start the Signal provider. Default: true. */ /** If false, do not start the Signal provider. Default: true. */
enabled?: boolean; enabled?: boolean;
@@ -415,7 +356,6 @@ export type QueueModeBySurface = {
whatsapp?: QueueMode; whatsapp?: QueueMode;
telegram?: QueueMode; telegram?: QueueMode;
discord?: QueueMode; discord?: QueueMode;
slack?: QueueMode;
signal?: QueueMode; signal?: QueueMode;
imessage?: QueueMode; imessage?: QueueMode;
webchat?: QueueMode; webchat?: QueueMode;
@@ -702,7 +642,6 @@ export type ClawdisConfig = {
| "whatsapp" | "whatsapp"
| "telegram" | "telegram"
| "discord" | "discord"
| "slack"
| "signal" | "signal"
| "imessage" | "imessage"
| "none"; | "none";
@@ -792,7 +731,6 @@ export type ClawdisConfig = {
whatsapp?: WhatsAppConfig; whatsapp?: WhatsAppConfig;
telegram?: TelegramConfig; telegram?: TelegramConfig;
discord?: DiscordConfig; discord?: DiscordConfig;
slack?: SlackConfig;
signal?: SignalConfig; signal?: SignalConfig;
imessage?: IMessageConfig; imessage?: IMessageConfig;
cron?: CronConfig; cron?: CronConfig;
-56
View File
@@ -86,7 +86,6 @@ const QueueModeBySurfaceSchema = z
whatsapp: QueueModeSchema.optional(), whatsapp: QueueModeSchema.optional(),
telegram: QueueModeSchema.optional(), telegram: QueueModeSchema.optional(),
discord: QueueModeSchema.optional(), discord: QueueModeSchema.optional(),
slack: QueueModeSchema.optional(),
signal: QueueModeSchema.optional(), signal: QueueModeSchema.optional(),
imessage: QueueModeSchema.optional(), imessage: QueueModeSchema.optional(),
webchat: QueueModeSchema.optional(), webchat: QueueModeSchema.optional(),
@@ -164,7 +163,6 @@ const HeartbeatSchema = z
z.literal("whatsapp"), z.literal("whatsapp"),
z.literal("telegram"), z.literal("telegram"),
z.literal("discord"), z.literal("discord"),
z.literal("slack"),
z.literal("signal"), z.literal("signal"),
z.literal("imessage"), z.literal("imessage"),
z.literal("none"), z.literal("none"),
@@ -227,7 +225,6 @@ const HookMappingSchema = z
z.literal("whatsapp"), z.literal("whatsapp"),
z.literal("telegram"), z.literal("telegram"),
z.literal("discord"), z.literal("discord"),
z.literal("slack"),
z.literal("signal"), z.literal("signal"),
z.literal("imessage"), z.literal("imessage"),
]) ])
@@ -417,7 +414,6 @@ export const ClawdisSchema = z.object({
whatsapp: z.array(z.string()).optional(), whatsapp: z.array(z.string()).optional(),
telegram: z.array(z.union([z.string(), z.number()])).optional(), telegram: z.array(z.union([z.string(), z.number()])).optional(),
discord: z.array(z.union([z.string(), z.number()])).optional(), discord: z.array(z.union([z.string(), z.number()])).optional(),
slack: z.array(z.union([z.string(), z.number()])).optional(),
signal: z.array(z.union([z.string(), z.number()])).optional(), signal: z.array(z.union([z.string(), z.number()])).optional(),
imessage: z.array(z.union([z.string(), z.number()])).optional(), imessage: z.array(z.union([z.string(), z.number()])).optional(),
webchat: z.array(z.union([z.string(), z.number()])).optional(), webchat: z.array(z.union([z.string(), z.number()])).optional(),
@@ -623,58 +619,6 @@ export const ClawdisSchema = z.object({
.optional(), .optional(),
}) })
.optional(), .optional(),
slack: z
.object({
enabled: z.boolean().optional(),
botToken: z.string().optional(),
appToken: z.string().optional(),
textChunkLimit: z.number().int().positive().optional(),
mediaMaxMb: z.number().positive().optional(),
reactionNotifications: z
.enum(["off", "own", "all", "allowlist"])
.optional(),
reactionAllowlist: z.array(z.union([z.string(), z.number()])).optional(),
actions: z
.object({
reactions: z.boolean().optional(),
messages: z.boolean().optional(),
pins: z.boolean().optional(),
search: z.boolean().optional(),
permissions: z.boolean().optional(),
memberInfo: z.boolean().optional(),
channelInfo: z.boolean().optional(),
emojiList: z.boolean().optional(),
})
.optional(),
slashCommand: z
.object({
enabled: z.boolean().optional(),
name: z.string().optional(),
sessionPrefix: z.string().optional(),
ephemeral: z.boolean().optional(),
})
.optional(),
dm: z
.object({
enabled: z.boolean().optional(),
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
groupEnabled: z.boolean().optional(),
groupChannels: z.array(z.union([z.string(), z.number()])).optional(),
})
.optional(),
channels: z
.record(
z.string(),
z
.object({
allow: z.boolean().optional(),
requireMention: z.boolean().optional(),
})
.optional(),
)
.optional(),
})
.optional(),
signal: z signal: z
.object({ .object({
enabled: z.boolean().optional(), enabled: z.boolean().optional(),
-42
View File
@@ -64,7 +64,6 @@ function resolveDeliveryTarget(
| "whatsapp" | "whatsapp"
| "telegram" | "telegram"
| "discord" | "discord"
| "slack"
| "signal" | "signal"
| "imessage"; | "imessage";
to?: string; to?: string;
@@ -93,7 +92,6 @@ function resolveDeliveryTarget(
requestedChannel === "whatsapp" || requestedChannel === "whatsapp" ||
requestedChannel === "telegram" || requestedChannel === "telegram" ||
requestedChannel === "discord" || requestedChannel === "discord" ||
requestedChannel === "slack" ||
requestedChannel === "signal" || requestedChannel === "signal" ||
requestedChannel === "imessage" requestedChannel === "imessage"
) { ) {
@@ -449,46 +447,6 @@ export async function runCronIsolatedAgentTurn(params: {
return { status: "error", summary, error: String(err) }; return { status: "error", summary, error: String(err) };
return { status: "ok", summary }; return { status: "ok", summary };
} }
} else if (resolvedDelivery.channel === "slack") {
if (!resolvedDelivery.to) {
if (!bestEffortDeliver)
return {
status: "error",
summary,
error:
"Cron delivery to Slack requires --channel slack and --to <channelId|user:ID>",
};
return {
status: "skipped",
summary: "Delivery skipped (no Slack destination).",
};
}
const slackTarget = resolvedDelivery.to;
const textLimit = resolveTextChunkLimit(params.cfg, "slack");
try {
for (const payload of payloads) {
const mediaList =
payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
if (mediaList.length === 0) {
for (const chunk of chunkText(payload.text ?? "", textLimit)) {
await params.deps.sendMessageSlack(slackTarget, chunk);
}
} else {
let first = true;
for (const url of mediaList) {
const caption = first ? (payload.text ?? "") : "";
first = false;
await params.deps.sendMessageSlack(slackTarget, caption, {
mediaUrl: url,
});
}
}
}
} catch (err) {
if (!bestEffortDeliver)
return { status: "error", summary, error: String(err) };
return { status: "ok", summary };
}
} else if (resolvedDelivery.channel === "signal") { } else if (resolvedDelivery.channel === "signal") {
if (!resolvedDelivery.to) { if (!resolvedDelivery.to) {
if (!bestEffortDeliver) if (!bestEffortDeliver)
-1
View File
@@ -19,7 +19,6 @@ export type CronPayload =
| "whatsapp" | "whatsapp"
| "telegram" | "telegram"
| "discord" | "discord"
| "slack"
| "signal" | "signal"
| "imessage"; | "imessage";
to?: string; to?: string;
-6
View File
@@ -15,7 +15,6 @@ export type ProviderKind =
| "whatsapp" | "whatsapp"
| "telegram" | "telegram"
| "discord" | "discord"
| "slack"
| "signal" | "signal"
| "imessage"; | "imessage";
@@ -48,7 +47,6 @@ type ReloadAction =
| "restart-provider:whatsapp" | "restart-provider:whatsapp"
| "restart-provider:telegram" | "restart-provider:telegram"
| "restart-provider:discord" | "restart-provider:discord"
| "restart-provider:slack"
| "restart-provider:signal" | "restart-provider:signal"
| "restart-provider:imessage"; | "restart-provider:imessage";
@@ -72,7 +70,6 @@ const RELOAD_RULES: ReloadRule[] = [
{ prefix: "web", kind: "hot", actions: ["restart-provider:whatsapp"] }, { prefix: "web", kind: "hot", actions: ["restart-provider:whatsapp"] },
{ prefix: "telegram", kind: "hot", actions: ["restart-provider:telegram"] }, { prefix: "telegram", kind: "hot", actions: ["restart-provider:telegram"] },
{ prefix: "discord", kind: "hot", actions: ["restart-provider:discord"] }, { prefix: "discord", kind: "hot", actions: ["restart-provider:discord"] },
{ prefix: "slack", kind: "hot", actions: ["restart-provider:slack"] },
{ prefix: "signal", kind: "hot", actions: ["restart-provider:signal"] }, { prefix: "signal", kind: "hot", actions: ["restart-provider:signal"] },
{ prefix: "imessage", kind: "hot", actions: ["restart-provider:imessage"] }, { prefix: "imessage", kind: "hot", actions: ["restart-provider:imessage"] },
{ prefix: "identity", kind: "none" }, { prefix: "identity", kind: "none" },
@@ -203,9 +200,6 @@ export function buildGatewayReloadPlan(
case "restart-provider:discord": case "restart-provider:discord":
plan.restartProviders.add("discord"); plan.restartProviders.add("discord");
break; break;
case "restart-provider:slack":
plan.restartProviders.add("slack");
break;
case "restart-provider:signal": case "restart-provider:signal":
plan.restartProviders.add("signal"); plan.restartProviders.add("signal");
break; break;
+1 -10
View File
@@ -23,7 +23,6 @@ export type HookMappingResolved = {
| "whatsapp" | "whatsapp"
| "telegram" | "telegram"
| "discord" | "discord"
| "slack"
| "signal" | "signal"
| "imessage"; | "imessage";
to?: string; to?: string;
@@ -62,7 +61,6 @@ export type HookAction =
| "whatsapp" | "whatsapp"
| "telegram" | "telegram"
| "discord" | "discord"
| "slack"
| "signal" | "signal"
| "imessage"; | "imessage";
to?: string; to?: string;
@@ -101,14 +99,7 @@ type HookTransformResult = Partial<{
name: string; name: string;
sessionKey: string; sessionKey: string;
deliver: boolean; deliver: boolean;
channel: channel: "last" | "whatsapp" | "telegram" | "discord" | "signal" | "imessage";
| "last"
| "whatsapp"
| "telegram"
| "discord"
| "slack"
| "signal"
| "imessage";
to: string; to: string;
thinking: string; thinking: string;
timeoutSeconds: number; timeoutSeconds: number;
+2 -11
View File
@@ -137,14 +137,7 @@ export type HookAgentPayload = {
wakeMode: "now" | "next-heartbeat"; wakeMode: "now" | "next-heartbeat";
sessionKey: string; sessionKey: string;
deliver: boolean; deliver: boolean;
channel: channel: "last" | "whatsapp" | "telegram" | "discord" | "signal" | "imessage";
| "last"
| "whatsapp"
| "telegram"
| "discord"
| "slack"
| "signal"
| "imessage";
to?: string; to?: string;
thinking?: string; thinking?: string;
timeoutSeconds?: number; timeoutSeconds?: number;
@@ -178,7 +171,6 @@ export function normalizeAgentPayload(
channelRaw === "whatsapp" || channelRaw === "whatsapp" ||
channelRaw === "telegram" || channelRaw === "telegram" ||
channelRaw === "discord" || channelRaw === "discord" ||
channelRaw === "slack" ||
channelRaw === "signal" || channelRaw === "signal" ||
channelRaw === "imessage" || channelRaw === "imessage" ||
channelRaw === "last" channelRaw === "last"
@@ -191,8 +183,7 @@ export function normalizeAgentPayload(
if (channel === null) { if (channel === null) {
return { return {
ok: false, ok: false,
error: error: "channel must be last|whatsapp|telegram|discord|signal|imessage",
"channel must be last|whatsapp|telegram|discord|slack|signal|imessage",
}; };
} }
const toRaw = payload.to; const toRaw = payload.to;
-1
View File
@@ -635,7 +635,6 @@ export const CronPayloadSchema = Type.Union([
Type.Literal("whatsapp"), Type.Literal("whatsapp"),
Type.Literal("telegram"), Type.Literal("telegram"),
Type.Literal("discord"), Type.Literal("discord"),
Type.Literal("slack"),
]), ]),
), ),
to: Type.Optional(Type.String()), to: Type.Optional(Type.String()),
-1
View File
@@ -37,7 +37,6 @@ type HookDispatchers = {
| "whatsapp" | "whatsapp"
| "telegram" | "telegram"
| "discord" | "discord"
| "slack"
| "signal" | "signal"
| "imessage"; | "imessage";
to?: string; to?: string;
-51
View File
@@ -8,11 +8,6 @@ import { type DiscordProbe, probeDiscord } from "../../discord/probe.js";
import { type IMessageProbe, probeIMessage } from "../../imessage/probe.js"; import { type IMessageProbe, probeIMessage } from "../../imessage/probe.js";
import { webAuthExists } from "../../providers/web/index.js"; import { webAuthExists } from "../../providers/web/index.js";
import { probeSignal, type SignalProbe } from "../../signal/probe.js"; import { probeSignal, type SignalProbe } from "../../signal/probe.js";
import { probeSlack, type SlackProbe } from "../../slack/probe.js";
import {
resolveSlackAppToken,
resolveSlackBotToken,
} from "../../slack/token.js";
import { probeTelegram, type TelegramProbe } from "../../telegram/probe.js"; import { probeTelegram, type TelegramProbe } from "../../telegram/probe.js";
import { resolveTelegramToken } from "../../telegram/token.js"; import { resolveTelegramToken } from "../../telegram/token.js";
import { getWebAuthAgeMs, readWebSelfId } from "../../web/session.js"; import { getWebAuthAgeMs, readWebSelfId } from "../../web/session.js";
@@ -79,41 +74,6 @@ export const providersHandlers: GatewayRequestHandlers = {
discordLastProbeAt = Date.now(); discordLastProbeAt = Date.now();
} }
const slackCfg = cfg.slack;
const slackEnabled = slackCfg?.enabled !== false;
const slackBotEnvToken = slackEnabled
? resolveSlackBotToken(process.env.SLACK_BOT_TOKEN)
: undefined;
const slackBotConfigToken = slackEnabled
? resolveSlackBotToken(slackCfg?.botToken)
: undefined;
const slackBotToken = slackBotEnvToken ?? slackBotConfigToken ?? "";
const slackBotTokenSource = slackBotEnvToken
? "env"
: slackBotConfigToken
? "config"
: "none";
const slackAppEnvToken = slackEnabled
? resolveSlackAppToken(process.env.SLACK_APP_TOKEN)
: undefined;
const slackAppConfigToken = slackEnabled
? resolveSlackAppToken(slackCfg?.appToken)
: undefined;
const slackAppToken = slackAppEnvToken ?? slackAppConfigToken ?? "";
const slackAppTokenSource = slackAppEnvToken
? "env"
: slackAppConfigToken
? "config"
: "none";
const slackConfigured =
slackEnabled && Boolean(slackBotToken) && Boolean(slackAppToken);
let slackProbe: SlackProbe | undefined;
let slackLastProbeAt: number | null = null;
if (probe && slackConfigured) {
slackProbe = await probeSlack(slackBotToken, timeoutMs);
slackLastProbeAt = Date.now();
}
const signalCfg = cfg.signal; const signalCfg = cfg.signal;
const signalEnabled = signalCfg?.enabled !== false; const signalEnabled = signalCfg?.enabled !== false;
const signalHost = signalCfg?.httpHost?.trim() || "127.0.0.1"; const signalHost = signalCfg?.httpHost?.trim() || "127.0.0.1";
@@ -192,17 +152,6 @@ export const providersHandlers: GatewayRequestHandlers = {
probe: discordProbe, probe: discordProbe,
lastProbeAt: discordLastProbeAt, lastProbeAt: discordLastProbeAt,
}, },
slack: {
configured: slackConfigured,
botTokenSource: slackBotTokenSource,
appTokenSource: slackAppTokenSource,
running: runtime.slack.running,
lastStartAt: runtime.slack.lastStartAt ?? null,
lastStopAt: runtime.slack.lastStopAt ?? null,
lastError: runtime.slack.lastError ?? null,
probe: slackProbe,
lastProbeAt: slackLastProbeAt,
},
signal: { signal: {
configured: signalConfigured, configured: signalConfigured,
baseUrl: signalBaseUrl, baseUrl: signalBaseUrl,
-17
View File
@@ -3,7 +3,6 @@ import { sendMessageDiscord } from "../../discord/index.js";
import { shouldLogVerbose } from "../../globals.js"; import { shouldLogVerbose } from "../../globals.js";
import { sendMessageIMessage } from "../../imessage/index.js"; import { sendMessageIMessage } from "../../imessage/index.js";
import { sendMessageSignal } from "../../signal/index.js"; import { sendMessageSignal } from "../../signal/index.js";
import { sendMessageSlack } from "../../slack/send.js";
import { sendMessageTelegram } from "../../telegram/send.js"; import { sendMessageTelegram } from "../../telegram/send.js";
import { resolveTelegramToken } from "../../telegram/token.js"; import { resolveTelegramToken } from "../../telegram/token.js";
import { sendMessageWhatsApp } from "../../web/outbound.js"; import { sendMessageWhatsApp } from "../../web/outbound.js";
@@ -88,22 +87,6 @@ export const sendHandlers: GatewayRequestHandlers = {
payload, payload,
}); });
respond(true, payload, undefined, { provider }); respond(true, payload, undefined, { provider });
} else if (provider === "slack") {
const result = await sendMessageSlack(to, message, {
mediaUrl: request.mediaUrl,
});
const payload = {
runId: idem,
messageId: result.messageId,
channelId: result.channelId,
provider,
};
context.dedupe.set(`send:${idem}`, {
ts: Date.now(),
ok: true,
payload,
});
respond(true, payload, undefined, { provider });
} else if (provider === "signal") { } else if (provider === "signal") {
const cfg = loadConfig(); const cfg = loadConfig();
const host = cfg.signal?.httpHost?.trim() || "127.0.0.1"; const host = cfg.signal?.httpHost?.trim() || "127.0.0.1";
-118
View File
@@ -7,11 +7,6 @@ import type { createSubsystemLogger } from "../logging.js";
import { monitorWebProvider, webAuthExists } from "../providers/web/index.js"; import { monitorWebProvider, webAuthExists } from "../providers/web/index.js";
import type { RuntimeEnv } from "../runtime.js"; import type { RuntimeEnv } from "../runtime.js";
import { monitorSignalProvider } from "../signal/index.js"; import { monitorSignalProvider } from "../signal/index.js";
import {
monitorSlackProvider,
resolveSlackAppToken,
resolveSlackBotToken,
} from "../slack/index.js";
import { monitorTelegramProvider } from "../telegram/monitor.js"; import { monitorTelegramProvider } from "../telegram/monitor.js";
import { probeTelegram } from "../telegram/probe.js"; import { probeTelegram } from "../telegram/probe.js";
import { resolveTelegramToken } from "../telegram/token.js"; import { resolveTelegramToken } from "../telegram/token.js";
@@ -34,13 +29,6 @@ export type DiscordRuntimeStatus = {
lastError?: string | null; lastError?: string | null;
}; };
export type SlackRuntimeStatus = {
running: boolean;
lastStartAt?: number | null;
lastStopAt?: number | null;
lastError?: string | null;
};
export type SignalRuntimeStatus = { export type SignalRuntimeStatus = {
running: boolean; running: boolean;
lastStartAt?: number | null; lastStartAt?: number | null;
@@ -62,7 +50,6 @@ export type ProviderRuntimeSnapshot = {
whatsapp: WebProviderStatus; whatsapp: WebProviderStatus;
telegram: TelegramRuntimeStatus; telegram: TelegramRuntimeStatus;
discord: DiscordRuntimeStatus; discord: DiscordRuntimeStatus;
slack: SlackRuntimeStatus;
signal: SignalRuntimeStatus; signal: SignalRuntimeStatus;
imessage: IMessageRuntimeStatus; imessage: IMessageRuntimeStatus;
}; };
@@ -74,13 +61,11 @@ type ProviderManagerOptions = {
logWhatsApp: SubsystemLogger; logWhatsApp: SubsystemLogger;
logTelegram: SubsystemLogger; logTelegram: SubsystemLogger;
logDiscord: SubsystemLogger; logDiscord: SubsystemLogger;
logSlack: SubsystemLogger;
logSignal: SubsystemLogger; logSignal: SubsystemLogger;
logIMessage: SubsystemLogger; logIMessage: SubsystemLogger;
whatsappRuntimeEnv: RuntimeEnv; whatsappRuntimeEnv: RuntimeEnv;
telegramRuntimeEnv: RuntimeEnv; telegramRuntimeEnv: RuntimeEnv;
discordRuntimeEnv: RuntimeEnv; discordRuntimeEnv: RuntimeEnv;
slackRuntimeEnv: RuntimeEnv;
signalRuntimeEnv: RuntimeEnv; signalRuntimeEnv: RuntimeEnv;
imessageRuntimeEnv: RuntimeEnv; imessageRuntimeEnv: RuntimeEnv;
}; };
@@ -94,8 +79,6 @@ export type ProviderManager = {
stopTelegramProvider: () => Promise<void>; stopTelegramProvider: () => Promise<void>;
startDiscordProvider: () => Promise<void>; startDiscordProvider: () => Promise<void>;
stopDiscordProvider: () => Promise<void>; stopDiscordProvider: () => Promise<void>;
startSlackProvider: () => Promise<void>;
stopSlackProvider: () => Promise<void>;
startSignalProvider: () => Promise<void>; startSignalProvider: () => Promise<void>;
stopSignalProvider: () => Promise<void>; stopSignalProvider: () => Promise<void>;
startIMessageProvider: () => Promise<void>; startIMessageProvider: () => Promise<void>;
@@ -111,13 +94,11 @@ export function createProviderManager(
logWhatsApp, logWhatsApp,
logTelegram, logTelegram,
logDiscord, logDiscord,
logSlack,
logSignal, logSignal,
logIMessage, logIMessage,
whatsappRuntimeEnv, whatsappRuntimeEnv,
telegramRuntimeEnv, telegramRuntimeEnv,
discordRuntimeEnv, discordRuntimeEnv,
slackRuntimeEnv,
signalRuntimeEnv, signalRuntimeEnv,
imessageRuntimeEnv, imessageRuntimeEnv,
} = opts; } = opts;
@@ -125,13 +106,11 @@ export function createProviderManager(
let whatsappAbort: AbortController | null = null; let whatsappAbort: AbortController | null = null;
let telegramAbort: AbortController | null = null; let telegramAbort: AbortController | null = null;
let discordAbort: AbortController | null = null; let discordAbort: AbortController | null = null;
let slackAbort: AbortController | null = null;
let signalAbort: AbortController | null = null; let signalAbort: AbortController | null = null;
let imessageAbort: AbortController | null = null; let imessageAbort: AbortController | null = null;
let whatsappTask: Promise<unknown> | null = null; let whatsappTask: Promise<unknown> | null = null;
let telegramTask: Promise<unknown> | null = null; let telegramTask: Promise<unknown> | null = null;
let discordTask: Promise<unknown> | null = null; let discordTask: Promise<unknown> | null = null;
let slackTask: Promise<unknown> | null = null;
let signalTask: Promise<unknown> | null = null; let signalTask: Promise<unknown> | null = null;
let imessageTask: Promise<unknown> | null = null; let imessageTask: Promise<unknown> | null = null;
@@ -158,12 +137,6 @@ export function createProviderManager(
lastStopAt: null, lastStopAt: null,
lastError: null, lastError: null,
}; };
let slackRuntime: SlackRuntimeStatus = {
running: false,
lastStartAt: null,
lastStopAt: null,
lastError: null,
};
let signalRuntime: SignalRuntimeStatus = { let signalRuntime: SignalRuntimeStatus = {
running: false, running: false,
lastStartAt: null, lastStartAt: null,
@@ -459,93 +432,6 @@ export function createProviderManager(
}; };
}; };
const startSlackProvider = async () => {
if (slackTask) return;
const cfg = loadConfig();
if (cfg.slack?.enabled === false) {
slackRuntime = {
...slackRuntime,
running: false,
lastError: "disabled",
};
if (shouldLogVerbose()) {
logSlack.debug("slack provider disabled (slack.enabled=false)");
}
return;
}
const botToken = resolveSlackBotToken(
process.env.SLACK_BOT_TOKEN ?? cfg.slack?.botToken ?? undefined,
);
const appToken = resolveSlackAppToken(
process.env.SLACK_APP_TOKEN ?? cfg.slack?.appToken ?? undefined,
);
if (!botToken || !appToken) {
slackRuntime = {
...slackRuntime,
running: false,
lastError: "not configured",
};
if (shouldLogVerbose()) {
logSlack.debug(
"slack provider not configured (missing SLACK_BOT_TOKEN/SLACK_APP_TOKEN)",
);
}
return;
}
logSlack.info(
`starting provider${cfg.slack ? "" : " (no slack config; tokens via env)"}`,
);
slackAbort = new AbortController();
slackRuntime = {
...slackRuntime,
running: true,
lastStartAt: Date.now(),
lastError: null,
};
const task = monitorSlackProvider({
botToken,
appToken,
runtime: slackRuntimeEnv,
abortSignal: slackAbort.signal,
mediaMaxMb: cfg.slack?.mediaMaxMb,
slashCommand: cfg.slack?.slashCommand,
})
.catch((err) => {
slackRuntime = {
...slackRuntime,
lastError: formatError(err),
};
logSlack.error(`provider exited: ${formatError(err)}`);
})
.finally(() => {
slackAbort = null;
slackTask = null;
slackRuntime = {
...slackRuntime,
running: false,
lastStopAt: Date.now(),
};
});
slackTask = task;
};
const stopSlackProvider = async () => {
if (!slackAbort && !slackTask) return;
slackAbort?.abort();
try {
await slackTask;
} catch {
// ignore
}
slackAbort = null;
slackTask = null;
slackRuntime = {
...slackRuntime,
running: false,
lastStopAt: Date.now(),
};
};
const startSignalProvider = async () => { const startSignalProvider = async () => {
if (signalTask) return; if (signalTask) return;
const cfg = loadConfig(); const cfg = loadConfig();
@@ -748,7 +634,6 @@ export function createProviderManager(
const startProviders = async () => { const startProviders = async () => {
await startWhatsAppProvider(); await startWhatsAppProvider();
await startDiscordProvider(); await startDiscordProvider();
await startSlackProvider();
await startTelegramProvider(); await startTelegramProvider();
await startSignalProvider(); await startSignalProvider();
await startIMessageProvider(); await startIMessageProvider();
@@ -767,7 +652,6 @@ export function createProviderManager(
whatsapp: { ...whatsappRuntime }, whatsapp: { ...whatsappRuntime },
telegram: { ...telegramRuntime }, telegram: { ...telegramRuntime },
discord: { ...discordRuntime }, discord: { ...discordRuntime },
slack: { ...slackRuntime },
signal: { ...signalRuntime }, signal: { ...signalRuntime },
imessage: { ...imessageRuntime }, imessage: { ...imessageRuntime },
}); });
@@ -781,8 +665,6 @@ export function createProviderManager(
stopTelegramProvider, stopTelegramProvider,
startDiscordProvider, startDiscordProvider,
stopDiscordProvider, stopDiscordProvider,
startSlackProvider,
stopSlackProvider,
startSignalProvider, startSignalProvider,
stopSignalProvider, stopSignalProvider,
startIMessageProvider, startIMessageProvider,
-8
View File
@@ -55,12 +55,6 @@ const hoisted = vi.hoisted(() => {
lastStopAt: null, lastStopAt: null,
lastError: null, lastError: null,
}, },
slack: {
running: false,
lastStartAt: null,
lastStopAt: null,
lastError: null,
},
signal: { signal: {
running: false, running: false,
lastStartAt: null, lastStartAt: null,
@@ -84,8 +78,6 @@ const hoisted = vi.hoisted(() => {
stopTelegramProvider: vi.fn(async () => {}), stopTelegramProvider: vi.fn(async () => {}),
startDiscordProvider: vi.fn(async () => {}), startDiscordProvider: vi.fn(async () => {}),
stopDiscordProvider: vi.fn(async () => {}), stopDiscordProvider: vi.fn(async () => {}),
startSlackProvider: vi.fn(async () => {}),
stopSlackProvider: vi.fn(async () => {}),
startSignalProvider: vi.fn(async () => {}), startSignalProvider: vi.fn(async () => {}),
stopSignalProvider: vi.fn(async () => {}), stopSignalProvider: vi.fn(async () => {}),
startIMessageProvider: vi.fn(async () => {}), startIMessageProvider: vi.fn(async () => {}),
+46 -68
View File
@@ -148,14 +148,12 @@ const logWsControl = log.child("ws");
const logWhatsApp = logProviders.child("whatsapp"); const logWhatsApp = logProviders.child("whatsapp");
const logTelegram = logProviders.child("telegram"); const logTelegram = logProviders.child("telegram");
const logDiscord = logProviders.child("discord"); const logDiscord = logProviders.child("discord");
const logSlack = logProviders.child("slack");
const logSignal = logProviders.child("signal"); const logSignal = logProviders.child("signal");
const logIMessage = logProviders.child("imessage"); const logIMessage = logProviders.child("imessage");
const canvasRuntime = runtimeForLogger(logCanvas); const canvasRuntime = runtimeForLogger(logCanvas);
const whatsappRuntimeEnv = runtimeForLogger(logWhatsApp); const whatsappRuntimeEnv = runtimeForLogger(logWhatsApp);
const telegramRuntimeEnv = runtimeForLogger(logTelegram); const telegramRuntimeEnv = runtimeForLogger(logTelegram);
const discordRuntimeEnv = runtimeForLogger(logDiscord); const discordRuntimeEnv = runtimeForLogger(logDiscord);
const slackRuntimeEnv = runtimeForLogger(logSlack);
const signalRuntimeEnv = runtimeForLogger(logSignal); const signalRuntimeEnv = runtimeForLogger(logSignal);
const imessageRuntimeEnv = runtimeForLogger(logIMessage); const imessageRuntimeEnv = runtimeForLogger(logIMessage);
@@ -480,7 +478,6 @@ export async function startGatewayServer(
| "whatsapp" | "whatsapp"
| "telegram" | "telegram"
| "discord" | "discord"
| "slack"
| "signal" | "signal"
| "imessage"; | "imessage";
to?: string; to?: string;
@@ -725,13 +722,11 @@ export async function startGatewayServer(
logWhatsApp, logWhatsApp,
logTelegram, logTelegram,
logDiscord, logDiscord,
logSlack,
logSignal, logSignal,
logIMessage, logIMessage,
whatsappRuntimeEnv, whatsappRuntimeEnv,
telegramRuntimeEnv, telegramRuntimeEnv,
discordRuntimeEnv, discordRuntimeEnv,
slackRuntimeEnv,
signalRuntimeEnv, signalRuntimeEnv,
imessageRuntimeEnv, imessageRuntimeEnv,
}); });
@@ -741,13 +736,11 @@ export async function startGatewayServer(
startWhatsAppProvider, startWhatsAppProvider,
startTelegramProvider, startTelegramProvider,
startDiscordProvider, startDiscordProvider,
startSlackProvider,
startSignalProvider, startSignalProvider,
startIMessageProvider, startIMessageProvider,
stopWhatsAppProvider, stopWhatsAppProvider,
stopTelegramProvider, stopTelegramProvider,
stopDiscordProvider, stopDiscordProvider,
stopSlackProvider,
stopSignalProvider, stopSignalProvider,
stopIMessageProvider, stopIMessageProvider,
markWhatsAppLoggedOut, markWhatsAppLoggedOut,
@@ -936,36 +929,23 @@ export async function startGatewayServer(
? bridgeHost ? bridgeHost
: undefined; : undefined;
const nodePresenceTimers = new Map<string, ReturnType<typeof setInterval>>(); if (bridgeEnabled && bridgePort > 0 && bridgeHost) {
try {
const stopNodePresenceTimer = (nodeId: string) => { const started = await startNodeBridgeServer({
const timer = nodePresenceTimers.get(nodeId); host: bridgeHost,
if (timer) { port: bridgePort,
clearInterval(timer); serverName: machineDisplayName,
} canvasHostPort: canvasHostPortForBridge,
nodePresenceTimers.delete(nodeId); canvasHostHost: canvasHostHostForBridge,
}; onRequest: (nodeId, req) => handleBridgeRequest(nodeId, req),
onAuthenticated: async (node) => {
const beaconNodePresence = (
node: {
nodeId: string;
displayName?: string;
remoteIp?: string;
version?: string;
platform?: string;
deviceFamily?: string;
modelIdentifier?: string;
},
reason: string,
) => {
const host = node.displayName?.trim() || node.nodeId; const host = node.displayName?.trim() || node.nodeId;
const rawIp = node.remoteIp?.trim(); const ip = node.remoteIp?.trim();
const ip = rawIp && !isLoopbackAddress(rawIp) ? rawIp : undefined;
const version = node.version?.trim() || "unknown"; const version = node.version?.trim() || "unknown";
const platform = node.platform?.trim() || undefined; const platform = node.platform?.trim() || undefined;
const deviceFamily = node.deviceFamily?.trim() || undefined; const deviceFamily = node.deviceFamily?.trim() || undefined;
const modelIdentifier = node.modelIdentifier?.trim() || undefined; const modelIdentifier = node.modelIdentifier?.trim() || undefined;
const text = `Node: ${host}${ip ? ` (${ip})` : ""} · app ${version} · last input 0s ago · mode remote · reason ${reason}`; const text = `Node: ${host}${ip ? ` (${ip})` : ""} · app ${version} · last input 0s ago · mode remote · reason node-connected`;
upsertPresence(node.nodeId, { upsertPresence(node.nodeId, {
host, host,
ip, ip,
@@ -974,7 +954,7 @@ export async function startGatewayServer(
deviceFamily, deviceFamily,
modelIdentifier, modelIdentifier,
mode: "remote", mode: "remote",
reason, reason: "node-connected",
lastInputSeconds: 0, lastInputSeconds: 0,
instanceId: node.nodeId, instanceId: node.nodeId,
text, text,
@@ -991,30 +971,6 @@ export async function startGatewayServer(
}, },
}, },
); );
};
const startNodePresenceTimer = (node: { nodeId: string }) => {
stopNodePresenceTimer(node.nodeId);
nodePresenceTimers.set(
node.nodeId,
setInterval(() => {
beaconNodePresence(node, "periodic");
}, 180_000),
);
};
if (bridgeEnabled && bridgePort > 0 && bridgeHost) {
try {
const started = await startNodeBridgeServer({
host: bridgeHost,
port: bridgePort,
serverName: machineDisplayName,
canvasHostPort: canvasHostPortForBridge,
canvasHostHost: canvasHostHostForBridge,
onRequest: (nodeId, req) => handleBridgeRequest(nodeId, req),
onAuthenticated: async (node) => {
beaconNodePresence(node, "node-connected");
startNodePresenceTimer(node);
try { try {
const cfg = await loadVoiceWakeConfig(); const cfg = await loadVoiceWakeConfig();
@@ -1029,8 +985,38 @@ export async function startGatewayServer(
}, },
onDisconnected: (node) => { onDisconnected: (node) => {
bridgeUnsubscribeAll(node.nodeId); bridgeUnsubscribeAll(node.nodeId);
stopNodePresenceTimer(node.nodeId); const host = node.displayName?.trim() || node.nodeId;
beaconNodePresence(node, "node-disconnected"); const ip = node.remoteIp?.trim();
const version = node.version?.trim() || "unknown";
const platform = node.platform?.trim() || undefined;
const deviceFamily = node.deviceFamily?.trim() || undefined;
const modelIdentifier = node.modelIdentifier?.trim() || undefined;
const text = `Node: ${host}${ip ? ` (${ip})` : ""} · app ${version} · last input 0s ago · mode remote · reason node-disconnected`;
upsertPresence(node.nodeId, {
host,
ip,
version,
platform,
deviceFamily,
modelIdentifier,
mode: "remote",
reason: "node-disconnected",
lastInputSeconds: 0,
instanceId: node.nodeId,
text,
});
presenceVersion += 1;
broadcast(
"presence",
{ presence: listSystemPresence() },
{
dropIfSlow: true,
stateVersion: {
presence: presenceVersion,
health: healthVersion,
},
},
);
}, },
onEvent: handleBridgeEvent, onEvent: handleBridgeEvent,
onPairRequested: (request) => { onPairRequested: (request) => {
@@ -1607,7 +1593,7 @@ export async function startGatewayServer(
} }
} }
// Launch configured providers (WhatsApp Web, Discord, Slack, Telegram) so gateway replies via the // Launch configured providers (WhatsApp Web, Discord, Telegram) so gateway replies via the
// surface the message came from. Tests can opt out via CLAWDIS_SKIP_PROVIDERS. // surface the message came from. Tests can opt out via CLAWDIS_SKIP_PROVIDERS.
if (process.env.CLAWDIS_SKIP_PROVIDERS !== "1") { if (process.env.CLAWDIS_SKIP_PROVIDERS !== "1") {
try { try {
@@ -1717,9 +1703,6 @@ export async function startGatewayServer(
startDiscordProvider, startDiscordProvider,
); );
} }
if (plan.restartProviders.has("slack")) {
await restartProvider("slack", stopSlackProvider, startSlackProvider);
}
if (plan.restartProviders.has("signal")) { if (plan.restartProviders.has("signal")) {
await restartProvider( await restartProvider(
"signal", "signal",
@@ -1823,16 +1806,11 @@ export async function startGatewayServer(
await stopWhatsAppProvider(); await stopWhatsAppProvider();
await stopTelegramProvider(); await stopTelegramProvider();
await stopDiscordProvider(); await stopDiscordProvider();
await stopSlackProvider();
await stopSignalProvider(); await stopSignalProvider();
await stopIMessageProvider(); await stopIMessageProvider();
await stopGmailWatcher(); await stopGmailWatcher();
cron.stop(); cron.stop();
heartbeatRunner.stop(); heartbeatRunner.stop();
for (const timer of nodePresenceTimers.values()) {
clearInterval(timer);
}
nodePresenceTimers.clear();
broadcast("shutdown", { broadcast("shutdown", {
reason, reason,
restartExpectedMs, restartExpectedMs,
+2 -37
View File
@@ -38,20 +38,12 @@ export type HeartbeatTarget =
| "whatsapp" | "whatsapp"
| "telegram" | "telegram"
| "discord" | "discord"
| "slack"
| "signal" | "signal"
| "imessage" | "imessage"
| "none"; | "none";
export type HeartbeatDeliveryTarget = { export type HeartbeatDeliveryTarget = {
channel: channel: "whatsapp" | "telegram" | "discord" | "signal" | "imessage" | "none";
| "whatsapp"
| "telegram"
| "discord"
| "slack"
| "signal"
| "imessage"
| "none";
to?: string; to?: string;
reason?: string; reason?: string;
}; };
@@ -61,7 +53,6 @@ type HeartbeatDeps = {
sendWhatsApp?: typeof sendMessageWhatsApp; sendWhatsApp?: typeof sendMessageWhatsApp;
sendTelegram?: typeof sendMessageTelegram; sendTelegram?: typeof sendMessageTelegram;
sendDiscord?: typeof sendMessageDiscord; sendDiscord?: typeof sendMessageDiscord;
sendSlack?: typeof sendMessageSlack;
sendSignal?: typeof sendMessageSignal; sendSignal?: typeof sendMessageSignal;
sendIMessage?: typeof sendMessageIMessage; sendIMessage?: typeof sendMessageIMessage;
getQueueSize?: (lane?: string) => number; getQueueSize?: (lane?: string) => number;
@@ -192,7 +183,6 @@ export function resolveHeartbeatDeliveryTarget(params: {
rawTarget === "whatsapp" || rawTarget === "whatsapp" ||
rawTarget === "telegram" || rawTarget === "telegram" ||
rawTarget === "discord" || rawTarget === "discord" ||
rawTarget === "slack" ||
rawTarget === "signal" || rawTarget === "signal" ||
rawTarget === "imessage" || rawTarget === "imessage" ||
rawTarget === "none" || rawTarget === "none" ||
@@ -219,7 +209,6 @@ export function resolveHeartbeatDeliveryTarget(params: {
| "whatsapp" | "whatsapp"
| "telegram" | "telegram"
| "discord" | "discord"
| "slack"
| "signal" | "signal"
| "imessage" | "imessage"
| undefined = | undefined =
@@ -228,7 +217,6 @@ export function resolveHeartbeatDeliveryTarget(params: {
: target === "whatsapp" || : target === "whatsapp" ||
target === "telegram" || target === "telegram" ||
target === "discord" || target === "discord" ||
target === "slack" ||
target === "signal" || target === "signal" ||
target === "imessage" target === "imessage"
? target ? target
@@ -300,13 +288,7 @@ function normalizeHeartbeatReply(
} }
async function deliverHeartbeatReply(params: { async function deliverHeartbeatReply(params: {
channel: channel: "whatsapp" | "telegram" | "discord" | "signal" | "imessage";
| "whatsapp"
| "telegram"
| "discord"
| "slack"
| "signal"
| "imessage";
to: string; to: string;
text: string; text: string;
mediaUrls: string[]; mediaUrls: string[];
@@ -317,7 +299,6 @@ async function deliverHeartbeatReply(params: {
| "sendWhatsApp" | "sendWhatsApp"
| "sendTelegram" | "sendTelegram"
| "sendDiscord" | "sendDiscord"
| "sendSlack"
| "sendSignal" | "sendSignal"
| "sendIMessage" | "sendIMessage"
> >
@@ -388,21 +369,6 @@ async function deliverHeartbeatReply(params: {
return; return;
} }
if (channel === "slack") {
if (mediaUrls.length === 0) {
for (const chunk of chunkText(text, textLimit)) {
await deps.sendSlack(to, chunk);
}
return;
}
let first = true;
for (const url of mediaUrls) {
const caption = first ? text : "";
first = false;
await deps.sendSlack(to, caption, { mediaUrl: url });
}
return;
}
if (mediaUrls.length === 0) { if (mediaUrls.length === 0) {
await deps.sendDiscord(to, text, { verbose: false }); await deps.sendDiscord(to, text, { verbose: false });
return; return;
@@ -532,7 +498,6 @@ export async function runHeartbeatOnce(opts: {
sendWhatsApp: opts.deps?.sendWhatsApp ?? sendMessageWhatsApp, sendWhatsApp: opts.deps?.sendWhatsApp ?? sendMessageWhatsApp,
sendTelegram: opts.deps?.sendTelegram ?? sendMessageTelegram, sendTelegram: opts.deps?.sendTelegram ?? sendMessageTelegram,
sendDiscord: opts.deps?.sendDiscord ?? sendMessageDiscord, sendDiscord: opts.deps?.sendDiscord ?? sendMessageDiscord,
sendSlack: opts.deps?.sendSlack ?? sendMessageSlack,
sendSignal: opts.deps?.sendSignal ?? sendMessageSignal, sendSignal: opts.deps?.sendSignal ?? sendMessageSignal,
sendIMessage: opts.deps?.sendIMessage ?? sendMessageIMessage, sendIMessage: opts.deps?.sendIMessage ?? sendMessageIMessage,
}; };
+6 -134
View File
@@ -1,8 +1,5 @@
import { import { convertTools } from "@mariozechner/pi-ai/dist/providers/google-shared.js";
convertMessages, import type { Tool } from "@mariozechner/pi-ai/dist/types.js";
convertTools,
} from "@mariozechner/pi-ai/dist/providers/google-shared.js";
import type { Context, Model, Tool } from "@mariozechner/pi-ai/dist/types.js";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
const asRecord = (value: unknown): Record<string, unknown> => { const asRecord = (value: unknown): Record<string, unknown> => {
@@ -12,47 +9,9 @@ const asRecord = (value: unknown): Record<string, unknown> => {
return value as Record<string, unknown>; return value as Record<string, unknown>;
}; };
const makeModel = (id: string): Model<"google-generative-ai"> =>
({
id,
name: id,
api: "google-generative-ai",
provider: "google",
baseUrl: "https://example.invalid",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1,
maxTokens: 1,
}) as Model<"google-generative-ai">;
describe("google-shared convertTools", () => { describe("google-shared convertTools", () => {
it("adds type:object when properties/required exist but type is missing", () => {
const tools = [
{
name: "noType",
description: "Tool with properties but no type",
parameters: {
properties: {
action: { type: "string" },
},
required: ["action"],
},
},
] as unknown as Tool[];
const converted = convertTools(tools);
const params = asRecord(
converted?.[0]?.functionDeclarations?.[0]?.parameters,
);
expect(params.type).toBe("object");
expect(params.properties).toBeDefined();
expect(params.required).toEqual(["action"]);
});
it("strips unsupported JSON Schema keywords", () => { it("strips unsupported JSON Schema keywords", () => {
const tools = [ const tools: Tool[] = [
{ {
name: "example", name: "example",
description: "Example tool", description: "Example tool",
@@ -81,7 +40,7 @@ describe("google-shared convertTools", () => {
required: ["mode"], required: ["mode"],
}, },
}, },
] as unknown as Tool[]; ];
const converted = convertTools(tools); const converted = convertTools(tools);
const params = asRecord( const params = asRecord(
@@ -102,7 +61,7 @@ describe("google-shared convertTools", () => {
}); });
it("keeps supported schema fields", () => { it("keeps supported schema fields", () => {
const tools = [ const tools: Tool[] = [
{ {
name: "settings", name: "settings",
description: "Settings tool", description: "Settings tool",
@@ -124,7 +83,7 @@ describe("google-shared convertTools", () => {
required: ["config"], required: ["config"],
}, },
}, },
] as unknown as Tool[]; ];
const converted = convertTools(tools); const converted = convertTools(tools);
const params = asRecord( const params = asRecord(
@@ -145,90 +104,3 @@ describe("google-shared convertTools", () => {
expect(params.required).toEqual(["config"]); expect(params.required).toEqual(["config"]);
}); });
}); });
describe("google-shared convertMessages", () => {
it("skips thinking blocks for Gemini to avoid mimicry", () => {
const model = makeModel("gemini-1.5-pro");
const context = {
messages: [
{
role: "assistant",
content: [
{
type: "thinking",
thinking: "hidden",
thinkingSignature: "sig",
},
],
api: "google-generative-ai",
provider: "google",
model: "gemini-1.5-pro",
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
total: 0,
},
},
stopReason: "stop",
timestamp: 0,
},
],
} as unknown as Context;
const contents = convertMessages(model, context);
expect(contents).toHaveLength(0);
});
it("keeps thought signatures for Claude models", () => {
const model = makeModel("claude-3-opus");
const context = {
messages: [
{
role: "assistant",
content: [
{
type: "thinking",
thinking: "structured",
thinkingSignature: "sig",
},
],
api: "google-generative-ai",
provider: "google",
model: "claude-3-opus",
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
total: 0,
},
},
stopReason: "stop",
timestamp: 0,
},
],
} as unknown as Context;
const contents = convertMessages(model, context);
const parts = contents?.[0]?.parts ?? [];
expect(parts).toHaveLength(1);
expect(parts[0]).toMatchObject({
thought: true,
thoughtSignature: "sig",
});
});
});
-183
View File
@@ -1,183 +0,0 @@
import { WebClient } from "@slack/web-api";
import { loadConfig } from "../config/config.js";
import { sendMessageSlack } from "./send.js";
import { resolveSlackBotToken } from "./token.js";
export type SlackActionClientOpts = {
token?: string;
client?: WebClient;
};
export type SlackMessageSummary = {
ts?: string;
text?: string;
user?: string;
thread_ts?: string;
reply_count?: number;
reactions?: Array<{
name?: string;
count?: number;
users?: string[];
}>;
};
export type SlackPin = {
type?: string;
message?: { ts?: string; text?: string };
file?: { id?: string; name?: string };
};
function resolveToken(explicit?: string) {
const cfgToken = loadConfig().slack?.botToken;
const token = resolveSlackBotToken(
explicit ?? process.env.SLACK_BOT_TOKEN ?? cfgToken ?? undefined,
);
if (!token) {
throw new Error(
"SLACK_BOT_TOKEN or slack.botToken is required for Slack actions",
);
}
return token;
}
function normalizeEmoji(raw: string) {
const trimmed = raw.trim();
if (!trimmed) {
throw new Error("Emoji is required for Slack reactions");
}
return trimmed.replace(/^:+|:+$/g, "");
}
async function getClient(opts: SlackActionClientOpts = {}) {
const token = resolveToken(opts.token);
return opts.client ?? new WebClient(token);
}
export async function reactSlackMessage(
channelId: string,
messageId: string,
emoji: string,
opts: SlackActionClientOpts = {},
) {
const client = await getClient(opts);
await client.reactions.add({
channel: channelId,
timestamp: messageId,
name: normalizeEmoji(emoji),
});
}
export async function listSlackReactions(
channelId: string,
messageId: string,
opts: SlackActionClientOpts = {},
): Promise<SlackMessageSummary["reactions"]> {
const client = await getClient(opts);
const result = await client.reactions.get({
channel: channelId,
timestamp: messageId,
full: true,
});
const message = result.message as SlackMessageSummary | undefined;
return message?.reactions ?? [];
}
export async function sendSlackMessage(
to: string,
content: string,
opts: SlackActionClientOpts & { mediaUrl?: string } = {},
) {
return await sendMessageSlack(to, content, {
token: opts.token,
mediaUrl: opts.mediaUrl,
client: opts.client,
});
}
export async function editSlackMessage(
channelId: string,
messageId: string,
content: string,
opts: SlackActionClientOpts = {},
) {
const client = await getClient(opts);
await client.chat.update({
channel: channelId,
ts: messageId,
text: content,
});
}
export async function deleteSlackMessage(
channelId: string,
messageId: string,
opts: SlackActionClientOpts = {},
) {
const client = await getClient(opts);
await client.chat.delete({
channel: channelId,
ts: messageId,
});
}
export async function readSlackMessages(
channelId: string,
opts: SlackActionClientOpts & {
limit?: number;
before?: string;
after?: string;
} = {},
): Promise<{ messages: SlackMessageSummary[]; hasMore: boolean }> {
const client = await getClient(opts);
const result = await client.conversations.history({
channel: channelId,
limit: opts.limit,
latest: opts.before,
oldest: opts.after,
});
return {
messages: (result.messages ?? []) as SlackMessageSummary[],
hasMore: Boolean(result.has_more),
};
}
export async function getSlackMemberInfo(
userId: string,
opts: SlackActionClientOpts = {},
) {
const client = await getClient(opts);
return await client.users.info({ user: userId });
}
export async function listSlackEmojis(opts: SlackActionClientOpts = {}) {
const client = await getClient(opts);
return await client.emoji.list();
}
export async function pinSlackMessage(
channelId: string,
messageId: string,
opts: SlackActionClientOpts = {},
) {
const client = await getClient(opts);
await client.pins.add({ channel: channelId, timestamp: messageId });
}
export async function unpinSlackMessage(
channelId: string,
messageId: string,
opts: SlackActionClientOpts = {},
) {
const client = await getClient(opts);
await client.pins.remove({ channel: channelId, timestamp: messageId });
}
export async function listSlackPins(
channelId: string,
opts: SlackActionClientOpts = {},
): Promise<SlackPin[]> {
const client = await getClient(opts);
const result = await client.pins.list({ channel: channelId });
return (result.items ?? []) as SlackPin[];
}
-17
View File
@@ -1,17 +0,0 @@
export {
deleteSlackMessage,
editSlackMessage,
getSlackMemberInfo,
listSlackEmojis,
listSlackPins,
listSlackReactions,
pinSlackMessage,
reactSlackMessage,
readSlackMessages,
sendSlackMessage,
unpinSlackMessage,
} from "./actions.js";
export { monitorSlackProvider } from "./monitor.js";
export { probeSlack } from "./probe.js";
export { sendMessageSlack } from "./send.js";
export { resolveSlackAppToken, resolveSlackBotToken } from "./token.js";
-1350
View File
File diff suppressed because it is too large. Load diff
-59
View File
@@ -1,59 +0,0 @@
import { WebClient } from "@slack/web-api";
export type SlackProbe = {
ok: boolean;
status?: number | null;
error?: string | null;
elapsedMs?: number | null;
bot?: { id?: string; name?: string };
team?: { id?: string; name?: string };
};
function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
if (!timeoutMs || timeoutMs <= 0) return promise;
let timer: NodeJS.Timeout | null = null;
const timeout = new Promise<T>((_, reject) => {
timer = setTimeout(() => reject(new Error("timeout")), timeoutMs);
});
return Promise.race([promise, timeout]).finally(() => {
if (timer) clearTimeout(timer);
});
}
export async function probeSlack(
token: string,
timeoutMs = 2500,
): Promise<SlackProbe> {
const client = new WebClient(token);
const start = Date.now();
try {
const result = await withTimeout(client.auth.test(), timeoutMs);
if (!result.ok) {
return {
ok: false,
status: 200,
error: result.error ?? "unknown",
elapsedMs: Date.now() - start,
};
}
return {
ok: true,
status: 200,
elapsedMs: Date.now() - start,
bot: { id: result.user_id ?? undefined, name: result.user ?? undefined },
team: { id: result.team_id ?? undefined, name: result.team ?? undefined },
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const status =
typeof (err as { status?: number }).status === "number"
? (err as { status?: number }).status
: null;
return {
ok: false,
status,
error: message,
elapsedMs: Date.now() - start,
};
}
}
-187
View File
@@ -1,187 +0,0 @@
import { type FilesUploadV2Arguments, WebClient } from "@slack/web-api";
import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
import { loadConfig } from "../config/config.js";
import { loadWebMedia } from "../web/media.js";
import { resolveSlackBotToken } from "./token.js";
const SLACK_TEXT_LIMIT = 4000;
type SlackRecipient =
| {
kind: "user";
id: string;
}
| {
kind: "channel";
id: string;
};
type SlackSendOpts = {
token?: string;
mediaUrl?: string;
client?: WebClient;
threadTs?: string;
};
export type SlackSendResult = {
messageId: string;
channelId: string;
};
function resolveToken(explicit?: string) {
const cfgToken = loadConfig().slack?.botToken;
const token = resolveSlackBotToken(
explicit ?? process.env.SLACK_BOT_TOKEN ?? cfgToken ?? undefined,
);
if (!token) {
throw new Error(
"SLACK_BOT_TOKEN or slack.botToken is required for Slack sends",
);
}
return token;
}
function parseRecipient(raw: string): SlackRecipient {
const trimmed = raw.trim();
if (!trimmed) {
throw new Error("Recipient is required for Slack sends");
}
const mentionMatch = trimmed.match(/^<@([A-Z0-9]+)>$/i);
if (mentionMatch) {
return { kind: "user", id: mentionMatch[1] };
}
if (trimmed.startsWith("user:")) {
return { kind: "user", id: trimmed.slice("user:".length) };
}
if (trimmed.startsWith("channel:")) {
return { kind: "channel", id: trimmed.slice("channel:".length) };
}
if (trimmed.startsWith("slack:")) {
return { kind: "user", id: trimmed.slice("slack:".length) };
}
if (trimmed.startsWith("@")) {
const candidate = trimmed.slice(1);
if (!/^[A-Z0-9]+$/i.test(candidate)) {
throw new Error("Slack DMs require a user id (use user:<id> or <@id>)");
}
return { kind: "user", id: candidate };
}
if (trimmed.startsWith("#")) {
const candidate = trimmed.slice(1);
if (!/^[A-Z0-9]+$/i.test(candidate)) {
throw new Error("Slack channels require a channel id (use channel:<id>)");
}
return { kind: "channel", id: candidate };
}
return { kind: "channel", id: trimmed };
}
async function resolveChannelId(
client: WebClient,
recipient: SlackRecipient,
): Promise<{ channelId: string; isDm?: boolean }> {
if (recipient.kind === "channel") {
return { channelId: recipient.id };
}
const response = await client.conversations.open({ users: recipient.id });
const channelId = response.channel?.id;
if (!channelId) {
throw new Error("Failed to open Slack DM channel");
}
return { channelId, isDm: true };
}
async function uploadSlackFile(params: {
client: WebClient;
channelId: string;
mediaUrl: string;
caption?: string;
threadTs?: string;
maxBytes?: number;
}): Promise<string> {
const { buffer, contentType, fileName } = await loadWebMedia(
params.mediaUrl,
params.maxBytes,
);
const basePayload = {
channel_id: params.channelId,
file: buffer,
filename: fileName,
...(params.caption ? { initial_comment: params.caption } : {}),
...(contentType ? { filetype: contentType } : {}),
};
const payload: FilesUploadV2Arguments = params.threadTs
? { ...basePayload, thread_ts: params.threadTs }
: basePayload;
const response = await params.client.files.uploadV2(payload);
const parsed = response as {
files?: Array<{ id?: string; name?: string }>;
file?: { id?: string; name?: string };
};
const fileId =
parsed.files?.[0]?.id ??
parsed.file?.id ??
parsed.files?.[0]?.name ??
parsed.file?.name ??
"unknown";
return fileId;
}
export async function sendMessageSlack(
to: string,
message: string,
opts: SlackSendOpts = {},
): Promise<SlackSendResult> {
const trimmedMessage = message?.trim() ?? "";
if (!trimmedMessage && !opts.mediaUrl) {
throw new Error("Slack send requires text or media");
}
const token = resolveToken(opts.token);
const client = opts.client ?? new WebClient(token);
const recipient = parseRecipient(to);
const { channelId } = await resolveChannelId(client, recipient);
const cfg = loadConfig();
const textLimit = resolveTextChunkLimit(cfg, "slack");
const chunkLimit = Math.min(textLimit, SLACK_TEXT_LIMIT);
const chunks = chunkText(trimmedMessage, chunkLimit);
const mediaMaxBytes =
typeof cfg.slack?.mediaMaxMb === "number"
? cfg.slack.mediaMaxMb * 1024 * 1024
: undefined;
let lastMessageId = "";
if (opts.mediaUrl) {
const [firstChunk, ...rest] = chunks;
lastMessageId = await uploadSlackFile({
client,
channelId,
mediaUrl: opts.mediaUrl,
caption: firstChunk,
threadTs: opts.threadTs,
maxBytes: mediaMaxBytes,
});
for (const chunk of rest) {
const response = await client.chat.postMessage({
channel: channelId,
text: chunk,
thread_ts: opts.threadTs,
});
lastMessageId = response.ts ?? lastMessageId;
}
} else {
for (const chunk of chunks.length ? chunks : [""]) {
const response = await client.chat.postMessage({
channel: channelId,
text: chunk,
thread_ts: opts.threadTs,
});
lastMessageId = response.ts ?? lastMessageId;
}
}
return {
messageId: lastMessageId || "unknown",
channelId,
};
}
-12
View File
@@ -1,12 +0,0 @@
export function normalizeSlackToken(raw?: string): string | undefined {
const trimmed = raw?.trim();
return trimmed ? trimmed : undefined;
}
export function resolveSlackBotToken(raw?: string): string | undefined {
return normalizeSlackToken(raw);
}
export function resolveSlackAppToken(raw?: string): string | undefined {
return normalizeSlackToken(raw);
}
-14
View File
@@ -27,7 +27,6 @@ import type {
CronFormState, CronFormState,
DiscordForm, DiscordForm,
IMessageForm, IMessageForm,
SlackForm,
SignalForm, SignalForm,
TelegramForm, TelegramForm,
} from "./ui-types"; } from "./ui-types";
@@ -45,7 +44,6 @@ import {
loadProviders, loadProviders,
updateDiscordForm, updateDiscordForm,
updateIMessageForm, updateIMessageForm,
updateSlackForm,
updateSignalForm, updateSignalForm,
updateTelegramForm, updateTelegramForm,
} from "./controllers/connections"; } from "./controllers/connections";
@@ -119,11 +117,6 @@ export type AppViewState = {
discordSaving: boolean; discordSaving: boolean;
discordTokenLocked: boolean; discordTokenLocked: boolean;
discordConfigStatus: string | null; discordConfigStatus: string | null;
slackForm: SlackForm;
slackSaving: boolean;
slackTokenLocked: boolean;
slackAppTokenLocked: boolean;
slackConfigStatus: string | null;
signalForm: SignalForm; signalForm: SignalForm;
signalSaving: boolean; signalSaving: boolean;
signalConfigStatus: string | null; signalConfigStatus: string | null;
@@ -276,11 +269,6 @@ export function renderApp(state: AppViewState) {
discordTokenLocked: state.discordTokenLocked, discordTokenLocked: state.discordTokenLocked,
discordSaving: state.discordSaving, discordSaving: state.discordSaving,
discordStatus: state.discordConfigStatus, discordStatus: state.discordConfigStatus,
slackForm: state.slackForm,
slackTokenLocked: state.slackTokenLocked,
slackAppTokenLocked: state.slackAppTokenLocked,
slackSaving: state.slackSaving,
slackStatus: state.slackConfigStatus,
signalForm: state.signalForm, signalForm: state.signalForm,
signalSaving: state.signalSaving, signalSaving: state.signalSaving,
signalStatus: state.signalConfigStatus, signalStatus: state.signalConfigStatus,
@@ -295,8 +283,6 @@ export function renderApp(state: AppViewState) {
onTelegramSave: () => state.handleTelegramSave(), onTelegramSave: () => state.handleTelegramSave(),
onDiscordChange: (patch) => updateDiscordForm(state, patch), onDiscordChange: (patch) => updateDiscordForm(state, patch),
onDiscordSave: () => state.handleDiscordSave(), onDiscordSave: () => state.handleDiscordSave(),
onSlackChange: (patch) => updateSlackForm(state, patch),
onSlackSave: () => state.handleSlackSave(),
onSignalChange: (patch) => updateSignalForm(state, patch), onSignalChange: (patch) => updateSignalForm(state, patch),
onSignalSave: () => state.handleSignalSave(), onSignalSave: () => state.handleSignalSave(),
onIMessageChange: (patch) => updateIMessageForm(state, patch), onIMessageChange: (patch) => updateIMessageForm(state, patch),
+1 -32
View File
@@ -36,11 +36,9 @@ import type {
} from "./types"; } from "./types";
import { import {
defaultDiscordActions, defaultDiscordActions,
defaultSlackActions,
type CronFormState, type CronFormState,
type DiscordForm, type DiscordForm,
type IMessageForm, type IMessageForm,
type SlackForm,
type SignalForm, type SignalForm,
type TelegramForm, type TelegramForm,
} from "./ui-types"; } from "./ui-types";
@@ -61,7 +59,6 @@ import {
logoutWhatsApp, logoutWhatsApp,
saveDiscordConfig, saveDiscordConfig,
saveIMessageConfig, saveIMessageConfig,
saveSlackConfig,
saveSignalConfig, saveSignalConfig,
saveTelegramConfig, saveTelegramConfig,
startWhatsAppLogin, startWhatsAppLogin,
@@ -236,6 +233,7 @@ export class ClawdisApp extends LitElement {
mediaMaxMb: "", mediaMaxMb: "",
historyLimit: "", historyLimit: "",
textChunkLimit: "", textChunkLimit: "",
replyToMode: "off",
guilds: [], guilds: [],
actions: { ...defaultDiscordActions }, actions: { ...defaultDiscordActions },
slashEnabled: false, slashEnabled: false,
@@ -246,29 +244,6 @@ export class ClawdisApp extends LitElement {
@state() discordSaving = false; @state() discordSaving = false;
@state() discordTokenLocked = false; @state() discordTokenLocked = false;
@state() discordConfigStatus: string | null = null; @state() discordConfigStatus: string | null = null;
@state() slackForm: SlackForm = {
enabled: true,
botToken: "",
appToken: "",
dmEnabled: true,
allowFrom: "",
groupEnabled: false,
groupChannels: "",
mediaMaxMb: "",
textChunkLimit: "",
reactionNotifications: "own",
reactionAllowlist: "",
slashEnabled: false,
slashName: "",
slashSessionPrefix: "",
slashEphemeral: true,
actions: { ...defaultSlackActions },
channels: [],
};
@state() slackSaving = false;
@state() slackTokenLocked = false;
@state() slackAppTokenLocked = false;
@state() slackConfigStatus: string | null = null;
@state() signalForm: SignalForm = { @state() signalForm: SignalForm = {
enabled: true, enabled: true,
account: "", account: "",
@@ -799,12 +774,6 @@ export class ClawdisApp extends LitElement {
await loadProviders(this, true); await loadProviders(this, true);
} }
async handleSlackSave() {
await saveSlackConfig(this);
await loadConfig(this);
await loadProviders(this, true);
}
async handleSignalSave() { async handleSignalSave() {
await saveSignalConfig(this); await saveSignalConfig(this);
await loadConfig(this); await loadConfig(this);
-88
View File
@@ -6,14 +6,11 @@ import type {
} from "../types"; } from "../types";
import { import {
defaultDiscordActions, defaultDiscordActions,
defaultSlackActions,
type DiscordActionForm, type DiscordActionForm,
type DiscordForm, type DiscordForm,
type DiscordGuildChannelForm, type DiscordGuildChannelForm,
type DiscordGuildForm, type DiscordGuildForm,
type IMessageForm, type IMessageForm,
type SlackChannelForm,
type SlackForm,
type SignalForm, type SignalForm,
type TelegramForm, type TelegramForm,
} from "../ui-types"; } from "../ui-types";
@@ -37,12 +34,10 @@ export type ConfigState = {
lastError: string | null; lastError: string | null;
telegramForm: TelegramForm; telegramForm: TelegramForm;
discordForm: DiscordForm; discordForm: DiscordForm;
slackForm: SlackForm;
signalForm: SignalForm; signalForm: SignalForm;
imessageForm: IMessageForm; imessageForm: IMessageForm;
telegramConfigStatus: string | null; telegramConfigStatus: string | null;
discordConfigStatus: string | null; discordConfigStatus: string | null;
slackConfigStatus: string | null;
signalConfigStatus: string | null; signalConfigStatus: string | null;
imessageConfigStatus: string | null; imessageConfigStatus: string | null;
}; };
@@ -240,88 +235,6 @@ export function applyConfigSnapshot(state: ConfigState, snapshot: ConfigSnapshot
typeof slash.ephemeral === "boolean" ? slash.ephemeral : true, typeof slash.ephemeral === "boolean" ? slash.ephemeral : true,
}; };
const slackDm = (slack.dm ?? {}) as Record<string, unknown>;
const slackChannels = slack.channels;
const slackSlash = (slack.slashCommand ?? {}) as Record<string, unknown>;
const slackActions =
(slack.actions ?? {}) as Partial<Record<keyof typeof defaultSlackActions, unknown>>;
state.slackForm = {
enabled: typeof slack.enabled === "boolean" ? slack.enabled : true,
botToken: typeof slack.botToken === "string" ? slack.botToken : "",
appToken: typeof slack.appToken === "string" ? slack.appToken : "",
dmEnabled: typeof slackDm.enabled === "boolean" ? slackDm.enabled : true,
allowFrom: toList(slackDm.allowFrom),
groupEnabled:
typeof slackDm.groupEnabled === "boolean" ? slackDm.groupEnabled : false,
groupChannels: toList(slackDm.groupChannels),
mediaMaxMb:
typeof slack.mediaMaxMb === "number" ? String(slack.mediaMaxMb) : "",
textChunkLimit:
typeof slack.textChunkLimit === "number"
? String(slack.textChunkLimit)
: "",
reactionNotifications:
slack.reactionNotifications === "off" ||
slack.reactionNotifications === "all" ||
slack.reactionNotifications === "allowlist"
? slack.reactionNotifications
: "own",
reactionAllowlist: toList(slack.reactionAllowlist),
slashEnabled:
typeof slackSlash.enabled === "boolean" ? slackSlash.enabled : false,
slashName: typeof slackSlash.name === "string" ? slackSlash.name : "",
slashSessionPrefix:
typeof slackSlash.sessionPrefix === "string"
? slackSlash.sessionPrefix
: "",
slashEphemeral:
typeof slackSlash.ephemeral === "boolean" ? slackSlash.ephemeral : true,
actions: {
...defaultSlackActions,
reactions:
typeof slackActions.reactions === "boolean"
? slackActions.reactions
: defaultSlackActions.reactions,
messages:
typeof slackActions.messages === "boolean"
? slackActions.messages
: defaultSlackActions.messages,
pins:
typeof slackActions.pins === "boolean"
? slackActions.pins
: defaultSlackActions.pins,
memberInfo:
typeof slackActions.memberInfo === "boolean"
? slackActions.memberInfo
: defaultSlackActions.memberInfo,
emojiList:
typeof slackActions.emojiList === "boolean"
? slackActions.emojiList
: defaultSlackActions.emojiList,
},
channels: Array.isArray(slackChannels)
? []
: typeof slackChannels === "object" && slackChannels
? Object.entries(slackChannels as Record<string, unknown>).map(
([key, value]): SlackChannelForm => {
const entry =
value && typeof value === "object"
? (value as Record<string, unknown>)
: {};
return {
key,
allow:
typeof entry.allow === "boolean" ? entry.allow : true,
requireMention:
typeof entry.requireMention === "boolean"
? entry.requireMention
: false,
};
},
)
: [],
};
state.signalForm = { state.signalForm = {
enabled: typeof signal.enabled === "boolean" ? signal.enabled : true, enabled: typeof signal.enabled === "boolean" ? signal.enabled : true,
account: typeof signal.account === "string" ? signal.account : "", account: typeof signal.account === "string" ? signal.account : "",
@@ -368,7 +281,6 @@ export function applyConfigSnapshot(state: ConfigState, snapshot: ConfigSnapshot
const configInvalid = snapshot.valid === false ? "Config invalid." : null; const configInvalid = snapshot.valid === false ? "Config invalid." : null;
state.telegramConfigStatus = configInvalid; state.telegramConfigStatus = configInvalid;
state.discordConfigStatus = configInvalid; state.discordConfigStatus = configInvalid;
state.slackConfigStatus = configInvalid;
state.signalConfigStatus = configInvalid; state.signalConfigStatus = configInvalid;
state.imessageConfigStatus = configInvalid; state.imessageConfigStatus = configInvalid;
-163
View File
@@ -3,14 +3,11 @@ import { parseList } from "../format";
import type { ConfigSnapshot, ProvidersStatusSnapshot } from "../types"; import type { ConfigSnapshot, ProvidersStatusSnapshot } from "../types";
import { import {
defaultDiscordActions, defaultDiscordActions,
defaultSlackActions,
type DiscordActionForm, type DiscordActionForm,
type DiscordForm, type DiscordForm,
type DiscordGuildChannelForm, type DiscordGuildChannelForm,
type DiscordGuildForm, type DiscordGuildForm,
type IMessageForm, type IMessageForm,
type SlackActionForm,
type SlackForm,
type SignalForm, type SignalForm,
type TelegramForm, type TelegramForm,
} from "../ui-types"; } from "../ui-types";
@@ -34,11 +31,6 @@ export type ConnectionsState = {
discordSaving: boolean; discordSaving: boolean;
discordTokenLocked: boolean; discordTokenLocked: boolean;
discordConfigStatus: string | null; discordConfigStatus: string | null;
slackForm: SlackForm;
slackSaving: boolean;
slackTokenLocked: boolean;
slackAppTokenLocked: boolean;
slackConfigStatus: string | null;
signalForm: SignalForm; signalForm: SignalForm;
signalSaving: boolean; signalSaving: boolean;
signalConfigStatus: string | null; signalConfigStatus: string | null;
@@ -62,8 +54,6 @@ export async function loadProviders(state: ConnectionsState, probe: boolean) {
state.providersLastSuccess = Date.now(); state.providersLastSuccess = Date.now();
state.telegramTokenLocked = res.telegram.tokenSource === "env"; state.telegramTokenLocked = res.telegram.tokenSource === "env";
state.discordTokenLocked = res.discord?.tokenSource === "env"; state.discordTokenLocked = res.discord?.tokenSource === "env";
state.slackTokenLocked = res.slack?.botTokenSource === "env";
state.slackAppTokenLocked = res.slack?.appTokenSource === "env";
} catch (err) { } catch (err) {
state.providersError = String(err); state.providersError = String(err);
} finally { } finally {
@@ -146,21 +136,6 @@ export function updateDiscordForm(
state.discordForm = { ...state.discordForm, ...patch }; state.discordForm = { ...state.discordForm, ...patch };
} }
export function updateSlackForm(
state: ConnectionsState,
patch: Partial<SlackForm>,
) {
if (patch.actions) {
state.slackForm = {
...state.slackForm,
...patch,
actions: { ...state.slackForm.actions, ...patch.actions },
};
return;
}
state.slackForm = { ...state.slackForm, ...patch };
}
export function updateSignalForm( export function updateSignalForm(
state: ConnectionsState, state: ConnectionsState,
patch: Partial<SignalForm>, patch: Partial<SignalForm>,
@@ -404,144 +379,6 @@ export async function saveDiscordConfig(state: ConnectionsState) {
} }
} }
export async function saveSlackConfig(state: ConnectionsState) {
if (!state.client || !state.connected) return;
if (state.slackSaving) return;
state.slackSaving = true;
state.slackConfigStatus = null;
try {
const base = state.configSnapshot?.config ?? {};
const config = { ...base } as Record<string, unknown>;
const slack = { ...(config.slack ?? {}) } as Record<string, unknown>;
const form = state.slackForm;
if (form.enabled) {
delete slack.enabled;
} else {
slack.enabled = false;
}
if (!state.slackTokenLocked) {
const token = form.botToken.trim();
if (token) slack.botToken = token;
else delete slack.botToken;
}
if (!state.slackAppTokenLocked) {
const token = form.appToken.trim();
if (token) slack.appToken = token;
else delete slack.appToken;
}
const dm = { ...(slack.dm ?? {}) } as Record<string, unknown>;
dm.enabled = form.dmEnabled;
const allowFrom = parseList(form.allowFrom);
if (allowFrom.length > 0) dm.allowFrom = allowFrom;
else delete dm.allowFrom;
if (form.groupEnabled) {
dm.groupEnabled = true;
} else {
delete dm.groupEnabled;
}
const groupChannels = parseList(form.groupChannels);
if (groupChannels.length > 0) dm.groupChannels = groupChannels;
else delete dm.groupChannels;
if (Object.keys(dm).length > 0) slack.dm = dm;
else delete slack.dm;
const mediaMaxMb = Number.parseFloat(form.mediaMaxMb);
if (Number.isFinite(mediaMaxMb) && mediaMaxMb > 0) {
slack.mediaMaxMb = mediaMaxMb;
} else {
delete slack.mediaMaxMb;
}
const textChunkLimit = Number.parseInt(form.textChunkLimit, 10);
if (Number.isFinite(textChunkLimit) && textChunkLimit > 0) {
slack.textChunkLimit = textChunkLimit;
} else {
delete slack.textChunkLimit;
}
if (form.reactionNotifications === "own") {
delete slack.reactionNotifications;
} else {
slack.reactionNotifications = form.reactionNotifications;
}
const reactionAllowlist = parseList(form.reactionAllowlist);
if (reactionAllowlist.length > 0) {
slack.reactionAllowlist = reactionAllowlist;
} else {
delete slack.reactionAllowlist;
}
const slash = { ...(slack.slashCommand ?? {}) } as Record<string, unknown>;
if (form.slashEnabled) {
slash.enabled = true;
} else {
delete slash.enabled;
}
if (form.slashName.trim()) slash.name = form.slashName.trim();
else delete slash.name;
if (form.slashSessionPrefix.trim())
slash.sessionPrefix = form.slashSessionPrefix.trim();
else delete slash.sessionPrefix;
if (form.slashEphemeral) {
delete slash.ephemeral;
} else {
slash.ephemeral = false;
}
if (Object.keys(slash).length > 0) slack.slashCommand = slash;
else delete slack.slashCommand;
const actions: Partial<SlackActionForm> = {};
const applyAction = (key: keyof SlackActionForm) => {
const value = form.actions[key];
if (value !== defaultSlackActions[key]) actions[key] = value;
};
applyAction("reactions");
applyAction("messages");
applyAction("pins");
applyAction("memberInfo");
applyAction("emojiList");
if (Object.keys(actions).length > 0) {
slack.actions = actions;
} else {
delete slack.actions;
}
const channels = form.channels
.map((entry): [string, Record<string, unknown>] | null => {
const key = entry.key.trim();
if (!key) return null;
const record: Record<string, unknown> = {
allow: entry.allow,
requireMention: entry.requireMention,
};
return [key, record];
})
.filter((value): value is [string, Record<string, unknown>] => Boolean(value));
if (channels.length > 0) {
slack.channels = Object.fromEntries(channels);
} else {
delete slack.channels;
}
if (Object.keys(slack).length > 0) {
config.slack = slack;
} else {
delete config.slack;
}
const raw = `${JSON.stringify(config, null, 2).trimEnd()}\n`;
await state.client.request("config.set", { raw });
state.slackConfigStatus = "Saved. Restart gateway if needed.";
} catch (err) {
state.slackConfigStatus = String(err);
} finally {
state.slackSaving = false;
}
}
export async function saveSignalConfig(state: ConnectionsState) { export async function saveSignalConfig(state: ConnectionsState) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) return;
if (state.signalSaving) return; if (state.signalSaving) return;
-17
View File
@@ -192,23 +192,6 @@
"kick": { "label": "kick", "detailKeys": ["guildId", "userId"] }, "kick": { "label": "kick", "detailKeys": ["guildId", "userId"] },
"ban": { "label": "ban", "detailKeys": ["guildId", "userId"] } "ban": { "label": "ban", "detailKeys": ["guildId", "userId"] }
} }
},
"slack": {
"emoji": "💬",
"title": "Slack",
"actions": {
"react": { "label": "react", "detailKeys": ["channelId", "messageId", "emoji"] },
"reactions": { "label": "reactions", "detailKeys": ["channelId", "messageId"] },
"sendMessage": { "label": "send", "detailKeys": ["to", "content"] },
"editMessage": { "label": "edit", "detailKeys": ["channelId", "messageId"] },
"deleteMessage": { "label": "delete", "detailKeys": ["channelId", "messageId"] },
"readMessages": { "label": "read messages", "detailKeys": ["channelId", "limit"] },
"pinMessage": { "label": "pin", "detailKeys": ["channelId", "messageId"] },
"unpinMessage": { "label": "unpin", "detailKeys": ["channelId", "messageId"] },
"listPins": { "label": "list pins", "detailKeys": ["channelId"] },
"memberInfo": { "label": "member", "detailKeys": ["userId"] },
"emojiList": { "label": "emoji list" }
}
} }
} }
} }
-32
View File
@@ -3,7 +3,6 @@ export type ProvidersStatusSnapshot = {
whatsapp: WhatsAppStatus; whatsapp: WhatsAppStatus;
telegram: TelegramStatus; telegram: TelegramStatus;
discord?: DiscordStatus | null; discord?: DiscordStatus | null;
slack?: SlackStatus | null;
signal?: SignalStatus | null; signal?: SignalStatus | null;
imessage?: IMessageStatus | null; imessage?: IMessageStatus | null;
}; };
@@ -90,37 +89,6 @@ export type DiscordStatus = {
lastProbeAt?: number | null; lastProbeAt?: number | null;
}; };
export type SlackBot = {
id?: string | null;
name?: string | null;
};
export type SlackTeam = {
id?: string | null;
name?: string | null;
};
export type SlackProbe = {
ok: boolean;
status?: number | null;
error?: string | null;
elapsedMs?: number | null;
bot?: SlackBot | null;
team?: SlackTeam | null;
};
export type SlackStatus = {
configured: boolean;
botTokenSource?: string | null;
appTokenSource?: string | null;
running: boolean;
lastStartAt?: number | null;
lastStopAt?: number | null;
lastError?: string | null;
probe?: SlackProbe | null;
lastProbeAt?: number | null;
};
export type SignalProbe = { export type SignalProbe = {
ok: boolean; ok: boolean;
status?: number | null; status?: number | null;
-42
View File
@@ -60,40 +60,6 @@ export type DiscordActionForm = {
moderation: boolean; moderation: boolean;
}; };
export type SlackChannelForm = {
key: string;
allow: boolean;
requireMention: boolean;
};
export type SlackActionForm = {
reactions: boolean;
messages: boolean;
pins: boolean;
memberInfo: boolean;
emojiList: boolean;
};
export type SlackForm = {
enabled: boolean;
botToken: string;
appToken: string;
dmEnabled: boolean;
allowFrom: string;
groupEnabled: boolean;
groupChannels: string;
mediaMaxMb: string;
textChunkLimit: string;
reactionNotifications: "off" | "own" | "all" | "allowlist";
reactionAllowlist: string;
slashEnabled: boolean;
slashName: string;
slashSessionPrefix: string;
slashEphemeral: boolean;
actions: SlackActionForm;
channels: SlackChannelForm[];
};
export const defaultDiscordActions: DiscordActionForm = { export const defaultDiscordActions: DiscordActionForm = {
reactions: true, reactions: true,
stickers: true, stickers: true,
@@ -112,14 +78,6 @@ export const defaultDiscordActions: DiscordActionForm = {
moderation: false, moderation: false,
}; };
export const defaultSlackActions: SlackActionForm = {
reactions: true,
messages: true,
pins: true,
memberInfo: true,
emojiList: true,
};
export type SignalForm = { export type SignalForm = {
enabled: boolean; enabled: boolean;
account: string; account: string;
+2 -420
View File
@@ -6,8 +6,6 @@ import type {
DiscordActionForm, DiscordActionForm,
DiscordForm, DiscordForm,
IMessageForm, IMessageForm,
SlackActionForm,
SlackForm,
SignalForm, SignalForm,
TelegramForm, TelegramForm,
} from "../ui-types"; } from "../ui-types";
@@ -30,14 +28,6 @@ const discordActionOptions = [
{ key: "moderation", label: "Moderation" }, { key: "moderation", label: "Moderation" },
] satisfies Array<{ key: keyof DiscordActionForm; label: string }>; ] satisfies Array<{ key: keyof DiscordActionForm; label: string }>;
const slackActionOptions = [
{ key: "reactions", label: "Reactions" },
{ key: "messages", label: "Messages" },
{ key: "pins", label: "Pins" },
{ key: "memberInfo", label: "Member info" },
{ key: "emojiList", label: "Emoji list" },
] satisfies Array<{ key: keyof SlackActionForm; label: string }>;
export type ConnectionsProps = { export type ConnectionsProps = {
connected: boolean; connected: boolean;
loading: boolean; loading: boolean;
@@ -56,11 +46,6 @@ export type ConnectionsProps = {
discordTokenLocked: boolean; discordTokenLocked: boolean;
discordSaving: boolean; discordSaving: boolean;
discordStatus: string | null; discordStatus: string | null;
slackForm: SlackForm;
slackTokenLocked: boolean;
slackAppTokenLocked: boolean;
slackSaving: boolean;
slackStatus: string | null;
signalForm: SignalForm; signalForm: SignalForm;
signalSaving: boolean; signalSaving: boolean;
signalStatus: string | null; signalStatus: string | null;
@@ -75,8 +60,6 @@ export type ConnectionsProps = {
onTelegramSave: () => void; onTelegramSave: () => void;
onDiscordChange: (patch: Partial<DiscordForm>) => void; onDiscordChange: (patch: Partial<DiscordForm>) => void;
onDiscordSave: () => void; onDiscordSave: () => void;
onSlackChange: (patch: Partial<SlackForm>) => void;
onSlackSave: () => void;
onSignalChange: (patch: Partial<SignalForm>) => void; onSignalChange: (patch: Partial<SignalForm>) => void;
onSignalSave: () => void; onSignalSave: () => void;
onIMessageChange: (patch: Partial<IMessageForm>) => void; onIMessageChange: (patch: Partial<IMessageForm>) => void;
@@ -87,14 +70,12 @@ export function renderConnections(props: ConnectionsProps) {
const whatsapp = props.snapshot?.whatsapp; const whatsapp = props.snapshot?.whatsapp;
const telegram = props.snapshot?.telegram; const telegram = props.snapshot?.telegram;
const discord = props.snapshot?.discord ?? null; const discord = props.snapshot?.discord ?? null;
const slack = props.snapshot?.slack ?? null;
const signal = props.snapshot?.signal ?? null; const signal = props.snapshot?.signal ?? null;
const imessage = props.snapshot?.imessage ?? null; const imessage = props.snapshot?.imessage ?? null;
const providerOrder: ProviderKey[] = [ const providerOrder: ProviderKey[] = [
"whatsapp", "whatsapp",
"telegram", "telegram",
"discord", "discord",
"slack",
"signal", "signal",
"imessage", "imessage",
]; ];
@@ -112,14 +93,7 @@ export function renderConnections(props: ConnectionsProps) {
return html` return html`
<section class="grid grid-cols-2"> <section class="grid grid-cols-2">
${orderedProviders.map((provider) => ${orderedProviders.map((provider) =>
renderProvider(provider.key, props, { renderProvider(provider.key, props, { whatsapp, telegram, discord, signal, imessage }),
whatsapp,
telegram,
discord,
slack,
signal,
imessage,
}),
)} )}
</section> </section>
@@ -153,13 +127,7 @@ function formatDuration(ms?: number | null) {
return `${hr}h`; return `${hr}h`;
} }
type ProviderKey = type ProviderKey = "whatsapp" | "telegram" | "discord" | "signal" | "imessage";
| "whatsapp"
| "telegram"
| "discord"
| "slack"
| "signal"
| "imessage";
function providerEnabled(key: ProviderKey, props: ConnectionsProps) { function providerEnabled(key: ProviderKey, props: ConnectionsProps) {
const snapshot = props.snapshot; const snapshot = props.snapshot;
@@ -175,8 +143,6 @@ function providerEnabled(key: ProviderKey, props: ConnectionsProps) {
return snapshot.telegram.configured || snapshot.telegram.running; return snapshot.telegram.configured || snapshot.telegram.running;
case "discord": case "discord":
return Boolean(snapshot.discord?.configured || snapshot.discord?.running); return Boolean(snapshot.discord?.configured || snapshot.discord?.running);
case "slack":
return Boolean(snapshot.slack?.configured || snapshot.slack?.running);
case "signal": case "signal":
return Boolean(snapshot.signal?.configured || snapshot.signal?.running); return Boolean(snapshot.signal?.configured || snapshot.signal?.running);
case "imessage": case "imessage":
@@ -193,7 +159,6 @@ function renderProvider(
whatsapp?: ProvidersStatusSnapshot["whatsapp"]; whatsapp?: ProvidersStatusSnapshot["whatsapp"];
telegram?: ProvidersStatusSnapshot["telegram"]; telegram?: ProvidersStatusSnapshot["telegram"];
discord?: ProvidersStatusSnapshot["discord"] | null; discord?: ProvidersStatusSnapshot["discord"] | null;
slack?: ProvidersStatusSnapshot["slack"] | null;
signal?: ProvidersStatusSnapshot["signal"] | null; signal?: ProvidersStatusSnapshot["signal"] | null;
imessage?: ProvidersStatusSnapshot["imessage"] | null; imessage?: ProvidersStatusSnapshot["imessage"] | null;
}, },
@@ -976,389 +941,6 @@ function renderProvider(
</div> </div>
`; `;
} }
case "slack": {
const slack = data.slack;
const botName = slack?.probe?.bot?.name;
const teamName = slack?.probe?.team?.name;
return html`
<div class="card">
<div class="card-title">Slack</div>
<div class="card-sub">Socket mode status and bot details.</div>
<div class="status-list" style="margin-top: 16px;">
<div>
<span class="label">Configured</span>
<span>${slack?.configured ? "Yes" : "No"}</span>
</div>
<div>
<span class="label">Running</span>
<span>${slack?.running ? "Yes" : "No"}</span>
</div>
<div>
<span class="label">Bot</span>
<span>${botName ? botName : "n/a"}</span>
</div>
<div>
<span class="label">Team</span>
<span>${teamName ? teamName : "n/a"}</span>
</div>
<div>
<span class="label">Last start</span>
<span>${slack?.lastStartAt ? formatAgo(slack.lastStartAt) : "n/a"}</span>
</div>
<div>
<span class="label">Last probe</span>
<span>${slack?.lastProbeAt ? formatAgo(slack.lastProbeAt) : "n/a"}</span>
</div>
</div>
${slack?.lastError
? html`<div class="callout danger" style="margin-top: 12px;">
${slack.lastError}
</div>`
: nothing}
${slack?.probe
? html`<div class="callout" style="margin-top: 12px;">
Probe ${slack.probe.ok ? "ok" : "failed"} ·
${slack.probe.status ?? ""}
${slack.probe.error ?? ""}
</div>`
: nothing}
<div class="form-grid" style="margin-top: 16px;">
<label class="field">
<span>Enabled</span>
<select
.value=${props.slackForm.enabled ? "yes" : "no"}
@change=${(e: Event) =>
props.onSlackChange({
enabled: (e.target as HTMLSelectElement).value === "yes",
})}
>
<option value="yes">Yes</option>
<option value="no">No</option>
</select>
</label>
<label class="field">
<span>Bot token</span>
<input
type="password"
.value=${props.slackForm.botToken}
?disabled=${props.slackTokenLocked}
@input=${(e: Event) =>
props.onSlackChange({
botToken: (e.target as HTMLInputElement).value,
})}
/>
</label>
<label class="field">
<span>App token</span>
<input
type="password"
.value=${props.slackForm.appToken}
?disabled=${props.slackAppTokenLocked}
@input=${(e: Event) =>
props.onSlackChange({
appToken: (e.target as HTMLInputElement).value,
})}
/>
</label>
<label class="field">
<span>DMs enabled</span>
<select
.value=${props.slackForm.dmEnabled ? "yes" : "no"}
@change=${(e: Event) =>
props.onSlackChange({
dmEnabled: (e.target as HTMLSelectElement).value === "yes",
})}
>
<option value="yes">Enabled</option>
<option value="no">Disabled</option>
</select>
</label>
<label class="field">
<span>Allow DMs from</span>
<input
.value=${props.slackForm.allowFrom}
@input=${(e: Event) =>
props.onSlackChange({
allowFrom: (e.target as HTMLInputElement).value,
})}
placeholder="U123, U456, *"
/>
</label>
<label class="field">
<span>Group DMs enabled</span>
<select
.value=${props.slackForm.groupEnabled ? "yes" : "no"}
@change=${(e: Event) =>
props.onSlackChange({
groupEnabled: (e.target as HTMLSelectElement).value === "yes",
})}
>
<option value="yes">Enabled</option>
<option value="no">Disabled</option>
</select>
</label>
<label class="field">
<span>Group DM channels</span>
<input
.value=${props.slackForm.groupChannels}
@input=${(e: Event) =>
props.onSlackChange({
groupChannels: (e.target as HTMLInputElement).value,
})}
placeholder="G123, #team"
/>
</label>
<label class="field">
<span>Reaction notifications</span>
<select
.value=${props.slackForm.reactionNotifications}
@change=${(e: Event) =>
props.onSlackChange({
reactionNotifications: (e.target as HTMLSelectElement)
.value as "off" | "own" | "all" | "allowlist",
})}
>
<option value="off">Off</option>
<option value="own">Own</option>
<option value="all">All</option>
<option value="allowlist">Allowlist</option>
</select>
</label>
<label class="field">
<span>Reaction allowlist</span>
<input
.value=${props.slackForm.reactionAllowlist}
@input=${(e: Event) =>
props.onSlackChange({
reactionAllowlist: (e.target as HTMLInputElement).value,
})}
placeholder="U123, U456"
/>
</label>
<label class="field">
<span>Text chunk limit</span>
<input
.value=${props.slackForm.textChunkLimit}
@input=${(e: Event) =>
props.onSlackChange({
textChunkLimit: (e.target as HTMLInputElement).value,
})}
placeholder="4000"
/>
</label>
<label class="field">
<span>Media max (MB)</span>
<input
.value=${props.slackForm.mediaMaxMb}
@input=${(e: Event) =>
props.onSlackChange({
mediaMaxMb: (e.target as HTMLInputElement).value,
})}
placeholder="20"
/>
</label>
</div>
<div class="card-sub" style="margin-top: 16px;">Slash command</div>
<div class="form-grid" style="margin-top: 8px;">
<label class="field">
<span>Slash enabled</span>
<select
.value=${props.slackForm.slashEnabled ? "yes" : "no"}
@change=${(e: Event) =>
props.onSlackChange({
slashEnabled: (e.target as HTMLSelectElement).value === "yes",
})}
>
<option value="yes">Enabled</option>
<option value="no">Disabled</option>
</select>
</label>
<label class="field">
<span>Slash name</span>
<input
.value=${props.slackForm.slashName}
@input=${(e: Event) =>
props.onSlackChange({
slashName: (e.target as HTMLInputElement).value,
})}
placeholder="clawd"
/>
</label>
<label class="field">
<span>Slash session prefix</span>
<input
.value=${props.slackForm.slashSessionPrefix}
@input=${(e: Event) =>
props.onSlackChange({
slashSessionPrefix: (e.target as HTMLInputElement).value,
})}
placeholder="slack:slash"
/>
</label>
<label class="field">
<span>Slash ephemeral</span>
<select
.value=${props.slackForm.slashEphemeral ? "yes" : "no"}
@change=${(e: Event) =>
props.onSlackChange({
slashEphemeral: (e.target as HTMLSelectElement).value === "yes",
})}
>
<option value="yes">Yes</option>
<option value="no">No</option>
</select>
</label>
</div>
<div class="card-sub" style="margin-top: 16px;">Channels</div>
<div class="card-sub">
Add channel ids or #names and optionally require mentions.
</div>
<div class="list">
${props.slackForm.channels.map(
(channel, channelIndex) => html`
<div class="list-item">
<div class="list-main">
<div class="form-grid">
<label class="field">
<span>Channel id / name</span>
<input
.value=${channel.key}
@input=${(e: Event) => {
const next = [...props.slackForm.channels];
next[channelIndex] = {
...next[channelIndex],
key: (e.target as HTMLInputElement).value,
};
props.onSlackChange({ channels: next });
}}
/>
</label>
<label class="field">
<span>Allow</span>
<select
.value=${channel.allow ? "yes" : "no"}
@change=${(e: Event) => {
const next = [...props.slackForm.channels];
next[channelIndex] = {
...next[channelIndex],
allow:
(e.target as HTMLSelectElement).value === "yes",
};
props.onSlackChange({ channels: next });
}}
>
<option value="yes">Yes</option>
<option value="no">No</option>
</select>
</label>
<label class="field">
<span>Require mention</span>
<select
.value=${channel.requireMention ? "yes" : "no"}
@change=${(e: Event) => {
const next = [...props.slackForm.channels];
next[channelIndex] = {
...next[channelIndex],
requireMention:
(e.target as HTMLSelectElement).value === "yes",
};
props.onSlackChange({ channels: next });
}}
>
<option value="yes">Yes</option>
<option value="no">No</option>
</select>
</label>
<label class="field">
<span>&nbsp;</span>
<button
class="btn"
@click=${() => {
const next = [...props.slackForm.channels];
next.splice(channelIndex, 1);
props.onSlackChange({ channels: next });
}}
>
Remove
</button>
</label>
</div>
</div>
</div>
`,
)}
</div>
<button
class="btn"
style="margin-top: 8px;"
@click=${() =>
props.onSlackChange({
channels: [
...props.slackForm.channels,
{ key: "", allow: true, requireMention: false },
],
})}
>
Add channel
</button>
<div class="card-sub" style="margin-top: 16px;">Tool actions</div>
<div class="form-grid" style="margin-top: 8px;">
${slackActionOptions.map(
(action) => html`<label class="field">
<span>${action.label}</span>
<select
.value=${props.slackForm.actions[action.key] ? "yes" : "no"}
@change=${(e: Event) =>
props.onSlackChange({
actions: {
...props.slackForm.actions,
[action.key]: (e.target as HTMLSelectElement).value === "yes",
},
})}
>
<option value="yes">Enabled</option>
<option value="no">Disabled</option>
</select>
</label>`,
)}
</div>
${props.slackTokenLocked || props.slackAppTokenLocked
? html`<div class="callout" style="margin-top: 12px;">
${props.slackTokenLocked ? "SLACK_BOT_TOKEN " : ""}
${props.slackAppTokenLocked ? "SLACK_APP_TOKEN " : ""}
is set in the environment. Config edits will not override it.
</div>`
: nothing}
${props.slackStatus
? html`<div class="callout" style="margin-top: 12px;">
${props.slackStatus}
</div>`
: nothing}
<div class="row" style="margin-top: 14px;">
<button
class="btn primary"
?disabled=${props.slackSaving}
@click=${() => props.onSlackSave()}
>
${props.slackSaving ? "Saving…" : "Save"}
</button>
<button class="btn" @click=${() => props.onRefresh(true)}>
Probe
</button>
</div>
</div>
`;
}
case "signal": { case "signal": {
const signal = data.signal; const signal = data.signal;
return html` return html`