Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b3f9a5934 | ||
|
|
0372457ba8 | ||
|
|
f28a03c407 | ||
|
|
f5abc8e9c9 |
No files matched your search
+8
-4
@@ -9,28 +9,31 @@
|
||||
- Sessions: primary session key is fixed to `main` (or `global` for global scope); `session.mainKey` is ignored.
|
||||
|
||||
### 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: 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.
|
||||
- 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.
|
||||
- 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 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.
|
||||
- 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.
|
||||
- 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
|
||||
- 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: consolidate checks to avoid redundant installs (#144) — thanks @thewilloftheshadow.
|
||||
- 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: 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: 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.
|
||||
- Bash tool: default auto-background delay to 10s.
|
||||
- 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.
|
||||
- Sandbox: document per-session agent sandbox setup, browser image, and Docker build.
|
||||
- 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
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ Welcome to the lobster tank! 🦞
|
||||
- **Peter Steinberger** - Benevolent Dictator
|
||||
- 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)
|
||||
|
||||
- **Jos** - Telegram, API, Nix mode
|
||||
|
||||
+1
-1
Submodule Peekaboo updated: 5c8eedd642...b69e4e8dc0.
@@ -73,7 +73,6 @@ struct InstancesSettings: View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack(spacing: 8) {
|
||||
Text(inst.host ?? "unknown host").font(.subheadline.bold())
|
||||
self.presenceIndicator(inst)
|
||||
if let ip = inst.ip { Text("(") + Text(ip).monospaced() + Text(")") }
|
||||
}
|
||||
|
||||
@@ -147,29 +146,6 @@ struct InstancesSettings: View {
|
||||
.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
|
||||
private func leadingDeviceIcon(_ inst: InstanceInfo, device: DevicePresentation?) -> some View {
|
||||
let symbol = self.leadingDeviceSymbol(inst, device: device)
|
||||
@@ -331,10 +307,6 @@ struct InstancesSettings: View {
|
||||
return "Connect"
|
||||
case "disconnect":
|
||||
return "Disconnect"
|
||||
case "node-connected":
|
||||
return "Node connect"
|
||||
case "node-disconnected":
|
||||
return "Node disconnect"
|
||||
case "launch":
|
||||
return "Launch"
|
||||
case "periodic":
|
||||
|
||||
@@ -292,63 +292,6 @@ Reaction notification modes:
|
||||
- `all`: all reactions on all messages.
|
||||
- `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)
|
||||
|
||||
Clawdis spawns `imsg rpc` (JSON-RPC over stdio). No daemon or port required.
|
||||
|
||||
+1
-1
@@ -100,7 +100,7 @@ Pairing details: `docs/gateway/pairing.md`.
|
||||
|
||||
## 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):
|
||||
```bash
|
||||
clawdis nodes status
|
||||
|
||||
+1
-14
@@ -24,7 +24,7 @@ Presence entries are structured objects with (some) fields:
|
||||
- `modelIdentifier` (optional): hardware model identifier like `iPad16,6` or `Mac16,6`
|
||||
- `mode`: e.g. `gateway`, `app`, `webchat`, `cli`
|
||||
- `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)
|
||||
- `ts`: last update timestamp (ms since epoch)
|
||||
|
||||
@@ -61,16 +61,6 @@ Implementation:
|
||||
- Gateway: `src/gateway/server.ts` handles method `system-event` by calling `updateSystemPresence(...)`.
|
||||
- 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)
|
||||
|
||||
All producers write into a single in-memory presence map.
|
||||
@@ -119,9 +109,6 @@ Implementation:
|
||||
- View: `apps/macos/Sources/Clawdis/InstancesSettings.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.
|
||||
|
||||
## Debugging tips
|
||||
|
||||
@@ -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
@@ -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
@@ -79,8 +79,6 @@
|
||||
"@mariozechner/pi-ai": "^0.32.3",
|
||||
"@mariozechner/pi-coding-agent": "^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",
|
||||
"@whiskeysockets/baileys": "7.0.0-rc.9",
|
||||
"ajv": "^8.17.1",
|
||||
@@ -140,7 +138,8 @@
|
||||
"@sinclair/typebox": "0.34.46"
|
||||
},
|
||||
"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": {
|
||||
|
||||
@@ -1,39 +1,8 @@
|
||||
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
|
||||
+++ b/dist/providers/google-shared.js
|
||||
@@ -51,9 +51,19 @@ 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) {
|
||||
@@ -140,6 +140,72 @@ export function convertMessages(model, context) {
|
||||
}
|
||||
return contents;
|
||||
}
|
||||
@@ -101,17 +70,12 @@ index 7bc0a9f5d6241f191cd607ecb37b3acac8d58267..76166a34784cbc0718d4b9bd1fa6336a
|
||||
+ 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;
|
||||
+}
|
||||
/**
|
||||
* 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) => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
|
||||
Generated
+15
-325
@@ -9,8 +9,11 @@ overrides:
|
||||
|
||||
patchedDependencies:
|
||||
'@mariozechner/pi-ai':
|
||||
hash: 628fb051b6f4886984a846a5ee7aa0a571c3360d35b8d114e4684e5edcd100c5
|
||||
hash: 969db6f3f4cc91fec48124e1f5e515b386b1f1bed807769d0a80c28abadbaaae
|
||||
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:
|
||||
|
||||
@@ -30,22 +33,16 @@ importers:
|
||||
version: 0.32.3(ws@8.18.3)(zod@4.3.4)
|
||||
'@mariozechner/pi-ai':
|
||||
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':
|
||||
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':
|
||||
specifier: ^0.32.3
|
||||
version: 0.32.3
|
||||
'@sinclair/typebox':
|
||||
specifier: 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':
|
||||
specifier: 7.0.0-rc.9
|
||||
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':
|
||||
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':
|
||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||
|
||||
@@ -1185,9 +1156,6 @@ packages:
|
||||
'@types/http-errors@2.0.5':
|
||||
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':
|
||||
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
|
||||
|
||||
@@ -1203,9 +1171,6 @@ packages:
|
||||
'@types/mime-types@2.1.4':
|
||||
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':
|
||||
resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==}
|
||||
|
||||
@@ -1221,9 +1186,6 @@ packages:
|
||||
'@types/range-parser@1.2.7':
|
||||
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
|
||||
|
||||
'@types/retry@0.12.0':
|
||||
resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
|
||||
|
||||
'@types/send@1.2.1':
|
||||
resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==}
|
||||
|
||||
@@ -1398,9 +1360,6 @@ packages:
|
||||
async-mutex@0.5.0:
|
||||
resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==}
|
||||
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
|
||||
atomic-sleep@1.0.0:
|
||||
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
@@ -1415,9 +1374,6 @@ packages:
|
||||
resolution: {integrity: sha512-En9AY6EG1qYqEy5L/quryzbA4akBpJrnBZNxeKTqGHC2xT9Qc4aZ8b7CcbOMFTTc/MGdoNyp+SN4zInZNKxMYA==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
axios@1.13.2:
|
||||
resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
|
||||
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
@@ -1533,10 +1489,6 @@ packages:
|
||||
color-name@1.1.4:
|
||||
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:
|
||||
resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==}
|
||||
engines: {node: '>=20'}
|
||||
@@ -1591,10 +1543,6 @@ packages:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
depd@2.0.0:
|
||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -1673,10 +1621,6 @@ packages:
|
||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1700,9 +1644,6 @@ packages:
|
||||
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
eventemitter3@4.0.7:
|
||||
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
||||
|
||||
eventemitter3@5.0.1:
|
||||
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
|
||||
|
||||
@@ -1759,23 +1700,10 @@ packages:
|
||||
resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
form-data@4.0.5:
|
||||
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
formdata-polyfill@4.0.10:
|
||||
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
@@ -1872,10 +1800,6 @@ packages:
|
||||
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
has-tostringtag@1.0.2:
|
||||
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hashery@1.4.0:
|
||||
resolution: {integrity: sha512-Wn2i1In6XFxl8Az55kkgnFRiAlIAushzh26PTjL2AKtQcEfXrcLa7Hn5QOWGZEf3LU057P9TwwZjFyxfS1VuvQ==}
|
||||
engines: {node: '>=20'}
|
||||
@@ -1929,9 +1853,6 @@ packages:
|
||||
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
is-electron@2.2.2:
|
||||
resolution: {integrity: sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==}
|
||||
|
||||
is-extglob@2.1.1:
|
||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1951,10 +1872,6 @@ packages:
|
||||
is-promise@4.0.0:
|
||||
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
|
||||
|
||||
is-stream@2.0.1:
|
||||
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
is-url@1.2.4:
|
||||
resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
|
||||
|
||||
@@ -2018,10 +1935,6 @@ packages:
|
||||
jsonc-parser@3.3.1:
|
||||
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:
|
||||
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
|
||||
|
||||
@@ -2123,27 +2036,6 @@ packages:
|
||||
lit@3.3.2:
|
||||
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:
|
||||
resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==}
|
||||
|
||||
@@ -2223,18 +2115,10 @@ packages:
|
||||
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
mime-db@1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-db@1.54.0:
|
||||
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
|
||||
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:
|
||||
resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -2280,6 +2164,9 @@ packages:
|
||||
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
node-addon-api@7.1.1:
|
||||
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||
|
||||
node-domexception@1.0.0:
|
||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||
engines: {node: '>=10.5.0'}
|
||||
@@ -2363,26 +2250,10 @@ packages:
|
||||
oxlint-tsgolint:
|
||||
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:
|
||||
resolution: {integrity: sha512-RhBdVhSwJb7Ocn3e8ULk4NMwBEuOxe+1zcgphUy9c2e5aR/xbEsdVXxHJ3lynw6Qiqu7OINEyHlZkiblEpaq7w==}
|
||||
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:
|
||||
resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==}
|
||||
engines: {node: '>=20'}
|
||||
@@ -2506,9 +2377,6 @@ packages:
|
||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
proxy-from-env@1.1.0:
|
||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||
|
||||
punycode.js@2.3.1:
|
||||
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -2582,10 +2450,6 @@ packages:
|
||||
resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
retry@0.13.1:
|
||||
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
reusify@1.1.0:
|
||||
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
|
||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||
@@ -2833,10 +2697,6 @@ packages:
|
||||
resolution: {integrity: sha512-XuELoRpMR+sq8fuWwX7P0bcj+PRNiicOKDEb3fGNURhxWVyykCi9BNq7c4uVz7h7P0sj8qgBsr5SWS6yBClq3g==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
tsscmp@1.0.6:
|
||||
resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==}
|
||||
engines: {node: '>=0.6.x'}
|
||||
|
||||
tsx@4.21.0:
|
||||
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
|
||||
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)':
|
||||
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
|
||||
transitivePeerDependencies:
|
||||
- '@modelcontextprotocol/sdk'
|
||||
@@ -3497,7 +3357,7 @@ snapshots:
|
||||
- ws
|
||||
- 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:
|
||||
'@anthropic-ai/sdk': 0.71.2(zod@4.3.4)
|
||||
'@google/genai': 1.34.0
|
||||
@@ -3517,10 +3377,10 @@ snapshots:
|
||||
- ws
|
||||
- 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:
|
||||
'@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
|
||||
chalk: 5.6.2
|
||||
cli-highlight: 2.1.11
|
||||
@@ -3766,71 +3626,6 @@ snapshots:
|
||||
|
||||
'@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': {}
|
||||
|
||||
'@testing-library/dom@10.4.1':
|
||||
@@ -3908,11 +3703,6 @@ snapshots:
|
||||
|
||||
'@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/long@4.0.2': {}
|
||||
@@ -3926,8 +3716,6 @@ snapshots:
|
||||
|
||||
'@types/mime-types@2.1.4': {}
|
||||
|
||||
'@types/ms@2.1.0': {}
|
||||
|
||||
'@types/node@10.17.60': {}
|
||||
|
||||
'@types/node@25.0.3':
|
||||
@@ -3940,8 +3728,6 @@ snapshots:
|
||||
|
||||
'@types/range-parser@1.2.7': {}
|
||||
|
||||
'@types/retry@0.12.0': {}
|
||||
|
||||
'@types/send@1.2.1':
|
||||
dependencies:
|
||||
'@types/node': 25.0.3
|
||||
@@ -4175,8 +3961,6 @@ snapshots:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
atomic-sleep@1.0.0: {}
|
||||
|
||||
audio-buffer@5.0.0:
|
||||
@@ -4197,14 +3981,6 @@ snapshots:
|
||||
audio-type@2.2.1:
|
||||
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@3.0.1: {}
|
||||
@@ -4337,10 +4113,6 @@ snapshots:
|
||||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
|
||||
commander@14.0.2: {}
|
||||
|
||||
commander@8.3.0: {}
|
||||
@@ -4377,8 +4149,6 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
depd@2.0.0: {}
|
||||
|
||||
dequal@2.0.3:
|
||||
@@ -4452,13 +4222,6 @@ snapshots:
|
||||
dependencies:
|
||||
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:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.27.2
|
||||
@@ -4500,8 +4263,6 @@ snapshots:
|
||||
|
||||
event-target-shim@5.0.1: {}
|
||||
|
||||
eventemitter3@4.0.7: {}
|
||||
|
||||
eventemitter3@5.0.1: {}
|
||||
|
||||
events@3.3.0: {}
|
||||
@@ -4592,21 +4353,11 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
follow-redirects@1.15.11: {}
|
||||
|
||||
foreground-child@3.3.1:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
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:
|
||||
dependencies:
|
||||
fetch-blob: 3.2.0
|
||||
@@ -4727,10 +4478,6 @@ snapshots:
|
||||
|
||||
has-symbols@1.1.0: {}
|
||||
|
||||
has-tostringtag@1.0.2:
|
||||
dependencies:
|
||||
has-symbols: 1.1.0
|
||||
|
||||
hashery@1.4.0:
|
||||
dependencies:
|
||||
hookified: 1.14.0
|
||||
@@ -4780,8 +4527,6 @@ snapshots:
|
||||
dependencies:
|
||||
binary-extensions: 2.3.0
|
||||
|
||||
is-electron@2.2.2: {}
|
||||
|
||||
is-extglob@2.1.1: {}
|
||||
|
||||
is-fullwidth-code-point@3.0.0: {}
|
||||
@@ -4794,8 +4539,6 @@ snapshots:
|
||||
|
||||
is-promise@4.0.0: {}
|
||||
|
||||
is-stream@2.0.1: {}
|
||||
|
||||
is-url@1.2.4: {}
|
||||
|
||||
isarray@1.0.0: {}
|
||||
@@ -4857,19 +4600,6 @@ snapshots:
|
||||
|
||||
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:
|
||||
dependencies:
|
||||
lie: 3.3.0
|
||||
@@ -4970,20 +4700,6 @@ snapshots:
|
||||
lit-element: 4.2.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@4.17.21: {}
|
||||
@@ -5047,14 +4763,8 @@ snapshots:
|
||||
braces: 3.0.3
|
||||
picomatch: 2.3.1
|
||||
|
||||
mime-db@1.52.0: {}
|
||||
|
||||
mime-db@1.54.0: {}
|
||||
|
||||
mime-types@2.1.35:
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
|
||||
mime-types@3.0.2:
|
||||
dependencies:
|
||||
mime-db: 1.54.0
|
||||
@@ -5104,6 +4814,8 @@ snapshots:
|
||||
|
||||
negotiator@1.0.0: {}
|
||||
|
||||
node-addon-api@7.1.1: {}
|
||||
|
||||
node-domexception@1.0.0: {}
|
||||
|
||||
node-fetch@2.7.0:
|
||||
@@ -5180,27 +4892,11 @@ snapshots:
|
||||
'@oxlint/win32-x64': 1.36.0
|
||||
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:
|
||||
dependencies:
|
||||
eventemitter3: 5.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: {}
|
||||
|
||||
package-json-from-dist@1.0.1: {}
|
||||
@@ -5340,8 +5036,6 @@ snapshots:
|
||||
forwarded: 0.2.0
|
||||
ipaddr.js: 1.9.1
|
||||
|
||||
proxy-from-env@1.1.0: {}
|
||||
|
||||
punycode.js@2.3.1: {}
|
||||
|
||||
qified@0.5.3:
|
||||
@@ -5428,8 +5122,6 @@ snapshots:
|
||||
|
||||
retry@0.12.0: {}
|
||||
|
||||
retry@0.13.1: {}
|
||||
|
||||
reusify@1.1.0: {}
|
||||
|
||||
rimraf@5.0.10:
|
||||
@@ -5740,8 +5432,6 @@ snapshots:
|
||||
|
||||
tslog@4.10.2: {}
|
||||
|
||||
tsscmp@1.0.6: {}
|
||||
|
||||
tsx@4.21.0:
|
||||
dependencies:
|
||||
esbuild: 0.27.2
|
||||
|
||||
@@ -205,71 +205,3 @@ git worktree remove /tmp/issue-99
|
||||
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
|
||||
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
@@ -1,38 +1,27 @@
|
||||
---
|
||||
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
|
||||
metadata: {"clawdis":{"emoji":"📦","requires":{"bins":["mcporter"]},"install":[{"id":"node","kind":"node","package":"mcporter","bins":["mcporter"],"label":"Install mcporter (node)"}]}}
|
||||
---
|
||||
|
||||
# mcporter
|
||||
|
||||
Use `mcporter` to work with MCP servers directly.
|
||||
Use `mcporter` to list MCP servers and call tools.
|
||||
|
||||
Quick start
|
||||
- `mcporter list`
|
||||
- `mcporter list <server> --schema`
|
||||
- `mcporter call <server.tool> key=value`
|
||||
- `mcporter call <server.tool> arg=value`
|
||||
|
||||
Call tools
|
||||
- Selector: `mcporter call linear.list_issues team=ENG limit:5`
|
||||
- Function syntax: `mcporter call "linear.create_issue(title: \"Bug\")"`
|
||||
- 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 + lifecycle
|
||||
- OAuth: `mcporter auth <server>`
|
||||
- Daemon: `mcporter daemon status|start|stop`
|
||||
|
||||
Auth + config
|
||||
- OAuth: `mcporter auth <server | url> [--reset]`
|
||||
- Config: `mcporter config list|get|add|remove|import|login|logout`
|
||||
|
||||
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`
|
||||
Ad-hoc servers
|
||||
- HTTP: `mcporter list --http-url https://host/mcp --name <name>`
|
||||
- STDIO: `mcporter call --stdio "bun run ./server.ts" --name <name>`
|
||||
|
||||
Notes
|
||||
- Config default: `./config/mcporter.json` (override with `--config`).
|
||||
- Prefer `--output json` for machine-readable results.
|
||||
- Config sources: `~/.mcporter/mcporter.json[c]` and `config/mcporter.json`.
|
||||
- Prefer `--json` when you need machine-readable output.
|
||||
@@ -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.
|
||||
@@ -8,7 +8,6 @@ import { createNodesTool } from "./tools/nodes-tool.js";
|
||||
import { createSessionsHistoryTool } from "./tools/sessions-history-tool.js";
|
||||
import { createSessionsListTool } from "./tools/sessions-list-tool.js";
|
||||
import { createSessionsSendTool } from "./tools/sessions-send-tool.js";
|
||||
import { createSlackTool } from "./tools/slack-tool.js";
|
||||
|
||||
export function createClawdisTools(options?: {
|
||||
browserControlUrl?: string;
|
||||
@@ -21,7 +20,6 @@ export function createClawdisTools(options?: {
|
||||
createNodesTool(),
|
||||
createCronTool(),
|
||||
createDiscordTool(),
|
||||
createSlackTool(),
|
||||
createGatewayTool(),
|
||||
createSessionsListTool(),
|
||||
createSessionsHistoryTool(),
|
||||
|
||||
@@ -248,9 +248,7 @@ export function subscribeEmbeddedPiSession(params: {
|
||||
};
|
||||
|
||||
const emitBlockChunk = (text: string) => {
|
||||
// Strip any <thinking> tags that may have leaked into the output (e.g., from Gemini mimicking history)
|
||||
const strippedText = stripThinkingSegments(stripUnpairedThinkingTags(text));
|
||||
const chunk = strippedText.trimEnd();
|
||||
const chunk = text.trimEnd();
|
||||
if (!chunk) return;
|
||||
if (chunk === lastBlockReplyText) return;
|
||||
lastBlockReplyText = chunk;
|
||||
|
||||
@@ -88,14 +88,6 @@ describe("createClawdisCodingTools", () => {
|
||||
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 () => {
|
||||
const tools = createClawdisCodingTools();
|
||||
const readTool = tools.find((tool) => tool.name === "read");
|
||||
|
||||
+3
-12
@@ -441,12 +441,6 @@ function shouldIncludeDiscordTool(surface?: string): boolean {
|
||||
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?: {
|
||||
bash?: BashToolDefaults & ProcessToolDefaults;
|
||||
surface?: string;
|
||||
@@ -500,12 +494,9 @@ export function createClawdisCodingTools(options?: {
|
||||
}),
|
||||
];
|
||||
const allowDiscord = shouldIncludeDiscordTool(options?.surface);
|
||||
const allowSlack = shouldIncludeSlackTool(options?.surface);
|
||||
const filtered = tools.filter((tool) => {
|
||||
if (tool.name === "discord") return allowDiscord;
|
||||
if (tool.name === "slack") return allowSlack;
|
||||
return true;
|
||||
});
|
||||
const filtered = allowDiscord
|
||||
? tools
|
||||
: tools.filter((tool) => tool.name !== "discord");
|
||||
const sandboxed = sandbox
|
||||
? filterToolsByPolicy(filtered, sandbox.tools)
|
||||
: filtered;
|
||||
|
||||
@@ -209,24 +209,6 @@
|
||||
"kick": { "label": "kick", "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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
@@ -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"),
|
||||
}),
|
||||
]);
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -50,7 +50,6 @@ describe("resolveTextChunkLimit", () => {
|
||||
it("uses per-surface defaults", () => {
|
||||
expect(resolveTextChunkLimit(undefined, "whatsapp")).toBe(4000);
|
||||
expect(resolveTextChunkLimit(undefined, "telegram")).toBe(4000);
|
||||
expect(resolveTextChunkLimit(undefined, "slack")).toBe(4000);
|
||||
expect(resolveTextChunkLimit(undefined, "signal")).toBe(4000);
|
||||
expect(resolveTextChunkLimit(undefined, "imessage")).toBe(4000);
|
||||
expect(resolveTextChunkLimit(undefined, "discord")).toBe(2000);
|
||||
@@ -63,12 +62,8 @@ describe("resolveTextChunkLimit", () => {
|
||||
});
|
||||
|
||||
it("uses the matching provider override", () => {
|
||||
const cfg = {
|
||||
discord: { textChunkLimit: 111 },
|
||||
slack: { textChunkLimit: 222 },
|
||||
};
|
||||
const cfg = { discord: { textChunkLimit: 111 } };
|
||||
expect(resolveTextChunkLimit(cfg, "discord")).toBe(111);
|
||||
expect(resolveTextChunkLimit(cfg, "slack")).toBe(222);
|
||||
expect(resolveTextChunkLimit(cfg, "telegram")).toBe(4000);
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,6 @@ export type TextChunkSurface =
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage"
|
||||
| "webchat";
|
||||
@@ -17,7 +16,6 @@ const DEFAULT_CHUNK_LIMIT_BY_SURFACE: Record<TextChunkSurface, number> = {
|
||||
whatsapp: 4000,
|
||||
telegram: 4000,
|
||||
discord: 2000,
|
||||
slack: 4000,
|
||||
signal: 4000,
|
||||
imessage: 4000,
|
||||
webchat: 4000,
|
||||
@@ -32,7 +30,6 @@ export function resolveTextChunkLimit(
|
||||
if (surface === "whatsapp") return cfg?.whatsapp?.textChunkLimit;
|
||||
if (surface === "telegram") return cfg?.telegram?.textChunkLimit;
|
||||
if (surface === "discord") return cfg?.discord?.textChunkLimit;
|
||||
if (surface === "slack") return cfg?.slack?.textChunkLimit;
|
||||
if (surface === "signal") return cfg?.signal?.textChunkLimit;
|
||||
if (surface === "imessage") return cfg?.imessage?.textChunkLimit;
|
||||
return undefined;
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -305,7 +305,7 @@ export async function getReplyFromConfig(
|
||||
(sessionEntry?.elevatedLevel as ElevatedLevel | undefined) ??
|
||||
(agentCfg?.elevatedDefault as ElevatedLevel | undefined) ??
|
||||
"on")
|
||||
: "off";
|
||||
: "on";
|
||||
const resolvedBlockStreaming =
|
||||
agentCfg?.blockStreamingDefault === "off" ? "off" : "on";
|
||||
const resolvedBlockStreamingBreak: "text_end" | "message_end" =
|
||||
|
||||
@@ -8,7 +8,6 @@ const BLOCK_CHUNK_SURFACES = new Set<TextChunkSurface>([
|
||||
"whatsapp",
|
||||
"telegram",
|
||||
"discord",
|
||||
"slack",
|
||||
"signal",
|
||||
"imessage",
|
||||
"webchat",
|
||||
|
||||
@@ -8,7 +8,6 @@ export type CliDeps = {
|
||||
sendMessageWhatsApp: typeof sendMessageWhatsApp;
|
||||
sendMessageTelegram: typeof sendMessageTelegram;
|
||||
sendMessageDiscord: typeof sendMessageDiscord;
|
||||
sendMessageSlack: typeof sendMessageSlack;
|
||||
sendMessageSignal: typeof sendMessageSignal;
|
||||
sendMessageIMessage: typeof sendMessageIMessage;
|
||||
};
|
||||
@@ -18,7 +17,6 @@ export function createDefaultDeps(): CliDeps {
|
||||
sendMessageWhatsApp,
|
||||
sendMessageTelegram,
|
||||
sendMessageDiscord,
|
||||
sendMessageSlack,
|
||||
sendMessageSignal,
|
||||
sendMessageIMessage,
|
||||
};
|
||||
|
||||
+5
-5
@@ -300,7 +300,7 @@ export function buildProgram() {
|
||||
program
|
||||
.command("send")
|
||||
.description(
|
||||
"Send a message (WhatsApp Web, Telegram bot, Discord, Slack, Signal, iMessage)",
|
||||
"Send a message (WhatsApp Web, Telegram bot, Discord, Signal, iMessage)",
|
||||
)
|
||||
.requiredOption(
|
||||
"-t, --to <number>",
|
||||
@@ -318,7 +318,7 @@ export function buildProgram() {
|
||||
)
|
||||
.option(
|
||||
"--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("--json", "Output result as JSON", false)
|
||||
@@ -361,7 +361,7 @@ Examples:
|
||||
.option("--verbose <on|off>", "Persist agent verbose level for the session")
|
||||
.option(
|
||||
"--provider <provider>",
|
||||
"Delivery provider: whatsapp|telegram|discord|slack|signal|imessage (default: whatsapp)",
|
||||
"Delivery provider: whatsapp|telegram|discord|signal|imessage (default: whatsapp)",
|
||||
)
|
||||
.option(
|
||||
"--deliver",
|
||||
@@ -411,7 +411,7 @@ Examples:
|
||||
.option("--json", "Output JSON instead of text", false)
|
||||
.option(
|
||||
"--deep",
|
||||
"Probe providers (WhatsApp Web + Telegram + Discord + Slack + Signal)",
|
||||
"Probe providers (WhatsApp Web + Telegram + Discord + Signal)",
|
||||
false,
|
||||
)
|
||||
.option("--timeout <ms>", "Probe timeout in milliseconds", "10000")
|
||||
@@ -422,7 +422,7 @@ Examples:
|
||||
Examples:
|
||||
clawdis status # show linked account + session store summary
|
||||
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`,
|
||||
)
|
||||
.action(async (opts) => {
|
||||
|
||||
+5
-37
@@ -473,7 +473,6 @@ export async function agentCommand(
|
||||
const whatsappTarget = opts.to ? normalizeE164(opts.to) : allowFrom[0];
|
||||
const telegramTarget = opts.to?.trim() || undefined;
|
||||
const discordTarget = opts.to?.trim() || undefined;
|
||||
const slackTarget = opts.to?.trim() || undefined;
|
||||
const signalTarget = opts.to?.trim() || undefined;
|
||||
const imessageTarget = opts.to?.trim() || undefined;
|
||||
|
||||
@@ -485,13 +484,11 @@ export async function agentCommand(
|
||||
? whatsappTarget
|
||||
: deliveryProvider === "discord"
|
||||
? discordTarget
|
||||
: deliveryProvider === "slack"
|
||||
? slackTarget
|
||||
: deliveryProvider === "signal"
|
||||
? signalTarget
|
||||
: deliveryProvider === "imessage"
|
||||
? imessageTarget
|
||||
: undefined;
|
||||
: deliveryProvider === "signal"
|
||||
? signalTarget
|
||||
: deliveryProvider === "imessage"
|
||||
? imessageTarget
|
||||
: undefined;
|
||||
const message = `Delivery failed (${deliveryProvider}${deliveryTarget ? ` to ${deliveryTarget}` : ""}): ${String(err)}`;
|
||||
runtime.error?.(message);
|
||||
if (!runtime.error) runtime.log(message);
|
||||
@@ -517,13 +514,6 @@ export async function agentCommand(
|
||||
if (!bestEffortDeliver) throw 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) {
|
||||
const err = new Error(
|
||||
"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 !== "telegram" &&
|
||||
deliveryProvider !== "discord" &&
|
||||
deliveryProvider !== "slack" &&
|
||||
deliveryProvider !== "signal" &&
|
||||
deliveryProvider !== "imessage" &&
|
||||
deliveryProvider !== "webchat"
|
||||
@@ -585,7 +574,6 @@ export async function agentCommand(
|
||||
deliveryProvider === "whatsapp" ||
|
||||
deliveryProvider === "telegram" ||
|
||||
deliveryProvider === "discord" ||
|
||||
deliveryProvider === "slack" ||
|
||||
deliveryProvider === "signal" ||
|
||||
deliveryProvider === "imessage"
|
||||
? 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) {
|
||||
try {
|
||||
if (media.length === 0) {
|
||||
|
||||
@@ -31,7 +31,6 @@ async function noteProviderPrimer(prompter: WizardPrompter): Promise<void> {
|
||||
"WhatsApp: dedicated second number recommended; primary number OK (self-chat).",
|
||||
"Telegram: Bot API (token from @BotFather), replies via your bot.",
|
||||
"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.",
|
||||
"iMessage: local imsg CLI; separate Apple ID recommended only on a separate Mac.",
|
||||
].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[]) {
|
||||
return {
|
||||
...cfg,
|
||||
@@ -244,16 +153,10 @@ export async function setupProviders(
|
||||
const whatsappLinked = await detectWhatsAppLinked();
|
||||
const telegramEnv = Boolean(process.env.TELEGRAM_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(
|
||||
telegramEnv || cfg.telegram?.botToken || cfg.telegram?.tokenFile,
|
||||
);
|
||||
const discordConfigured = Boolean(discordEnv || cfg.discord?.token);
|
||||
const slackConfigured = Boolean(
|
||||
(slackBotEnv && slackAppEnv) ||
|
||||
(cfg.slack?.botToken && cfg.slack?.appToken),
|
||||
);
|
||||
const signalConfigured = Boolean(
|
||||
cfg.signal?.account || cfg.signal?.httpUrl || cfg.signal?.httpPort,
|
||||
);
|
||||
@@ -270,7 +173,6 @@ export async function setupProviders(
|
||||
`WhatsApp: ${whatsappLinked ? "linked" : "not linked"}`,
|
||||
`Telegram: ${telegramConfigured ? "configured" : "needs token"}`,
|
||||
`Discord: ${discordConfigured ? "configured" : "needs token"}`,
|
||||
`Slack: ${slackConfigured ? "configured" : "needs tokens"}`,
|
||||
`Signal: ${signalConfigured ? "configured" : "needs setup"}`,
|
||||
`iMessage: ${imessageConfigured ? "configured" : "needs setup"}`,
|
||||
`signal-cli: ${signalCliDetected ? "found" : "missing"} (${signalCliPath})`,
|
||||
@@ -305,11 +207,6 @@ export async function setupProviders(
|
||||
label: "Discord (Bot API)",
|
||||
hint: discordConfigured ? "configured" : "needs token",
|
||||
},
|
||||
{
|
||||
value: "slack",
|
||||
label: "Slack (Socket Mode)",
|
||||
hint: slackConfigured ? "configured" : "needs tokens",
|
||||
},
|
||||
{
|
||||
value: "signal",
|
||||
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")) {
|
||||
let resolvedCliPath = signalCliPath;
|
||||
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) {
|
||||
const disable = await prompter.confirm({
|
||||
message: "Disable Signal provider?",
|
||||
|
||||
@@ -14,7 +14,6 @@ export type ProviderChoice =
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage";
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ const makeDeps = (overrides: Partial<CliDeps> = {}): CliDeps => ({
|
||||
sendMessageWhatsApp: vi.fn(),
|
||||
sendMessageTelegram: vi.fn(),
|
||||
sendMessageDiscord: vi.fn(),
|
||||
sendMessageSlack: vi.fn(),
|
||||
sendMessageSignal: vi.fn(),
|
||||
sendMessageIMessage: vi.fn(),
|
||||
...overrides,
|
||||
@@ -174,25 +173,6 @@ describe("sendCommand", () => {
|
||||
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 () => {
|
||||
const deps = makeDeps({
|
||||
sendMessageIMessage: vi.fn().mockResolvedValue({ messageId: "i1" }),
|
||||
|
||||
@@ -86,34 +86,6 @@ export async function sendCommand(
|
||||
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") {
|
||||
const result = await deps.sendMessageSignal(opts.to, opts.message, {
|
||||
mediaUrl: opts.media,
|
||||
|
||||
@@ -40,7 +40,6 @@ const GROUP_LABELS: Record<string, string> = {
|
||||
talk: "Talk",
|
||||
telegram: "Telegram",
|
||||
discord: "Discord",
|
||||
slack: "Slack",
|
||||
signal: "Signal",
|
||||
imessage: "iMessage",
|
||||
whatsapp: "WhatsApp",
|
||||
@@ -66,7 +65,6 @@ const GROUP_ORDER: Record<string, number> = {
|
||||
talk: 130,
|
||||
telegram: 140,
|
||||
discord: 150,
|
||||
slack: 155,
|
||||
signal: 160,
|
||||
imessage: 170,
|
||||
whatsapp: 180,
|
||||
@@ -94,8 +92,6 @@ const FIELD_LABELS: Record<string, string> = {
|
||||
"talk.apiKey": "Talk API Key",
|
||||
"telegram.botToken": "Telegram Bot Token",
|
||||
"discord.token": "Discord Bot Token",
|
||||
"slack.botToken": "Slack Bot Token",
|
||||
"slack.appToken": "Slack App Token",
|
||||
"signal.account": "Signal Account",
|
||||
"imessage.cliPath": "iMessage CLI Path",
|
||||
};
|
||||
|
||||
@@ -61,7 +61,6 @@ export type SessionEntry = {
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage"
|
||||
| "webchat";
|
||||
|
||||
@@ -65,7 +65,6 @@ export type AgentElevatedAllowFromConfig = {
|
||||
whatsapp?: string[];
|
||||
telegram?: Array<string | number>;
|
||||
discord?: Array<string | number>;
|
||||
slack?: Array<string | number>;
|
||||
signal?: Array<string | number>;
|
||||
imessage?: Array<string | number>;
|
||||
webchat?: Array<string | number>;
|
||||
@@ -145,7 +144,6 @@ export type HookMappingConfig = {
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage";
|
||||
to?: string;
|
||||
@@ -292,63 +290,6 @@ export type DiscordConfig = {
|
||||
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 = {
|
||||
/** If false, do not start the Signal provider. Default: true. */
|
||||
enabled?: boolean;
|
||||
@@ -415,7 +356,6 @@ export type QueueModeBySurface = {
|
||||
whatsapp?: QueueMode;
|
||||
telegram?: QueueMode;
|
||||
discord?: QueueMode;
|
||||
slack?: QueueMode;
|
||||
signal?: QueueMode;
|
||||
imessage?: QueueMode;
|
||||
webchat?: QueueMode;
|
||||
@@ -702,7 +642,6 @@ export type ClawdisConfig = {
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage"
|
||||
| "none";
|
||||
@@ -792,7 +731,6 @@ export type ClawdisConfig = {
|
||||
whatsapp?: WhatsAppConfig;
|
||||
telegram?: TelegramConfig;
|
||||
discord?: DiscordConfig;
|
||||
slack?: SlackConfig;
|
||||
signal?: SignalConfig;
|
||||
imessage?: IMessageConfig;
|
||||
cron?: CronConfig;
|
||||
|
||||
@@ -86,7 +86,6 @@ const QueueModeBySurfaceSchema = z
|
||||
whatsapp: QueueModeSchema.optional(),
|
||||
telegram: QueueModeSchema.optional(),
|
||||
discord: QueueModeSchema.optional(),
|
||||
slack: QueueModeSchema.optional(),
|
||||
signal: QueueModeSchema.optional(),
|
||||
imessage: QueueModeSchema.optional(),
|
||||
webchat: QueueModeSchema.optional(),
|
||||
@@ -164,7 +163,6 @@ const HeartbeatSchema = z
|
||||
z.literal("whatsapp"),
|
||||
z.literal("telegram"),
|
||||
z.literal("discord"),
|
||||
z.literal("slack"),
|
||||
z.literal("signal"),
|
||||
z.literal("imessage"),
|
||||
z.literal("none"),
|
||||
@@ -227,7 +225,6 @@ const HookMappingSchema = z
|
||||
z.literal("whatsapp"),
|
||||
z.literal("telegram"),
|
||||
z.literal("discord"),
|
||||
z.literal("slack"),
|
||||
z.literal("signal"),
|
||||
z.literal("imessage"),
|
||||
])
|
||||
@@ -417,7 +414,6 @@ export const ClawdisSchema = z.object({
|
||||
whatsapp: z.array(z.string()).optional(),
|
||||
telegram: 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(),
|
||||
imessage: 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(),
|
||||
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
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
|
||||
@@ -64,7 +64,6 @@ function resolveDeliveryTarget(
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage";
|
||||
to?: string;
|
||||
@@ -93,7 +92,6 @@ function resolveDeliveryTarget(
|
||||
requestedChannel === "whatsapp" ||
|
||||
requestedChannel === "telegram" ||
|
||||
requestedChannel === "discord" ||
|
||||
requestedChannel === "slack" ||
|
||||
requestedChannel === "signal" ||
|
||||
requestedChannel === "imessage"
|
||||
) {
|
||||
@@ -449,46 +447,6 @@ export async function runCronIsolatedAgentTurn(params: {
|
||||
return { status: "error", summary, error: String(err) };
|
||||
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") {
|
||||
if (!resolvedDelivery.to) {
|
||||
if (!bestEffortDeliver)
|
||||
|
||||
@@ -19,7 +19,6 @@ export type CronPayload =
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage";
|
||||
to?: string;
|
||||
|
||||
@@ -15,7 +15,6 @@ export type ProviderKind =
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage";
|
||||
|
||||
@@ -48,7 +47,6 @@ type ReloadAction =
|
||||
| "restart-provider:whatsapp"
|
||||
| "restart-provider:telegram"
|
||||
| "restart-provider:discord"
|
||||
| "restart-provider:slack"
|
||||
| "restart-provider:signal"
|
||||
| "restart-provider:imessage";
|
||||
|
||||
@@ -72,7 +70,6 @@ const RELOAD_RULES: ReloadRule[] = [
|
||||
{ prefix: "web", kind: "hot", actions: ["restart-provider:whatsapp"] },
|
||||
{ prefix: "telegram", kind: "hot", actions: ["restart-provider:telegram"] },
|
||||
{ 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: "imessage", kind: "hot", actions: ["restart-provider:imessage"] },
|
||||
{ prefix: "identity", kind: "none" },
|
||||
@@ -203,9 +200,6 @@ export function buildGatewayReloadPlan(
|
||||
case "restart-provider:discord":
|
||||
plan.restartProviders.add("discord");
|
||||
break;
|
||||
case "restart-provider:slack":
|
||||
plan.restartProviders.add("slack");
|
||||
break;
|
||||
case "restart-provider:signal":
|
||||
plan.restartProviders.add("signal");
|
||||
break;
|
||||
|
||||
@@ -23,7 +23,6 @@ export type HookMappingResolved = {
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage";
|
||||
to?: string;
|
||||
@@ -62,7 +61,6 @@ export type HookAction =
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage";
|
||||
to?: string;
|
||||
@@ -101,14 +99,7 @@ type HookTransformResult = Partial<{
|
||||
name: string;
|
||||
sessionKey: string;
|
||||
deliver: boolean;
|
||||
channel:
|
||||
| "last"
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage";
|
||||
channel: "last" | "whatsapp" | "telegram" | "discord" | "signal" | "imessage";
|
||||
to: string;
|
||||
thinking: string;
|
||||
timeoutSeconds: number;
|
||||
|
||||
+2
-11
@@ -137,14 +137,7 @@ export type HookAgentPayload = {
|
||||
wakeMode: "now" | "next-heartbeat";
|
||||
sessionKey: string;
|
||||
deliver: boolean;
|
||||
channel:
|
||||
| "last"
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage";
|
||||
channel: "last" | "whatsapp" | "telegram" | "discord" | "signal" | "imessage";
|
||||
to?: string;
|
||||
thinking?: string;
|
||||
timeoutSeconds?: number;
|
||||
@@ -178,7 +171,6 @@ export function normalizeAgentPayload(
|
||||
channelRaw === "whatsapp" ||
|
||||
channelRaw === "telegram" ||
|
||||
channelRaw === "discord" ||
|
||||
channelRaw === "slack" ||
|
||||
channelRaw === "signal" ||
|
||||
channelRaw === "imessage" ||
|
||||
channelRaw === "last"
|
||||
@@ -191,8 +183,7 @@ export function normalizeAgentPayload(
|
||||
if (channel === null) {
|
||||
return {
|
||||
ok: false,
|
||||
error:
|
||||
"channel must be last|whatsapp|telegram|discord|slack|signal|imessage",
|
||||
error: "channel must be last|whatsapp|telegram|discord|signal|imessage",
|
||||
};
|
||||
}
|
||||
const toRaw = payload.to;
|
||||
|
||||
@@ -635,7 +635,6 @@ export const CronPayloadSchema = Type.Union([
|
||||
Type.Literal("whatsapp"),
|
||||
Type.Literal("telegram"),
|
||||
Type.Literal("discord"),
|
||||
Type.Literal("slack"),
|
||||
]),
|
||||
),
|
||||
to: Type.Optional(Type.String()),
|
||||
|
||||
@@ -37,7 +37,6 @@ type HookDispatchers = {
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage";
|
||||
to?: string;
|
||||
|
||||
@@ -8,11 +8,6 @@ import { type DiscordProbe, probeDiscord } from "../../discord/probe.js";
|
||||
import { type IMessageProbe, probeIMessage } from "../../imessage/probe.js";
|
||||
import { webAuthExists } from "../../providers/web/index.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 { resolveTelegramToken } from "../../telegram/token.js";
|
||||
import { getWebAuthAgeMs, readWebSelfId } from "../../web/session.js";
|
||||
@@ -79,41 +74,6 @@ export const providersHandlers: GatewayRequestHandlers = {
|
||||
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 signalEnabled = signalCfg?.enabled !== false;
|
||||
const signalHost = signalCfg?.httpHost?.trim() || "127.0.0.1";
|
||||
@@ -192,17 +152,6 @@ export const providersHandlers: GatewayRequestHandlers = {
|
||||
probe: discordProbe,
|
||||
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: {
|
||||
configured: signalConfigured,
|
||||
baseUrl: signalBaseUrl,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { sendMessageDiscord } from "../../discord/index.js";
|
||||
import { shouldLogVerbose } from "../../globals.js";
|
||||
import { sendMessageIMessage } from "../../imessage/index.js";
|
||||
import { sendMessageSignal } from "../../signal/index.js";
|
||||
import { sendMessageSlack } from "../../slack/send.js";
|
||||
import { sendMessageTelegram } from "../../telegram/send.js";
|
||||
import { resolveTelegramToken } from "../../telegram/token.js";
|
||||
import { sendMessageWhatsApp } from "../../web/outbound.js";
|
||||
@@ -88,22 +87,6 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
payload,
|
||||
});
|
||||
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") {
|
||||
const cfg = loadConfig();
|
||||
const host = cfg.signal?.httpHost?.trim() || "127.0.0.1";
|
||||
|
||||
@@ -7,11 +7,6 @@ import type { createSubsystemLogger } from "../logging.js";
|
||||
import { monitorWebProvider, webAuthExists } from "../providers/web/index.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { monitorSignalProvider } from "../signal/index.js";
|
||||
import {
|
||||
monitorSlackProvider,
|
||||
resolveSlackAppToken,
|
||||
resolveSlackBotToken,
|
||||
} from "../slack/index.js";
|
||||
import { monitorTelegramProvider } from "../telegram/monitor.js";
|
||||
import { probeTelegram } from "../telegram/probe.js";
|
||||
import { resolveTelegramToken } from "../telegram/token.js";
|
||||
@@ -34,13 +29,6 @@ export type DiscordRuntimeStatus = {
|
||||
lastError?: string | null;
|
||||
};
|
||||
|
||||
export type SlackRuntimeStatus = {
|
||||
running: boolean;
|
||||
lastStartAt?: number | null;
|
||||
lastStopAt?: number | null;
|
||||
lastError?: string | null;
|
||||
};
|
||||
|
||||
export type SignalRuntimeStatus = {
|
||||
running: boolean;
|
||||
lastStartAt?: number | null;
|
||||
@@ -62,7 +50,6 @@ export type ProviderRuntimeSnapshot = {
|
||||
whatsapp: WebProviderStatus;
|
||||
telegram: TelegramRuntimeStatus;
|
||||
discord: DiscordRuntimeStatus;
|
||||
slack: SlackRuntimeStatus;
|
||||
signal: SignalRuntimeStatus;
|
||||
imessage: IMessageRuntimeStatus;
|
||||
};
|
||||
@@ -74,13 +61,11 @@ type ProviderManagerOptions = {
|
||||
logWhatsApp: SubsystemLogger;
|
||||
logTelegram: SubsystemLogger;
|
||||
logDiscord: SubsystemLogger;
|
||||
logSlack: SubsystemLogger;
|
||||
logSignal: SubsystemLogger;
|
||||
logIMessage: SubsystemLogger;
|
||||
whatsappRuntimeEnv: RuntimeEnv;
|
||||
telegramRuntimeEnv: RuntimeEnv;
|
||||
discordRuntimeEnv: RuntimeEnv;
|
||||
slackRuntimeEnv: RuntimeEnv;
|
||||
signalRuntimeEnv: RuntimeEnv;
|
||||
imessageRuntimeEnv: RuntimeEnv;
|
||||
};
|
||||
@@ -94,8 +79,6 @@ export type ProviderManager = {
|
||||
stopTelegramProvider: () => Promise<void>;
|
||||
startDiscordProvider: () => Promise<void>;
|
||||
stopDiscordProvider: () => Promise<void>;
|
||||
startSlackProvider: () => Promise<void>;
|
||||
stopSlackProvider: () => Promise<void>;
|
||||
startSignalProvider: () => Promise<void>;
|
||||
stopSignalProvider: () => Promise<void>;
|
||||
startIMessageProvider: () => Promise<void>;
|
||||
@@ -111,13 +94,11 @@ export function createProviderManager(
|
||||
logWhatsApp,
|
||||
logTelegram,
|
||||
logDiscord,
|
||||
logSlack,
|
||||
logSignal,
|
||||
logIMessage,
|
||||
whatsappRuntimeEnv,
|
||||
telegramRuntimeEnv,
|
||||
discordRuntimeEnv,
|
||||
slackRuntimeEnv,
|
||||
signalRuntimeEnv,
|
||||
imessageRuntimeEnv,
|
||||
} = opts;
|
||||
@@ -125,13 +106,11 @@ export function createProviderManager(
|
||||
let whatsappAbort: AbortController | null = null;
|
||||
let telegramAbort: AbortController | null = null;
|
||||
let discordAbort: AbortController | null = null;
|
||||
let slackAbort: AbortController | null = null;
|
||||
let signalAbort: AbortController | null = null;
|
||||
let imessageAbort: AbortController | null = null;
|
||||
let whatsappTask: Promise<unknown> | null = null;
|
||||
let telegramTask: Promise<unknown> | null = null;
|
||||
let discordTask: Promise<unknown> | null = null;
|
||||
let slackTask: Promise<unknown> | null = null;
|
||||
let signalTask: Promise<unknown> | null = null;
|
||||
let imessageTask: Promise<unknown> | null = null;
|
||||
|
||||
@@ -158,12 +137,6 @@ export function createProviderManager(
|
||||
lastStopAt: null,
|
||||
lastError: null,
|
||||
};
|
||||
let slackRuntime: SlackRuntimeStatus = {
|
||||
running: false,
|
||||
lastStartAt: null,
|
||||
lastStopAt: null,
|
||||
lastError: null,
|
||||
};
|
||||
let signalRuntime: SignalRuntimeStatus = {
|
||||
running: false,
|
||||
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 () => {
|
||||
if (signalTask) return;
|
||||
const cfg = loadConfig();
|
||||
@@ -748,7 +634,6 @@ export function createProviderManager(
|
||||
const startProviders = async () => {
|
||||
await startWhatsAppProvider();
|
||||
await startDiscordProvider();
|
||||
await startSlackProvider();
|
||||
await startTelegramProvider();
|
||||
await startSignalProvider();
|
||||
await startIMessageProvider();
|
||||
@@ -767,7 +652,6 @@ export function createProviderManager(
|
||||
whatsapp: { ...whatsappRuntime },
|
||||
telegram: { ...telegramRuntime },
|
||||
discord: { ...discordRuntime },
|
||||
slack: { ...slackRuntime },
|
||||
signal: { ...signalRuntime },
|
||||
imessage: { ...imessageRuntime },
|
||||
});
|
||||
@@ -781,8 +665,6 @@ export function createProviderManager(
|
||||
stopTelegramProvider,
|
||||
startDiscordProvider,
|
||||
stopDiscordProvider,
|
||||
startSlackProvider,
|
||||
stopSlackProvider,
|
||||
startSignalProvider,
|
||||
stopSignalProvider,
|
||||
startIMessageProvider,
|
||||
|
||||
@@ -55,12 +55,6 @@ const hoisted = vi.hoisted(() => {
|
||||
lastStopAt: null,
|
||||
lastError: null,
|
||||
},
|
||||
slack: {
|
||||
running: false,
|
||||
lastStartAt: null,
|
||||
lastStopAt: null,
|
||||
lastError: null,
|
||||
},
|
||||
signal: {
|
||||
running: false,
|
||||
lastStartAt: null,
|
||||
@@ -84,8 +78,6 @@ const hoisted = vi.hoisted(() => {
|
||||
stopTelegramProvider: vi.fn(async () => {}),
|
||||
startDiscordProvider: vi.fn(async () => {}),
|
||||
stopDiscordProvider: vi.fn(async () => {}),
|
||||
startSlackProvider: vi.fn(async () => {}),
|
||||
stopSlackProvider: vi.fn(async () => {}),
|
||||
startSignalProvider: vi.fn(async () => {}),
|
||||
stopSignalProvider: vi.fn(async () => {}),
|
||||
startIMessageProvider: vi.fn(async () => {}),
|
||||
|
||||
+65
-87
@@ -148,14 +148,12 @@ const logWsControl = log.child("ws");
|
||||
const logWhatsApp = logProviders.child("whatsapp");
|
||||
const logTelegram = logProviders.child("telegram");
|
||||
const logDiscord = logProviders.child("discord");
|
||||
const logSlack = logProviders.child("slack");
|
||||
const logSignal = logProviders.child("signal");
|
||||
const logIMessage = logProviders.child("imessage");
|
||||
const canvasRuntime = runtimeForLogger(logCanvas);
|
||||
const whatsappRuntimeEnv = runtimeForLogger(logWhatsApp);
|
||||
const telegramRuntimeEnv = runtimeForLogger(logTelegram);
|
||||
const discordRuntimeEnv = runtimeForLogger(logDiscord);
|
||||
const slackRuntimeEnv = runtimeForLogger(logSlack);
|
||||
const signalRuntimeEnv = runtimeForLogger(logSignal);
|
||||
const imessageRuntimeEnv = runtimeForLogger(logIMessage);
|
||||
|
||||
@@ -480,7 +478,6 @@ export async function startGatewayServer(
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage";
|
||||
to?: string;
|
||||
@@ -725,13 +722,11 @@ export async function startGatewayServer(
|
||||
logWhatsApp,
|
||||
logTelegram,
|
||||
logDiscord,
|
||||
logSlack,
|
||||
logSignal,
|
||||
logIMessage,
|
||||
whatsappRuntimeEnv,
|
||||
telegramRuntimeEnv,
|
||||
discordRuntimeEnv,
|
||||
slackRuntimeEnv,
|
||||
signalRuntimeEnv,
|
||||
imessageRuntimeEnv,
|
||||
});
|
||||
@@ -741,13 +736,11 @@ export async function startGatewayServer(
|
||||
startWhatsAppProvider,
|
||||
startTelegramProvider,
|
||||
startDiscordProvider,
|
||||
startSlackProvider,
|
||||
startSignalProvider,
|
||||
startIMessageProvider,
|
||||
stopWhatsAppProvider,
|
||||
stopTelegramProvider,
|
||||
stopDiscordProvider,
|
||||
stopSlackProvider,
|
||||
stopSignalProvider,
|
||||
stopIMessageProvider,
|
||||
markWhatsAppLoggedOut,
|
||||
@@ -936,73 +929,6 @@ export async function startGatewayServer(
|
||||
? bridgeHost
|
||||
: undefined;
|
||||
|
||||
const nodePresenceTimers = new Map<string, ReturnType<typeof setInterval>>();
|
||||
|
||||
const stopNodePresenceTimer = (nodeId: string) => {
|
||||
const timer = nodePresenceTimers.get(nodeId);
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
nodePresenceTimers.delete(nodeId);
|
||||
};
|
||||
|
||||
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 rawIp = node.remoteIp?.trim();
|
||||
const ip = rawIp && !isLoopbackAddress(rawIp) ? rawIp : undefined;
|
||||
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 ${reason}`;
|
||||
upsertPresence(node.nodeId, {
|
||||
host,
|
||||
ip,
|
||||
version,
|
||||
platform,
|
||||
deviceFamily,
|
||||
modelIdentifier,
|
||||
mode: "remote",
|
||||
reason,
|
||||
lastInputSeconds: 0,
|
||||
instanceId: node.nodeId,
|
||||
text,
|
||||
});
|
||||
presenceVersion += 1;
|
||||
broadcast(
|
||||
"presence",
|
||||
{ presence: listSystemPresence() },
|
||||
{
|
||||
dropIfSlow: true,
|
||||
stateVersion: {
|
||||
presence: presenceVersion,
|
||||
health: healthVersion,
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
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({
|
||||
@@ -1013,8 +939,38 @@ export async function startGatewayServer(
|
||||
canvasHostHost: canvasHostHostForBridge,
|
||||
onRequest: (nodeId, req) => handleBridgeRequest(nodeId, req),
|
||||
onAuthenticated: async (node) => {
|
||||
beaconNodePresence(node, "node-connected");
|
||||
startNodePresenceTimer(node);
|
||||
const host = node.displayName?.trim() || node.nodeId;
|
||||
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-connected`;
|
||||
upsertPresence(node.nodeId, {
|
||||
host,
|
||||
ip,
|
||||
version,
|
||||
platform,
|
||||
deviceFamily,
|
||||
modelIdentifier,
|
||||
mode: "remote",
|
||||
reason: "node-connected",
|
||||
lastInputSeconds: 0,
|
||||
instanceId: node.nodeId,
|
||||
text,
|
||||
});
|
||||
presenceVersion += 1;
|
||||
broadcast(
|
||||
"presence",
|
||||
{ presence: listSystemPresence() },
|
||||
{
|
||||
dropIfSlow: true,
|
||||
stateVersion: {
|
||||
presence: presenceVersion,
|
||||
health: healthVersion,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
const cfg = await loadVoiceWakeConfig();
|
||||
@@ -1029,8 +985,38 @@ export async function startGatewayServer(
|
||||
},
|
||||
onDisconnected: (node) => {
|
||||
bridgeUnsubscribeAll(node.nodeId);
|
||||
stopNodePresenceTimer(node.nodeId);
|
||||
beaconNodePresence(node, "node-disconnected");
|
||||
const host = node.displayName?.trim() || node.nodeId;
|
||||
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,
|
||||
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.
|
||||
if (process.env.CLAWDIS_SKIP_PROVIDERS !== "1") {
|
||||
try {
|
||||
@@ -1717,9 +1703,6 @@ export async function startGatewayServer(
|
||||
startDiscordProvider,
|
||||
);
|
||||
}
|
||||
if (plan.restartProviders.has("slack")) {
|
||||
await restartProvider("slack", stopSlackProvider, startSlackProvider);
|
||||
}
|
||||
if (plan.restartProviders.has("signal")) {
|
||||
await restartProvider(
|
||||
"signal",
|
||||
@@ -1823,16 +1806,11 @@ export async function startGatewayServer(
|
||||
await stopWhatsAppProvider();
|
||||
await stopTelegramProvider();
|
||||
await stopDiscordProvider();
|
||||
await stopSlackProvider();
|
||||
await stopSignalProvider();
|
||||
await stopIMessageProvider();
|
||||
await stopGmailWatcher();
|
||||
cron.stop();
|
||||
heartbeatRunner.stop();
|
||||
for (const timer of nodePresenceTimers.values()) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
nodePresenceTimers.clear();
|
||||
broadcast("shutdown", {
|
||||
reason,
|
||||
restartExpectedMs,
|
||||
|
||||
@@ -38,20 +38,12 @@ export type HeartbeatTarget =
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage"
|
||||
| "none";
|
||||
|
||||
export type HeartbeatDeliveryTarget = {
|
||||
channel:
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage"
|
||||
| "none";
|
||||
channel: "whatsapp" | "telegram" | "discord" | "signal" | "imessage" | "none";
|
||||
to?: string;
|
||||
reason?: string;
|
||||
};
|
||||
@@ -61,7 +53,6 @@ type HeartbeatDeps = {
|
||||
sendWhatsApp?: typeof sendMessageWhatsApp;
|
||||
sendTelegram?: typeof sendMessageTelegram;
|
||||
sendDiscord?: typeof sendMessageDiscord;
|
||||
sendSlack?: typeof sendMessageSlack;
|
||||
sendSignal?: typeof sendMessageSignal;
|
||||
sendIMessage?: typeof sendMessageIMessage;
|
||||
getQueueSize?: (lane?: string) => number;
|
||||
@@ -192,7 +183,6 @@ export function resolveHeartbeatDeliveryTarget(params: {
|
||||
rawTarget === "whatsapp" ||
|
||||
rawTarget === "telegram" ||
|
||||
rawTarget === "discord" ||
|
||||
rawTarget === "slack" ||
|
||||
rawTarget === "signal" ||
|
||||
rawTarget === "imessage" ||
|
||||
rawTarget === "none" ||
|
||||
@@ -219,7 +209,6 @@ export function resolveHeartbeatDeliveryTarget(params: {
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage"
|
||||
| undefined =
|
||||
@@ -228,7 +217,6 @@ export function resolveHeartbeatDeliveryTarget(params: {
|
||||
: target === "whatsapp" ||
|
||||
target === "telegram" ||
|
||||
target === "discord" ||
|
||||
target === "slack" ||
|
||||
target === "signal" ||
|
||||
target === "imessage"
|
||||
? target
|
||||
@@ -300,13 +288,7 @@ function normalizeHeartbeatReply(
|
||||
}
|
||||
|
||||
async function deliverHeartbeatReply(params: {
|
||||
channel:
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage";
|
||||
channel: "whatsapp" | "telegram" | "discord" | "signal" | "imessage";
|
||||
to: string;
|
||||
text: string;
|
||||
mediaUrls: string[];
|
||||
@@ -317,7 +299,6 @@ async function deliverHeartbeatReply(params: {
|
||||
| "sendWhatsApp"
|
||||
| "sendTelegram"
|
||||
| "sendDiscord"
|
||||
| "sendSlack"
|
||||
| "sendSignal"
|
||||
| "sendIMessage"
|
||||
>
|
||||
@@ -388,21 +369,6 @@ async function deliverHeartbeatReply(params: {
|
||||
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) {
|
||||
await deps.sendDiscord(to, text, { verbose: false });
|
||||
return;
|
||||
@@ -532,7 +498,6 @@ export async function runHeartbeatOnce(opts: {
|
||||
sendWhatsApp: opts.deps?.sendWhatsApp ?? sendMessageWhatsApp,
|
||||
sendTelegram: opts.deps?.sendTelegram ?? sendMessageTelegram,
|
||||
sendDiscord: opts.deps?.sendDiscord ?? sendMessageDiscord,
|
||||
sendSlack: opts.deps?.sendSlack ?? sendMessageSlack,
|
||||
sendSignal: opts.deps?.sendSignal ?? sendMessageSignal,
|
||||
sendIMessage: opts.deps?.sendIMessage ?? sendMessageIMessage,
|
||||
};
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import {
|
||||
convertMessages,
|
||||
convertTools,
|
||||
} from "@mariozechner/pi-ai/dist/providers/google-shared.js";
|
||||
import type { Context, Model, Tool } from "@mariozechner/pi-ai/dist/types.js";
|
||||
import { convertTools } from "@mariozechner/pi-ai/dist/providers/google-shared.js";
|
||||
import type { Tool } from "@mariozechner/pi-ai/dist/types.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const asRecord = (value: unknown): Record<string, unknown> => {
|
||||
@@ -12,47 +9,9 @@ const asRecord = (value: unknown): 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", () => {
|
||||
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", () => {
|
||||
const tools = [
|
||||
const tools: Tool[] = [
|
||||
{
|
||||
name: "example",
|
||||
description: "Example tool",
|
||||
@@ -81,7 +40,7 @@ describe("google-shared convertTools", () => {
|
||||
required: ["mode"],
|
||||
},
|
||||
},
|
||||
] as unknown as Tool[];
|
||||
];
|
||||
|
||||
const converted = convertTools(tools);
|
||||
const params = asRecord(
|
||||
@@ -102,7 +61,7 @@ describe("google-shared convertTools", () => {
|
||||
});
|
||||
|
||||
it("keeps supported schema fields", () => {
|
||||
const tools = [
|
||||
const tools: Tool[] = [
|
||||
{
|
||||
name: "settings",
|
||||
description: "Settings tool",
|
||||
@@ -124,7 +83,7 @@ describe("google-shared convertTools", () => {
|
||||
required: ["config"],
|
||||
},
|
||||
},
|
||||
] as unknown as Tool[];
|
||||
];
|
||||
|
||||
const converted = convertTools(tools);
|
||||
const params = asRecord(
|
||||
@@ -145,90 +104,3 @@ describe("google-shared convertTools", () => {
|
||||
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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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";
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -27,7 +27,6 @@ import type {
|
||||
CronFormState,
|
||||
DiscordForm,
|
||||
IMessageForm,
|
||||
SlackForm,
|
||||
SignalForm,
|
||||
TelegramForm,
|
||||
} from "./ui-types";
|
||||
@@ -45,7 +44,6 @@ import {
|
||||
loadProviders,
|
||||
updateDiscordForm,
|
||||
updateIMessageForm,
|
||||
updateSlackForm,
|
||||
updateSignalForm,
|
||||
updateTelegramForm,
|
||||
} from "./controllers/connections";
|
||||
@@ -119,11 +117,6 @@ export type AppViewState = {
|
||||
discordSaving: boolean;
|
||||
discordTokenLocked: boolean;
|
||||
discordConfigStatus: string | null;
|
||||
slackForm: SlackForm;
|
||||
slackSaving: boolean;
|
||||
slackTokenLocked: boolean;
|
||||
slackAppTokenLocked: boolean;
|
||||
slackConfigStatus: string | null;
|
||||
signalForm: SignalForm;
|
||||
signalSaving: boolean;
|
||||
signalConfigStatus: string | null;
|
||||
@@ -276,11 +269,6 @@ export function renderApp(state: AppViewState) {
|
||||
discordTokenLocked: state.discordTokenLocked,
|
||||
discordSaving: state.discordSaving,
|
||||
discordStatus: state.discordConfigStatus,
|
||||
slackForm: state.slackForm,
|
||||
slackTokenLocked: state.slackTokenLocked,
|
||||
slackAppTokenLocked: state.slackAppTokenLocked,
|
||||
slackSaving: state.slackSaving,
|
||||
slackStatus: state.slackConfigStatus,
|
||||
signalForm: state.signalForm,
|
||||
signalSaving: state.signalSaving,
|
||||
signalStatus: state.signalConfigStatus,
|
||||
@@ -295,8 +283,6 @@ export function renderApp(state: AppViewState) {
|
||||
onTelegramSave: () => state.handleTelegramSave(),
|
||||
onDiscordChange: (patch) => updateDiscordForm(state, patch),
|
||||
onDiscordSave: () => state.handleDiscordSave(),
|
||||
onSlackChange: (patch) => updateSlackForm(state, patch),
|
||||
onSlackSave: () => state.handleSlackSave(),
|
||||
onSignalChange: (patch) => updateSignalForm(state, patch),
|
||||
onSignalSave: () => state.handleSignalSave(),
|
||||
onIMessageChange: (patch) => updateIMessageForm(state, patch),
|
||||
|
||||
+1
-32
@@ -36,11 +36,9 @@ import type {
|
||||
} from "./types";
|
||||
import {
|
||||
defaultDiscordActions,
|
||||
defaultSlackActions,
|
||||
type CronFormState,
|
||||
type DiscordForm,
|
||||
type IMessageForm,
|
||||
type SlackForm,
|
||||
type SignalForm,
|
||||
type TelegramForm,
|
||||
} from "./ui-types";
|
||||
@@ -61,7 +59,6 @@ import {
|
||||
logoutWhatsApp,
|
||||
saveDiscordConfig,
|
||||
saveIMessageConfig,
|
||||
saveSlackConfig,
|
||||
saveSignalConfig,
|
||||
saveTelegramConfig,
|
||||
startWhatsAppLogin,
|
||||
@@ -236,6 +233,7 @@ export class ClawdisApp extends LitElement {
|
||||
mediaMaxMb: "",
|
||||
historyLimit: "",
|
||||
textChunkLimit: "",
|
||||
replyToMode: "off",
|
||||
guilds: [],
|
||||
actions: { ...defaultDiscordActions },
|
||||
slashEnabled: false,
|
||||
@@ -246,29 +244,6 @@ export class ClawdisApp extends LitElement {
|
||||
@state() discordSaving = false;
|
||||
@state() discordTokenLocked = false;
|
||||
@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 = {
|
||||
enabled: true,
|
||||
account: "",
|
||||
@@ -799,12 +774,6 @@ export class ClawdisApp extends LitElement {
|
||||
await loadProviders(this, true);
|
||||
}
|
||||
|
||||
async handleSlackSave() {
|
||||
await saveSlackConfig(this);
|
||||
await loadConfig(this);
|
||||
await loadProviders(this, true);
|
||||
}
|
||||
|
||||
async handleSignalSave() {
|
||||
await saveSignalConfig(this);
|
||||
await loadConfig(this);
|
||||
|
||||
@@ -6,14 +6,11 @@ import type {
|
||||
} from "../types";
|
||||
import {
|
||||
defaultDiscordActions,
|
||||
defaultSlackActions,
|
||||
type DiscordActionForm,
|
||||
type DiscordForm,
|
||||
type DiscordGuildChannelForm,
|
||||
type DiscordGuildForm,
|
||||
type IMessageForm,
|
||||
type SlackChannelForm,
|
||||
type SlackForm,
|
||||
type SignalForm,
|
||||
type TelegramForm,
|
||||
} from "../ui-types";
|
||||
@@ -37,12 +34,10 @@ export type ConfigState = {
|
||||
lastError: string | null;
|
||||
telegramForm: TelegramForm;
|
||||
discordForm: DiscordForm;
|
||||
slackForm: SlackForm;
|
||||
signalForm: SignalForm;
|
||||
imessageForm: IMessageForm;
|
||||
telegramConfigStatus: string | null;
|
||||
discordConfigStatus: string | null;
|
||||
slackConfigStatus: string | null;
|
||||
signalConfigStatus: string | null;
|
||||
imessageConfigStatus: string | null;
|
||||
};
|
||||
@@ -240,88 +235,6 @@ export function applyConfigSnapshot(state: ConfigState, snapshot: ConfigSnapshot
|
||||
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 = {
|
||||
enabled: typeof signal.enabled === "boolean" ? signal.enabled : true,
|
||||
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;
|
||||
state.telegramConfigStatus = configInvalid;
|
||||
state.discordConfigStatus = configInvalid;
|
||||
state.slackConfigStatus = configInvalid;
|
||||
state.signalConfigStatus = configInvalid;
|
||||
state.imessageConfigStatus = configInvalid;
|
||||
|
||||
|
||||
@@ -3,14 +3,11 @@ import { parseList } from "../format";
|
||||
import type { ConfigSnapshot, ProvidersStatusSnapshot } from "../types";
|
||||
import {
|
||||
defaultDiscordActions,
|
||||
defaultSlackActions,
|
||||
type DiscordActionForm,
|
||||
type DiscordForm,
|
||||
type DiscordGuildChannelForm,
|
||||
type DiscordGuildForm,
|
||||
type IMessageForm,
|
||||
type SlackActionForm,
|
||||
type SlackForm,
|
||||
type SignalForm,
|
||||
type TelegramForm,
|
||||
} from "../ui-types";
|
||||
@@ -34,11 +31,6 @@ export type ConnectionsState = {
|
||||
discordSaving: boolean;
|
||||
discordTokenLocked: boolean;
|
||||
discordConfigStatus: string | null;
|
||||
slackForm: SlackForm;
|
||||
slackSaving: boolean;
|
||||
slackTokenLocked: boolean;
|
||||
slackAppTokenLocked: boolean;
|
||||
slackConfigStatus: string | null;
|
||||
signalForm: SignalForm;
|
||||
signalSaving: boolean;
|
||||
signalConfigStatus: string | null;
|
||||
@@ -62,8 +54,6 @@ export async function loadProviders(state: ConnectionsState, probe: boolean) {
|
||||
state.providersLastSuccess = Date.now();
|
||||
state.telegramTokenLocked = res.telegram.tokenSource === "env";
|
||||
state.discordTokenLocked = res.discord?.tokenSource === "env";
|
||||
state.slackTokenLocked = res.slack?.botTokenSource === "env";
|
||||
state.slackAppTokenLocked = res.slack?.appTokenSource === "env";
|
||||
} catch (err) {
|
||||
state.providersError = String(err);
|
||||
} finally {
|
||||
@@ -146,21 +136,6 @@ export function updateDiscordForm(
|
||||
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(
|
||||
state: ConnectionsState,
|
||||
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) {
|
||||
if (!state.client || !state.connected) return;
|
||||
if (state.signalSaving) return;
|
||||
|
||||
@@ -192,23 +192,6 @@
|
||||
"kick": { "label": "kick", "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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ export type ProvidersStatusSnapshot = {
|
||||
whatsapp: WhatsAppStatus;
|
||||
telegram: TelegramStatus;
|
||||
discord?: DiscordStatus | null;
|
||||
slack?: SlackStatus | null;
|
||||
signal?: SignalStatus | null;
|
||||
imessage?: IMessageStatus | null;
|
||||
};
|
||||
@@ -90,37 +89,6 @@ export type DiscordStatus = {
|
||||
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 = {
|
||||
ok: boolean;
|
||||
status?: number | null;
|
||||
|
||||
@@ -60,40 +60,6 @@ export type DiscordActionForm = {
|
||||
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 = {
|
||||
reactions: true,
|
||||
stickers: true,
|
||||
@@ -112,14 +78,6 @@ export const defaultDiscordActions: DiscordActionForm = {
|
||||
moderation: false,
|
||||
};
|
||||
|
||||
export const defaultSlackActions: SlackActionForm = {
|
||||
reactions: true,
|
||||
messages: true,
|
||||
pins: true,
|
||||
memberInfo: true,
|
||||
emojiList: true,
|
||||
};
|
||||
|
||||
export type SignalForm = {
|
||||
enabled: boolean;
|
||||
account: string;
|
||||
|
||||
@@ -6,8 +6,6 @@ import type {
|
||||
DiscordActionForm,
|
||||
DiscordForm,
|
||||
IMessageForm,
|
||||
SlackActionForm,
|
||||
SlackForm,
|
||||
SignalForm,
|
||||
TelegramForm,
|
||||
} from "../ui-types";
|
||||
@@ -30,14 +28,6 @@ const discordActionOptions = [
|
||||
{ key: "moderation", label: "Moderation" },
|
||||
] 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 = {
|
||||
connected: boolean;
|
||||
loading: boolean;
|
||||
@@ -56,11 +46,6 @@ export type ConnectionsProps = {
|
||||
discordTokenLocked: boolean;
|
||||
discordSaving: boolean;
|
||||
discordStatus: string | null;
|
||||
slackForm: SlackForm;
|
||||
slackTokenLocked: boolean;
|
||||
slackAppTokenLocked: boolean;
|
||||
slackSaving: boolean;
|
||||
slackStatus: string | null;
|
||||
signalForm: SignalForm;
|
||||
signalSaving: boolean;
|
||||
signalStatus: string | null;
|
||||
@@ -75,8 +60,6 @@ export type ConnectionsProps = {
|
||||
onTelegramSave: () => void;
|
||||
onDiscordChange: (patch: Partial<DiscordForm>) => void;
|
||||
onDiscordSave: () => void;
|
||||
onSlackChange: (patch: Partial<SlackForm>) => void;
|
||||
onSlackSave: () => void;
|
||||
onSignalChange: (patch: Partial<SignalForm>) => void;
|
||||
onSignalSave: () => void;
|
||||
onIMessageChange: (patch: Partial<IMessageForm>) => void;
|
||||
@@ -87,14 +70,12 @@ export function renderConnections(props: ConnectionsProps) {
|
||||
const whatsapp = props.snapshot?.whatsapp;
|
||||
const telegram = props.snapshot?.telegram;
|
||||
const discord = props.snapshot?.discord ?? null;
|
||||
const slack = props.snapshot?.slack ?? null;
|
||||
const signal = props.snapshot?.signal ?? null;
|
||||
const imessage = props.snapshot?.imessage ?? null;
|
||||
const providerOrder: ProviderKey[] = [
|
||||
"whatsapp",
|
||||
"telegram",
|
||||
"discord",
|
||||
"slack",
|
||||
"signal",
|
||||
"imessage",
|
||||
];
|
||||
@@ -112,14 +93,7 @@ export function renderConnections(props: ConnectionsProps) {
|
||||
return html`
|
||||
<section class="grid grid-cols-2">
|
||||
${orderedProviders.map((provider) =>
|
||||
renderProvider(provider.key, props, {
|
||||
whatsapp,
|
||||
telegram,
|
||||
discord,
|
||||
slack,
|
||||
signal,
|
||||
imessage,
|
||||
}),
|
||||
renderProvider(provider.key, props, { whatsapp, telegram, discord, signal, imessage }),
|
||||
)}
|
||||
</section>
|
||||
|
||||
@@ -153,13 +127,7 @@ function formatDuration(ms?: number | null) {
|
||||
return `${hr}h`;
|
||||
}
|
||||
|
||||
type ProviderKey =
|
||||
| "whatsapp"
|
||||
| "telegram"
|
||||
| "discord"
|
||||
| "slack"
|
||||
| "signal"
|
||||
| "imessage";
|
||||
type ProviderKey = "whatsapp" | "telegram" | "discord" | "signal" | "imessage";
|
||||
|
||||
function providerEnabled(key: ProviderKey, props: ConnectionsProps) {
|
||||
const snapshot = props.snapshot;
|
||||
@@ -175,8 +143,6 @@ function providerEnabled(key: ProviderKey, props: ConnectionsProps) {
|
||||
return snapshot.telegram.configured || snapshot.telegram.running;
|
||||
case "discord":
|
||||
return Boolean(snapshot.discord?.configured || snapshot.discord?.running);
|
||||
case "slack":
|
||||
return Boolean(snapshot.slack?.configured || snapshot.slack?.running);
|
||||
case "signal":
|
||||
return Boolean(snapshot.signal?.configured || snapshot.signal?.running);
|
||||
case "imessage":
|
||||
@@ -193,7 +159,6 @@ function renderProvider(
|
||||
whatsapp?: ProvidersStatusSnapshot["whatsapp"];
|
||||
telegram?: ProvidersStatusSnapshot["telegram"];
|
||||
discord?: ProvidersStatusSnapshot["discord"] | null;
|
||||
slack?: ProvidersStatusSnapshot["slack"] | null;
|
||||
signal?: ProvidersStatusSnapshot["signal"] | null;
|
||||
imessage?: ProvidersStatusSnapshot["imessage"] | null;
|
||||
},
|
||||
@@ -976,389 +941,6 @@ function renderProvider(
|
||||
</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> </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": {
|
||||
const signal = data.signal;
|
||||
return html`
|
||||
|
||||
Reference in new issue
Block a user