Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 29 additions & 12 deletions modules/slack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,35 @@ Because classification spends tokens, the bridge is bounded on every axis:

## Setup

1. Create a Slack app (https://api.slack.com/apps).
- **Socket Mode:** on. Generate an app-level token with `connections:write`
→ `SLACK_APP_TOKEN` (`xapp-…`).
- **Bot token scopes:** `chat:write`, `app_mentions:read`, `im:history`,
`channels:read` → install to workspace → `SLACK_BOT_TOKEN` (`xoxb-…`).
- **Event Subscriptions:** subscribe to bot events `app_mention` and
`message.im`.
- Invite the bot to the channel(s) you want it to watch / post in.
2. Fill in the Slack section of `config.env` (see `config.example.env`).
3. `./setup.sh server <project-dir>` (or `exe`). This installs the bridge to
`~/scripts/slack/`, writes `~/scripts/slack/.env`, and enables the
`ty-slack` systemd service.
1. **Create the app from the manifest.** https://api.slack.com/apps → *Create
New App* → *From an app manifest* → pick your workspace → paste
[`manifest.yaml`](manifest.yaml) → *Create*. (Pre-sets Socket Mode, the bot
scopes, and the event subscriptions — no manual toggles.)
2. **Mint two tokens** (a manifest can't):
- *Basic Information → App-Level Tokens → Generate* with `connections:write`
→ `xapp-…` (`SLACK_APP_TOKEN`).
- *OAuth & Permissions → Install to Workspace* → copy the **Bot User OAuth
Token** `xoxb-…` (`SLACK_BOT_TOKEN`).
3. **Run the wizard** for your GM:
```bash
./setup.sh slack ~/Projects/gms/<gm>
```
It takes the two tokens, resolves your Slack user from your email (via the
bot token), writes the `SLACK_*` block into that GM's `config.env`, and
installs + enables the bridge (systemd on a server/exe.dev; rendered files on
a local Mac). Leave the app token blank for **outbound-only** (pings, no
inbound control).
4. **Invite the bot** to your notify channel in Slack: `/invite @taskyou`.

A few minutes, no hand-editing config or hunting for member IDs.

<details>
<summary>Manual setup (without the wizard)</summary>

Do steps 1–2, then put the `SLACK_*` block (see `config.example.env`) into the
GM's `config.env` yourself and run `./setup.sh server <gm>` (or `exe`).
`SLACK_ALLOWED_USERS` takes Slack **member IDs** (profile → More → Copy member ID).
</details>

## Config

Expand Down
40 changes: 40 additions & 0 deletions modules/slack/manifest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# TaskYou Slack app manifest.
# Create the app in one step: https://api.slack.com/apps → Create New App →
# "From an app manifest" → pick your workspace → paste this file → Create.
#
# This pre-configures Socket Mode, the bot scopes, and the event subscriptions
# the bridge needs. Two things a manifest can't mint for you, so do them after:
# 1. Basic Information → App-Level Tokens → Generate (scope: connections:write)
# → this is your SLACK_APP_TOKEN (xapp-…).
# 2. OAuth & Permissions → Install to Workspace → copy the Bot User OAuth
# Token → this is your SLACK_BOT_TOKEN (xoxb-…).
# Then run: ./setup.sh slack <your-gm-dir>
display_information:
name: TaskYou
description: Manage TaskYou tasks from Slack
background_color: "#1a1a1a"
features:
bot_user:
display_name: taskyou
always_online: true
app_home:
home_tab_enabled: false
messages_tab_enabled: true
messages_tab_read_only_enabled: false
oauth_config:
scopes:
bot:
- chat:write
- app_mentions:read
- im:history
- channels:read
settings:
event_subscriptions:
bot_events:
- app_mention
- message.im
interactivity:
is_enabled: false
org_deploy_enabled: false
socket_mode_enabled: true
token_rotation_enabled: false
183 changes: 156 additions & 27 deletions setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ usage() {
echo " server — Provision the remote server only"
echo " exe — Deploy GM to an exe.dev VM"
echo " all — Both local and server"
echo " slack — Guided Slack setup wizard for an existing GM"
echo ""
echo "The project-dir must contain a config.env file."
echo "See config.example.env for all available variables."
Expand Down Expand Up @@ -195,8 +196,8 @@ MODE="$1"
PROJECT_DIR="$(cd "$2" && pwd 2>/dev/null || echo "$2")"
CONFIG_FILE="$PROJECT_DIR/config.env"

if [[ "$MODE" != "local" && "$MODE" != "server" && "$MODE" != "exe" && "$MODE" != "all" ]]; then
echo "Error: mode must be local, server, exe, or all"
if [[ "$MODE" != "local" && "$MODE" != "server" && "$MODE" != "exe" && "$MODE" != "all" && "$MODE" != "slack" ]]; then
echo "Error: mode must be local, server, exe, all, or slack"
usage
fi

Expand Down Expand Up @@ -593,6 +594,155 @@ EOF
fi
}

# Install the Slack bridge when the daemon is on THIS machine (local/macOS):
# render the script + .env; no systemd (operator starts it or adds a launchd
# agent). $1 = home dir.
setup_slack_local() {
local home_dir="$1"

log "Setting up Slack integration (local)"
local slack_dir="$home_dir/scripts/slack"
mkdir -p "$slack_dir"
cp "$MODULES_DIR/slack/slack-bridge.mjs" "$slack_dir/slack-bridge.mjs"
chmod +x "$slack_dir/slack-bridge.mjs"

local project_map_json="$SLACK_PROJECT_MAP"
[[ -z "$project_map_json" ]] && project_map_json="{}"
local default_project
default_project=$(echo "$PROJECTS" | cut -d',' -f1 | xargs)
cat > "$slack_dir/.env" <<EOF
SLACK_BOT_TOKEN=$SLACK_BOT_TOKEN
SLACK_APP_TOKEN=$SLACK_APP_TOKEN
SLACK_NOTIFY_CHANNEL=$SLACK_NOTIFY_CHANNEL
SLACK_ALLOWED_USERS=$SLACK_ALLOWED_USERS
SLACK_PROJECT_MAP=$project_map_json
DEFAULT_PROJECT=$default_project
TY_PATH=$(command -v ty)
NOTIFICATIONS_FILE=$home_dir/notifications.jsonl
ANTHROPIC_API_KEY=$SLACK_ANTHROPIC_API_KEY
ANTHROPIC_MODEL=$SLACK_CLASSIFIER_MODEL
EOF
chmod 600 "$slack_dir/.env"
ok "slack-bridge installed: $slack_dir"
warn "Local mode: start it with 'node $slack_dir/slack-bridge.mjs' (or a launchd agent)."
}

# ── Slack setup wizard (./setup.sh slack <project-dir>) ───────────────────────

# Prompt for a value. Default shown is the current value (from config on a
# re-run) so pressing enter keeps it and typing a new value updates it. A
# WIZ_<VAR> env var pre-answers non-interactively (for tests/scripts) without
# colliding with the sourced config; a closed stdin keeps the current/default.
ask() {
local __var="$1" __prompt="$2"
local __override="WIZ_${__var}"
if [[ -n "${!__override:-}" ]]; then printf -v "$__var" '%s' "${!__override}"; return; fi
local __default="${!__var:-${3:-}}" __val
if [[ -t 0 ]]; then
read -r -p " $__prompt${__default:+ [$__default]}: " __val || true
printf -v "$__var" '%s' "${__val:-$__default}"
else
printf -v "$__var" '%s' "$__default"
fi
}

# Resolve a Slack email → user id via the bot token; pass through a bare U… id.
resolve_slack_user() {
local token="$1" who="$2"
[[ -z "$who" ]] && { echo ""; return; }
if [[ "$who" != *@* ]]; then echo "$who"; return; fi
local resp
resp=$(curl -sS -G --data-urlencode "email=$who" \
-H "Authorization: Bearer $token" \
"https://slack.com/api/users.lookupByEmail" 2>/dev/null || true)
echo "$resp" | python3 -c 'import sys,json
try:
d=json.load(sys.stdin); print(d.get("user",{}).get("id","") if d.get("ok") else "")
except Exception:
print("")' 2>/dev/null || echo ""
}

# Rewrite the SLACK_* block in the GM's config.env (idempotent).
write_slack_config() {
local tmp="$CONFIG_FILE.slacktmp"
# Strip any prior managed block (vars + our comment marker) so re-runs replace
# rather than accumulate.
grep -vE '^(export )?SLACK_(ENABLED|BOT_TOKEN|APP_TOKEN|NOTIFY_CHANNEL|ALLOWED_USERS|PROJECT_MAP)=|^# === Slack integration \(managed by' "$CONFIG_FILE" > "$tmp" || true
{
echo ""
echo "# === Slack integration (managed by 'setup.sh slack') ==="
echo "SLACK_ENABLED=\"true\""
echo "SLACK_BOT_TOKEN=\"$SLACK_BOT_TOKEN\""
echo "SLACK_APP_TOKEN=\"$SLACK_APP_TOKEN\""
echo "SLACK_NOTIFY_CHANNEL=\"$SLACK_NOTIFY_CHANNEL\""
echo "SLACK_ALLOWED_USERS=\"$SLACK_ALLOWED_USERS\""
echo "SLACK_PROJECT_MAP='$SLACK_PROJECT_MAP'"
} >> "$tmp"
mv "$tmp" "$CONFIG_FILE"
ok "wrote Slack config to $CONFIG_FILE"
}

setup_slack_wizard() {
echo ""
log "Slack setup wizard for $PROJECT_DISPLAY_NAME"
echo " 1. Create the app: https://api.slack.com/apps → Create New App →"
echo " From an app manifest → paste modules/slack/manifest.yaml → Create."
echo " 2. Basic Information → App-Level Tokens → Generate (scope connections:write)"
echo " → copy the xapp-… token. Then OAuth & Permissions → Install to Workspace"
echo " → copy the Bot User OAuth Token xoxb-…."
echo " (Leave the app token blank for outbound-only — pings, no inbound control.)"
echo ""

ask SLACK_BOT_TOKEN "Bot User OAuth Token (xoxb-…)"
ask SLACK_APP_TOKEN "App-Level Token (xapp-…, blank = outbound-only)"
ask SLACK_NOTIFY_CHANNEL "Channel for task pings" "#taskyou"
ask SLACK_OWNER "Your Slack email or member ID (to allow-list)"
ask SLACK_PROJECT_MAP "Channel→project map JSON (optional)" "{}"

if [[ -z "$SLACK_BOT_TOKEN" ]]; then
echo "Error: a bot token is required."
exit 1
fi

# Verify the bot token and resolve the allow-listed user.
local auth
auth=$(curl -sS -X POST -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
"https://slack.com/api/auth.test" 2>/dev/null || true)
if echo "$auth" | grep -q '"ok":true'; then
local team botname
team=$(echo "$auth" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("team",""))' 2>/dev/null || echo "")
botname=$(echo "$auth" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("user",""))' 2>/dev/null || echo "")
ok "bot @$botname authenticated in ${team:-workspace}"
else
warn "couldn't verify the bot token (auth.test). Continuing — fix it in $CONFIG_FILE if Slack errors."
fi

SLACK_ALLOWED_USERS=$(resolve_slack_user "$SLACK_BOT_TOKEN" "$SLACK_OWNER")
if [[ -z "$SLACK_ALLOWED_USERS" ]]; then
warn "couldn't resolve '$SLACK_OWNER' to a Slack user id — set SLACK_ALLOWED_USERS in $CONFIG_FILE before anyone can drive ty."
else
ok "allow-listed: $SLACK_ALLOWED_USERS"
fi

write_slack_config
export SLACK_ENABLED="true" SLACK_BOT_TOKEN SLACK_APP_TOKEN SLACK_NOTIFY_CHANNEL \
SLACK_ALLOWED_USERS SLACK_PROJECT_MAP

# Install on whichever target this GM uses.
if is_local_server; then
export SERVER_HOME="${SERVER_HOME:-$HOME}"
setup_slack_local "$SERVER_HOME"
elif [[ "$EXE_DEV_ENABLED" == "true" && -n "$EXE_DEV_VM_NAME" ]]; then
setup_slack_remote "exedev@${EXE_DEV_VM_NAME}.exe.xyz" "/home/exedev"
else
setup_slack_remote "$SERVER_HOST" "$SERVER_HOME"
fi

echo ""
ok "Slack wired up. Final step: invite the bot in Slack → /invite @<botname> in $SLACK_NOTIFY_CHANNEL"
echo " Then try: @<botname> what's on the board?"
}

# ── Daemon systemd service ────────────────────────────────────────────────────

install_daemon_service() {
Expand Down Expand Up @@ -717,31 +867,7 @@ setup_server_local() {
# Slack module (local mode: render files + .env; no systemd on macOS, so the
# operator starts it — directly or via a launchd agent).
if [[ "$SLACK_ENABLED" == "true" ]]; then
log "Setting up Slack integration (local)"
local slack_dir="$home_dir/scripts/slack"
mkdir -p "$slack_dir"
cp "$MODULES_DIR/slack/slack-bridge.mjs" "$slack_dir/slack-bridge.mjs"
chmod +x "$slack_dir/slack-bridge.mjs"

local project_map_json="$SLACK_PROJECT_MAP"
[[ -z "$project_map_json" ]] && project_map_json="{}"
local default_project
default_project=$(echo "$PROJECTS" | cut -d',' -f1 | xargs)
cat > "$slack_dir/.env" <<EOF
SLACK_BOT_TOKEN=$SLACK_BOT_TOKEN
SLACK_APP_TOKEN=$SLACK_APP_TOKEN
SLACK_NOTIFY_CHANNEL=$SLACK_NOTIFY_CHANNEL
SLACK_ALLOWED_USERS=$SLACK_ALLOWED_USERS
SLACK_PROJECT_MAP=$project_map_json
DEFAULT_PROJECT=$default_project
TY_PATH=$(command -v ty)
NOTIFICATIONS_FILE=$home_dir/notifications.jsonl
ANTHROPIC_API_KEY=$SLACK_ANTHROPIC_API_KEY
ANTHROPIC_MODEL=$SLACK_CLASSIFIER_MODEL
EOF
chmod 600 "$slack_dir/.env"
ok "slack-bridge installed: $slack_dir"
warn "Local mode: start it with 'node $slack_dir/slack-bridge.mjs' (or a launchd agent)."
setup_slack_local "$home_dir"
fi

log "Local server setup complete (daemon assumed running on this machine)"
Expand Down Expand Up @@ -1353,4 +1479,7 @@ case "$MODE" in
setup_server
print_checklist
;;
slack)
setup_slack_wizard
;;
esac
Loading