add tmux session detection and spiceflow links

- Detect tmux session name when running inside tmux
- Add spiceflow-url config option for session links in notifications
- Include clickable "Open in Spiceflow" link in Discord messages
- Add `iamwaiting status` command to show full configuration
- Separate toggle status from configuration display
- Update installation docs to use symlink instead of bbin

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-21 12:13:31 -05:00
parent bec42359e0
commit bfda9c6e31
2 changed files with 81 additions and 20 deletions
+20 -8
View File
@@ -16,9 +16,10 @@ The tool integrates with Claude Code's hook system to send real-time notificatio
The entire implementation is a single Babashka script (`iamwaiting`) with these key functions: The entire implementation is a single Babashka script (`iamwaiting`) with these key functions:
- `load-config` - Loads webhook URL and toggles from config file or environment variable - `load-config` - Loads webhook URL, toggles, and Spiceflow URL from config file or environment variables
- `send-discord-webhook` - Makes HTTP POST request to Discord webhook API - `send-discord-webhook` - Makes HTTP POST request to Discord webhook API
- `format-waiting-message` - Formats the notification message with project context - `get-tmux-session` - Detects current tmux session name (if running inside tmux)
- `format-waiting-message` - Formats the notification message with project context, tmux session, and Spiceflow link
- `setup-config` - Interactive setup wizard for webhook configuration - `setup-config` - Interactive setup wizard for webhook configuration
- `test-webhook` - Sends a test message to verify configuration - `test-webhook` - Sends a test message to verify configuration
- `send-waiting-notification` - Main function that sends the notification (respects toggles) - `send-waiting-notification` - Main function that sends the notification (respects toggles)
@@ -33,6 +34,7 @@ Configuration is stored in `~/.iamwaiting/config.edn` with the following structu
```clojure ```clojure
{:webhook-url "https://discord.com/api/webhooks/..." {:webhook-url "https://discord.com/api/webhooks/..."
:user-id "123456789012345678" ; Optional: Discord user ID for @mentions :user-id "123456789012345678" ; Optional: Discord user ID for @mentions
:spiceflow-url "https://hostname:5173" ; Optional: Spiceflow URL for session links
:toggles {:idle-prompt true ; Enable idle prompt notifications :toggles {:idle-prompt true ; Enable idle prompt notifications
:permission-prompt true ; Enable permission prompt notifications :permission-prompt true ; Enable permission prompt notifications
:permission-prompt-ping true}} ; Enable @mentions on permission prompts :permission-prompt-ping true}} ; Enable @mentions on permission prompts
@@ -41,6 +43,7 @@ Configuration is stored in `~/.iamwaiting/config.edn` with the following structu
Alternatively, configuration can be set via environment variables: Alternatively, configuration can be set via environment variables:
- `IAMWAITING_WEBHOOK_URL` - Discord webhook URL (required) - `IAMWAITING_WEBHOOK_URL` - Discord webhook URL (required)
- `IAMWAITING_USER_ID` - Discord user ID for @mentions (optional) - `IAMWAITING_USER_ID` - Discord user ID for @mentions (optional)
- `IAMWAITING_SPICEFLOW_URL` - Spiceflow base URL for session links (optional)
### Notification Toggles ### Notification Toggles
@@ -96,13 +99,13 @@ The `:permission-prompt-ping` toggle controls whether you get @mentioned (pinged
## Installation ## Installation
Install via bbin (recommended): Install via symlink (recommended):
```bash ```bash
bbin install git@git.ajet.fyi:ajet-industries/iamwaiting.git ln -s /path/to/repos/iamwaiting/iamwaiting ~/.local/bin/iamwaiting
``` ```
This installs `iamwaiting` to `~/.local/bin/iamwaiting` (ensure `~/.local/bin` is in your PATH). This creates a symlink at `~/.local/bin/iamwaiting` pointing to the script (ensure `~/.local/bin` is in your PATH). Using a symlink means changes to the script are immediately available without reinstalling.
## Common Commands ## Common Commands
@@ -116,6 +119,9 @@ iamwaiting setup
# Test the webhook configuration # Test the webhook configuration
iamwaiting test iamwaiting test
# Show current configuration
iamwaiting status
``` ```
### Usage ### Usage
@@ -198,13 +204,17 @@ The tool uses Discord's webhook API which requires:
Notifications include: Notifications include:
- ⏳ Waiting indicator - ⏳ Waiting indicator
- 📁 Current working directory / project name - 📁 Current working directory / project name
- 🖥️ Tmux session name (if running inside tmux)
- 🔗 Spiceflow session link (if Spiceflow URL configured and running in tmux)
- 🕐 Timestamp (HH:mm:ss format) - 🕐 Timestamp (HH:mm:ss format)
- @mention (if user ID is configured, it's a permission prompt, and ping toggle is enabled) - @mention (if user ID is configured, it's a permission prompt, and ping toggle is enabled)
Example message (idle prompt): Example message (with Spiceflow link):
``` ```
⏳ **Claude is waiting** in `ajet-industries` ⏳ **Claude is waiting** in `ajet-industries`
📁 Path: `/home/user/repos/ajet-industries` 📁 Path: `/home/user/repos/ajet-industries`
🖥️ Tmux: `spiceflow-bold-sun-9841`
🔗 **[Open in Spiceflow](https://hostname:5173/session/spiceflow-bold-sun-9841)**
🕐 Time: 14:23:45 🕐 Time: 14:23:45
``` ```
@@ -212,16 +222,17 @@ Example message (permission prompt with ping enabled):
``` ```
<@123456789012345678> ⏳ **Claude is waiting** in `ajet-industries` <@123456789012345678> ⏳ **Claude is waiting** in `ajet-industries`
📁 Path: `/home/user/repos/ajet-industries` 📁 Path: `/home/user/repos/ajet-industries`
🖥️ Tmux: `spiceflow-bold-sun-9841`
🔗 **[Open in Spiceflow](https://hostname:5173/session/spiceflow-bold-sun-9841)**
🕐 Time: 14:23:45 🕐 Time: 14:23:45
📢 Type: `permission_prompt` 📢 Type: `permission_prompt`
``` ```
Example message (permission prompt with ping disabled): Example message (without tmux/Spiceflow):
``` ```
⏳ **Claude is waiting** in `ajet-industries` ⏳ **Claude is waiting** in `ajet-industries`
📁 Path: `/home/user/repos/ajet-industries` 📁 Path: `/home/user/repos/ajet-industries`
🕐 Time: 14:23:45 🕐 Time: 14:23:45
📢 Type: `permission_prompt`
``` ```
### Error Handling ### Error Handling
@@ -236,6 +247,7 @@ Example message (permission prompt with ping disabled):
Uses Babashka standard libraries: Uses Babashka standard libraries:
- `babashka.http-client` - HTTP requests to Discord API - `babashka.http-client` - HTTP requests to Discord API
- `babashka.fs` - File system operations for config management - `babashka.fs` - File system operations for config management
- `babashka.process` - Shell command execution (for tmux session detection)
- `cheshire.core` - JSON encoding/decoding - `cheshire.core` - JSON encoding/decoding
- `clojure.string` - String manipulation - `clojure.string` - String manipulation
+61 -12
View File
@@ -3,6 +3,7 @@
(ns iamwaiting.core (ns iamwaiting.core
(:require [babashka.http-client :as http] (:require [babashka.http-client :as http]
[babashka.fs :as fs] [babashka.fs :as fs]
[babashka.process :as process]
[cheshire.core :as json] [cheshire.core :as json]
[clojure.string :as str])) [clojure.string :as str]))
@@ -16,12 +17,15 @@
(System/getenv "IAMWAITING_WEBHOOK_URL")) (System/getenv "IAMWAITING_WEBHOOK_URL"))
user-id (or (:user-id config) user-id (or (:user-id config)
(System/getenv "IAMWAITING_USER_ID")) (System/getenv "IAMWAITING_USER_ID"))
spiceflow-url (or (:spiceflow-url config)
(System/getenv "IAMWAITING_SPICEFLOW_URL"))
toggles (or (:toggles config) toggles (or (:toggles config)
{:idle-prompt true {:idle-prompt true
:permission-prompt true :permission-prompt true
:permission-prompt-ping true})] :permission-prompt-ping true})]
{:webhook-url webhook-url {:webhook-url webhook-url
:user-id user-id :user-id user-id
:spiceflow-url spiceflow-url
:toggles toggles})) :toggles toggles}))
(defn send-discord-webhook [webhook-url message] (defn send-discord-webhook [webhook-url message]
@@ -39,9 +43,20 @@
(catch Exception e (catch Exception e
{:success false :error (.getMessage e)}))) {:success false :error (.getMessage e)})))
(defn format-waiting-message [event-data user-id toggles] (defn get-tmux-session []
"Get the current tmux session name if running inside tmux"
(when (System/getenv "TMUX")
(try
(let [result (process/shell {:out :string :err :string}
"tmux" "display-message" "-p" "#S")]
(when (zero? (:exit result))
(str/trim (:out result))))
(catch Exception _ nil))))
(defn format-waiting-message [event-data user-id toggles spiceflow-url]
"Format a message for Claude waiting event" "Format a message for Claude waiting event"
(let [cwd (or (:cwd event-data) (System/getProperty "user.dir")) (let [cwd (or (:cwd event-data) (System/getProperty "user.dir"))
tmux-session (get-tmux-session)
project-name (fs/file-name cwd) project-name (fs/file-name cwd)
timestamp (java.time.LocalDateTime/now) timestamp (java.time.LocalDateTime/now)
formatted-time (.format timestamp formatted-time (.format timestamp
@@ -58,10 +73,15 @@
;; Ping user if configured, it's a permission prompt, and ping toggle is enabled ;; Ping user if configured, it's a permission prompt, and ping toggle is enabled
ping-enabled? (get toggles :permission-prompt-ping true) ping-enabled? (get toggles :permission-prompt-ping true)
user-ping (when (and user-id is-permission-prompt? ping-enabled?) user-ping (when (and user-id is-permission-prompt? ping-enabled?)
(str "<@" user-id "> "))] (str "<@" user-id "> "))
;; Build Spiceflow session link if both URL and tmux session are available
spiceflow-link (when (and spiceflow-url tmux-session)
(str spiceflow-url "/session/" tmux-session))]
(str user-ping (str user-ping
"⏳ **Claude is waiting** in `" project-name "`\n" "⏳ **Claude is waiting** in `" project-name "`\n"
"📁 Path: `" cwd "`\n" "📁 Path: `" cwd "`\n"
(when tmux-session (str "🖥️ Tmux: `" tmux-session "`\n"))
(when spiceflow-link (str "🔗 **[Open in Spiceflow](" spiceflow-link ")**\n"))
"🕐 Time: " formatted-time "\n" "🕐 Time: " formatted-time "\n"
(when session-id (str "🔑 Session: `" session-id "`\n")) (when session-id (str "🔑 Session: `" session-id "`\n"))
(when notification-type (str "📢 Type: `" notification-type "`\n")) (when notification-type (str "📢 Type: `" notification-type "`\n"))
@@ -89,7 +109,16 @@
(let [user-id-input (str/trim (read-line)) (let [user-id-input (str/trim (read-line))
user-id (when-not (str/blank? user-id-input) user-id-input)] user-id (when-not (str/blank? user-id-input) user-id-input)]
;; NEW: Toggle prompts (println "\nEnter your Spiceflow URL (optional, for session links in notifications):")
(println "(e.g., https://framework-desktop:5173)\n")
(print "> ")
(flush)
(let [spiceflow-url-input (str/trim (read-line))
spiceflow-url (when-not (str/blank? spiceflow-url-input)
;; Remove trailing slash if present
(str/replace spiceflow-url-input #"/$" ""))]
;; Toggle prompts
(println "\n📬 Notification Preferences") (println "\n📬 Notification Preferences")
(print "Enable notifications for idle prompts? (y/n, default: y): ") (print "Enable notifications for idle prompts? (y/n, default: y): ")
(flush) (flush)
@@ -115,7 +144,8 @@
:toggles {:idle-prompt idle-enabled? :toggles {:idle-prompt idle-enabled?
:permission-prompt perm-enabled? :permission-prompt perm-enabled?
:permission-prompt-ping ping-enabled?}} :permission-prompt-ping ping-enabled?}}
user-id (assoc :user-id user-id))] user-id (assoc :user-id user-id)
spiceflow-url (assoc :spiceflow-url spiceflow-url))]
;; Create config directory ;; Create config directory
(fs/create-dirs (fs/parent config-file)) (fs/create-dirs (fs/parent config-file))
@@ -126,9 +156,11 @@
(when user-id (when user-id
(println (str "✓ User ID configured - you will be @mentioned on permission prompts: " (println (str "✓ User ID configured - you will be @mentioned on permission prompts: "
(if ping-enabled? "enabled" "disabled")))) (if ping-enabled? "enabled" "disabled"))))
(when spiceflow-url
(println (str "✓ Spiceflow URL: " spiceflow-url)))
(println (str "✓ Idle prompts: " (if idle-enabled? "enabled" "disabled"))) (println (str "✓ Idle prompts: " (if idle-enabled? "enabled" "disabled")))
(println (str "✓ Permission prompts: " (if perm-enabled? "enabled" "disabled"))) (println (str "✓ Permission prompts: " (if perm-enabled? "enabled" "disabled")))
(println "\nTest the webhook with: ./iamwaiting test")))))) (println "\nTest the webhook with: ./iamwaiting test")))))))
(defn test-webhook [] (defn test-webhook []
"Test the webhook configuration" "Test the webhook configuration"
@@ -190,7 +222,7 @@
(println (str "Notification disabled for " toggle-key))) (println (str "Notification disabled for " toggle-key)))
;; Original notification logic ;; Original notification logic
(let [message (format-waiting-message event-data (:user-id config) (:toggles config)) (let [message (format-waiting-message event-data (:user-id config) (:toggles config) (:spiceflow-url config))
result (send-discord-webhook (:webhook-url config) message)] result (send-discord-webhook (:webhook-url config) message)]
(if (:success result) (if (:success result)
(println "✓ Notification sent") (println "✓ Notification sent")
@@ -234,14 +266,29 @@
toggles (or (:toggles config) toggles (or (:toggles config)
{:idle-prompt true {:idle-prompt true
:permission-prompt true :permission-prompt true
:permission-prompt-ping true}) :permission-prompt-ping true})]
user-id (:user-id config)] (println "📊 Toggle Status\n")
(println "📊 Current Toggle Status\n")
(println (str "Idle prompts: " (if (:idle-prompt toggles) "✅ enabled" "❌ disabled"))) (println (str "Idle prompts: " (if (:idle-prompt toggles) "✅ enabled" "❌ disabled")))
(println (str "Permission prompts: " (if (:permission-prompt toggles) "✅ enabled" "❌ disabled"))) (println (str "Permission prompts: " (if (:permission-prompt toggles) "✅ enabled" "❌ disabled")))
(println (str "Permission ping: " (if (:permission-prompt-ping toggles) "✅ enabled" "❌ disabled"))) (println (str "Permission ping: " (if (:permission-prompt-ping toggles) "✅ enabled" "❌ disabled")))))
(when user-id
(println (str "\n👤 User ID configured: " user-id))))) (defn show-config []
"Display current configuration"
(let [config (load-config)
toggles (or (:toggles config)
{:idle-prompt true
:permission-prompt true
:permission-prompt-ping true})
user-id (:user-id config)
spiceflow-url (:spiceflow-url config)]
(println "⚙️ Configuration\n")
(println (str "Webhook URL: " (if (:webhook-url config) "✅ configured" "❌ not configured")))
(println (str "User ID: " (or user-id "not configured")))
(println (str "Spiceflow URL: " (or spiceflow-url "not configured")))
(println "\n📊 Toggles\n")
(println (str "Idle prompts: " (if (:idle-prompt toggles) "✅ enabled" "❌ disabled")))
(println (str "Permission prompts: " (if (:permission-prompt toggles) "✅ enabled" "❌ disabled")))
(println (str "Permission ping: " (if (:permission-prompt-ping toggles) "✅ enabled" "❌ disabled")))))
(defn show-help [] (defn show-help []
(println "iamwaiting - Send Discord notifications when Claude is waiting") (println "iamwaiting - Send Discord notifications when Claude is waiting")
@@ -249,6 +296,7 @@
(println "Usage:") (println "Usage:")
(println " iamwaiting setup Set up Discord webhook configuration") (println " iamwaiting setup Set up Discord webhook configuration")
(println " iamwaiting test Test webhook configuration") (println " iamwaiting test Test webhook configuration")
(println " iamwaiting status Show current configuration")
(println " iamwaiting toggle status Show current toggle status") (println " iamwaiting toggle status Show current toggle status")
(println " iamwaiting toggle idle-prompt <on|off> Toggle idle prompt notifications") (println " iamwaiting toggle idle-prompt <on|off> Toggle idle prompt notifications")
(println " iamwaiting toggle permission-prompt <on|off> Toggle permission prompt notifications") (println " iamwaiting toggle permission-prompt <on|off> Toggle permission prompt notifications")
@@ -273,6 +321,7 @@
(case command (case command
"setup" (setup-config) "setup" (setup-config)
"test" (test-webhook) "test" (test-webhook)
"status" (show-config)
"toggle" (let [subcommand (second args)] "toggle" (let [subcommand (second args)]
(case subcommand (case subcommand
"status" (show-toggle-status) "status" (show-toggle-status)