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
66 changes: 66 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Release

on:
push:
tags:
- 'v*'

permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0

- uses: actions/setup-node@v6
with:
node-version: '22'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Verify tag matches package.json version
run: |
tag="${GITHUB_REF#refs/tags/v}"
pkg="$(node -p 'require("./package.json").version')"
if [ "$tag" != "$pkg" ]; then
echo "Tag v$tag does not match package.json version $pkg" >&2
exit 1
fi

- name: Lint and type-check
run: npx tsc --noEmit

- name: Test
run: npm test

- name: Build
run: npm run build

- name: Package release tarball
id: package
run: |
tag="${GITHUB_REF#refs/tags/}"
staging="maestro-discord-${tag}"
mkdir -p "$staging"
cp -R dist "$staging/"
cp -R bin templates "$staging/"
cp package.json package-lock.json .env.example README.md "$staging/"
cp LICENSE "$staging/" 2>/dev/null || true
tar -czf "${staging}.tar.gz" "$staging"
sha256sum "${staging}.tar.gz" > "${staging}.tar.gz.sha256"
echo "tarball=${staging}.tar.gz" >> "$GITHUB_OUTPUT"
echo "checksum=${staging}.tar.gz.sha256" >> "$GITHUB_OUTPUT"

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
files: |
${{ steps.package.outputs.tarball }}
${{ steps.package.outputs.checksum }}
99 changes: 62 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,51 +13,47 @@ A Discord bot that connects your server to [Maestro](https://runmaestro.ai) AI a

## Prerequisites

- Node.js 18+
- Linux or macOS
- Node.js 22+
- A Discord application + bot token
- [Maestro CLI](https://docs.runmaestro.ai/cli) available on your `PATH` (no authentication required)
- [Maestro CLI](https://docs.runmaestro.ai/cli) on your `PATH`

### Install maestro-discord CLI
## Install (production)

The `maestro-discord` CLI lets your Maestro agents reach out to you on Discord — for example, to ping you when a long-running task finishes. See [docs/api.md](docs/api.md) for usage.

After building the project (`npm run build`), create a shell wrapper.

macOS / Linux:
One command — downloads the latest tagged release, installs dependencies, prompts for Discord credentials, and registers a user-level service.

```bash
printf '#!/bin/bash\nnode "%s/dist/cli/maestro-discord.js" "$@"\n' "$(pwd)" | sudo tee /usr/local/bin/maestro-discord && sudo chmod +x /usr/local/bin/maestro-discord
curl -fsSL https://raw.githubusercontent.com/RunMaestro/Maestro-Discord/main/install.sh | bash
```

Windows (PowerShell) — writes the wrapper to `%USERPROFILE%\bin` and adds it to your user `PATH`:
After install:

```powershell
$repoPath = (Get-Location).Path
$binDir = "$env:USERPROFILE\bin"
New-Item -ItemType Directory -Force -Path $binDir | Out-Null
@"
@echo off
node "$repoPath\dist\cli\maestro-discord.js" %*
"@ | Out-File -FilePath "$binDir\maestro-discord.cmd" -Encoding ASCII

# Add $binDir to user PATH if it isn't already (restart your shell afterwards)
$userPath = [Environment]::GetEnvironmentVariable('PATH', 'User')
if (-not ($userPath -split ';' -contains $binDir)) {
[Environment]::SetEnvironmentVariable('PATH', "$binDir;$userPath", 'User')
}
```bash
maestro-discord-ctl start # boot the bot
maestro-discord-ctl logs # tail logs
maestro-discord-ctl status # service status
maestro-discord-ctl update # upgrade to latest release (preserves config)
maestro-discord-ctl uninstall # remove install + service files
```

Or use `npm link`:
Defaults:

```bash
npm link
```
| Path | Purpose |
| ----------------------------- | ---------------------------------------- |
| `~/.local/share/maestro-discord/` | Installed bot (built JS + dependencies) |
| `~/.config/maestro-discord/.env` | Configuration (preserved across updates) |
| `~/.local/bin/maestro-discord-ctl` | Service control wrapper |
| systemd user / launchd agent | Auto-start unit |

Override any of these with `MAESTRO_DISCORD_HOME`, `XDG_CONFIG_HOME`, or `MAESTRO_DISCORD_BIN_DIR`. Pin a specific version with `MAESTRO_DISCORD_VERSION=v1.0.0`.

## Quick start
## Install (development from source)

1. Install dependencies:
1. Clone and install:

```bash
git clone https://github.com/RunMaestro/Maestro-Discord.git
cd Maestro-Discord
npm install
```

Expand Down Expand Up @@ -93,6 +89,42 @@ npm run deploy-commands
npm run dev
```

### Install maestro-discord CLI (dev)

The `maestro-discord` CLI lets your Maestro agents reach out to you on Discord — for example, to ping you when a long-running task finishes. See [docs/api.md](docs/api.md) for usage.

After building the project (`npm run build`), create a shell wrapper.

macOS / Linux:

```bash
printf '#!/bin/bash\nnode "%s/dist/cli/maestro-discord.js" "$@"\n' "$(pwd)" | sudo tee /usr/local/bin/maestro-discord && sudo chmod +x /usr/local/bin/maestro-discord
```

Windows (PowerShell) — writes the wrapper to `%USERPROFILE%\bin` and adds it to your user `PATH`:

```powershell
$repoPath = (Get-Location).Path
$binDir = "$env:USERPROFILE\bin"
New-Item -ItemType Directory -Force -Path $binDir | Out-Null
@"
@echo off
node "$repoPath\dist\cli\maestro-discord.js" %*
"@ | Out-File -FilePath "$binDir\maestro-discord.cmd" -Encoding ASCII

# Add $binDir to user PATH if it isn't already (restart your shell afterwards)
$userPath = [Environment]::GetEnvironmentVariable('PATH', 'User')
if (-not ($userPath -split ';' -contains $binDir)) {
[Environment]::SetEnvironmentVariable('PATH', "$binDir;$userPath", 'User')
}
```

Or use `npm link`:

```bash
npm link
```

## Voice Transcription (optional)

When a user posts a Discord **voice message** (the mic-button recording, not an arbitrary `.ogg` upload) in a session thread, the bot transcribes the audio with `whisper.cpp` and forwards the transcript to the agent. The original `.ogg` is **not** sent to the agent — only the transcribed text — and a `🎧` reaction marks the message while transcription runs.
Expand Down Expand Up @@ -132,13 +164,6 @@ WHISPER_MODEL_PATH=models/ggml-base.en.bin

The bot probes these at startup; any missing piece is logged as `⚠️ Transcription disabled: …` and transcription is skipped at runtime.

## Production run

```bash
npm run build
npm start
```

## Tests

```bash
Expand Down
177 changes: 177 additions & 0 deletions bin/maestro-discord-ctl.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
#!/usr/bin/env bash
# Service wrapper for the Maestro Discord bot.
# Subcommands: start | stop | restart | status | logs | deploy | update | uninstall | version

set -euo pipefail

INSTALL_DIR="${MAESTRO_DISCORD_HOME:-$HOME/.local/share/maestro-discord}"
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/maestro-discord"
BIN_DIR="${MAESTRO_DISCORD_BIN_DIR:-$HOME/.local/bin}"
REPO="${MAESTRO_DISCORD_REPO:-RunMaestro/Maestro-Discord}"
SERVICE_NAME="maestro-discord"
LAUNCHD_LABEL="sh.maestro.discord"
LAUNCHD_PLIST="$HOME/Library/LaunchAgents/${LAUNCHD_LABEL}.plist"
Comment thread
coderabbitai[bot] marked this conversation as resolved.

die() { printf '✗ %s\n' "$*" >&2; exit 1; }
info() { printf '==> %s\n' "$*"; }

detect_os() {
case "$(uname -s)" in
Linux) echo linux ;;
Darwin) echo macos ;;
*) echo unsupported ;;
esac
}

usage() {
cat <<'EOF'
maestro-discord-ctl — control the Maestro Discord bot service.

Usage:
maestro-discord-ctl <command>

Commands:
start Start the bot service
stop Stop the bot service
restart Restart the bot service
status Show service status
logs Tail service logs (Ctrl+C to stop)
deploy Deploy slash commands to Discord
update Reinstall the latest release (preserves config)
uninstall Remove the bot, service files, and CLI symlink
version Print installed version

Environment:
MAESTRO_DISCORD_HOME Override install dir (default: ~/.local/share/maestro-discord)
XDG_CONFIG_HOME Config dir parent (default: ~/.config)
EOF
}

require_install() {
[ -d "$INSTALL_DIR" ] || die "Not installed at $INSTALL_DIR. Run install.sh first."
}

cmd_start() {
require_install
case "$(detect_os)" in
linux)
systemctl --user start "$SERVICE_NAME"
info "Started $SERVICE_NAME (systemd user)"
;;
macos)
[ -f "$LAUNCHD_PLIST" ] || die "Plist not installed: $LAUNCHD_PLIST"
launchctl load -w "$LAUNCHD_PLIST" 2>/dev/null || launchctl start "$LAUNCHD_LABEL"
info "Started $LAUNCHD_LABEL (launchd)"
;;
*) die "Unsupported OS for service management" ;;
esac
}

cmd_stop() {
case "$(detect_os)" in
linux)
systemctl --user stop "$SERVICE_NAME" || true
info "Stopped $SERVICE_NAME"
;;
macos)
launchctl unload -w "$LAUNCHD_PLIST" 2>/dev/null || launchctl stop "$LAUNCHD_LABEL" || true
info "Stopped $LAUNCHD_LABEL"
;;
*) die "Unsupported OS for service management" ;;
esac
}

cmd_restart() {
cmd_stop || true
cmd_start
}

cmd_status() {
case "$(detect_os)" in
linux) systemctl --user status "$SERVICE_NAME" --no-pager || true ;;
macos) launchctl list | grep -F "$LAUNCHD_LABEL" || echo "(not loaded)" ;;
*) die "Unsupported OS for service management" ;;
esac
}

cmd_logs() {
case "$(detect_os)" in
linux) journalctl --user -u "$SERVICE_NAME" -f --no-pager ;;
macos)
local log_file="$INSTALL_DIR/logs/maestro-discord.log"
mkdir -p "$INSTALL_DIR/logs"
[ -f "$log_file" ] || touch "$log_file"
tail -f "$log_file"
;;
*) die "Unsupported OS for log tailing" ;;
esac
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

cmd_deploy() {
require_install
[ -f "$INSTALL_DIR/.env" ] || die "Config missing: $INSTALL_DIR/.env"
(cd "$INSTALL_DIR" && node dist/deploy-commands.js)
}

cmd_update() {
info "Re-running installer to pull the latest release"
local tag config_parent
tag="$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" | sed -nE 's/.*"tag_name"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' | head -n1)"
[ -n "$tag" ] || die "Could not resolve latest release tag"
config_parent="${CONFIG_DIR%/maestro-discord}"
curl -fsSL "https://raw.githubusercontent.com/${REPO}/${tag}/install.sh" \
| env \
MAESTRO_DISCORD_HOME="$INSTALL_DIR" \
MAESTRO_DISCORD_BIN_DIR="$BIN_DIR" \
MAESTRO_DISCORD_REPO="$REPO" \
XDG_CONFIG_HOME="$config_parent" \
bash
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

cmd_uninstall() {
read -r -p "Remove $INSTALL_DIR, service files, and CLI symlink? [y/N] " ans
case "${ans:-n}" in
y|Y|yes|YES) ;;
*) info "Aborted"; exit 0 ;;
esac
cmd_stop || true
case "$(detect_os)" in
linux)
systemctl --user disable --now "$SERVICE_NAME" 2>/dev/null || true
rm -f "${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/${SERVICE_NAME}.service"
systemctl --user daemon-reload || true
systemctl --user reset-failed "$SERVICE_NAME" 2>/dev/null || true
;;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
macos) rm -f "$LAUNCHD_PLIST" ;;
esac
rm -rf "$INSTALL_DIR"
rm -f "$BIN_DIR/maestro-discord-ctl"
info "Uninstalled. Config preserved at $CONFIG_DIR (delete manually if desired)."
}

cmd_version() {
if [ -f "$INSTALL_DIR/.version" ]; then
cat "$INSTALL_DIR/.version"
else
die "No version file at $INSTALL_DIR/.version"
fi
}

main() {
local sub="${1:-}"
case "$sub" in
start) cmd_start ;;
stop) cmd_stop ;;
restart) cmd_restart ;;
status) cmd_status ;;
logs) cmd_logs ;;
deploy) cmd_deploy ;;
update) cmd_update ;;
uninstall) cmd_uninstall ;;
version) cmd_version ;;
-h|--help|help|"") usage ;;
*) usage; exit 2 ;;
esac
}

main "$@"
Loading