init commit
This commit is contained in:
commit
38fc49ddcc
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
@ -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
|
||||||
160
CLAUDE.md
Normal file
160
CLAUDE.md
Normal file
@ -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
|
||||||
148
README.md
Normal file
148
README.md
Normal file
@ -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
|
||||||
10
bb.edn
Normal file
10
bb.edn
Normal file
@ -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)}}}
|
||||||
163
iamwaiting
Executable file
163
iamwaiting
Executable file
@ -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*))
|
||||||
Loading…
x
Reference in New Issue
Block a user