Slack: add some fixes and connect it all up

This commit is contained in:
Shadow
2026-01-04 01:53:15 -06:00
parent 02d7e286ea
commit 8c38a7fee8
45 changed files with 1568 additions and 89 deletions

View File

@@ -327,14 +327,13 @@ Slack runs in Socket Mode and requires both a bot token and app token:
sessionPrefix: "slack:slash", sessionPrefix: "slack:slash",
ephemeral: true ephemeral: true
}, },
replyToMode: "off", // off | first | all
textChunkLimit: 4000, textChunkLimit: 4000,
mediaMaxMb: 20 mediaMaxMb: 20
} }
} }
``` ```
Clawdis starts Slack only when a `slack` config section exists and both tokens are set (unless `slack.enabled` is `false`). Provide `SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN` env vars if you prefer. Use `user:<id>` (DM) or `channel:<id>` when specifying delivery targets for cron/CLI commands. 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: Reaction notification modes:
- `off`: no reaction events. - `off`: no reaction events.

View File

@@ -121,7 +121,6 @@ Slack uses Socket Mode only (no HTTP webhook server). Provide both tokens:
"sessionPrefix": "slack:slash", "sessionPrefix": "slack:slash",
"ephemeral": true "ephemeral": true
}, },
"replyToMode": "off",
"textChunkLimit": 4000, "textChunkLimit": 4000,
"mediaMaxMb": 20 "mediaMaxMb": 20
} }
@@ -137,13 +136,6 @@ Tokens can also be supplied via env vars:
- Channels map to `slack:channel:<channelId>` sessions. - Channels map to `slack:channel:<channelId>` sessions.
- Slash commands use `slack:slash:<userId>` sessions. - Slash commands use `slack:slash:<userId>` sessions.
## Reply threading
Slack replies can be threaded when reply tags are present and `slack.replyToMode` is enabled.
```json
{ "slack": { "replyToMode": "first" } }
```
## Delivery targets ## Delivery targets
Use these with cron/CLI sends: Use these with cron/CLI sends:
- `user:<id>` for DMs - `user:<id>` for DMs

View File

@@ -79,6 +79,8 @@
"@mariozechner/pi-ai": "^0.32.3", "@mariozechner/pi-ai": "^0.32.3",
"@mariozechner/pi-coding-agent": "^0.32.3", "@mariozechner/pi-coding-agent": "^0.32.3",
"@mariozechner/pi-tui": "^0.32.3", "@mariozechner/pi-tui": "^0.32.3",
"@slack/bolt": "^4.5.0",
"@slack/web-api": "^7.11.1",
"@sinclair/typebox": "0.34.46", "@sinclair/typebox": "0.34.46",
"@whiskeysockets/baileys": "7.0.0-rc.9", "@whiskeysockets/baileys": "7.0.0-rc.9",
"ajv": "^8.17.1", "ajv": "^8.17.1",

323
pnpm-lock.yaml generated
View File

@@ -43,6 +43,12 @@ importers:
'@sinclair/typebox': '@sinclair/typebox':
specifier: 0.34.46 specifier: 0.34.46
version: 0.34.46 version: 0.34.46
'@slack/bolt':
specifier: ^4.5.0
version: 4.6.0(@types/express@5.0.6)
'@slack/web-api':
specifier: ^7.11.1
version: 7.13.0
'@whiskeysockets/baileys': '@whiskeysockets/baileys':
specifier: 7.0.0-rc.9 specifier: 7.0.0-rc.9
version: 7.0.0-rc.9(audio-decode@2.2.3)(sharp@0.34.5) version: 7.0.0-rc.9(audio-decode@2.2.3)(sharp@0.34.5)
@@ -1098,6 +1104,32 @@ packages:
'@sinclair/typebox@0.34.46': '@sinclair/typebox@0.34.46':
resolution: {integrity: sha512-kiW7CtS/NkdvTUjkjUJo7d5JsFfbJ14YjdhDk9KoEgK6nFjKNXZPrX0jfLA8ZlET4cFLHxOZ/0vFKOP+bOxIOQ==} resolution: {integrity: sha512-kiW7CtS/NkdvTUjkjUJo7d5JsFfbJ14YjdhDk9KoEgK6nFjKNXZPrX0jfLA8ZlET4cFLHxOZ/0vFKOP+bOxIOQ==}
'@slack/bolt@4.6.0':
resolution: {integrity: sha512-xPgfUs2+OXSugz54Ky07pA890+Qydk22SYToi8uGpXeHSt1JWwFJkRyd/9Vlg5I1AdfdpGXExDpwnbuN9Q/2dQ==}
engines: {node: '>=18', npm: '>=8.6.0'}
peerDependencies:
'@types/express': ^5.0.0
'@slack/logger@4.0.0':
resolution: {integrity: sha512-Wz7QYfPAlG/DR+DfABddUZeNgoeY7d1J39OCR2jR+v7VBsB8ezulDK5szTnDDPDwLH5IWhLvXIHlCFZV7MSKgA==}
engines: {node: '>= 18', npm: '>= 8.6.0'}
'@slack/oauth@3.0.4':
resolution: {integrity: sha512-+8H0g7mbrHndEUbYCP7uYyBCbwqmm3E6Mo3nfsDvZZW74zKk1ochfH/fWSvGInYNCVvaBUbg3RZBbTp0j8yJCg==}
engines: {node: '>=18', npm: '>=8.6.0'}
'@slack/socket-mode@2.0.5':
resolution: {integrity: sha512-VaapvmrAifeFLAFaDPfGhEwwunTKsI6bQhYzxRXw7BSujZUae5sANO76WqlVsLXuhVtCVrBWPiS2snAQR2RHJQ==}
engines: {node: '>= 18', npm: '>= 8.6.0'}
'@slack/types@2.19.0':
resolution: {integrity: sha512-7+QZ38HGcNh/b/7MpvPG6jnw7mliV6UmrquJLqgdxkzJgQEYUcEztvFWRU49z0x4vthF0ixL5lTK601AXrS8IA==}
engines: {node: '>= 12.13.0', npm: '>= 6.12.0'}
'@slack/web-api@7.13.0':
resolution: {integrity: sha512-ERcExbWrnkDN8ovoWWe6Wgt/usanj1dWUd18dJLpctUI4mlPS0nKt81Joh8VI+OPbNnY1lIilVt9gdMBD9U2ig==}
engines: {node: '>= 18', npm: '>= 8.6.0'}
'@standard-schema/spec@1.1.0': '@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
@@ -1156,6 +1188,9 @@ packages:
'@types/http-errors@2.0.5': '@types/http-errors@2.0.5':
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
'@types/jsonwebtoken@9.0.10':
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
'@types/linkify-it@5.0.0': '@types/linkify-it@5.0.0':
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
@@ -1171,6 +1206,9 @@ packages:
'@types/mime-types@2.1.4': '@types/mime-types@2.1.4':
resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==} resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==}
'@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
'@types/node@10.17.60': '@types/node@10.17.60':
resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==} resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==}
@@ -1186,6 +1224,9 @@ packages:
'@types/range-parser@1.2.7': '@types/range-parser@1.2.7':
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
'@types/retry@0.12.0':
resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
'@types/send@1.2.1': '@types/send@1.2.1':
resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==}
@@ -1360,6 +1401,9 @@ packages:
async-mutex@0.5.0: async-mutex@0.5.0:
resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
atomic-sleep@1.0.0: atomic-sleep@1.0.0:
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
engines: {node: '>=8.0.0'} engines: {node: '>=8.0.0'}
@@ -1374,6 +1418,9 @@ packages:
resolution: {integrity: sha512-En9AY6EG1qYqEy5L/quryzbA4akBpJrnBZNxeKTqGHC2xT9Qc4aZ8b7CcbOMFTTc/MGdoNyp+SN4zInZNKxMYA==} resolution: {integrity: sha512-En9AY6EG1qYqEy5L/quryzbA4akBpJrnBZNxeKTqGHC2xT9Qc4aZ8b7CcbOMFTTc/MGdoNyp+SN4zInZNKxMYA==}
engines: {node: '>=14'} engines: {node: '>=14'}
axios@1.13.2:
resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
balanced-match@1.0.2: balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@@ -1489,6 +1536,10 @@ packages:
color-name@1.1.4: color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
commander@14.0.2: commander@14.0.2:
resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==}
engines: {node: '>=20'} engines: {node: '>=20'}
@@ -1543,6 +1594,10 @@ packages:
supports-color: supports-color:
optional: true optional: true
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
depd@2.0.0: depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@@ -1621,6 +1676,10 @@ packages:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
es-set-tostringtag@2.1.0:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
esbuild@0.27.2: esbuild@0.27.2:
resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -1644,6 +1703,9 @@ packages:
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
eventemitter3@4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
eventemitter3@5.0.1: eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
@@ -1700,10 +1762,23 @@ packages:
resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==}
engines: {node: '>= 18.0.0'} engines: {node: '>= 18.0.0'}
follow-redirects@1.15.11:
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
foreground-child@3.3.1: foreground-child@3.3.1:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'} engines: {node: '>=14'}
form-data@4.0.5:
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'}
formdata-polyfill@4.0.10: formdata-polyfill@4.0.10:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
engines: {node: '>=12.20.0'} engines: {node: '>=12.20.0'}
@@ -1800,6 +1875,10 @@ packages:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
has-tostringtag@1.0.2:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
hashery@1.4.0: hashery@1.4.0:
resolution: {integrity: sha512-Wn2i1In6XFxl8Az55kkgnFRiAlIAushzh26PTjL2AKtQcEfXrcLa7Hn5QOWGZEf3LU057P9TwwZjFyxfS1VuvQ==} resolution: {integrity: sha512-Wn2i1In6XFxl8Az55kkgnFRiAlIAushzh26PTjL2AKtQcEfXrcLa7Hn5QOWGZEf3LU057P9TwwZjFyxfS1VuvQ==}
engines: {node: '>=20'} engines: {node: '>=20'}
@@ -1853,6 +1932,9 @@ packages:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'} engines: {node: '>=8'}
is-electron@2.2.2:
resolution: {integrity: sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==}
is-extglob@2.1.1: is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -1872,6 +1954,10 @@ packages:
is-promise@4.0.0: is-promise@4.0.0:
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
is-stream@2.0.1:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'}
is-url@1.2.4: is-url@1.2.4:
resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
@@ -1935,6 +2021,10 @@ packages:
jsonc-parser@3.3.1: jsonc-parser@3.3.1:
resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==}
jsonwebtoken@9.0.3:
resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==}
engines: {node: '>=12', npm: '>=6'}
jszip@3.10.1: jszip@3.10.1:
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
@@ -2036,6 +2126,27 @@ packages:
lit@3.3.2: lit@3.3.2:
resolution: {integrity: sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==} resolution: {integrity: sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==}
lodash.includes@4.3.0:
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
lodash.isboolean@3.0.3:
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
lodash.isinteger@4.0.4:
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
lodash.isnumber@3.0.3:
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
lodash.isplainobject@4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
lodash.isstring@4.0.1:
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
lodash.once@4.1.1:
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
lodash.snakecase@4.1.1: lodash.snakecase@4.1.1:
resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==}
@@ -2115,10 +2226,18 @@ packages:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'} engines: {node: '>=8.6'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
mime-db@1.54.0: mime-db@1.54.0:
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
mime-types@3.0.2: mime-types@3.0.2:
resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -2164,9 +2283,6 @@ packages:
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
node-addon-api@7.1.1:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
node-domexception@1.0.0: node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'} engines: {node: '>=10.5.0'}
@@ -2250,10 +2366,26 @@ packages:
oxlint-tsgolint: oxlint-tsgolint:
optional: true optional: true
p-finally@1.0.0:
resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==}
engines: {node: '>=4'}
p-queue@6.6.2:
resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==}
engines: {node: '>=8'}
p-queue@9.0.1: p-queue@9.0.1:
resolution: {integrity: sha512-RhBdVhSwJb7Ocn3e8ULk4NMwBEuOxe+1zcgphUy9c2e5aR/xbEsdVXxHJ3lynw6Qiqu7OINEyHlZkiblEpaq7w==} resolution: {integrity: sha512-RhBdVhSwJb7Ocn3e8ULk4NMwBEuOxe+1zcgphUy9c2e5aR/xbEsdVXxHJ3lynw6Qiqu7OINEyHlZkiblEpaq7w==}
engines: {node: '>=20'} engines: {node: '>=20'}
p-retry@4.6.2:
resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==}
engines: {node: '>=8'}
p-timeout@3.2.0:
resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==}
engines: {node: '>=8'}
p-timeout@7.0.1: p-timeout@7.0.1:
resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==} resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==}
engines: {node: '>=20'} engines: {node: '>=20'}
@@ -2377,6 +2509,9 @@ packages:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
punycode.js@2.3.1: punycode.js@2.3.1:
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -2450,6 +2585,10 @@ packages:
resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
retry@0.13.1:
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
engines: {node: '>= 4'}
reusify@1.1.0: reusify@1.1.0:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'} engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
@@ -2697,6 +2836,10 @@ packages:
resolution: {integrity: sha512-XuELoRpMR+sq8fuWwX7P0bcj+PRNiicOKDEb3fGNURhxWVyykCi9BNq7c4uVz7h7P0sj8qgBsr5SWS6yBClq3g==} resolution: {integrity: sha512-XuELoRpMR+sq8fuWwX7P0bcj+PRNiicOKDEb3fGNURhxWVyykCi9BNq7c4uVz7h7P0sj8qgBsr5SWS6yBClq3g==}
engines: {node: '>=16'} engines: {node: '>=16'}
tsscmp@1.0.6:
resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==}
engines: {node: '>=0.6.x'}
tsx@4.21.0: tsx@4.21.0:
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
@@ -3626,6 +3769,71 @@ snapshots:
'@sinclair/typebox@0.34.46': {} '@sinclair/typebox@0.34.46': {}
'@slack/bolt@4.6.0(@types/express@5.0.6)':
dependencies:
'@slack/logger': 4.0.0
'@slack/oauth': 3.0.4
'@slack/socket-mode': 2.0.5
'@slack/types': 2.19.0
'@slack/web-api': 7.13.0
'@types/express': 5.0.6
axios: 1.13.2
express: 5.2.1
path-to-regexp: 8.3.0
raw-body: 3.0.2
tsscmp: 1.0.6
transitivePeerDependencies:
- bufferutil
- debug
- supports-color
- utf-8-validate
'@slack/logger@4.0.0':
dependencies:
'@types/node': 25.0.3
'@slack/oauth@3.0.4':
dependencies:
'@slack/logger': 4.0.0
'@slack/web-api': 7.13.0
'@types/jsonwebtoken': 9.0.10
'@types/node': 25.0.3
jsonwebtoken: 9.0.3
transitivePeerDependencies:
- debug
'@slack/socket-mode@2.0.5':
dependencies:
'@slack/logger': 4.0.0
'@slack/web-api': 7.13.0
'@types/node': 25.0.3
'@types/ws': 8.18.1
eventemitter3: 5.0.1
ws: 8.18.3
transitivePeerDependencies:
- bufferutil
- debug
- utf-8-validate
'@slack/types@2.19.0': {}
'@slack/web-api@7.13.0':
dependencies:
'@slack/logger': 4.0.0
'@slack/types': 2.19.0
'@types/node': 25.0.3
'@types/retry': 0.12.0
axios: 1.13.2
eventemitter3: 5.0.1
form-data: 4.0.5
is-electron: 2.2.2
is-stream: 2.0.1
p-queue: 6.6.2
p-retry: 4.6.2
retry: 0.13.1
transitivePeerDependencies:
- debug
'@standard-schema/spec@1.1.0': {} '@standard-schema/spec@1.1.0': {}
'@testing-library/dom@10.4.1': '@testing-library/dom@10.4.1':
@@ -3703,6 +3911,11 @@ snapshots:
'@types/http-errors@2.0.5': {} '@types/http-errors@2.0.5': {}
'@types/jsonwebtoken@9.0.10':
dependencies:
'@types/ms': 2.1.0
'@types/node': 25.0.3
'@types/linkify-it@5.0.0': {} '@types/linkify-it@5.0.0': {}
'@types/long@4.0.2': {} '@types/long@4.0.2': {}
@@ -3716,6 +3929,8 @@ snapshots:
'@types/mime-types@2.1.4': {} '@types/mime-types@2.1.4': {}
'@types/ms@2.1.0': {}
'@types/node@10.17.60': {} '@types/node@10.17.60': {}
'@types/node@25.0.3': '@types/node@25.0.3':
@@ -3728,6 +3943,8 @@ snapshots:
'@types/range-parser@1.2.7': {} '@types/range-parser@1.2.7': {}
'@types/retry@0.12.0': {}
'@types/send@1.2.1': '@types/send@1.2.1':
dependencies: dependencies:
'@types/node': 25.0.3 '@types/node': 25.0.3
@@ -3961,6 +4178,8 @@ snapshots:
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
asynckit@0.4.0: {}
atomic-sleep@1.0.0: {} atomic-sleep@1.0.0: {}
audio-buffer@5.0.0: audio-buffer@5.0.0:
@@ -3981,6 +4200,14 @@ snapshots:
audio-type@2.2.1: audio-type@2.2.1:
optional: true optional: true
axios@1.13.2:
dependencies:
follow-redirects: 1.15.11
form-data: 4.0.5
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
balanced-match@1.0.2: {} balanced-match@1.0.2: {}
balanced-match@3.0.1: {} balanced-match@3.0.1: {}
@@ -4113,6 +4340,10 @@ snapshots:
color-name@1.1.4: {} color-name@1.1.4: {}
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
commander@14.0.2: {} commander@14.0.2: {}
commander@8.3.0: {} commander@8.3.0: {}
@@ -4149,6 +4380,8 @@ snapshots:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
delayed-stream@1.0.0: {}
depd@2.0.0: {} depd@2.0.0: {}
dequal@2.0.3: dequal@2.0.3:
@@ -4222,6 +4455,13 @@ snapshots:
dependencies: dependencies:
es-errors: 1.3.0 es-errors: 1.3.0
es-set-tostringtag@2.1.0:
dependencies:
es-errors: 1.3.0
get-intrinsic: 1.3.0
has-tostringtag: 1.0.2
hasown: 2.0.2
esbuild@0.27.2: esbuild@0.27.2:
optionalDependencies: optionalDependencies:
'@esbuild/aix-ppc64': 0.27.2 '@esbuild/aix-ppc64': 0.27.2
@@ -4263,6 +4503,8 @@ snapshots:
event-target-shim@5.0.1: {} event-target-shim@5.0.1: {}
eventemitter3@4.0.7: {}
eventemitter3@5.0.1: {} eventemitter3@5.0.1: {}
events@3.3.0: {} events@3.3.0: {}
@@ -4353,11 +4595,21 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
follow-redirects@1.15.11: {}
foreground-child@3.3.1: foreground-child@3.3.1:
dependencies: dependencies:
cross-spawn: 7.0.6 cross-spawn: 7.0.6
signal-exit: 4.1.0 signal-exit: 4.1.0
form-data@4.0.5:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
hasown: 2.0.2
mime-types: 2.1.35
formdata-polyfill@4.0.10: formdata-polyfill@4.0.10:
dependencies: dependencies:
fetch-blob: 3.2.0 fetch-blob: 3.2.0
@@ -4478,6 +4730,10 @@ snapshots:
has-symbols@1.1.0: {} has-symbols@1.1.0: {}
has-tostringtag@1.0.2:
dependencies:
has-symbols: 1.1.0
hashery@1.4.0: hashery@1.4.0:
dependencies: dependencies:
hookified: 1.14.0 hookified: 1.14.0
@@ -4527,6 +4783,8 @@ snapshots:
dependencies: dependencies:
binary-extensions: 2.3.0 binary-extensions: 2.3.0
is-electron@2.2.2: {}
is-extglob@2.1.1: {} is-extglob@2.1.1: {}
is-fullwidth-code-point@3.0.0: {} is-fullwidth-code-point@3.0.0: {}
@@ -4539,6 +4797,8 @@ snapshots:
is-promise@4.0.0: {} is-promise@4.0.0: {}
is-stream@2.0.1: {}
is-url@1.2.4: {} is-url@1.2.4: {}
isarray@1.0.0: {} isarray@1.0.0: {}
@@ -4600,6 +4860,19 @@ snapshots:
jsonc-parser@3.3.1: {} jsonc-parser@3.3.1: {}
jsonwebtoken@9.0.3:
dependencies:
jws: 4.0.1
lodash.includes: 4.3.0
lodash.isboolean: 3.0.3
lodash.isinteger: 4.0.4
lodash.isnumber: 3.0.3
lodash.isplainobject: 4.0.6
lodash.isstring: 4.0.1
lodash.once: 4.1.1
ms: 2.1.3
semver: 7.7.3
jszip@3.10.1: jszip@3.10.1:
dependencies: dependencies:
lie: 3.3.0 lie: 3.3.0
@@ -4700,6 +4973,20 @@ snapshots:
lit-element: 4.2.2 lit-element: 4.2.2
lit-html: 3.3.2 lit-html: 3.3.2
lodash.includes@4.3.0: {}
lodash.isboolean@3.0.3: {}
lodash.isinteger@4.0.4: {}
lodash.isnumber@3.0.3: {}
lodash.isplainobject@4.0.6: {}
lodash.isstring@4.0.1: {}
lodash.once@4.1.1: {}
lodash.snakecase@4.1.1: {} lodash.snakecase@4.1.1: {}
lodash@4.17.21: {} lodash@4.17.21: {}
@@ -4763,8 +5050,14 @@ snapshots:
braces: 3.0.3 braces: 3.0.3
picomatch: 2.3.1 picomatch: 2.3.1
mime-db@1.52.0: {}
mime-db@1.54.0: {} mime-db@1.54.0: {}
mime-types@2.1.35:
dependencies:
mime-db: 1.52.0
mime-types@3.0.2: mime-types@3.0.2:
dependencies: dependencies:
mime-db: 1.54.0 mime-db: 1.54.0
@@ -4814,8 +5107,6 @@ snapshots:
negotiator@1.0.0: {} negotiator@1.0.0: {}
node-addon-api@7.1.1: {}
node-domexception@1.0.0: {} node-domexception@1.0.0: {}
node-fetch@2.7.0: node-fetch@2.7.0:
@@ -4892,11 +5183,27 @@ snapshots:
'@oxlint/win32-x64': 1.36.0 '@oxlint/win32-x64': 1.36.0
oxlint-tsgolint: 0.10.1 oxlint-tsgolint: 0.10.1
p-finally@1.0.0: {}
p-queue@6.6.2:
dependencies:
eventemitter3: 4.0.7
p-timeout: 3.2.0
p-queue@9.0.1: p-queue@9.0.1:
dependencies: dependencies:
eventemitter3: 5.0.1 eventemitter3: 5.0.1
p-timeout: 7.0.1 p-timeout: 7.0.1
p-retry@4.6.2:
dependencies:
'@types/retry': 0.12.0
retry: 0.13.1
p-timeout@3.2.0:
dependencies:
p-finally: 1.0.0
p-timeout@7.0.1: {} p-timeout@7.0.1: {}
package-json-from-dist@1.0.1: {} package-json-from-dist@1.0.1: {}
@@ -5036,6 +5343,8 @@ snapshots:
forwarded: 0.2.0 forwarded: 0.2.0
ipaddr.js: 1.9.1 ipaddr.js: 1.9.1
proxy-from-env@1.1.0: {}
punycode.js@2.3.1: {} punycode.js@2.3.1: {}
qified@0.5.3: qified@0.5.3:
@@ -5122,6 +5431,8 @@ snapshots:
retry@0.12.0: {} retry@0.12.0: {}
retry@0.13.1: {}
reusify@1.1.0: {} reusify@1.1.0: {}
rimraf@5.0.10: rimraf@5.0.10:
@@ -5432,6 +5743,8 @@ snapshots:
tslog@4.10.2: {} tslog@4.10.2: {}
tsscmp@1.0.6: {}
tsx@4.21.0: tsx@4.21.0:
dependencies: dependencies:
esbuild: 0.27.2 esbuild: 0.27.2

