293 lines
13 KiB
Clojure
Executable File
293 lines
13 KiB
Clojure
Executable File
#!/usr/bin/env bb
|
|
|
|
(ns iamwaiting.core
|
|
(:require [babashka.http-client :as http]
|
|
[babashka.fs :as fs]
|
|
[cheshire.core :as json]
|
|
[clojure.string :as str]))
|
|
|
|
(def config-file (str (System/getenv "HOME") "/.iamwaiting/config.edn"))
|
|
|
|
(defn load-config []
|
|
"Load configuration from config file or environment variables"
|
|
(let [config (when (fs/exists? config-file)
|
|
(read-string (slurp config-file)))
|
|
webhook-url (or (:webhook-url config)
|
|
(System/getenv "IAMWAITING_WEBHOOK_URL"))
|
|
user-id (or (:user-id config)
|
|
(System/getenv "IAMWAITING_USER_ID"))
|
|
toggles (or (:toggles config)
|
|
{:idle-prompt true
|
|
:permission-prompt true
|
|
:permission-prompt-ping true})]
|
|
{:webhook-url webhook-url
|
|
:user-id user-id
|
|
:toggles toggles}))
|
|
|
|
(defn send-discord-webhook [webhook-url message]
|
|
"Send a message to Discord via webhook"
|
|
(try
|
|
(let [payload {:content message
|
|
:username "Claude Code"
|
|
:avatar_url "https://www.anthropic.com/images/icons/apple-touch-icon.png"}
|
|
response (http/post webhook-url
|
|
{:headers {"Content-Type" "application/json"}
|
|
:body (json/generate-string payload)})]
|
|
(if (< (:status response) 300)
|
|
{:success true}
|
|
{:success false :error (str "HTTP " (:status response))}))
|
|
(catch Exception e
|
|
{:success false :error (.getMessage e)})))
|
|
|
|
(defn format-waiting-message [event-data user-id toggles]
|
|
"Format a message for Claude waiting event"
|
|
(let [cwd (or (:cwd event-data) (System/getProperty "user.dir"))
|
|
project-name (fs/file-name cwd)
|
|
timestamp (java.time.LocalDateTime/now)
|
|
formatted-time (.format timestamp
|
|
(java.time.format.DateTimeFormatter/ofPattern "HH:mm:ss"))
|
|
session-id (:session_id event-data)
|
|
notification-type (:notification_type event-data)
|
|
permission-mode (:permission_mode event-data)
|
|
hook-message (:message event-data)
|
|
transcript-path (:transcript_path event-data)
|
|
transcript-file (when transcript-path (fs/file-name transcript-path))
|
|
;; Check if this is a permission prompt
|
|
is-permission-prompt? (or (= notification-type "permission_prompt")
|
|
(some? permission-mode))
|
|
;; Ping user if configured, it's a permission prompt, and ping toggle is enabled
|
|
ping-enabled? (get toggles :permission-prompt-ping true)
|
|
user-ping (when (and user-id is-permission-prompt? ping-enabled?)
|
|
(str "<@" user-id "> "))]
|
|
(str user-ping
|
|
"⏳ **Claude is waiting** in `" project-name "`\n"
|
|
"📁 Path: `" cwd "`\n"
|
|
"🕐 Time: " formatted-time "\n"
|
|
(when session-id (str "🔑 Session: `" session-id "`\n"))
|
|
(when notification-type (str "📢 Type: `" notification-type "`\n"))
|
|
(when permission-mode (str "🔐 Mode: `" permission-mode "`\n"))
|
|
(when transcript-file (str "📝 Transcript: `" transcript-file "`\n"))
|
|
(when hook-message (str "\n> " hook-message)))))
|
|
|
|
(defn setup-config []
|
|
"Interactive setup for webhook configuration"
|
|
(println "iamwaiting setup")
|
|
(println "================\n")
|
|
(println "Enter your Discord webhook URL:")
|
|
(println "(Get this from Discord: Server Settings > Integrations > Webhooks)\n")
|
|
(print "> ")
|
|
(flush)
|
|
(let [webhook-url (str/trim (read-line))]
|
|
(when (str/blank? webhook-url)
|
|
(println "Error: webhook URL cannot be empty")
|
|
(System/exit 1))
|
|
|
|
(println "\nEnter your Discord user ID (optional, for @mentions on permission prompts):")
|
|
(println "(Get this from Discord: User Settings > Advanced > Developer Mode, then right-click your name)\n")
|
|
(print "> ")
|
|
(flush)
|
|
(let [user-id-input (str/trim (read-line))
|
|
user-id (when-not (str/blank? user-id-input) user-id-input)]
|
|
|
|
;; NEW: Toggle prompts
|
|
(println "\n📬 Notification Preferences")
|
|
(print "Enable notifications for idle prompts? (y/n, default: y): ")
|
|
(flush)
|
|
(let [idle-response (str/lower-case (str/trim (read-line)))
|
|
idle-enabled? (not= idle-response "n")]
|
|
|
|
(print "Enable notifications for permission prompts? (y/n, default: y): ")
|
|
(flush)
|
|
(let [perm-response (str/lower-case (str/trim (read-line)))
|
|
perm-enabled? (not= perm-response "n")
|
|
|
|
;; Ask about ping preference if user ID provided
|
|
ping-enabled? (if user-id
|
|
(do
|
|
(print "@mention you on permission prompts? (y/n, default: y): ")
|
|
(flush)
|
|
(let [ping-response (str/lower-case (str/trim (read-line)))]
|
|
(not= ping-response "n")))
|
|
true)
|
|
|
|
;; Build config with toggles
|
|
config (cond-> {:webhook-url webhook-url
|
|
:toggles {:idle-prompt idle-enabled?
|
|
:permission-prompt perm-enabled?
|
|
:permission-prompt-ping ping-enabled?}}
|
|
user-id (assoc :user-id user-id))]
|
|
|
|
;; Create config directory
|
|
(fs/create-dirs (fs/parent config-file))
|
|
|
|
;; Write config
|
|
(spit config-file (pr-str config))
|
|
(println "\n✓ Configuration saved to" config-file)
|
|
(when user-id
|
|
(println (str "✓ User ID configured - you will be @mentioned on permission prompts: "
|
|
(if ping-enabled? "enabled" "disabled"))))
|
|
(println (str "✓ Idle prompts: " (if idle-enabled? "enabled" "disabled")))
|
|
(println (str "✓ Permission prompts: " (if perm-enabled? "enabled" "disabled")))
|
|
(println "\nTest the webhook with: ./iamwaiting test"))))))
|
|
|
|
(defn test-webhook []
|
|
"Test the webhook configuration"
|
|
(let [config (load-config)]
|
|
(if-not (:webhook-url config)
|
|
(do
|
|
(println "Error: No webhook URL configured")
|
|
(println "Run: ./iamwaiting setup")
|
|
(System/exit 1))
|
|
(do
|
|
(println "Sending test message...")
|
|
(let [result (send-discord-webhook
|
|
(:webhook-url config)
|
|
"🧪 **Test message from iamwaiting**\n\nIf you see this, your webhook is configured correctly!")]
|
|
(if (:success result)
|
|
(println "✓ Test message sent successfully!")
|
|
(do
|
|
(println "✗ Failed to send message:" (:error result))
|
|
(System/exit 1))))))))
|
|
|
|
(defn read-hook-data []
|
|
"Read hook data from stdin (JSON format)"
|
|
(try
|
|
(let [stdin (slurp *in*)]
|
|
(when-not (str/blank? stdin)
|
|
(json/parse-string stdin true)))
|
|
(catch Exception e
|
|
(binding [*out* *err*]
|
|
(println "Warning: Failed to parse stdin JSON:" (.getMessage e)))
|
|
{})))
|
|
|
|
(defn send-waiting-notification [& event-data-args]
|
|
"Send a waiting notification to Discord"
|
|
(let [config (load-config)]
|
|
(if-not (:webhook-url config)
|
|
(do
|
|
(println "Error: No webhook URL configured")
|
|
(println "Run: ./iamwaiting setup")
|
|
(System/exit 1))
|
|
(let [;; Read from stdin first (hook data), fallback to command-line args
|
|
event-data (or (read-hook-data)
|
|
(when (seq event-data-args)
|
|
(try
|
|
(json/parse-string (first event-data-args) true)
|
|
(catch Exception _ {})))
|
|
{})
|
|
;; Determine notification type
|
|
notification-type (:notification_type event-data)
|
|
permission-mode (:permission_mode event-data)
|
|
is-permission-prompt? (or (= notification-type "permission_prompt")
|
|
(some? permission-mode))
|
|
;; Determine which toggle to check
|
|
toggle-key (if is-permission-prompt? :permission-prompt :idle-prompt)
|
|
enabled? (get-in config [:toggles toggle-key] true)]
|
|
|
|
;; Exit early if notification type is disabled
|
|
(if-not enabled?
|
|
(when (System/getenv "DEBUG")
|
|
(println (str "Notification disabled for " toggle-key)))
|
|
|
|
;; Original notification logic
|
|
(let [message (format-waiting-message event-data (:user-id config) (:toggles config))
|
|
result (send-discord-webhook (:webhook-url config) message)]
|
|
(if (:success result)
|
|
(println "✓ Notification sent")
|
|
(do
|
|
(println "✗ Failed to send notification:" (:error result))
|
|
(System/exit 1)))))))))
|
|
|
|
(defn toggle-feature [feature action]
|
|
"Toggle a notification feature on or off"
|
|
(let [config (load-config)
|
|
feature-key (case feature
|
|
"idle-prompt" :idle-prompt
|
|
"permission-prompt" :permission-prompt
|
|
"permission-prompt-ping" :permission-prompt-ping
|
|
nil)
|
|
action-bool (case action
|
|
"on" true
|
|
"off" false
|
|
nil)]
|
|
|
|
(cond
|
|
(nil? feature-key)
|
|
(do
|
|
(println "❌ Invalid feature. Use: idle-prompt | permission-prompt | permission-prompt-ping")
|
|
(System/exit 1))
|
|
|
|
(nil? action-bool)
|
|
(do
|
|
(println "❌ Invalid action. Use: on | off")
|
|
(System/exit 1))
|
|
|
|
:else
|
|
(let [updated-config (assoc-in config [:toggles feature-key] action-bool)]
|
|
(fs/create-dirs (fs/parent config-file))
|
|
(spit config-file (pr-str updated-config))
|
|
(println (str "✅ " feature " notifications: " action))))))
|
|
|
|
(defn show-toggle-status []
|
|
"Display current toggle status"
|
|
(let [config (load-config)
|
|
toggles (or (:toggles config)
|
|
{:idle-prompt true
|
|
:permission-prompt true
|
|
:permission-prompt-ping true})
|
|
user-id (:user-id config)]
|
|
(println "📊 Current Toggle Status\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")))
|
|
(when user-id
|
|
(println (str "\n👤 User ID configured: " user-id)))))
|
|
|
|
(defn show-help []
|
|
(println "iamwaiting - Send Discord notifications when Claude is waiting")
|
|
(println "")
|
|
(println "Usage:")
|
|
(println " iamwaiting setup Set up Discord webhook configuration")
|
|
(println " iamwaiting test Test webhook configuration")
|
|
(println " iamwaiting toggle status Show current toggle status")
|
|
(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-ping <on|off> Toggle @mentions on permission prompts")
|
|
(println " iamwaiting [event-data-json] Send waiting notification")
|
|
(println "")
|
|
(println "Configuration:")
|
|
(println " Config file: ~/.iamwaiting/config.edn")
|
|
(println " Environment: IAMWAITING_WEBHOOK_URL")
|
|
(println "")
|
|
(println "Hook Integration:")
|
|
(println " Add to ~/.claude/hooks.edn:")
|
|
(println " {:agent-waiting-for-user {:command [\"iamwaiting\"]}}")
|
|
(System/exit 0))
|
|
|
|
;; CLI entry point
|
|
(defn -main [& args]
|
|
(if (empty? args)
|
|
(send-waiting-notification)
|
|
(let [command (first args)
|
|
rest-args (rest args)]
|
|
(case command
|
|
"setup" (setup-config)
|
|
"test" (test-webhook)
|
|
"toggle" (let [subcommand (second args)]
|
|
(case subcommand
|
|
"status" (show-toggle-status)
|
|
"idle-prompt" (toggle-feature "idle-prompt" (nth args 2 nil))
|
|
"permission-prompt" (toggle-feature "permission-prompt" (nth args 2 nil))
|
|
"permission-prompt-ping" (toggle-feature "permission-prompt-ping" (nth args 2 nil))
|
|
(do
|
|
(println "Usage: iamwaiting toggle <status|idle-prompt|permission-prompt|permission-prompt-ping> [on|off]")
|
|
(System/exit 1))))
|
|
"help" (show-help)
|
|
"--help" (show-help)
|
|
"-h" (show-help)
|
|
;; Default: treat first arg as event data JSON
|
|
(apply send-waiting-notification args)))))
|
|
|
|
(when (= *file* (System/getProperty "babashka.file"))
|
|
(apply -main *command-line-args*))
|