commit 38fc49ddcc07022b48d316378872cd5f83125d4c Author: Adam Jeniski Date: Mon Jan 5 18:38:20 2026 -0500 init commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..24d7c81 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Babashka +.cpcache/ +.nrepl-port + +# Editor files +.DS_Store +*.swp +*.swo +*~ + +# IDE +.idea/ +.vscode/ +*.iml + +# Local config (shouldn't be committed) +config.edn diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f9550e0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,160 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +`iamwaiting` is a Babashka CLI tool that sends Discord webhook notifications when Claude Code is waiting for user input. It's designed to be used as a hook in Claude Code to notify users on Discord when their attention is needed. + +## Architecture + +### Core Concept + +The tool integrates with Claude Code's hook system to send real-time notifications to Discord when Claude is waiting for user input. This allows users to be notified on Discord when they need to return to their terminal/editor. + +### Code Structure + +The entire implementation is a single Babashka script (`iamwaiting`) with these key functions: + +- `load-config` - Loads webhook URL from config file or environment variable +- `send-discord-webhook` - Makes HTTP POST request to Discord webhook API +- `format-waiting-message` - Formats the notification message with project context +- `setup-config` - Interactive setup wizard for webhook configuration +- `test-webhook` - Sends a test message to verify configuration +- `send-waiting-notification` - Main function that sends the notification +- `-main` - CLI argument parsing and entry point + +### Configuration + +Configuration is stored in `~/.iamwaiting/config.edn` with the following structure: + +```clojure +{:webhook-url "https://discord.com/api/webhooks/..."} +``` + +Alternatively, the webhook URL can be set via the `IAMWAITING_WEBHOOK_URL` environment variable. + +## Common Commands + +### Setup + +```bash +# Initial setup - configure Discord webhook +iamwaiting setup + +# Test the webhook configuration +iamwaiting test +``` + +### Usage + +```bash +# Send a basic waiting notification +iamwaiting + +# Send notification with event data (used by hooks) +iamwaiting '{"cwd": "/path/to/project"}' +``` + +### Running via Babashka Tasks + +```bash +bb setup # Run setup wizard +bb test # Test webhook +bb run # Send notification +``` + +## Hook Integration + +To use with Claude Code hooks, add to `~/.claude/settings.json`: + +```json +{ + "hooks": { + "Notification": [ + { + "matcher": "idle_prompt", + "hooks": [{"type": "command", "command": "iamwaiting"}] + }, + { + "matcher": "permission_prompt", + "hooks": [{"type": "command", "command": "iamwaiting"}] + } + ] + } +} +``` + +This configuration triggers notifications for both idle prompts and permission requests. + +## Important Implementation Details + +### Discord Webhook API + +The tool uses Discord's webhook API which requires: +- Webhook URL from Discord (Server Settings > Integrations > Webhooks) +- POST request with JSON payload containing `content`, `username`, and `avatar_url` +- No authentication required (webhook URL acts as the credential) + +### Message Format + +Notifications include: +- โณ Waiting indicator +- ๐Ÿ“ Current working directory / project name +- ๐Ÿ• Timestamp (HH:mm:ss format) + +Example message: +``` +โณ **Claude is waiting** in `ajet-industries` +๐Ÿ“ Path: `/home/user/repos/ajet-industries` +๐Ÿ• Time: 14:23:45 +``` + +### Error Handling + +- Missing configuration prompts user to run `./iamwaiting setup` +- Failed webhook requests exit with code 1 and display error message +- Invalid webhook URLs are caught and reported +- Network errors are caught and reported gracefully + +### Dependencies + +Uses Babashka standard libraries: +- `babashka.http-client` - HTTP requests to Discord API +- `babashka.fs` - File system operations for config management +- `cheshire.core` - JSON encoding/decoding +- `clojure.string` - String manipulation + +## Development + +The script is self-contained with minimal dependencies. To modify: + +1. Edit the `iamwaiting` script directly +2. Test changes using `iamwaiting test` +3. The script is executable via shebang `#!/usr/bin/env bb` + +## Security Considerations + +- Webhook URL should be kept secret (it allows posting to your Discord channel) +- Config file at `~/.iamwaiting/config.edn` has standard file permissions +- No sensitive data is sent in notifications beyond directory paths +- Webhook URLs are never logged or displayed (except during setup) + +## Context: Monorepo Usage + +This tool is part of the ajet-industries monorepo and can be used with any project. It's particularly useful when working on long-running tasks where Claude may need user input while the user is away from their terminal. + +Other tools in the monorepo: +- `commitly/` - Multi-repo commit and push tool +- `service-manager/` - Microservice orchestration +- `www/` - Dashboard website +- `gateway/` - Nginx reverse proxy + +## Future Enhancements + +Possible improvements: +- Support for multiple notification channels +- Customizable message templates +- Integration with other notification services (Slack, Telegram, etc.) +- Retry logic for failed webhook requests +- Rate limiting to avoid spam diff --git a/README.md b/README.md new file mode 100644 index 0000000..59128c1 --- /dev/null +++ b/README.md @@ -0,0 +1,148 @@ +# iamwaiting + +Send Discord notifications when Claude Code is waiting for your input. + +## What is this? + +`iamwaiting` is a lightweight CLI tool that integrates with Claude Code's hook system to send you Discord notifications when Claude needs your attention. Perfect for long-running tasks where you step away from your terminal. + +## Quick Start + +### 1. Install Dependencies + +Requires [Babashka](https://babashka.org/): + +```bash +# macOS +brew install borkdude/brew/babashka + +# Linux +bash <(curl -s https://raw.githubusercontent.com/babashka/babashka/master/install) +``` + +### 2. Set Up Discord Webhook + +```bash +./iamwaiting setup +``` + +Follow the prompts to enter your Discord webhook URL. Get a webhook URL from: +Discord Server โ†’ Server Settings โ†’ Integrations โ†’ Webhooks โ†’ New Webhook + +### 3. Test It + +```bash +./iamwaiting test +``` + +You should see a test message in your Discord channel! + +### 4. Configure Claude Code Hook + +Add to `~/.claude/hooks.edn`: + +```clojure +{:agent-waiting-for-user {:command ["/full/path/to/iamwaiting/iamwaiting"]}} +``` + +Or if `iamwaiting` is in your PATH: + +```clojure +{:agent-waiting-for-user {:command ["iamwaiting"]}} +``` + +## Usage + +### Manual Testing + +```bash +# Send a test notification +./iamwaiting test + +# Send a waiting notification +./iamwaiting + +# Send notification with custom data +./iamwaiting '{"cwd": "/path/to/project"}' +``` + +### Babashka Tasks + +```bash +bb setup # Run setup wizard +bb test # Send test message +bb run # Send waiting notification +``` + +## Configuration + +Configuration is stored in `~/.iamwaiting/config.edn`: + +```clojure +{:webhook-url "https://discord.com/api/webhooks/YOUR_WEBHOOK_ID/YOUR_WEBHOOK_TOKEN"} +``` + +You can also set the webhook URL via environment variable: + +```bash +export IAMWAITING_WEBHOOK_URL="https://discord.com/api/webhooks/..." +``` + +## How It Works + +When Claude Code is waiting for user input, the `agent-waiting-for-user` hook is triggered, which: + +1. Runs the `iamwaiting` command +2. Reads the Discord webhook URL from config +3. Formats a message with project context (name, path, timestamp) +4. Sends an HTTP POST to Discord's webhook API +5. Returns success/failure status + +The notification appears in your Discord channel with: +- โณ Waiting indicator +- ๐Ÿ“ Project name and path +- ๐Ÿ• Timestamp + +## Example Notification + +``` +โณ **Claude is waiting** in `my-project` +๐Ÿ“ Path: `/home/user/repos/my-project` +๐Ÿ• Time: 14:23:45 +``` + +## Troubleshooting + +**"No webhook URL configured"** +- Run `./iamwaiting setup` to configure your webhook + +**"Failed to send message"** +- Check your webhook URL is correct +- Verify the webhook hasn't been deleted in Discord +- Check your internet connection + +**Hook not triggering** +- Verify `hooks.edn` syntax is correct +- Use full absolute path to the script +- Check Claude Code hooks documentation + +## Security + +- Keep your webhook URL secret (treat it like a password) +- Config file has standard Unix permissions (readable only by you) +- Only directory paths are sent in notifications (no code or sensitive data) + +## Requirements + +- Babashka (bb command) +- Discord webhook URL +- Internet connection + +## License + +Part of the ajet-industries monorepo. + +## Related Tools + +- [commitly](../commitly) - Multi-repo commit and push tool +- [service-manager](../service-manager) - Microservice orchestration platform diff --git a/bb.edn b/bb.edn new file mode 100644 index 0000000..134f605 --- /dev/null +++ b/bb.edn @@ -0,0 +1,10 @@ +{:paths ["."] + :deps {org.babashka/http-client {:mvn/version "0.4.16"} + cheshire/cheshire {:mvn/version "5.12.0"}} + :tasks + {setup {:doc "Set up Discord webhook configuration" + :task (exec 'iamwaiting.core/-main "setup")} + test {:doc "Test webhook configuration" + :task (exec 'iamwaiting.core/-main "test")} + run {:doc "Send waiting notification" + :task (exec 'iamwaiting.core/-main)}}} diff --git a/iamwaiting b/iamwaiting new file mode 100755 index 0000000..659bdae --- /dev/null +++ b/iamwaiting @@ -0,0 +1,163 @@ +#!/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"))] + {:webhook-url webhook-url})) + +(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] + "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))] + (str "โณ **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)) + + ;; Create config directory + (fs/create-dirs (fs/parent config-file)) + + ;; Write config + (spit config-file (pr-str {:webhook-url webhook-url})) + (println "\nโœ“ Configuration saved to" config-file) + (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 _ {}))) + {}) + message (format-waiting-message event-data) + 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 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 [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) + "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*))