View File

@@ -8,6 +8,7 @@ import { createNodesTool } from "./tools/nodes-tool.js";
import { createSessionsHistoryTool } from "./tools/sessions-history-tool.js"; import { createSessionsHistoryTool } from "./tools/sessions-history-tool.js";
import { createSessionsListTool } from "./tools/sessions-list-tool.js"; import { createSessionsListTool } from "./tools/sessions-list-tool.js";
import { createSessionsSendTool } from "./tools/sessions-send-tool.js"; import { createSessionsSendTool } from "./tools/sessions-send-tool.js";
import { createSlackTool } from "./tools/slack-tool.js";
export function createClawdisTools(options?: { export function createClawdisTools(options?: {
browserControlUrl?: string; browserControlUrl?: string;
@@ -20,6 +21,7 @@ export function createClawdisTools(options?: {
createNodesTool(), createNodesTool(),
createCronTool(), createCronTool(),
createDiscordTool(), createDiscordTool(),
createSlackTool(),
createGatewayTool(), createGatewayTool(),
createSessionsListTool(), createSessionsListTool(),
createSessionsHistoryTool(), createSessionsHistoryTool(),

View File

@@ -88,6 +88,14 @@ describe("createClawdisCodingTools", () => {
expect(discord.some((tool) => tool.name === "discord")).toBe(true); expect(discord.some((tool) => tool.name === "discord")).toBe(true);
}); });
it("scopes slack tool to slack surface", () => {
const other = createClawdisCodingTools({ surface: "whatsapp" });
expect(other.some((tool) => tool.name === "slack")).toBe(false);
const slack = createClawdisCodingTools({ surface: "slack" });
expect(slack.some((tool) => tool.name === "slack")).toBe(true);
});
it("keeps read tool image metadata intact", async () => { it("keeps read tool image metadata intact", async () => {
const tools = createClawdisCodingTools(); const tools = createClawdisCodingTools();
const readTool = tools.find((tool) => tool.name === "read"); const readTool = tools.find((tool) => tool.name === "read");

View File

@@ -441,6 +441,12 @@ function shouldIncludeDiscordTool(surface?: string): boolean {
return normalized === "discord" || normalized.startsWith("discord:"); return normalized === "discord" || normalized.startsWith("discord:");
} }
function shouldIncludeSlackTool(surface?: string): boolean {
const normalized = normalizeSurface(surface);
if (!normalized) return false;
return normalized === "slack" || normalized.startsWith("slack:");
}
export function createClawdisCodingTools(options?: { export function createClawdisCodingTools(options?: {
bash?: BashToolDefaults & ProcessToolDefaults; bash?: BashToolDefaults & ProcessToolDefaults;
surface?: string; surface?: string;
@@ -494,9 +500,12 @@ export function createClawdisCodingTools(options?: {
}), }),
]; ];
const allowDiscord = shouldIncludeDiscordTool(options?.surface); const allowDiscord = shouldIncludeDiscordTool(options?.surface);
const filtered = allowDiscord const allowSlack = shouldIncludeSlackTool(options?.surface);
? tools const filtered = tools.filter((tool) => {
: tools.filter((tool) => tool.name !== "discord"); if (tool.name === "discord") return allowDiscord;
if (tool.name === "slack") return allowSlack;
return true;
});
const sandboxed = sandbox const sandboxed = sandbox
? filterToolsByPolicy(filtered, sandbox.tools) ? filterToolsByPolicy(filtered, sandbox.tools)
: filtered; : filtered;

View File

@@ -66,10 +66,8 @@ export async function handleSlackAction(
const to = readStringParam(params, "to", { required: true }); const to = readStringParam(params, "to", { required: true });
const content = readStringParam(params, "content", { required: true }); const content = readStringParam(params, "content", { required: true });
const mediaUrl = readStringParam(params, "mediaUrl"); const mediaUrl = readStringParam(params, "mediaUrl");
const replyTo = readStringParam(params, "replyTo");
const result = await sendSlackMessage(to, content, { const result = await sendSlackMessage(to, content, {
mediaUrl: mediaUrl ?? undefined, mediaUrl: mediaUrl ?? undefined,
replyTo: replyTo ?? undefined,
}); });
return jsonResult({ ok: true, result }); return jsonResult({ ok: true, result });
} }

View File

@@ -17,7 +17,6 @@ export const SlackToolSchema = Type.Union([
to: Type.String(), to: Type.String(),
content: Type.String(), content: Type.String(),
mediaUrl: Type.Optional(Type.String()), mediaUrl: Type.Optional(Type.String()),
replyTo: Type.Optional(Type.String()),
}), }),
Type.Object({ Type.Object({
action: Type.Literal("editMessage"), action: Type.Literal("editMessage"),

View File

@@ -63,7 +63,10 @@ describe("resolveTextChunkLimit", () => {
}); });
it("uses the matching provider override", () => { it("uses the matching provider override", () => {
const cfg = { discord: { textChunkLimit: 111 }, slack: { textChunkLimit: 222 } }; const cfg = {
discord: { textChunkLimit: 111 },
slack: { textChunkLimit: 222 },
};
expect(resolveTextChunkLimit(cfg, "discord")).toBe(111); expect(resolveTextChunkLimit(cfg, "discord")).toBe(111);
expect(resolveTextChunkLimit(cfg, "slack")).toBe(222); expect(resolveTextChunkLimit(cfg, "slack")).toBe(222);
expect(resolveTextChunkLimit(cfg, "telegram")).toBe(4000); expect(resolveTextChunkLimit(cfg, "telegram")).toBe(4000);

View File

@@ -2,12 +2,14 @@ import { sendMessageDiscord } from "../discord/send.js";
import { sendMessageIMessage } from "../imessage/send.js"; import { sendMessageIMessage } from "../imessage/send.js";
import { logWebSelfId, sendMessageWhatsApp } from "../providers/web/index.js"; import { logWebSelfId, sendMessageWhatsApp } from "../providers/web/index.js";
import { sendMessageSignal } from "../signal/send.js"; import { sendMessageSignal } from "../signal/send.js";
import { sendMessageSlack } from "../slack/send.js";
import { sendMessageTelegram } from "../telegram/send.js"; import { sendMessageTelegram } from "../telegram/send.js";
export type CliDeps = { export type CliDeps = {
sendMessageWhatsApp: typeof sendMessageWhatsApp; sendMessageWhatsApp: typeof sendMessageWhatsApp;
sendMessageTelegram: typeof sendMessageTelegram; sendMessageTelegram: typeof sendMessageTelegram;
sendMessageDiscord: typeof sendMessageDiscord; sendMessageDiscord: typeof sendMessageDiscord;
sendMessageSlack: typeof sendMessageSlack;
sendMessageSignal: typeof sendMessageSignal; sendMessageSignal: typeof sendMessageSignal;
sendMessageIMessage: typeof sendMessageIMessage; sendMessageIMessage: typeof sendMessageIMessage;
}; };
@@ -17,6 +19,7 @@ export function createDefaultDeps(): CliDeps {
sendMessageWhatsApp, sendMessageWhatsApp,
sendMessageTelegram, sendMessageTelegram,
sendMessageDiscord, sendMessageDiscord,
sendMessageSlack,
sendMessageSignal, sendMessageSignal,
sendMessageIMessage, sendMessageIMessage,
}; };

View File

@@ -300,7 +300,7 @@ export function buildProgram() {
program program
.command("send") .command("send")
.description( .description(
"Send a message (WhatsApp Web, Telegram bot, Discord, Signal, iMessage)", "Send a message (WhatsApp Web, Telegram bot, Discord, Slack, Signal, iMessage)",
) )
.requiredOption( .requiredOption(
"-t, --to <number>", "-t, --to <number>",
@@ -318,7 +318,7 @@ export function buildProgram() {
) )
.option( .option(
"--provider <provider>", "--provider <provider>",
"Delivery provider: whatsapp|telegram|discord|signal|imessage (default: whatsapp)", "Delivery provider: whatsapp|telegram|discord|slack|signal|imessage (default: whatsapp)",
) )
.option("--dry-run", "Print payload and skip sending", false) .option("--dry-run", "Print payload and skip sending", false)
.option("--json", "Output result as JSON", false) .option("--json", "Output result as JSON", false)
@@ -361,7 +361,7 @@ Examples:
.option("--verbose <on|off>", "Persist agent verbose level for the session") .option("--verbose <on|off>", "Persist agent verbose level for the session")
.option( .option(
"--provider <provider>", "--provider <provider>",
"Delivery provider: whatsapp|telegram|discord|signal|imessage (default: whatsapp)", "Delivery provider: whatsapp|telegram|discord|slack|signal|imessage (default: whatsapp)",
) )
.option( .option(
"--deliver", "--deliver",
@@ -411,7 +411,7 @@ Examples:
.option("--json", "Output JSON instead of text", false) .option("--json", "Output JSON instead of text", false)
.option( .option(
"--deep", "--deep",
"Probe providers (WhatsApp Web + Telegram + Discord + Signal)", "Probe providers (WhatsApp Web + Telegram + Discord + Slack + Signal)",
false, false,
) )
.option("--timeout <ms>", "Probe timeout in milliseconds", "10000") .option("--timeout <ms>", "Probe timeout in milliseconds", "10000")
@@ -422,7 +422,7 @@ Examples:
Examples: Examples:
clawdis status # show linked account + session store summary clawdis status # show linked account + session store summary
clawdis status --json # machine-readable output clawdis status --json # machine-readable output
clawdis status --deep # run provider probes (WA + Telegram + Discord + Signal) clawdis status --deep # run provider probes (WA + Telegram + Discord + Slack + Signal)
clawdis status --deep --timeout 5000 # tighten probe timeout`, clawdis status --deep --timeout 5000 # tighten probe timeout`,
) )
.action(async (opts) => { .action(async (opts) => {

View File

@@ -473,6 +473,7 @@ export async function agentCommand(
const whatsappTarget = opts.to ? normalizeE164(opts.to) : allowFrom[0]; const whatsappTarget = opts.to ? normalizeE164(opts.to) : allowFrom[0];
const telegramTarget = opts.to?.trim() || undefined; const telegramTarget = opts.to?.trim() || undefined;
const discordTarget = opts.to?.trim() || undefined; const discordTarget = opts.to?.trim() || undefined;
const slackTarget = opts.to?.trim() || undefined;
const signalTarget = opts.to?.trim() || undefined; const signalTarget = opts.to?.trim() || undefined;
const imessageTarget = opts.to?.trim() || undefined; const imessageTarget = opts.to?.trim() || undefined;
@@ -484,11 +485,13 @@ export async function agentCommand(
? whatsappTarget ? whatsappTarget
: deliveryProvider === "discord" : deliveryProvider === "discord"
? discordTarget ? discordTarget
: deliveryProvider === "signal" : deliveryProvider === "slack"
? signalTarget ? slackTarget
: deliveryProvider === "imessage" : deliveryProvider === "signal"
? imessageTarget ? signalTarget
: undefined; : deliveryProvider === "imessage"
? imessageTarget
: undefined;
const message = `Delivery failed (${deliveryProvider}${deliveryTarget ? ` to ${deliveryTarget}` : ""}): ${String(err)}`; const message = `Delivery failed (${deliveryProvider}${deliveryTarget ? ` to ${deliveryTarget}` : ""}): ${String(err)}`;
runtime.error?.(message); runtime.error?.(message);
if (!runtime.error) runtime.log(message); if (!runtime.error) runtime.log(message);
@@ -514,6 +517,13 @@ export async function agentCommand(
if (!bestEffortDeliver) throw err; if (!bestEffortDeliver) throw err;
logDeliveryError(err); logDeliveryError(err);
} }
if (deliveryProvider === "slack" && !slackTarget) {
const err = new Error(
"Delivering to Slack requires --to <channelId|user:ID|channel:ID>",
);
if (!bestEffortDeliver) throw err;
logDeliveryError(err);
}
if (deliveryProvider === "signal" && !signalTarget) { if (deliveryProvider === "signal" && !signalTarget) {
const err = new Error( const err = new Error(
"Delivering to Signal requires --to <E.164|group:ID|signal:group:ID|signal:+E.164>", "Delivering to Signal requires --to <E.164|group:ID|signal:group:ID|signal:+E.164>",
@@ -539,6 +549,7 @@ export async function agentCommand(
deliveryProvider !== "whatsapp" && deliveryProvider !== "whatsapp" &&
deliveryProvider !== "telegram" && deliveryProvider !== "telegram" &&
deliveryProvider !== "discord" && deliveryProvider !== "discord" &&
deliveryProvider !== "slack" &&
deliveryProvider !== "signal" && deliveryProvider !== "signal" &&
deliveryProvider !== "imessage" && deliveryProvider !== "imessage" &&
deliveryProvider !== "webchat" deliveryProvider !== "webchat"
@@ -574,6 +585,7 @@ export async function agentCommand(
deliveryProvider === "whatsapp" || deliveryProvider === "whatsapp" ||
deliveryProvider === "telegram" || deliveryProvider === "telegram" ||
deliveryProvider === "discord" || deliveryProvider === "discord" ||
deliveryProvider === "slack" ||
deliveryProvider === "signal" || deliveryProvider === "signal" ||
deliveryProvider === "imessage" deliveryProvider === "imessage"
? resolveTextChunkLimit(cfg, deliveryProvider) ? resolveTextChunkLimit(cfg, deliveryProvider)
@@ -666,6 +678,26 @@ export async function agentCommand(
} }
} }
if (deliveryProvider === "slack" && slackTarget) {
try {
if (media.length === 0) {
await deps.sendMessageSlack(slackTarget, text);
} else {
let first = true;
for (const url of media) {
const caption = first ? text : "";
first = false;
await deps.sendMessageSlack(slackTarget, caption, {
mediaUrl: url,
});
}
}
} catch (err) {
if (!bestEffortDeliver) throw err;
logDeliveryError(err);
}
}
if (deliveryProvider === "signal" && signalTarget) { if (deliveryProvider === "signal" && signalTarget) {
try { try {
if (media.length === 0) { if (media.length === 0) {

View File

@@ -31,6 +31,7 @@ async function noteProviderPrimer(prompter: WizardPrompter): Promise<void> {
"WhatsApp: dedicated second number recommended; primary number OK (self-chat).", "WhatsApp: dedicated second number recommended; primary number OK (self-chat).",
"Telegram: Bot API (token from @BotFather), replies via your bot.", "Telegram: Bot API (token from @BotFather), replies via your bot.",
"Discord: Bot token from Discord Developer Portal; invite bot to your server.", "Discord: Bot token from Discord Developer Portal; invite bot to your server.",
"Slack: Socket Mode app token + bot token, DMs via App Home Messages tab.",
"Signal: signal-cli as a linked device; separate number recommended.", "Signal: signal-cli as a linked device; separate number recommended.",
"iMessage: local imsg CLI; separate Apple ID recommended only on a separate Mac.", "iMessage: local imsg CLI; separate Apple ID recommended only on a separate Mac.",
].join("\n"), ].join("\n"),
@@ -74,6 +75,10 @@ function buildSlackManifest(botName: string) {
display_name: safeName, display_name: safeName,
always_online: false, always_online: false,
}, },
app_home: {
messages_tab_enabled: true,
messages_tab_read_only_enabled: false,
},
slash_commands: [ slash_commands: [
{ {
command: "/clawd", command: "/clawd",
@@ -94,6 +99,7 @@ function buildSlackManifest(botName: string) {
"users:read", "users:read",
"app_mentions:read", "app_mentions:read",
"reactions:read", "reactions:read",
"reactions:write",
"pins:read", "pins:read",
"pins:write", "pins:write",
"emoji:read", "emoji:read",
@@ -137,6 +143,7 @@ async function noteSlackTokenHelp(
"2) Add Socket Mode + enable it to get the app-level token (xapp-...)", "2) Add Socket Mode + enable it to get the app-level token (xapp-...)",
"3) OAuth & Permissions → install app to workspace (xoxb- bot token)", "3) OAuth & Permissions → install app to workspace (xoxb- bot token)",
"4) Enable Event Subscriptions (socket) for message events", "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.", "Tip: set SLACK_BOT_TOKEN + SLACK_APP_TOKEN in your env.",
"", "",
"Manifest (JSON):", "Manifest (JSON):",
@@ -237,10 +244,16 @@ export async function setupProviders(
const whatsappLinked = await detectWhatsAppLinked(); const whatsappLinked = await detectWhatsAppLinked();
const telegramEnv = Boolean(process.env.TELEGRAM_BOT_TOKEN?.trim()); const telegramEnv = Boolean(process.env.TELEGRAM_BOT_TOKEN?.trim());
const discordEnv = Boolean(process.env.DISCORD_BOT_TOKEN?.trim()); const discordEnv = Boolean(process.env.DISCORD_BOT_TOKEN?.trim());
const slackBotEnv = Boolean(process.env.SLACK_BOT_TOKEN?.trim());
const slackAppEnv = Boolean(process.env.SLACK_APP_TOKEN?.trim());
const telegramConfigured = Boolean( const telegramConfigured = Boolean(
telegramEnv || cfg.telegram?.botToken || cfg.telegram?.tokenFile, telegramEnv || cfg.telegram?.botToken || cfg.telegram?.tokenFile,
); );
const discordConfigured = Boolean(discordEnv || cfg.discord?.token); const discordConfigured = Boolean(discordEnv || cfg.discord?.token);
const slackConfigured = Boolean(
(slackBotEnv && slackAppEnv) ||
(cfg.slack?.botToken && cfg.slack?.appToken),
);
const signalConfigured = Boolean( const signalConfigured = Boolean(
cfg.signal?.account || cfg.signal?.httpUrl || cfg.signal?.httpPort, cfg.signal?.account || cfg.signal?.httpUrl || cfg.signal?.httpPort,
); );
@@ -257,6 +270,7 @@ export async function setupProviders(
`WhatsApp: ${whatsappLinked ? "linked" : "not linked"}`, `WhatsApp: ${whatsappLinked ? "linked" : "not linked"}`,
`Telegram: ${telegramConfigured ? "configured" : "needs token"}`, `Telegram: ${telegramConfigured ? "configured" : "needs token"}`,
`Discord: ${discordConfigured ? "configured" : "needs token"}`, `Discord: ${discordConfigured ? "configured" : "needs token"}`,
`Slack: ${slackConfigured ? "configured" : "needs tokens"}`,
`Signal: ${signalConfigured ? "configured" : "needs setup"}`, `Signal: ${signalConfigured ? "configured" : "needs setup"}`,
`iMessage: ${imessageConfigured ? "configured" : "needs setup"}`, `iMessage: ${imessageConfigured ? "configured" : "needs setup"}`,
`signal-cli: ${signalCliDetected ? "found" : "missing"} (${signalCliPath})`, `signal-cli: ${signalCliDetected ? "found" : "missing"} (${signalCliPath})`,
@@ -291,6 +305,11 @@ export async function setupProviders(
label: "Discord (Bot API)", label: "Discord (Bot API)",
hint: discordConfigured ? "configured" : "needs token", hint: discordConfigured ? "configured" : "needs token",
}, },
{
value: "slack",
label: "Slack (Socket Mode)",
hint: slackConfigured ? "configured" : "needs tokens",
},
{ {
value: "signal", value: "signal",
label: "Signal (signal-cli)", label: "Signal (signal-cli)",
@@ -695,6 +714,19 @@ export async function setupProviders(
} }
} }
if (!selection.includes("slack") && slackConfigured) {
const disable = await prompter.confirm({
message: "Disable Slack provider?",
initialValue: false,
});
if (disable) {
next = {
...next,
slack: { ...next.slack, enabled: false },
};
}
}
if (!selection.includes("signal") && signalConfigured) { if (!selection.includes("signal") && signalConfigured) {
const disable = await prompter.confirm({ const disable = await prompter.confirm({
message: "Disable Signal provider?", message: "Disable Signal provider?",
@@ -724,4 +756,3 @@ export async function setupProviders(
return next; return next;
} }

View File

@@ -14,6 +14,7 @@ export type ProviderChoice =
| "whatsapp" | "whatsapp"
| "telegram" | "telegram"
| "discord" | "discord"
| "slack"
| "signal" | "signal"
| "imessage"; | "imessage";

View File

@@ -41,6 +41,7 @@ const makeDeps = (overrides: Partial<CliDeps> = {}): CliDeps => ({
sendMessageWhatsApp: vi.fn(), sendMessageWhatsApp: vi.fn(),
sendMessageTelegram: vi.fn(), sendMessageTelegram: vi.fn(),
sendMessageDiscord: vi.fn(), sendMessageDiscord: vi.fn(),
sendMessageSlack: vi.fn(),
sendMessageSignal: vi.fn(), sendMessageSignal: vi.fn(),
sendMessageIMessage: vi.fn(), sendMessageIMessage: vi.fn(),
...overrides, ...overrides,
@@ -173,6 +174,25 @@ describe("sendCommand", () => {
expect(deps.sendMessageWhatsApp).not.toHaveBeenCalled(); expect(deps.sendMessageWhatsApp).not.toHaveBeenCalled();
}); });
it("routes to slack provider", async () => {
const deps = makeDeps({
sendMessageSlack: vi
.fn()
.mockResolvedValue({ messageId: "s1", channelId: "C123" }),
});
await sendCommand(
{ to: "channel:C123", message: "hi", provider: "slack" },
deps,
runtime,
);
expect(deps.sendMessageSlack).toHaveBeenCalledWith(
"channel:C123",
"hi",
expect.objectContaining({ mediaUrl: undefined }),
);
expect(deps.sendMessageWhatsApp).not.toHaveBeenCalled();
});
it("routes to imessage provider", async () => { it("routes to imessage provider", async () => {
const deps = makeDeps({ const deps = makeDeps({
sendMessageIMessage: vi.fn().mockResolvedValue({ messageId: "i1" }), sendMessageIMessage: vi.fn().mockResolvedValue({ messageId: "i1" }),

View File

@@ -86,6 +86,34 @@ export async function sendCommand(
return; return;
} }
if (provider === "slack") {
const result = await deps.sendMessageSlack(opts.to, opts.message, {
mediaUrl: opts.media,
});
runtime.log(
success(
`✅ Sent via slack. Message ID: ${result.messageId} (channel ${result.channelId})`,
),
);
if (opts.json) {
runtime.log(
JSON.stringify(
{
provider: "slack",
via: "direct",
to: opts.to,
channelId: result.channelId,
messageId: result.messageId,
mediaUrl: opts.media ?? null,
},
null,
2,
),
);
}
return;
}
if (provider === "signal") { if (provider === "signal") {
const result = await deps.sendMessageSignal(opts.to, opts.message, { const result = await deps.sendMessageSignal(opts.to, opts.message, {
mediaUrl: opts.media, mediaUrl: opts.media,

View File

@@ -61,6 +61,7 @@ export type SessionEntry = {
| "whatsapp" | "whatsapp"
| "telegram" | "telegram"
| "discord" | "discord"
| "slack"
| "signal" | "signal"
| "imessage" | "imessage"
| "webchat"; | "webchat";

View File

@@ -65,6 +65,7 @@ export type AgentElevatedAllowFromConfig = {
whatsapp?: string[]; whatsapp?: string[];
telegram?: Array<string | number>; telegram?: Array<string | number>;
discord?: Array<string | number>; discord?: Array<string | number>;
slack?: Array<string | number>;
signal?: Array<string | number>; signal?: Array<string | number>;
imessage?: Array<string | number>; imessage?: Array<string | number>;
webchat?: Array<string | number>; webchat?: Array<string | number>;
@@ -337,7 +338,6 @@ export type SlackConfig = {
botToken?: string; botToken?: string;
appToken?: string; appToken?: string;
textChunkLimit?: number; textChunkLimit?: number;
replyToMode?: ReplyToMode;
mediaMaxMb?: number; mediaMaxMb?: number;
/** Reaction notification mode (off|own|all|allowlist). Default: own. */ /** Reaction notification mode (off|own|all|allowlist). Default: own. */
reactionNotifications?: SlackReactionNotificationMode; reactionNotifications?: SlackReactionNotificationMode;

View File

@@ -417,6 +417,7 @@ export const ClawdisSchema = z.object({
whatsapp: z.array(z.string()).optional(), whatsapp: z.array(z.string()).optional(),
telegram: z.array(z.union([z.string(), z.number()])).optional(), telegram: z.array(z.union([z.string(), z.number()])).optional(),
discord: z.array(z.union([z.string(), z.number()])).optional(), discord: z.array(z.union([z.string(), z.number()])).optional(),
slack: z.array(z.union([z.string(), z.number()])).optional(),
signal: z.array(z.union([z.string(), z.number()])).optional(), signal: z.array(z.union([z.string(), z.number()])).optional(),
imessage: z.array(z.union([z.string(), z.number()])).optional(), imessage: z.array(z.union([z.string(), z.number()])).optional(),
webchat: z.array(z.union([z.string(), z.number()])).optional(), webchat: z.array(z.union([z.string(), z.number()])).optional(),
@@ -628,7 +629,6 @@ export const ClawdisSchema = z.object({
botToken: z.string().optional(), botToken: z.string().optional(),
appToken: z.string().optional(), appToken: z.string().optional(),
textChunkLimit: z.number().int().positive().optional(), textChunkLimit: z.number().int().positive().optional(),
replyToMode: ReplyToModeSchema.optional(),
mediaMaxMb: z.number().positive().optional(), mediaMaxMb: z.number().positive().optional(),
reactionNotifications: z reactionNotifications: z
.enum(["off", "own", "all", "allowlist"]) .enum(["off", "own", "all", "allowlist"])

View File

@@ -64,6 +64,7 @@ function resolveDeliveryTarget(
| "whatsapp" | "whatsapp"
| "telegram" | "telegram"
| "discord" | "discord"
| "slack"
| "signal" | "signal"
| "imessage"; | "imessage";
to?: string; to?: string;
@@ -92,6 +93,7 @@ function resolveDeliveryTarget(
requestedChannel === "whatsapp" || requestedChannel === "whatsapp" ||
requestedChannel === "telegram" || requestedChannel === "telegram" ||
requestedChannel === "discord" || requestedChannel === "discord" ||
requestedChannel === "slack" ||
requestedChannel === "signal" || requestedChannel === "signal" ||
requestedChannel === "imessage" requestedChannel === "imessage"
) { ) {
@@ -447,6 +449,43 @@ export async function runCronIsolatedAgentTurn(params: {
return { status: "error", summary, error: String(err) }; return { status: "error", summary, error: String(err) };
return { status: "ok", summary }; return { status: "ok", summary };
} }
} else if (resolvedDelivery.channel === "slack") {
if (!resolvedDelivery.to) {
if (!bestEffortDeliver)
return {
status: "error",
summary,
error:
"Cron delivery to Slack requires --channel slack and --to <channelId|user:ID>",
};
return {
status: "skipped",
summary: "Delivery skipped (no Slack destination).",
};
}
const slackTarget = resolvedDelivery.to;
try {
for (const payload of payloads) {
const mediaList =
payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
if (mediaList.length === 0) {
await params.deps.sendMessageSlack(slackTarget, payload.text ?? "");
} else {
let first = true;
for (const url of mediaList) {
const caption = first ? (payload.text ?? "") : "";
first = false;
await params.deps.sendMessageSlack(slackTarget, caption, {
mediaUrl: url,
});
}
}
}
} catch (err) {
if (!bestEffortDeliver)
return { status: "error", summary, error: String(err) };
return { status: "ok", summary };
}
} else if (resolvedDelivery.channel === "signal") { } else if (resolvedDelivery.channel === "signal") {
if (!resolvedDelivery.to) { if (!resolvedDelivery.to) {
if (!bestEffortDeliver) if (!bestEffortDeliver)

View File

@@ -19,6 +19,7 @@ export type CronPayload =
| "whatsapp" | "whatsapp"
| "telegram" | "telegram"
| "discord" | "discord"
| "slack"
| "signal" | "signal"
| "imessage"; | "imessage";
to?: string; to?: string;

View File

@@ -15,6 +15,7 @@ export type ProviderKind =
| "whatsapp" | "whatsapp"
| "telegram" | "telegram"
| "discord" | "discord"
| "slack"
| "signal" | "signal"
| "imessage"; | "imessage";
@@ -47,6 +48,7 @@ type ReloadAction =
| "restart-provider:whatsapp" | "restart-provider:whatsapp"
| "restart-provider:telegram" | "restart-provider:telegram"
| "restart-provider:discord" | "restart-provider:discord"
| "restart-provider:slack"
| "restart-provider:signal" | "restart-provider:signal"
| "restart-provider:imessage"; | "restart-provider:imessage";
@@ -70,6 +72,7 @@ const RELOAD_RULES: ReloadRule[] = [
{ prefix: "web", kind: "hot", actions: ["restart-provider:whatsapp"] }, { prefix: "web", kind: "hot", actions: ["restart-provider:whatsapp"] },
{ prefix: "telegram", kind: "hot", actions: ["restart-provider:telegram"] }, { prefix: "telegram", kind: "hot", actions: ["restart-provider:telegram"] },
{ prefix: "discord", kind: "hot", actions: ["restart-provider:discord"] }, { prefix: "discord", kind: "hot", actions: ["restart-provider:discord"] },
{ prefix: "slack", kind: "hot", actions: ["restart-provider:slack"] },
{ prefix: "signal", kind: "hot", actions: ["restart-provider:signal"] }, { prefix: "signal", kind: "hot", actions: ["restart-provider:signal"] },
{ prefix: "imessage", kind: "hot", actions: ["restart-provider:imessage"] }, { prefix: "imessage", kind: "hot", actions: ["restart-provider:imessage"] },
{ prefix: "identity", kind: "none" }, { prefix: "identity", kind: "none" },
@@ -200,6 +203,9 @@ export function buildGatewayReloadPlan(
case "restart-provider:discord": case "restart-provider:discord":
plan.restartProviders.add("discord"); plan.restartProviders.add("discord");
break; break;
case "restart-provider:slack":
plan.restartProviders.add("slack");
break;
case "restart-provider:signal": case "restart-provider:signal":
plan.restartProviders.add("signal"); plan.restartProviders.add("signal");
break; break;

View File

@@ -23,6 +23,7 @@ export type HookMappingResolved = {
| "whatsapp" | "whatsapp"
| "telegram" | "telegram"
| "discord" | "discord"
| "slack"
| "signal" | "signal"
| "imessage"; | "imessage";
to?: string; to?: string;
@@ -61,6 +62,7 @@ export type HookAction =
| "whatsapp" | "whatsapp"
| "telegram" | "telegram"
| "discord" | "discord"
| "slack"
| "signal" | "signal"
| "imessage"; | "imessage";
to?: string; to?: string;
@@ -99,7 +101,14 @@ type HookTransformResult = Partial<{
name: string; name: string;
sessionKey: string; sessionKey: string;
deliver: boolean; deliver: boolean;
channel: "last" | "whatsapp" | "telegram" | "discord" | "signal" | "imessage"; channel:
| "last"
| "whatsapp"
| "telegram"
| "discord"
| "slack"
| "signal"
| "imessage";
to: string; to: string;
thinking: string; thinking: string;
timeoutSeconds: number; timeoutSeconds: number;

View File

@@ -635,6 +635,7 @@ export const CronPayloadSchema = Type.Union([
Type.Literal("whatsapp"), Type.Literal("whatsapp"),
Type.Literal("telegram"), Type.Literal("telegram"),
Type.Literal("discord"), Type.Literal("discord"),
Type.Literal("slack"),
]), ]),
), ),
to: Type.Optional(Type.String()), to: Type.Optional(Type.String()),

View File

@@ -37,6 +37,7 @@ type HookDispatchers = {
| "whatsapp" | "whatsapp"
| "telegram" | "telegram"
| "discord" | "discord"
| "slack"
| "signal" | "signal"
| "imessage"; | "imessage";
to?: string; to?: string;

View File

@@ -8,6 +8,11 @@ import { type DiscordProbe, probeDiscord } from "../../discord/probe.js";
import { type IMessageProbe, probeIMessage } from "../../imessage/probe.js"; import { type IMessageProbe, probeIMessage } from "../../imessage/probe.js";
import { webAuthExists } from "../../providers/web/index.js"; import { webAuthExists } from "../../providers/web/index.js";
import { probeSignal, type SignalProbe } from "../../signal/probe.js"; import { probeSignal, type SignalProbe } from "../../signal/probe.js";
import { probeSlack, type SlackProbe } from "../../slack/probe.js";
import {
resolveSlackAppToken,
resolveSlackBotToken,
} from "../../slack/token.js";
import { probeTelegram, type TelegramProbe } from "../../telegram/probe.js"; import { probeTelegram, type TelegramProbe } from "../../telegram/probe.js";
import { resolveTelegramToken } from "../../telegram/token.js"; import { resolveTelegramToken } from "../../telegram/token.js";
import { getWebAuthAgeMs, readWebSelfId } from "../../web/session.js"; import { getWebAuthAgeMs, readWebSelfId } from "../../web/session.js";
@@ -74,6 +79,41 @@ export const providersHandlers: GatewayRequestHandlers = {
discordLastProbeAt = Date.now(); discordLastProbeAt = Date.now();
} }
const slackCfg = cfg.slack;
const slackEnabled = slackCfg?.enabled !== false;
const slackBotEnvToken = slackEnabled
? resolveSlackBotToken(process.env.SLACK_BOT_TOKEN)
: undefined;
const slackBotConfigToken = slackEnabled
? resolveSlackBotToken(slackCfg?.botToken)
: undefined;
const slackBotToken = slackBotEnvToken ?? slackBotConfigToken ?? "";
const slackBotTokenSource = slackBotEnvToken
? "env"
: slackBotConfigToken
? "config"
: "none";
const slackAppEnvToken = slackEnabled
? resolveSlackAppToken(process.env.SLACK_APP_TOKEN)
: undefined;
const slackAppConfigToken = slackEnabled
? resolveSlackAppToken(slackCfg?.appToken)
: undefined;
const slackAppToken = slackAppEnvToken ?? slackAppConfigToken ?? "";
const slackAppTokenSource = slackAppEnvToken
? "env"
: slackAppConfigToken
? "config"
: "none";
const slackConfigured =
slackEnabled && Boolean(slackBotToken) && Boolean(slackAppToken);
let slackProbe: SlackProbe | undefined;
let slackLastProbeAt: number | null = null;
if (probe && slackConfigured) {
slackProbe = await probeSlack(slackBotToken, timeoutMs);
slackLastProbeAt = Date.now();
}
const signalCfg = cfg.signal; const signalCfg = cfg.signal;
const signalEnabled = signalCfg?.enabled !== false; const signalEnabled = signalCfg?.enabled !== false;
const signalHost = signalCfg?.httpHost?.trim() || "127.0.0.1"; const signalHost = signalCfg?.httpHost?.trim() || "127.0.0.1";
@@ -152,6 +192,17 @@ export const providersHandlers: GatewayRequestHandlers = {
probe: discordProbe, probe: discordProbe,
lastProbeAt: discordLastProbeAt, lastProbeAt: discordLastProbeAt,
}, },
slack: {
configured: slackConfigured,
botTokenSource: slackBotTokenSource,
appTokenSource: slackAppTokenSource,
running: runtime.slack.running,
lastStartAt: runtime.slack.lastStartAt ?? null,
lastStopAt: runtime.slack.lastStopAt ?? null,
lastError: runtime.slack.lastError ?? null,
probe: slackProbe,
lastProbeAt: slackLastProbeAt,
},
signal: { signal: {
configured: signalConfigured, configured: signalConfigured,
baseUrl: signalBaseUrl, baseUrl: signalBaseUrl,

View File

@@ -3,6 +3,7 @@ import { sendMessageDiscord } from "../../discord/index.js";
import { shouldLogVerbose } from "../../globals.js"; import { shouldLogVerbose } from "../../globals.js";
import { sendMessageIMessage } from "../../imessage/index.js"; import { sendMessageIMessage } from "../../imessage/index.js";
import { sendMessageSignal } from "../../signal/index.js"; import { sendMessageSignal } from "../../signal/index.js";
import { sendMessageSlack } from "../../slack/send.js";
import { sendMessageTelegram } from "../../telegram/send.js"; import { sendMessageTelegram } from "../../telegram/send.js";
import { resolveTelegramToken } from "../../telegram/token.js"; import { resolveTelegramToken } from "../../telegram/token.js";
import { sendMessageWhatsApp } from "../../web/outbound.js"; import { sendMessageWhatsApp } from "../../web/outbound.js";
@@ -87,6 +88,22 @@ export const sendHandlers: GatewayRequestHandlers = {
payload, payload,
}); });
respond(true, payload, undefined, { provider }); respond(true, payload, undefined, { provider });
} else if (provider === "slack") {
const result = await sendMessageSlack(to, message, {
mediaUrl: request.mediaUrl,
});
const payload = {
runId: idem,
messageId: result.messageId,
channelId: result.channelId,
provider,
};
context.dedupe.set(`send:${idem}`, {
ts: Date.now(),
ok: true,
payload,
});
respond(true, payload, undefined, { provider });
} else if (provider === "signal") { } else if (provider === "signal") {
const cfg = loadConfig(); const cfg = loadConfig();
const host = cfg.signal?.httpHost?.trim() || "127.0.0.1"; const host = cfg.signal?.httpHost?.trim() || "127.0.0.1";

View File

@@ -7,6 +7,11 @@ import type { createSubsystemLogger } from "../logging.js";
import { monitorWebProvider, webAuthExists } from "../providers/web/index.js"; import { monitorWebProvider, webAuthExists } from "../providers/web/index.js";
import type { RuntimeEnv } from "../runtime.js"; import type { RuntimeEnv } from "../runtime.js";
import { monitorSignalProvider } from "../signal/index.js"; import { monitorSignalProvider } from "../signal/index.js";
import {
monitorSlackProvider,
resolveSlackAppToken,
resolveSlackBotToken,
} from "../slack/index.js";
import { monitorTelegramProvider } from "../telegram/monitor.js"; import { monitorTelegramProvider } from "../telegram/monitor.js";
import { probeTelegram } from "../telegram/probe.js"; import { probeTelegram } from "../telegram/probe.js";
import { resolveTelegramToken } from "../telegram/token.js"; import { resolveTelegramToken } from "../telegram/token.js";
@@ -29,6 +34,13 @@ export type DiscordRuntimeStatus = {
lastError?: string | null; lastError?: string | null;
}; };
export type SlackRuntimeStatus = {
running: boolean;
lastStartAt?: number | null;
lastStopAt?: number | null;
lastError?: string | null;
};
export type SignalRuntimeStatus = { export type SignalRuntimeStatus = {
running: boolean; running: boolean;
lastStartAt?: number | null; lastStartAt?: number | null;
@@ -50,6 +62,7 @@ export type ProviderRuntimeSnapshot = {
whatsapp: WebProviderStatus; whatsapp: WebProviderStatus;
telegram: TelegramRuntimeStatus; telegram: TelegramRuntimeStatus;
discord: DiscordRuntimeStatus; discord: DiscordRuntimeStatus;
slack: SlackRuntimeStatus;
signal: SignalRuntimeStatus; signal: SignalRuntimeStatus;
imessage: IMessageRuntimeStatus; imessage: IMessageRuntimeStatus;
}; };
@@ -61,11 +74,13 @@ type ProviderManagerOptions = {
logWhatsApp: SubsystemLogger; logWhatsApp: SubsystemLogger;
logTelegram: SubsystemLogger; logTelegram: SubsystemLogger;
logDiscord: SubsystemLogger; logDiscord: SubsystemLogger;
logSlack: SubsystemLogger;
logSignal: SubsystemLogger; logSignal: SubsystemLogger;
logIMessage: SubsystemLogger; logIMessage: SubsystemLogger;
whatsappRuntimeEnv: RuntimeEnv; whatsappRuntimeEnv: RuntimeEnv;
telegramRuntimeEnv: RuntimeEnv; telegramRuntimeEnv: RuntimeEnv;
discordRuntimeEnv: RuntimeEnv; discordRuntimeEnv: RuntimeEnv;
slackRuntimeEnv: RuntimeEnv;
signalRuntimeEnv: RuntimeEnv; signalRuntimeEnv: RuntimeEnv;
imessageRuntimeEnv: RuntimeEnv; imessageRuntimeEnv: RuntimeEnv;
}; };
@@ -79,6 +94,8 @@ export type ProviderManager = {
stopTelegramProvider: () => Promise<void>; stopTelegramProvider: () => Promise<void>;
startDiscordProvider: () => Promise<void>; startDiscordProvider: () => Promise<void>;
stopDiscordProvider: () => Promise<void>; stopDiscordProvider: () => Promise<void>;
startSlackProvider: () => Promise<void>;
stopSlackProvider: () => Promise<void>;
startSignalProvider: () => Promise<void>; startSignalProvider: () => Promise<void>;
stopSignalProvider: () => Promise<void>; stopSignalProvider: () => Promise<void>;
startIMessageProvider: () => Promise<void>; startIMessageProvider: () => Promise<void>;
@@ -94,11 +111,13 @@ export function createProviderManager(
logWhatsApp, logWhatsApp,
logTelegram, logTelegram,
logDiscord, logDiscord,
logSlack,
logSignal, logSignal,
logIMessage, logIMessage,
whatsappRuntimeEnv, whatsappRuntimeEnv,
telegramRuntimeEnv, telegramRuntimeEnv,
discordRuntimeEnv, discordRuntimeEnv,
slackRuntimeEnv,
signalRuntimeEnv, signalRuntimeEnv,
imessageRuntimeEnv, imessageRuntimeEnv,
} = opts; } = opts;
@@ -106,11 +125,13 @@ export function createProviderManager(
let whatsappAbort: AbortController | null = null; let whatsappAbort: AbortController | null = null;
let telegramAbort: AbortController | null = null; let telegramAbort: AbortController | null = null;
let discordAbort: AbortController | null = null; let discordAbort: AbortController | null = null;
let slackAbort: AbortController | null = null;
let signalAbort: AbortController | null = null; let signalAbort: AbortController | null = null;
let imessageAbort: AbortController | null = null; let imessageAbort: AbortController | null = null;
let whatsappTask: Promise<unknown> | null = null; let whatsappTask: Promise<unknown> | null = null;
let telegramTask: Promise<unknown> | null = null; let telegramTask: Promise<unknown> | null = null;
let discordTask: Promise<unknown> | null = null; let discordTask: Promise<unknown> | null = null;
let slackTask: Promise<unknown> | null = null;
let signalTask: Promise<unknown> | null = null; let signalTask: Promise<unknown> | null = null;
let imessageTask: Promise<unknown> | null = null; let imessageTask: Promise<unknown> | null = null;
@@ -137,6 +158,12 @@ export function createProviderManager(
lastStopAt: null, lastStopAt: null,
lastError: null, lastError: null,
}; };
let slackRuntime: SlackRuntimeStatus = {
running: false,
lastStartAt: null,
lastStopAt: null,
lastError: null,
};
let signalRuntime: SignalRuntimeStatus = { let signalRuntime: SignalRuntimeStatus = {
running: false, running: false,
lastStartAt: null, lastStartAt: null,
@@ -432,6 +459,93 @@ export function createProviderManager(
}; };
}; };
const startSlackProvider = async () => {
if (slackTask) return;
const cfg = loadConfig();
if (cfg.slack?.enabled === false) {
slackRuntime = {
...slackRuntime,
running: false,
lastError: "disabled",
};
if (shouldLogVerbose()) {
logSlack.debug("slack provider disabled (slack.enabled=false)");
}
return;
}
const botToken = resolveSlackBotToken(
process.env.SLACK_BOT_TOKEN ?? cfg.slack?.botToken ?? undefined,
);
const appToken = resolveSlackAppToken(
process.env.SLACK_APP_TOKEN ?? cfg.slack?.appToken ?? undefined,
);
if (!botToken || !appToken) {
slackRuntime = {
...slackRuntime,
running: false,
lastError: "not configured",
};
if (shouldLogVerbose()) {
logSlack.debug(
"slack provider not configured (missing SLACK_BOT_TOKEN/SLACK_APP_TOKEN)",
);
}
return;
}
logSlack.info(
`starting provider${cfg.slack ? "" : " (no slack config; tokens via env)"}`,
);
slackAbort = new AbortController();
slackRuntime = {
...slackRuntime,
running: true,
lastStartAt: Date.now(),
lastError: null,
};
const task = monitorSlackProvider({
botToken,
appToken,
runtime: slackRuntimeEnv,
abortSignal: slackAbort.signal,
mediaMaxMb: cfg.slack?.mediaMaxMb,
slashCommand: cfg.slack?.slashCommand,
})
.catch((err) => {
slackRuntime = {
...slackRuntime,
lastError: formatError(err),
};
logSlack.error(`provider exited: ${formatError(err)}`);
})
.finally(() => {
slackAbort = null;
slackTask = null;
slackRuntime = {
...slackRuntime,
running: false,
lastStopAt: Date.now(),
};
});
slackTask = task;
};
const stopSlackProvider = async () => {
if (!slackAbort && !slackTask) return;
slackAbort?.abort();
try {
await slackTask;
} catch {
// ignore
}
slackAbort = null;
slackTask = null;
slackRuntime = {
...slackRuntime,
running: false,
lastStopAt: Date.now(),
};
};
const startSignalProvider = async () => { const startSignalProvider = async () => {
if (signalTask) return; if (signalTask) return;
const cfg = loadConfig(); const cfg = loadConfig();
@@ -634,6 +748,7 @@ export function createProviderManager(
const startProviders = async () => { const startProviders = async () => {
await startWhatsAppProvider(); await startWhatsAppProvider();
await startDiscordProvider(); await startDiscordProvider();
await startSlackProvider();
await startTelegramProvider(); await startTelegramProvider();
await startSignalProvider(); await startSignalProvider();
await startIMessageProvider(); await startIMessageProvider();
@@ -652,6 +767,7 @@ export function createProviderManager(
whatsapp: { ...whatsappRuntime }, whatsapp: { ...whatsappRuntime },
telegram: { ...telegramRuntime }, telegram: { ...telegramRuntime },
discord: { ...discordRuntime }, discord: { ...discordRuntime },
slack: { ...slackRuntime },
signal: { ...signalRuntime }, signal: { ...signalRuntime },
imessage: { ...imessageRuntime }, imessage: { ...imessageRuntime },
}); });
@@ -665,6 +781,8 @@ export function createProviderManager(
stopTelegramProvider, stopTelegramProvider,
startDiscordProvider, startDiscordProvider,
stopDiscordProvider, stopDiscordProvider,
startSlackProvider,
stopSlackProvider,
startSignalProvider, startSignalProvider,
stopSignalProvider, stopSignalProvider,
startIMessageProvider, startIMessageProvider,

View File

@@ -55,6 +55,12 @@ const hoisted = vi.hoisted(() => {
lastStopAt: null, lastStopAt: null,
lastError: null, lastError: null,
}, },
slack: {
running: false,
lastStartAt: null,
lastStopAt: null,
lastError: null,
},
signal: { signal: {
running: false, running: false,
lastStartAt: null, lastStartAt: null,
@@ -78,6 +84,8 @@ const hoisted = vi.hoisted(() => {
stopTelegramProvider: vi.fn(async () => {}), stopTelegramProvider: vi.fn(async () => {}),
startDiscordProvider: vi.fn(async () => {}), startDiscordProvider: vi.fn(async () => {}),
stopDiscordProvider: vi.fn(async () => {}), stopDiscordProvider: vi.fn(async () => {}),
startSlackProvider: vi.fn(async () => {}),
stopSlackProvider: vi.fn(async () => {}),
startSignalProvider: vi.fn(async () => {}), startSignalProvider: vi.fn(async () => {}),
stopSignalProvider: vi.fn(async () => {}), stopSignalProvider: vi.fn(async () => {}),
startIMessageProvider: vi.fn(async () => {}), startIMessageProvider: vi.fn(async () => {}),

View File

@@ -148,12 +148,14 @@ const logWsControl = log.child("ws");
const logWhatsApp = logProviders.child("whatsapp"); const logWhatsApp = logProviders.child("whatsapp");
const logTelegram = logProviders.child("telegram"); const logTelegram = logProviders.child("telegram");
const logDiscord = logProviders.child("discord"); const logDiscord = logProviders.child("discord");
const logSlack = logProviders.child("slack");
const logSignal = logProviders.child("signal"); const logSignal = logProviders.child("signal");
const logIMessage = logProviders.child("imessage"); const logIMessage = logProviders.child("imessage");
const canvasRuntime = runtimeForLogger(logCanvas); const canvasRuntime = runtimeForLogger(logCanvas);
const whatsappRuntimeEnv = runtimeForLogger(logWhatsApp); const whatsappRuntimeEnv = runtimeForLogger(logWhatsApp);
const telegramRuntimeEnv = runtimeForLogger(logTelegram); const telegramRuntimeEnv = runtimeForLogger(logTelegram);
const discordRuntimeEnv = runtimeForLogger(logDiscord); const discordRuntimeEnv = runtimeForLogger(logDiscord);
const slackRuntimeEnv = runtimeForLogger(logSlack);
const signalRuntimeEnv = runtimeForLogger(logSignal); const signalRuntimeEnv = runtimeForLogger(logSignal);
const imessageRuntimeEnv = runtimeForLogger(logIMessage); const imessageRuntimeEnv = runtimeForLogger(logIMessage);
@@ -478,6 +480,7 @@ export async function startGatewayServer(
| "whatsapp" | "whatsapp"
| "telegram" | "telegram"
| "discord" | "discord"
| "slack"
| "signal" | "signal"
| "imessage"; | "imessage";
to?: string; to?: string;
@@ -722,11 +725,13 @@ export async function startGatewayServer(
logWhatsApp, logWhatsApp,
logTelegram, logTelegram,
logDiscord, logDiscord,
logSlack,
logSignal, logSignal,
logIMessage, logIMessage,
whatsappRuntimeEnv, whatsappRuntimeEnv,
telegramRuntimeEnv, telegramRuntimeEnv,
discordRuntimeEnv, discordRuntimeEnv,
slackRuntimeEnv,
signalRuntimeEnv, signalRuntimeEnv,
imessageRuntimeEnv, imessageRuntimeEnv,
}); });
@@ -736,11 +741,13 @@ export async function startGatewayServer(
startWhatsAppProvider, startWhatsAppProvider,
startTelegramProvider, startTelegramProvider,
startDiscordProvider, startDiscordProvider,
startSlackProvider,
startSignalProvider, startSignalProvider,
startIMessageProvider, startIMessageProvider,
stopWhatsAppProvider, stopWhatsAppProvider,
stopTelegramProvider, stopTelegramProvider,
stopDiscordProvider, stopDiscordProvider,
stopSlackProvider,
stopSignalProvider, stopSignalProvider,
stopIMessageProvider, stopIMessageProvider,
markWhatsAppLoggedOut, markWhatsAppLoggedOut,
@@ -1593,7 +1600,7 @@ export async function startGatewayServer(
} }
} }
// Launch configured providers (WhatsApp Web, Discord, Telegram) so gateway replies via the // Launch configured providers (WhatsApp Web, Discord, Slack, Telegram) so gateway replies via the
// surface the message came from. Tests can opt out via CLAWDIS_SKIP_PROVIDERS. // surface the message came from. Tests can opt out via CLAWDIS_SKIP_PROVIDERS.
if (process.env.CLAWDIS_SKIP_PROVIDERS !== "1") { if (process.env.CLAWDIS_SKIP_PROVIDERS !== "1") {
try { try {
@@ -1703,6 +1710,9 @@ export async function startGatewayServer(
startDiscordProvider, startDiscordProvider,
); );
} }
if (plan.restartProviders.has("slack")) {
await restartProvider("slack", stopSlackProvider, startSlackProvider);
}
if (plan.restartProviders.has("signal")) { if (plan.restartProviders.has("signal")) {
await restartProvider( await restartProvider(
"signal", "signal",
@@ -1806,6 +1816,7 @@ export async function startGatewayServer(
await stopWhatsAppProvider(); await stopWhatsAppProvider();
await stopTelegramProvider(); await stopTelegramProvider();
await stopDiscordProvider(); await stopDiscordProvider();
await stopSlackProvider();
await stopSignalProvider(); await stopSignalProvider();
await stopIMessageProvider(); await stopIMessageProvider();
await stopGmailWatcher(); await stopGmailWatcher();

View File

@@ -22,6 +22,7 @@ import { getQueueSize } from "../process/command-queue.js";
import { webAuthExists } from "../providers/web/index.js"; import { webAuthExists } from "../providers/web/index.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { sendMessageSignal } from "../signal/send.js"; import { sendMessageSignal } from "../signal/send.js";
import { sendMessageSlack } from "../slack/send.js";
import { sendMessageTelegram } from "../telegram/send.js"; import { sendMessageTelegram } from "../telegram/send.js";
import { normalizeE164 } from "../utils.js"; import { normalizeE164 } from "../utils.js";
import { getActiveWebListener } from "../web/active-listener.js"; import { getActiveWebListener } from "../web/active-listener.js";
@@ -38,12 +39,20 @@ export type HeartbeatTarget =
| "whatsapp" | "whatsapp"
| "telegram" | "telegram"
| "discord" | "discord"
| "slack"
| "signal" | "signal"
| "imessage" | "imessage"
| "none"; | "none";
export type HeartbeatDeliveryTarget = { export type HeartbeatDeliveryTarget = {
channel: "whatsapp" | "telegram" | "discord" | "signal" | "imessage" | "none"; channel:
| "whatsapp"
| "telegram"
| "discord"
| "slack"
| "signal"
| "imessage"
| "none";
to?: string; to?: string;
reason?: string; reason?: string;
}; };
@@ -53,6 +62,7 @@ type HeartbeatDeps = {
sendWhatsApp?: typeof sendMessageWhatsApp; sendWhatsApp?: typeof sendMessageWhatsApp;
sendTelegram?: typeof sendMessageTelegram; sendTelegram?: typeof sendMessageTelegram;
sendDiscord?: typeof sendMessageDiscord; sendDiscord?: typeof sendMessageDiscord;
sendSlack?: typeof sendMessageSlack;
sendSignal?: typeof sendMessageSignal; sendSignal?: typeof sendMessageSignal;
sendIMessage?: typeof sendMessageIMessage; sendIMessage?: typeof sendMessageIMessage;
getQueueSize?: (lane?: string) => number; getQueueSize?: (lane?: string) => number;
@@ -183,6 +193,7 @@ export function resolveHeartbeatDeliveryTarget(params: {
rawTarget === "whatsapp" || rawTarget === "whatsapp" ||
rawTarget === "telegram" || rawTarget === "telegram" ||
rawTarget === "discord" || rawTarget === "discord" ||
rawTarget === "slack" ||
rawTarget === "signal" || rawTarget === "signal" ||
rawTarget === "imessage" || rawTarget === "imessage" ||
rawTarget === "none" || rawTarget === "none" ||
@@ -209,6 +220,7 @@ export function resolveHeartbeatDeliveryTarget(params: {
| "whatsapp" | "whatsapp"
| "telegram" | "telegram"
| "discord" | "discord"
| "slack"
| "signal" | "signal"
| "imessage" | "imessage"
| undefined = | undefined =
@@ -217,6 +229,7 @@ export function resolveHeartbeatDeliveryTarget(params: {
: target === "whatsapp" || : target === "whatsapp" ||
target === "telegram" || target === "telegram" ||
target === "discord" || target === "discord" ||
target === "slack" ||
target === "signal" || target === "signal" ||
target === "imessage" target === "imessage"
? target ? target
@@ -288,7 +301,13 @@ function normalizeHeartbeatReply(
} }
async function deliverHeartbeatReply(params: { async function deliverHeartbeatReply(params: {
channel: "whatsapp" | "telegram" | "discord" | "signal" | "imessage"; channel:
| "whatsapp"
| "telegram"
| "discord"
| "slack"
| "signal"
| "imessage";
to: string; to: string;
text: string; text: string;
mediaUrls: string[]; mediaUrls: string[];
@@ -299,6 +318,7 @@ async function deliverHeartbeatReply(params: {
| "sendWhatsApp" | "sendWhatsApp"
| "sendTelegram" | "sendTelegram"
| "sendDiscord" | "sendDiscord"
| "sendSlack"
| "sendSignal" | "sendSignal"
| "sendIMessage" | "sendIMessage"
> >
@@ -369,6 +389,20 @@ async function deliverHeartbeatReply(params: {
return; return;
} }
if (channel === "slack") {
if (mediaUrls.length === 0) {
await deps.sendSlack(to, text);
return;
}
let first = true;
for (const url of mediaUrls) {
const caption = first ? text : "";
first = false;
await deps.sendSlack(to, caption, { mediaUrl: url });
}
return;
}
if (mediaUrls.length === 0) { if (mediaUrls.length === 0) {
await deps.sendDiscord(to, text, { verbose: false }); await deps.sendDiscord(to, text, { verbose: false });
return; return;
@@ -498,6 +532,7 @@ export async function runHeartbeatOnce(opts: {
sendWhatsApp: opts.deps?.sendWhatsApp ?? sendMessageWhatsApp, sendWhatsApp: opts.deps?.sendWhatsApp ?? sendMessageWhatsApp,
sendTelegram: opts.deps?.sendTelegram ?? sendMessageTelegram, sendTelegram: opts.deps?.sendTelegram ?? sendMessageTelegram,
sendDiscord: opts.deps?.sendDiscord ?? sendMessageDiscord, sendDiscord: opts.deps?.sendDiscord ?? sendMessageDiscord,
sendSlack: opts.deps?.sendSlack ?? sendMessageSlack,
sendSignal: opts.deps?.sendSignal ?? sendMessageSignal, sendSignal: opts.deps?.sendSignal ?? sendMessageSignal,
sendIMessage: opts.deps?.sendIMessage ?? sendMessageIMessage, sendIMessage: opts.deps?.sendIMessage ?? sendMessageIMessage,
}; };

View File

@@ -86,12 +86,11 @@ export async function listSlackReactions(
export async function sendSlackMessage( export async function sendSlackMessage(
to: string, to: string,
content: string, content: string,
opts: SlackActionClientOpts & { mediaUrl?: string; replyTo?: string } = {}, opts: SlackActionClientOpts & { mediaUrl?: string } = {},
) { ) {
return await sendMessageSlack(to, content, { return await sendMessageSlack(to, content, {
token: opts.token, token: opts.token,
mediaUrl: opts.mediaUrl, mediaUrl: opts.mediaUrl,
threadTs: opts.replyTo,
client: opts.client, client: opts.client,
}); });
} }

View File

@@ -12,4 +12,6 @@ export {
unpinSlackMessage, unpinSlackMessage,
} from "./actions.js"; } from "./actions.js";
export { monitorSlackProvider } from "./monitor.js"; export { monitorSlackProvider } from "./monitor.js";
export { probeSlack } from "./probe.js";
export { sendMessageSlack } from "./send.js"; export { sendMessageSlack } from "./send.js";
export { resolveSlackAppToken, resolveSlackBotToken } from "./token.js";

View File

@@ -6,7 +6,6 @@ import { getReplyFromConfig } from "../auto-reply/reply.js";
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js"; import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
import type { ReplyPayload } from "../auto-reply/types.js"; import type { ReplyPayload } from "../auto-reply/types.js";
import type { import type {
ReplyToMode,
SlackReactionNotificationMode, SlackReactionNotificationMode,
SlackSlashCommandConfig, SlackSlashCommandConfig,
} from "../config/config.js"; } from "../config/config.js";
@@ -26,7 +25,6 @@ export type MonitorSlackOpts = {
appToken?: string; appToken?: string;
runtime?: RuntimeEnv; runtime?: RuntimeEnv;
abortSignal?: AbortSignal; abortSignal?: AbortSignal;
replyToMode?: ReplyToMode;
mediaMaxMb?: number; mediaMaxMb?: number;
slashCommand?: SlackSlashCommandConfig; slashCommand?: SlackSlashCommandConfig;
}; };
@@ -135,18 +133,6 @@ type SlackChannelConfigResolved = {
requireMention: boolean; requireMention: boolean;
}; };
export function resolveSlackReplyTarget(opts: {
replyToMode: ReplyToMode;
replyToId?: string;
hasReplied: boolean;
}): string | undefined {
if (opts.replyToMode === "off") return undefined;
const replyToId = opts.replyToId?.trim();
if (!replyToId) return undefined;
if (opts.replyToMode === "all") return replyToId;
return opts.hasReplied ? undefined : replyToId;
}
function normalizeSlackSlug(raw?: string) { function normalizeSlackSlug(raw?: string) {
const trimmed = raw?.trim().toLowerCase() ?? ""; const trimmed = raw?.trim().toLowerCase() ?? "";
if (!trimmed) return ""; if (!trimmed) return "";
@@ -353,7 +339,6 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
const groupDmChannels = normalizeAllowList(dmConfig?.groupChannels); const groupDmChannels = normalizeAllowList(dmConfig?.groupChannels);
const channelsConfig = cfg.slack?.channels; const channelsConfig = cfg.slack?.channels;
const dmEnabled = dmConfig?.enabled ?? true; const dmEnabled = dmConfig?.enabled ?? true;
const replyToMode = opts.replyToMode ?? cfg.slack?.replyToMode ?? "off";
const reactionMode = cfg.slack?.reactionNotifications ?? "own"; const reactionMode = cfg.slack?.reactionNotifications ?? "own";
const reactionAllowlist = cfg.slack?.reactionAllowlist ?? []; const reactionAllowlist = cfg.slack?.reactionAllowlist ?? [];
const slashCommand = resolveSlackSlashCommandConfig( const slashCommand = resolveSlackSlashCommandConfig(
@@ -583,6 +568,14 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
const senderName = sender?.name ?? message.user; const senderName = sender?.name ?? message.user;
const roomLabel = channelName ? `#${channelName}` : `#${message.channel}`; const roomLabel = channelName ? `#${channelName}` : `#${message.channel}`;
const preview = rawBody.replace(/\s+/g, " ").slice(0, 160);
const inboundLabel = isDirectMessage
? `Slack DM from ${senderName}`
: `Slack message in ${roomLabel} from ${senderName}`;
enqueueSystemEvent(`${inboundLabel}: ${preview}`, {
contextKey: `slack:message:${message.channel}:${message.ts ?? "unknown"}`,
});
const textWithId = `${rawBody}\n[slack message id: ${message.ts} channel: ${message.channel}]`; const textWithId = `${rawBody}\n[slack message id: ${message.ts} channel: ${message.channel}]`;
const body = formatAgentEnvelope({ const body = formatAgentEnvelope({
surface: "Slack", surface: "Slack",
@@ -634,7 +627,6 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
} }
if (shouldLogVerbose()) { if (shouldLogVerbose()) {
const preview = rawBody.replace(/\s+/g, " ").slice(0, 160);
logVerbose( logVerbose(
`slack inbound: channel=${message.channel} from=${ctxPayload.From} preview="${preview}"`, `slack inbound: channel=${message.channel} from=${ctxPayload.From} preview="${preview}"`,
); );
@@ -656,7 +648,6 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
target: replyTarget, target: replyTarget,
token: botToken, token: botToken,
runtime, runtime,
replyToMode,
textLimit, textLimit,
}); });
}) })
@@ -685,7 +676,6 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
target: replyTarget, target: replyTarget,
token: botToken, token: botToken,
runtime, runtime,
replyToMode,
textLimit, textLimit,
}); });
if (shouldLogVerbose()) { if (shouldLogVerbose()) {
@@ -1222,49 +1212,36 @@ async function deliverReplies(params: {
target: string; target: string;
token: string; token: string;
runtime: RuntimeEnv; runtime: RuntimeEnv;
replyToMode: ReplyToMode;
textLimit: number; textLimit: number;
}) { }) {
const chunkLimit = Math.min(params.textLimit, 4000); const chunkLimit = Math.min(params.textLimit, 4000);
let hasReplied = false;
for (const payload of params.replies) { for (const payload of params.replies) {
const mediaList = const mediaList =
payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []); payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
const text = payload.text ?? ""; const text = payload.text ?? "";
const replyToId = payload.replyToId;
if (!text && mediaList.length === 0) continue; if (!text && mediaList.length === 0) continue;
if (mediaList.length === 0) { if (mediaList.length === 0) {
for (const chunk of chunkText(text, chunkLimit)) { for (const chunk of chunkText(text, chunkLimit)) {
const threadTs = resolveSlackReplyTarget({ const threadTs = undefined;
replyToMode: params.replyToMode,
replyToId,
hasReplied,
});
const trimmed = chunk.trim(); const trimmed = chunk.trim();
if (!trimmed || trimmed === SILENT_REPLY_TOKEN) continue; if (!trimmed || trimmed === SILENT_REPLY_TOKEN) continue;
await sendMessageSlack(params.target, trimmed, { await sendMessageSlack(params.target, trimmed, {
token: params.token, token: params.token,
threadTs, threadTs,
}); });
if (threadTs && !hasReplied) hasReplied = true;
} }
} else { } else {
let first = true; let first = true;
for (const mediaUrl of mediaList) { for (const mediaUrl of mediaList) {
const caption = first ? text : ""; const caption = first ? text : "";
first = false; first = false;
const threadTs = resolveSlackReplyTarget({ const threadTs = undefined;
replyToMode: params.replyToMode,
replyToId,
hasReplied,
});
await sendMessageSlack(params.target, caption, { await sendMessageSlack(params.target, caption, {
token: params.token, token: params.token,
mediaUrl, mediaUrl,
threadTs, threadTs,
}); });
if (threadTs && !hasReplied) hasReplied = true;
} }
} }
params.runtime.log?.(`delivered reply to ${params.target}`); params.runtime.log?.(`delivered reply to ${params.target}`);

59
src/slack/probe.ts Normal file
View File

@@ -0,0 +1,59 @@
import { WebClient } from "@slack/web-api";
export type SlackProbe = {
ok: boolean;
status?: number | null;
error?: string | null;
elapsedMs?: number | null;
bot?: { id?: string; name?: string };
team?: { id?: string; name?: string };
};
function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
if (!timeoutMs || timeoutMs <= 0) return promise;
let timer: NodeJS.Timeout | null = null;
const timeout = new Promise<T>((_, reject) => {
timer = setTimeout(() => reject(new Error("timeout")), timeoutMs);
});
return Promise.race([promise, timeout]).finally(() => {
if (timer) clearTimeout(timer);
});
}
export async function probeSlack(
token: string,
timeoutMs = 2500,
): Promise<SlackProbe> {
const client = new WebClient(token);
const start = Date.now();
try {
const result = await withTimeout(client.auth.test(), timeoutMs);
if (!result.ok) {
return {
ok: false,
status: 200,
error: result.error ?? "unknown",
elapsedMs: Date.now() - start,
};
}
return {
ok: true,
status: 200,
elapsedMs: Date.now() - start,
bot: { id: result.user_id ?? undefined, name: result.user ?? undefined },
team: { id: result.team_id ?? undefined, name: result.team ?? undefined },
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const status =
typeof (err as { status?: number }).status === "number"
? (err as { status?: number }).status
: null;
return {
ok: false,
status,
error: message,
elapsedMs: Date.now() - start,
};
}
}

187
src/slack/send.ts Normal file
View File

@@ -0,0 +1,187 @@
import { type FilesUploadV2Arguments, WebClient } from "@slack/web-api";
import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
import { loadConfig } from "../config/config.js";
import { loadWebMedia } from "../web/media.js";
import { resolveSlackBotToken } from "./token.js";
const SLACK_TEXT_LIMIT = 4000;
type SlackRecipient =
| {
kind: "user";
id: string;
}
| {
kind: "channel";
id: string;
};
type SlackSendOpts = {
token?: string;
mediaUrl?: string;
client?: WebClient;
threadTs?: string;
};
export type SlackSendResult = {
messageId: string;
channelId: string;
};
function resolveToken(explicit?: string) {
const cfgToken = loadConfig().slack?.botToken;
const token = resolveSlackBotToken(
explicit ?? process.env.SLACK_BOT_TOKEN ?? cfgToken ?? undefined,
);
if (!token) {
throw new Error(
"SLACK_BOT_TOKEN or slack.botToken is required for Slack sends",
);
}
return token;
}
function parseRecipient(raw: string): SlackRecipient {
const trimmed = raw.trim();
if (!trimmed) {
throw new Error("Recipient is required for Slack sends");
}
const mentionMatch = trimmed.match(/^<@([A-Z0-9]+)>$/i);
if (mentionMatch) {
return { kind: "user", id: mentionMatch[1] };
}
if (trimmed.startsWith("user:")) {
return { kind: "user", id: trimmed.slice("user:".length) };
}
if (trimmed.startsWith("channel:")) {
return { kind: "channel", id: trimmed.slice("channel:".length) };
}
if (trimmed.startsWith("slack:")) {
return { kind: "user", id: trimmed.slice("slack:".length) };
}
if (trimmed.startsWith("@")) {
const candidate = trimmed.slice(1);
if (!/^[A-Z0-9]+$/i.test(candidate)) {
throw new Error("Slack DMs require a user id (use user:<id> or <@id>)");
}
return { kind: "user", id: candidate };
}
if (trimmed.startsWith("#")) {
const candidate = trimmed.slice(1);
if (!/^[A-Z0-9]+$/i.test(candidate)) {
throw new Error("Slack channels require a channel id (use channel:<id>)");
}
return { kind: "channel", id: candidate };
}
return { kind: "channel", id: trimmed };
}
async function resolveChannelId(
client: WebClient,
recipient: SlackRecipient,
): Promise<{ channelId: string; isDm?: boolean }> {
if (recipient.kind === "channel") {
return { channelId: recipient.id };
}
const response = await client.conversations.open({ users: recipient.id });
const channelId = response.channel?.id;
if (!channelId) {
throw new Error("Failed to open Slack DM channel");
}
return { channelId, isDm: true };
}
async function uploadSlackFile(params: {
client: WebClient;
channelId: string;
mediaUrl: string;
caption?: string;
threadTs?: string;
maxBytes?: number;
}): Promise<string> {
const { buffer, contentType, fileName } = await loadWebMedia(
params.mediaUrl,
params.maxBytes,
);
const basePayload = {
channel_id: params.channelId,
file: buffer,
filename: fileName,
...(params.caption ? { initial_comment: params.caption } : {}),
...(contentType ? { filetype: contentType } : {}),
};
const payload: FilesUploadV2Arguments = params.threadTs
? { ...basePayload, thread_ts: params.threadTs }
: basePayload;
const response = await params.client.files.uploadV2(payload);
const parsed = response as {
files?: Array<{ id?: string; name?: string }>;
file?: { id?: string; name?: string };
};
const fileId =
parsed.files?.[0]?.id ??
parsed.file?.id ??
parsed.files?.[0]?.name ??
parsed.file?.name ??
"unknown";
return fileId;
}
export async function sendMessageSlack(
to: string,
message: string,
opts: SlackSendOpts = {},
): Promise<SlackSendResult> {
const trimmedMessage = message?.trim() ?? "";
if (!trimmedMessage && !opts.mediaUrl) {
throw new Error("Slack send requires text or media");
}
const token = resolveToken(opts.token);
const client = opts.client ?? new WebClient(token);
const recipient = parseRecipient(to);
const { channelId } = await resolveChannelId(client, recipient);
const cfg = loadConfig();
const textLimit = resolveTextChunkLimit(cfg, "slack");
const chunkLimit = Math.min(textLimit, SLACK_TEXT_LIMIT);
const chunks = chunkText(trimmedMessage, chunkLimit);
const mediaMaxBytes =
typeof cfg.slack?.mediaMaxMb === "number"
? cfg.slack.mediaMaxMb * 1024 * 1024
: undefined;
let lastMessageId = "";
if (opts.mediaUrl) {
const [firstChunk, ...rest] = chunks;
lastMessageId = await uploadSlackFile({
client,
channelId,
mediaUrl: opts.mediaUrl,
caption: firstChunk,
threadTs: opts.threadTs,
maxBytes: mediaMaxBytes,
});
for (const chunk of rest) {
const response = await client.chat.postMessage({
channel: channelId,
text: chunk,
thread_ts: opts.threadTs,
});
lastMessageId = response.ts ?? lastMessageId;
}
} else {
for (const chunk of chunks.length ? chunks : [""]) {
const response = await client.chat.postMessage({
channel: channelId,
text: chunk,
thread_ts: opts.threadTs,
});
lastMessageId = response.ts ?? lastMessageId;
}
}
return {
messageId: lastMessageId || "unknown",
channelId,
};
}

12
src/slack/token.ts Normal file
View File

@@ -0,0 +1,12 @@
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);
}

View File

@@ -27,6 +27,7 @@ import type {
CronFormState, CronFormState,
DiscordForm, DiscordForm,
IMessageForm, IMessageForm,
SlackForm,
SignalForm, SignalForm,
TelegramForm, TelegramForm,
} from "./ui-types"; } from "./ui-types";
@@ -44,6 +45,7 @@ import {
loadProviders, loadProviders,
updateDiscordForm, updateDiscordForm,
updateIMessageForm, updateIMessageForm,
updateSlackForm,
updateSignalForm, updateSignalForm,
updateTelegramForm, updateTelegramForm,
} from "./controllers/connections"; } from "./controllers/connections";
@@ -117,6 +119,11 @@ export type AppViewState = {
discordSaving: boolean; discordSaving: boolean;
discordTokenLocked: boolean; discordTokenLocked: boolean;
discordConfigStatus: string | null; discordConfigStatus: string | null;
slackForm: SlackForm;
slackSaving: boolean;
slackTokenLocked: boolean;
slackAppTokenLocked: boolean;
slackConfigStatus: string | null;
signalForm: SignalForm; signalForm: SignalForm;
signalSaving: boolean; signalSaving: boolean;
signalConfigStatus: string | null; signalConfigStatus: string | null;
@@ -269,6 +276,11 @@ export function renderApp(state: AppViewState) {
discordTokenLocked: state.discordTokenLocked, discordTokenLocked: state.discordTokenLocked,
discordSaving: state.discordSaving, discordSaving: state.discordSaving,
discordStatus: state.discordConfigStatus, discordStatus: state.discordConfigStatus,
slackForm: state.slackForm,
slackTokenLocked: state.slackTokenLocked,
slackAppTokenLocked: state.slackAppTokenLocked,
slackSaving: state.slackSaving,
slackStatus: state.slackConfigStatus,
signalForm: state.signalForm, signalForm: state.signalForm,
signalSaving: state.signalSaving, signalSaving: state.signalSaving,
signalStatus: state.signalConfigStatus, signalStatus: state.signalConfigStatus,
@@ -283,6 +295,8 @@ export function renderApp(state: AppViewState) {
onTelegramSave: () => state.handleTelegramSave(), onTelegramSave: () => state.handleTelegramSave(),
onDiscordChange: (patch) => updateDiscordForm(state, patch), onDiscordChange: (patch) => updateDiscordForm(state, patch),
onDiscordSave: () => state.handleDiscordSave(), onDiscordSave: () => state.handleDiscordSave(),
onSlackChange: (patch) => updateSlackForm(state, patch),
onSlackSave: () => state.handleSlackSave(),
onSignalChange: (patch) => updateSignalForm(state, patch), onSignalChange: (patch) => updateSignalForm(state, patch),
onSignalSave: () => state.handleSignalSave(), onSignalSave: () => state.handleSignalSave(),
onIMessageChange: (patch) => updateIMessageForm(state, patch), onIMessageChange: (patch) => updateIMessageForm(state, patch),

View File

@@ -36,9 +36,11 @@ import type {
} from "./types"; } from "./types";
import { import {
defaultDiscordActions, defaultDiscordActions,
defaultSlackActions,
type CronFormState, type CronFormState,
type DiscordForm, type DiscordForm,
type IMessageForm, type IMessageForm,
type SlackForm,
type SignalForm, type SignalForm,
type TelegramForm, type TelegramForm,
} from "./ui-types"; } from "./ui-types";
@@ -59,6 +61,7 @@ import {
logoutWhatsApp, logoutWhatsApp,
saveDiscordConfig, saveDiscordConfig,
saveIMessageConfig, saveIMessageConfig,
saveSlackConfig,
saveSignalConfig, saveSignalConfig,
saveTelegramConfig, saveTelegramConfig,
startWhatsAppLogin, startWhatsAppLogin,
@@ -233,7 +236,6 @@ export class ClawdisApp extends LitElement {
mediaMaxMb: "", mediaMaxMb: "",
historyLimit: "", historyLimit: "",
textChunkLimit: "", textChunkLimit: "",
replyToMode: "off",
guilds: [], guilds: [],
actions: { ...defaultDiscordActions }, actions: { ...defaultDiscordActions },
slashEnabled: false, slashEnabled: false,
@@ -244,6 +246,29 @@ export class ClawdisApp extends LitElement {
@state() discordSaving = false; @state() discordSaving = false;
@state() discordTokenLocked = false; @state() discordTokenLocked = false;
@state() discordConfigStatus: string | null = null; @state() discordConfigStatus: string | null = null;
@state() slackForm: SlackForm = {
enabled: true,
botToken: "",
appToken: "",
dmEnabled: true,
allowFrom: "",
groupEnabled: false,
groupChannels: "",
mediaMaxMb: "",
textChunkLimit: "",
reactionNotifications: "own",
reactionAllowlist: "",
slashEnabled: false,
slashName: "",
slashSessionPrefix: "",
slashEphemeral: true,
actions: { ...defaultSlackActions },
channels: [],
};
@state() slackSaving = false;
@state() slackTokenLocked = false;
@state() slackAppTokenLocked = false;
@state() slackConfigStatus: string | null = null;
@state() signalForm: SignalForm = { @state() signalForm: SignalForm = {
enabled: true, enabled: true,
account: "", account: "",
@@ -774,6 +799,12 @@ export class ClawdisApp extends LitElement {
await loadProviders(this, true); await loadProviders(this, true);
} }
async handleSlackSave() {
await saveSlackConfig(this);
await loadConfig(this);
await loadProviders(this, true);
}
async handleSignalSave() { async handleSignalSave() {
await saveSignalConfig(this); await saveSignalConfig(this);
await loadConfig(this); await loadConfig(this);

View File

@@ -6,11 +6,14 @@ import type {
} from "../types"; } from "../types";
import { import {
defaultDiscordActions, defaultDiscordActions,
defaultSlackActions,
type DiscordActionForm, type DiscordActionForm,
type DiscordForm, type DiscordForm,
type DiscordGuildChannelForm, type DiscordGuildChannelForm,
type DiscordGuildForm, type DiscordGuildForm,
type IMessageForm, type IMessageForm,
type SlackChannelForm,
type SlackForm,
type SignalForm, type SignalForm,
type TelegramForm, type TelegramForm,
} from "../ui-types"; } from "../ui-types";
@@ -34,10 +37,12 @@ export type ConfigState = {
lastError: string | null; lastError: string | null;
telegramForm: TelegramForm; telegramForm: TelegramForm;
discordForm: DiscordForm; discordForm: DiscordForm;
slackForm: SlackForm;
signalForm: SignalForm; signalForm: SignalForm;
imessageForm: IMessageForm; imessageForm: IMessageForm;
telegramConfigStatus: string | null; telegramConfigStatus: string | null;
discordConfigStatus: string | null; discordConfigStatus: string | null;
slackConfigStatus: string | null;
signalConfigStatus: string | null; signalConfigStatus: string | null;
imessageConfigStatus: string | null; imessageConfigStatus: string | null;
}; };
@@ -255,10 +260,6 @@ export function applyConfigSnapshot(state: ConfigState, snapshot: ConfigSnapshot
typeof slack.textChunkLimit === "number" typeof slack.textChunkLimit === "number"
? String(slack.textChunkLimit) ? String(slack.textChunkLimit)
: "", : "",
replyToMode:
slack.replyToMode === "first" || slack.replyToMode === "all"
? slack.replyToMode
: "off",
reactionNotifications: reactionNotifications:
slack.reactionNotifications === "off" || slack.reactionNotifications === "off" ||
slack.reactionNotifications === "all" || slack.reactionNotifications === "all" ||
@@ -492,4 +493,3 @@ function removePathValue(
delete (current as Record<string, unknown>)[lastKey]; delete (current as Record<string, unknown>)[lastKey];
} }
} }

View File

@@ -3,11 +3,14 @@ import { parseList } from "../format";
import type { ConfigSnapshot, ProvidersStatusSnapshot } from "../types"; import type { ConfigSnapshot, ProvidersStatusSnapshot } from "../types";
import { import {
defaultDiscordActions, defaultDiscordActions,
defaultSlackActions,
type DiscordActionForm, type DiscordActionForm,
type DiscordForm, type DiscordForm,
type DiscordGuildChannelForm, type DiscordGuildChannelForm,
type DiscordGuildForm, type DiscordGuildForm,
type IMessageForm, type IMessageForm,
type SlackActionForm,
type SlackForm,
type SignalForm, type SignalForm,
type TelegramForm, type TelegramForm,
} from "../ui-types"; } from "../ui-types";
@@ -31,6 +34,11 @@ export type ConnectionsState = {
discordSaving: boolean; discordSaving: boolean;
discordTokenLocked: boolean; discordTokenLocked: boolean;
discordConfigStatus: string | null; discordConfigStatus: string | null;
slackForm: SlackForm;
slackSaving: boolean;
slackTokenLocked: boolean;
slackAppTokenLocked: boolean;
slackConfigStatus: string | null;
signalForm: SignalForm; signalForm: SignalForm;
signalSaving: boolean; signalSaving: boolean;
signalConfigStatus: string | null; signalConfigStatus: string | null;
@@ -54,6 +62,8 @@ export async function loadProviders(state: ConnectionsState, probe: boolean) {
state.providersLastSuccess = Date.now(); state.providersLastSuccess = Date.now();
state.telegramTokenLocked = res.telegram.tokenSource === "env"; state.telegramTokenLocked = res.telegram.tokenSource === "env";
state.discordTokenLocked = res.discord?.tokenSource === "env"; state.discordTokenLocked = res.discord?.tokenSource === "env";
state.slackTokenLocked = res.slack?.botTokenSource === "env";
state.slackAppTokenLocked = res.slack?.appTokenSource === "env";
} catch (err) { } catch (err) {
state.providersError = String(err); state.providersError = String(err);
} finally { } finally {
@@ -136,6 +146,21 @@ export function updateDiscordForm(
state.discordForm = { ...state.discordForm, ...patch }; state.discordForm = { ...state.discordForm, ...patch };
} }
export function updateSlackForm(
state: ConnectionsState,
patch: Partial<SlackForm>,
) {
if (patch.actions) {
state.slackForm = {
...state.slackForm,
...patch,
actions: { ...state.slackForm.actions, ...patch.actions },
};
return;
}
state.slackForm = { ...state.slackForm, ...patch };
}
export function updateSignalForm( export function updateSignalForm(
state: ConnectionsState, state: ConnectionsState,
patch: Partial<SignalForm>, patch: Partial<SignalForm>,
@@ -437,9 +462,6 @@ export async function saveSlackConfig(state: ConnectionsState) {
delete slack.textChunkLimit; delete slack.textChunkLimit;
} }
if (form.replyToMode === "off") delete slack.replyToMode;
else slack.replyToMode = form.replyToMode;
if (form.reactionNotifications === "own") { if (form.reactionNotifications === "own") {
delete slack.reactionNotifications; delete slack.reactionNotifications;
} else { } else {
@@ -670,4 +692,3 @@ export async function saveIMessageConfig(state: ConnectionsState) {
state.imessageSaving = false; state.imessageSaving = false;
} }
} }

View File

@@ -3,6 +3,7 @@ export type ProvidersStatusSnapshot = {
whatsapp: WhatsAppStatus; whatsapp: WhatsAppStatus;
telegram: TelegramStatus; telegram: TelegramStatus;
discord?: DiscordStatus | null; discord?: DiscordStatus | null;
slack?: SlackStatus | null;
signal?: SignalStatus | null; signal?: SignalStatus | null;
imessage?: IMessageStatus | null; imessage?: IMessageStatus | null;
}; };
@@ -89,6 +90,37 @@ export type DiscordStatus = {
lastProbeAt?: number | null; lastProbeAt?: number | null;
}; };
export type SlackBot = {
id?: string | null;
name?: string | null;
};
export type SlackTeam = {
id?: string | null;
name?: string | null;
};
export type SlackProbe = {
ok: boolean;
status?: number | null;
error?: string | null;
elapsedMs?: number | null;
bot?: SlackBot | null;
team?: SlackTeam | null;
};
export type SlackStatus = {
configured: boolean;
botTokenSource?: string | null;
appTokenSource?: string | null;
running: boolean;
lastStartAt?: number | null;
lastStopAt?: number | null;
lastError?: string | null;
probe?: SlackProbe | null;
lastProbeAt?: number | null;
};
export type SignalProbe = { export type SignalProbe = {
ok: boolean; ok: boolean;
status?: number | null; status?: number | null;

View File

@@ -84,7 +84,6 @@ export type SlackForm = {
groupChannels: string; groupChannels: string;
mediaMaxMb: string; mediaMaxMb: string;
textChunkLimit: string; textChunkLimit: string;
replyToMode: "off" | "first" | "all";
reactionNotifications: "off" | "own" | "all" | "allowlist"; reactionNotifications: "off" | "own" | "all" | "allowlist";
reactionAllowlist: string; reactionAllowlist: string;
slashEnabled: boolean; slashEnabled: boolean;
@@ -168,4 +167,3 @@ export type CronFormState = {
timeoutSeconds: string; timeoutSeconds: string;
postToMainPrefix: string; postToMainPrefix: string;
}; };

View File

@@ -6,6 +6,8 @@ import type {
DiscordActionForm, DiscordActionForm,
DiscordForm, DiscordForm,
IMessageForm, IMessageForm,
SlackActionForm,
SlackForm,
SignalForm, SignalForm,
TelegramForm, TelegramForm,
} from "../ui-types"; } from "../ui-types";
@@ -54,6 +56,11 @@ export type ConnectionsProps = {
discordTokenLocked: boolean; discordTokenLocked: boolean;
discordSaving: boolean; discordSaving: boolean;
discordStatus: string | null; discordStatus: string | null;
slackForm: SlackForm;
slackTokenLocked: boolean;
slackAppTokenLocked: boolean;
slackSaving: boolean;
slackStatus: string | null;
signalForm: SignalForm; signalForm: SignalForm;
signalSaving: boolean; signalSaving: boolean;
signalStatus: string | null; signalStatus: string | null;
@@ -68,6 +75,8 @@ export type ConnectionsProps = {
onTelegramSave: () => void; onTelegramSave: () => void;
onDiscordChange: (patch: Partial<DiscordForm>) => void; onDiscordChange: (patch: Partial<DiscordForm>) => void;
onDiscordSave: () => void; onDiscordSave: () => void;
onSlackChange: (patch: Partial<SlackForm>) => void;
onSlackSave: () => void;
onSignalChange: (patch: Partial<SignalForm>) => void; onSignalChange: (patch: Partial<SignalForm>) => void;
onSignalSave: () => void; onSignalSave: () => void;
onIMessageChange: (patch: Partial<IMessageForm>) => void; onIMessageChange: (patch: Partial<IMessageForm>) => void;
@@ -78,12 +87,14 @@ export function renderConnections(props: ConnectionsProps) {
const whatsapp = props.snapshot?.whatsapp; const whatsapp = props.snapshot?.whatsapp;
const telegram = props.snapshot?.telegram; const telegram = props.snapshot?.telegram;
const discord = props.snapshot?.discord ?? null; const discord = props.snapshot?.discord ?? null;
const slack = props.snapshot?.slack ?? null;
const signal = props.snapshot?.signal ?? null; const signal = props.snapshot?.signal ?? null;
const imessage = props.snapshot?.imessage ?? null; const imessage = props.snapshot?.imessage ?? null;
const providerOrder: ProviderKey[] = [ const providerOrder: ProviderKey[] = [
"whatsapp", "whatsapp",
"telegram", "telegram",
"discord", "discord",
"slack",
"signal", "signal",
"imessage", "imessage",
]; ];
@@ -101,7 +112,14 @@ export function renderConnections(props: ConnectionsProps) {
return html` return html`
<section class="grid grid-cols-2"> <section class="grid grid-cols-2">
${orderedProviders.map((provider) => ${orderedProviders.map((provider) =>
renderProvider(provider.key, props, { whatsapp, telegram, discord, signal, imessage }), renderProvider(provider.key, props, {
whatsapp,
telegram,
discord,
slack,
signal,
imessage,
}),
)} )}
</section> </section>
@@ -135,7 +153,13 @@ function formatDuration(ms?: number | null) {
return `${hr}h`; return `${hr}h`;
} }
type ProviderKey = "whatsapp" | "telegram" | "discord" | "signal" | "imessage"; type ProviderKey =
| "whatsapp"
| "telegram"
| "discord"
| "slack"
| "signal"
| "imessage";
function providerEnabled(key: ProviderKey, props: ConnectionsProps) { function providerEnabled(key: ProviderKey, props: ConnectionsProps) {
const snapshot = props.snapshot; const snapshot = props.snapshot;
@@ -151,6 +175,8 @@ function providerEnabled(key: ProviderKey, props: ConnectionsProps) {
return snapshot.telegram.configured || snapshot.telegram.running; return snapshot.telegram.configured || snapshot.telegram.running;
case "discord": case "discord":
return Boolean(snapshot.discord?.configured || snapshot.discord?.running); return Boolean(snapshot.discord?.configured || snapshot.discord?.running);
case "slack":
return Boolean(snapshot.slack?.configured || snapshot.slack?.running);
case "signal": case "signal":
return Boolean(snapshot.signal?.configured || snapshot.signal?.running); return Boolean(snapshot.signal?.configured || snapshot.signal?.running);
case "imessage": case "imessage":
@@ -167,6 +193,7 @@ function renderProvider(
whatsapp?: ProvidersStatusSnapshot["whatsapp"]; whatsapp?: ProvidersStatusSnapshot["whatsapp"];
telegram?: ProvidersStatusSnapshot["telegram"]; telegram?: ProvidersStatusSnapshot["telegram"];
discord?: ProvidersStatusSnapshot["discord"] | null; discord?: ProvidersStatusSnapshot["discord"] | null;
slack?: ProvidersStatusSnapshot["slack"] | null;
signal?: ProvidersStatusSnapshot["signal"] | null; signal?: ProvidersStatusSnapshot["signal"] | null;
imessage?: ProvidersStatusSnapshot["imessage"] | null; imessage?: ProvidersStatusSnapshot["imessage"] | null;
}, },
@@ -949,6 +976,389 @@ function renderProvider(
</div> </div>
`; `;
} }
case "slack": {
const slack = data.slack;
const botName = slack?.probe?.bot?.name;
const teamName = slack?.probe?.team?.name;
return html`
<div class="card">
<div class="card-title">Slack</div>
<div class="card-sub">Socket mode status and bot details.</div>
<div class="status-list" style="margin-top: 16px;">
<div>
<span class="label">Configured</span>
<span>${slack?.configured ? "Yes" : "No"}</span>
</div>
<div>
<span class="label">Running</span>
<span>${slack?.running ? "Yes" : "No"}</span>
</div>
<div>
<span class="label">Bot</span>
<span>${botName ? botName : "n/a"}</span>
</div>
<div>
<span class="label">Team</span>
<span>${teamName ? teamName : "n/a"}</span>
</div>
<div>
<span class="label">Last start</span>
<span>${slack?.lastStartAt ? formatAgo(slack.lastStartAt) : "n/a"}</span>
</div>
<div>
<span class="label">Last probe</span>
<span>${slack?.lastProbeAt ? formatAgo(slack.lastProbeAt) : "n/a"}</span>
</div>
</div>
${slack?.lastError
? html`<div class="callout danger" style="margin-top: 12px;">
${slack.lastError}
</div>`
: nothing}
${slack?.probe
? html`<div class="callout" style="margin-top: 12px;">
Probe ${slack.probe.ok ? "ok" : "failed"} ·
${slack.probe.status ?? ""}
${slack.probe.error ?? ""}
</div>`
: nothing}
<div class="form-grid" style="margin-top: 16px;">
<label class="field">
<span>Enabled</span>
<select
.value=${props.slackForm.enabled ? "yes" : "no"}
@change=${(e: Event) =>
props.onSlackChange({
enabled: (e.target as HTMLSelectElement).value === "yes",
})}
>
<option value="yes">Yes</option>
<option value="no">No</option>
</select>
</label>
<label class="field">
<span>Bot token</span>
<input
type="password"
.value=${props.slackForm.botToken}
?disabled=${props.slackTokenLocked}
@input=${(e: Event) =>
props.onSlackChange({
botToken: (e.target as HTMLInputElement).value,
})}
/>
</label>
<label class="field">
<span>App token</span>
<input
type="password"
.value=${props.slackForm.appToken}
?disabled=${props.slackAppTokenLocked}
@input=${(e: Event) =>
props.onSlackChange({
appToken: (e.target as HTMLInputElement).value,
})}
/>
</label>
<label class="field">
<span>DMs enabled</span>
<select
.value=${props.slackForm.dmEnabled ? "yes" : "no"}
@change=${(e: Event) =>
props.onSlackChange({
dmEnabled: (e.target as HTMLSelectElement).value === "yes",
})}
>
<option value="yes">Enabled</option>
<option value="no">Disabled</option>
</select>
</label>
<label class="field">
<span>Allow DMs from</span>
<input
.value=${props.slackForm.allowFrom}
@input=${(e: Event) =>
props.onSlackChange({
allowFrom: (e.target as HTMLInputElement).value,
})}
placeholder="U123, U456, *"
/>
</label>
<label class="field">
<span>Group DMs enabled</span>
<select
.value=${props.slackForm.groupEnabled ? "yes" : "no"}
@change=${(e: Event) =>
props.onSlackChange({
groupEnabled: (e.target as HTMLSelectElement).value === "yes",
})}
>
<option value="yes">Enabled</option>
<option value="no">Disabled</option>
</select>
</label>
<label class="field">
<span>Group DM channels</span>
<input
.value=${props.slackForm.groupChannels}
@input=${(e: Event) =>
props.onSlackChange({
groupChannels: (e.target as HTMLInputElement).value,
})}
placeholder="G123, #team"
/>
</label>
<label class="field">
<span>Reaction notifications</span>
<select
.value=${props.slackForm.reactionNotifications}
@change=${(e: Event) =>
props.onSlackChange({
reactionNotifications: (e.target as HTMLSelectElement)
.value as "off" | "own" | "all" | "allowlist",
})}
>
<option value="off">Off</option>
<option value="own">Own</option>
<option value="all">All</option>
<option value="allowlist">Allowlist</option>
</select>
</label>
<label class="field">
<span>Reaction allowlist</span>
<input
.value=${props.slackForm.reactionAllowlist}
@input=${(e: Event) =>
props.onSlackChange({
reactionAllowlist: (e.target as HTMLInputElement).value,
})}
placeholder="U123, U456"
/>
</label>
<label class="field">
<span>Text chunk limit</span>
<input
.value=${props.slackForm.textChunkLimit}
@input=${(e: Event) =>
props.onSlackChange({
textChunkLimit: (e.target as HTMLInputElement).value,
})}
placeholder="4000"
/>
</label>
<label class="field">
<span>Media max (MB)</span>
<input
.value=${props.slackForm.mediaMaxMb}
@input=${(e: Event) =>
props.onSlackChange({
mediaMaxMb: (e.target as HTMLInputElement).value,
})}
placeholder="20"
/>
</label>
</div>
<div class="card-sub" style="margin-top: 16px;">Slash command</div>
<div class="form-grid" style="margin-top: 8px;">
<label class="field">
<span>Slash enabled</span>
<select
.value=${props.slackForm.slashEnabled ? "yes" : "no"}
@change=${(e: Event) =>
props.onSlackChange({
slashEnabled: (e.target as HTMLSelectElement).value === "yes",
})}
>
<option value="yes">Enabled</option>
<option value="no">Disabled</option>
</select>
</label>
<label class="field">
<span>Slash name</span>
<input
.value=${props.slackForm.slashName}
@input=${(e: Event) =>
props.onSlackChange({
slashName: (e.target as HTMLInputElement).value,
})}
placeholder="clawd"
/>
</label>
<label class="field">
<span>Slash session prefix</span>
<input
.value=${props.slackForm.slashSessionPrefix}
@input=${(e: Event) =>
props.onSlackChange({
slashSessionPrefix: (e.target as HTMLInputElement).value,
})}
placeholder="slack:slash"
/>
</label>
<label class="field">
<span>Slash ephemeral</span>
<select
.value=${props.slackForm.slashEphemeral ? "yes" : "no"}
@change=${(e: Event) =>
props.onSlackChange({
slashEphemeral: (e.target as HTMLSelectElement).value === "yes",
})}
>
<option value="yes">Yes</option>
<option value="no">No</option>
</select>
</label>
</div>
<div class="card-sub" style="margin-top: 16px;">Channels</div>
<div class="card-sub">
Add channel ids or #names and optionally require mentions.
</div>
<div class="list">
${props.slackForm.channels.map(
(channel, channelIndex) => html`
<div class="list-item">
<div class="list-main">
<div class="form-grid">
<label class="field">
<span>Channel id / name</span>
<input
.value=${channel.key}
@input=${(e: Event) => {
const next = [...props.slackForm.channels];
next[channelIndex] = {
...next[channelIndex],
key: (e.target as HTMLInputElement).value,
};
props.onSlackChange({ channels: next });
}}
/>
</label>
<label class="field">
<span>Allow</span>
<select
.value=${channel.allow ? "yes" : "no"}
@change=${(e: Event) => {
const next = [...props.slackForm.channels];
next[channelIndex] = {
...next[channelIndex],
allow:
(e.target as HTMLSelectElement).value === "yes",
};
props.onSlackChange({ channels: next });
}}
>
<option value="yes">Yes</option>
<option value="no">No</option>
</select>
</label>
<label class="field">
<span>Require mention</span>
<select
.value=${channel.requireMention ? "yes" : "no"}
@change=${(e: Event) => {
const next = [...props.slackForm.channels];
next[channelIndex] = {
...next[channelIndex],
requireMention:
(e.target as HTMLSelectElement).value === "yes",
};
props.onSlackChange({ channels: next });
}}
>
<option value="yes">Yes</option>
<option value="no">No</option>
</select>
</label>
<label class="field">
<span>&nbsp;</span>
<button
class="btn"
@click=${() => {
const next = [...props.slackForm.channels];
next.splice(channelIndex, 1);
props.onSlackChange({ channels: next });
}}
>
Remove
</button>
</label>
</div>
</div>
</div>
`,
)}
</div>
<button
class="btn"
style="margin-top: 8px;"
@click=${() =>
props.onSlackChange({
channels: [
...props.slackForm.channels,
{ key: "", allow: true, requireMention: false },
],
})}
>
Add channel
</button>
<div class="card-sub" style="margin-top: 16px;">Tool actions</div>
<div class="form-grid" style="margin-top: 8px;">
${slackActionOptions.map(
(action) => html`<label class="field">
<span>${action.label}</span>
<select
.value=${props.slackForm.actions[action.key] ? "yes" : "no"}
@change=${(e: Event) =>
props.onSlackChange({
actions: {
...props.slackForm.actions,
[action.key]: (e.target as HTMLSelectElement).value === "yes",
},
})}
>
<option value="yes">Enabled</option>
<option value="no">Disabled</option>
</select>
</label>`,
)}
</div>
${props.slackTokenLocked || props.slackAppTokenLocked
? html`<div class="callout" style="margin-top: 12px;">
${props.slackTokenLocked ? "SLACK_BOT_TOKEN " : ""}
${props.slackAppTokenLocked ? "SLACK_APP_TOKEN " : ""}
is set in the environment. Config edits will not override it.
</div>`
: nothing}
${props.slackStatus
? html`<div class="callout" style="margin-top: 12px;">
${props.slackStatus}
</div>`
: nothing}
<div class="row" style="margin-top: 14px;">
<button
class="btn primary"
?disabled=${props.slackSaving}
@click=${() => props.onSlackSave()}
>
${props.slackSaving ? "Saving…" : "Save"}
</button>
<button class="btn" @click=${() => props.onRefresh(true)}>
Probe
</button>
</div>
</div>
`;
}
case "signal": { case "signal": {
const signal = data.signal; const signal = data.signal;
return html` return html`
@@ -1355,4 +1765,3 @@ function renderProvider(
return nothing; return nothing;
} }
} }