Skill: koba42corp/autoresponder (version 1.0.1)

--- name: imsg-autoresponder description: Monitor iMessage/SMS conversations and auto-respond based on configurable rules, AI prompts, and rate-limiting conditions. Use when you need to automatically reply to specific contacts with AI-generated responses based on conversation context. Also use when the user asks to manage auto-responder settings, contacts, prompts, or view status/history. --- # iMessage Auto-Responder Automatically respond to iMessages/SMS from specific contacts using AI-generated replies that match your voice and conversation context. ## ⚠️ Requirements Checklist Before using this skill, ensure you have: - [ ] **macOS** with Messages.app signed in to iMessage - [ ] **imsg CLI** installed: `brew install steipete/tap/imsg` - [ ] **OpenAI API key** configured in Clawdbot config - [ ] **Full Disk Access** granted to Terminal/iTerm - [ ] **Messages automation permission** (macOS will prompt on first use) ## Features - 🤖 **AI-powered responses** using OpenAI GPT-4 - 📱 **Contact-based prompts** - different AI personality per contact - ⏱️ **Rate limiting** - configurable delays between auto-responses - 💬 **Context-aware** - AI sees recent conversation history - 📊 **Telegram management** - slash commands + natural language - 🔄 **Background monitoring** - continuous polling for new messages - 🔧 **Auto-cleanup** - clears stale locks on restart (prevents stuck contacts) - 🧪 **Test mode** - generate real AI responses without sending - ⏰ **Time windows** - only respond during specific hours (e.g., 9 AM - 10 PM) - 🔑 **Keyword triggers** - only respond if message contains specific keywords (e.g., "urgent", "help") - 📊 **Statistics tracking** - track total responses, daily counts, and averages per contact - 🚦 **Daily cap** - limit max replies per day per contact (safety feature) ## Quick Start ### 1. Add contacts to watch list ```bash cd ~/clawd/imsg-autoresponder/scripts node manage.js add "+15551234567" "Reply with a middle finger emoji" "Best Friend" node manage.js add "+15559876543" "You are my helpful assistant. Reply warmly and briefly, as if I'm responding myself. Keep it under 160 characters." "Mom" ``` ### 2. Start the watcher ```bash node watcher.js ``` The watcher runs in the foreground and logs to `~/clawd/logs/imsg-autoresponder.log`. ### 3. Run in background (recommended) ```bash # Start in background nohup node ~/clawd/imsg-autoresponder/scripts/watcher.js > /dev/null 2>&1 & # Or use screen/tmux screen -S imsg-watcher node ~/clawd/imsg-autoresponder/scripts/watcher.js # Ctrl+A, D to detach ``` ## Configuration Config file: `~/clawd/imsg-autoresponder.json` ```json { "enabled": true, "defaultMinMinutesBetweenReplies": 15, "watchList": [ { "identifier": "+15551234567", "name": "Best Friend", "prompt": "Reply with a middle finger emoji", "minMinutesBetweenReplies": 10, "enabled": true } ] } ``` ## Management via Telegram (Recommended) The auto-responder can be managed directly through Telegram using **slash commands** or **natural language**. ### Slash Commands Both space and underscore formats are supported: ``` /autorespond list OR /autorespond_list /autorespond status OR /autorespond_status /autorespond add OR /autorespond_add <number> <name> <prompt> /autorespond remove OR /autorespond_remove <number> /autorespond edit OR /autorespond_edit <number> <prompt> /autorespond delay OR /autorespond_delay <number> <minutes> /autorespond history OR /autorespond_history <number> /autorespond test OR /autorespond_test <number> <message> /autorespond toggle OR /autorespond_toggle /autorespond restart OR /autorespond_restart Bulk Operations: /autorespond set-all-delays OR /autorespond_set_all_delays <minutes> /autorespond enable-all OR /autorespond_enable_all /autorespond disable-all OR /autorespond_disable_all Time Windows: /autorespond set-time-window OR /autorespond_set_time_window <number> <start> <end> /autorespond clear-time-windows OR /autorespond_clear_time_windows <number> Keyword Triggers: /autorespond add-keyword OR /autorespond_add_keyword <number> <keyword> /autorespond remove-keyword OR /autorespond_remove_keyword <number> <keyword> /autorespond clear-keywords OR /autorespond_clear_keywords <number> Statistics & Limits: /autorespond stats OR /autorespond_stats [<number>] /autorespond set-daily-cap OR /autorespond_set_daily_cap <number> <max> ``` **Examples:** ``` /autorespond_list /autorespond_status /autorespond_edit +15551234567 Be more sarcastic /autorespond_delay +15551234567 30 /autorespond_history +15551234567 /autorespond_set_time_window +15551234567 09:00 22:00 /autorespond_clear_time_windows +15551234567 /autorespond_add_keyword +15551234567 urgent /autorespond_add_keyword +15551234567 help /autorespond_clear_keywords +15551234567 /autorespond_stats /autorespond_stats +15551234567 /autorespond_set_daily_cap +15551234567 10 /autorespond_set_all_delays 30 /autorespond_disable_all /autorespond_restart ``` ### Natural Language You can also just ask naturally: - "Show me the auto-responder status" - "Add +15551234567 to the watch list with prompt: be sarcastic" - "Change Scott's prompt to be nicer" - "Disable auto-replies for Mom" - "What has the auto-responder sent to Foxy recently?" - "Restart the auto-responder" The agent will understand and execute the command using the `telegram-handler.js` script. ## Command-Line Management (Advanced) ```bash cd ~/clawd/imsg-autoresponder/scripts # List all contacts node manage.js list # Add contact node manage.js add "+15551234567" "Your custom prompt here" "Optional Name" # Remove contact node manage.js remove "+15551234567" # Enable/disable contact node manage.js enable "+15551234567" node manage.js disable "+15551234567" # Set custom delay for contact (in minutes) node manage.js set-delay "+15551234567" 30 # Toggle entire system on/off node manage.js toggle ``` ## How It Works 1. **Watcher** monitors all incoming messages via `imsg watch` 2. **Checks watch list** to see if sender is configured for auto-response 3. **Rate limiting** ensures we don't spam (configurable minutes between replies) 4. **Fetches message history** for the conversation (last 20 messages) 5. **Generates AI response** using Clawdbot + the contact's configured prompt 6. **Sends reply** via `imsg send` 7. **Logs everything** to `~/clawd/logs/imsg-autoresponder.log` ## State Tracking Response times are tracked in `~/clawd/data/imsg-autoresponder-state.json`: ```json { "lastResponses": { "+15551234567": 1706453280000 } } ``` This ensures rate limiting works correctly across restarts. ## Prompts Prompts define how the AI should respond to each contact. Be specific! **Examples:** ``` "Reply with a middle finger emoji" "You are my helpful assistant. Reply warmly and briefly, as if I'm responding myself. Keep it under 160 characters." "You are my sarcastic friend. Reply with witty, slightly snarky responses. Keep it short." "Politely decline any requests and say I'm busy. Be brief but friendly." ``` The AI will see: - The contact's custom prompt - Recent message history (last 5 messages) - The latest incoming message ## Requirements - macOS with Messages.app signed in - `imsg` CLI installed (`brew install steipete/tap/imsg`) - Full Disk Access for Terminal - Clawdbot installed and configured - Anthropic API key (configured in `~/.clawdbot/clawdbot.json` or `ANTHROPIC_API_KEY` env var) - `curl` (pre-installed on macOS) ## Safety - **Rate limiting** prevents spam (default: 15 minutes between replies per contact) - **Manual override** via `enabled: false` in config or `node manage.js disable <number>` - **System toggle** to disable all auto-responses: `node manage.js toggle` - **Logs** track all activity for review ## Troubleshooting **Watcher not responding:** - Check `~/clawd/logs/imsg-autoresponder.log` for errors - Verify `imsg watch` works manually: `imsg watch --json` - Ensure contact is in watch list: `node manage.js list` **Rate limited too aggressively:** - Adjust delay: `node manage.js set-delay "+15551234567" 5` - Or edit `defaultMinMinutesBetweenReplies` in config **AI responses are off:** - Refine the prompt for that contact - Check message history is being captured correctly (see logs) ## Agent Command Handling When the user uses slash commands or natural language about the auto-responder, use the `telegram-handler.js` script. ### Command Mapping (Both Formats Supported) | User Input | Normalize To | Handler Call | |------------|--------------|--------------| | `/autorespond list` or `/autorespond_list` | list | `node telegram-handler.js list` | | `/autorespond status` or `/autorespond_status` | status | `node telegram-handler.js status` | | `/autorespond add` or `/autorespond_add <args>` | add | `node telegram-handler.js add <number> <name> <prompt>` | | `/autorespond remove` or `/autorespond_remove <num>` | remove | `node telegram-handler.js remove <number>` | | `/autorespond edit` or `/autorespond_edit <args>` | edit | `node telegram-handler.js edit <number> <prompt>` | | `/autorespond delay` or `/autorespond_delay <args>` | delay | `node telegram-handler.js delay <number> <minutes>` | | `/autorespond history` or `/autorespond_history <num>` | history | `node telegram-handler.js history <number> [limit]` | | `/autorespond test` or `/autorespond_test <num> <msg>` | test | `node telegram-handler.js test <number> <message>` | | `/autorespond toggle` or `/autorespond_toggle` | toggle | `node telegram-handler.js toggle` | | `/autorespond restart` or `/autorespond_restart` | restart | `node telegram-handler.js restart` | | `/autorespond set-all-delays` or `/autorespond_set_all_delays <min>` | set-all-delays | `node telegram-handler.js set-all-delays <minutes>` | | `/autorespond enable-all` or `/autorespond_enable_all` | enable-all | `node telegram-handler.js enable-all` | | `/autorespond disable-all` or `/autorespond_disable_all` | disable-all | `node telegram-handler.js disable-all` | | `/autorespond set-time-window` or `/autorespond_set_time_window <num> 
 <e>` | set-time-window | `node telegram-handler.js set-time-window <number> <start> <end>` | | `/autorespond clear-time-windows` or `/autorespond_clear_time_windows <num>` | clear-time-windows | `node telegram-handler.js clear-time-windows <number>` | | `/autorespond add-keyword` or `/autorespond_add_keyword <num> <word>` | add-keyword | `node telegram-handler.js add-keyword <number> <keyword>` | | `/autorespond remove-keyword` or `/autorespond_remove_keyword <num> <word>` | remove-keyword | `node telegram-handler.js remove-keyword <number> <keyword>` | | `/autorespond clear-keywords` or `/autorespond_clear_keywords <num>` | clear-keywords | `node telegram-handler.js clear-keywords <number>` | | `/autorespond stats` or `/autorespond_stats [<num>]` | stats | `node telegram-handler.js stats [<number>]` | | `/autorespond set-daily-cap` or `/autorespond_set_daily_cap <num> <max>` | set-daily-cap | `node telegram-handler.js set-daily-cap <number> <max>` | **Processing steps:** 1. Detect `/autorespond` or `/autorespond_` prefix 2. Extract subcommand (normalize underscores to spaces) 3. Parse remaining arguments 4. Call telegram-handler.js with appropriate parameters ### Natural Language Pattern Matching - "show/list/view auto-responder" → `node telegram-handler.js list` - "add [contact] to auto-responder" → `node telegram-handler.js add <number> <name> <prompt>` - "change/edit/update [contact]'s prompt" → `node telegram-handler.js edit <number> <prompt>` - "set delay for [contact]" → `node telegram-handler.js delay <number> <minutes>` - "disable/remove [contact] from auto-responder" → `node telegram-handler.js remove <number>` - "auto-responder status" → `node telegram-handler.js status` - "what has auto-responder sent to [contact]" → `node telegram-handler.js history <number>` - "restart auto-responder" → `node telegram-handler.js restart` - "enable/disable auto-responder" → `node telegram-handler.js toggle` **Contact resolution:** - When user refers to contact names, look up their phone number from the config - Always use the full E.164 format (e.g., `+15551234567`) **After config changes:** Always remind the user to restart the watcher if the command output mentions it. ## Troubleshooting ### Watcher Not Responding **Check status:** ``` /autorespond_status ``` **View logs:** ```bash tail -f ~/clawd/logs/imsg-autoresponder.log ``` **Restart:** ``` /autorespond_restart ``` ### Common Issues **"OPENAI_API_KEY not found"** - Add API key to `~/.clawdbot/clawdbot.json`: ```json { "skills": { "openai-whisper-api": { "apiKey": "sk-proj-YOUR_KEY_HERE" } } } ``` - Restart watcher after adding key **Permission errors** - Grant Full Disk Access to Terminal in System Settings - Restart Terminal after granting access - Verify `imsg chats --json` works manually **Messages not detected** - Check Messages.app is signed in - Verify contact is in watch list: `/autorespond_list` - Ensure watcher is running: `/autorespond_status` **Duplicate responses** - Fixed in current version via processing locks - Restart watcher to apply fix: `/autorespond_restart` ### Testing Generate actual AI responses without sending (preview mode): ``` /autorespond_test +15551234567 Hey what's up? ``` This will: - Use the contact's actual prompt - Generate a real AI response via OpenAI - Show exactly what would be sent - **NOT actually send** the message Perfect for testing new prompts before going live! ## Privacy & Safety ⚠️ **Important:** This tool sends messages on your behalf automatically. - Only add contacts who know they're texting an AI or won't mind - Review responses regularly via `/autorespond_history` - Use rate limiting to avoid spam - Be transparent when appropriate - Disable instantly if needed: `/autorespond_toggle` ## Future Enhancements - Smart rate limiting based on conversation patterns - Group chat support - Web dashboard - Voice message transcription

path: README.md
content: # 📱 iMessage Auto-Responder

> Automatically respond to iMessages and SMS from specific contacts using AI-generated replies that match your voice and conversation context.

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Platform: macOS](https://img.shields.io/badge/Platform-macOS-blue.svg)](https://www.apple.com/macos/)
[![Node.js: v14+](https://img.shields.io/badge/Node.js-v14+-green.svg)](https://nodejs.org/)

---

## ✨ Features

| Feature | Description |
|---------|-------------|
| 🤖 **AI-Powered** | OpenAI GPT-4 generates contextual, natural responses |
| 📱 **Contact-Based Prompts** | Different AI personality per contact |
| ⏱️ **Rate Limiting** | Configurable delays between auto-responses |
| 💬 **Context-Aware** | AI sees recent conversation history for better replies |
| 📊 **Telegram Management** | Full control via slash commands or natural language |
| 🔄 **Background Monitoring** | Runs continuously via polling (5-second intervals) |
| 📝 **Activity Logging** | Track what was sent and when |
| 🔧 **Auto-Cleanup** | Clears stale processing locks on restart |
| 🧪 **Test Mode** | Preview AI responses without sending |
| ⏰ **Time Windows** | Only respond during specific hours (e.g., 9 AM - 10 PM) |
| 🔑 **Keyword Triggers** | Only respond if message contains specific keywords |
| 📊 **Statistics Tracking** | Track total responses, daily counts, averages |
| 🚦 **Daily Reply Cap** | Limit max replies per day per contact (safety feature) |

---

## 📋 Requirements

### System Requirements

- ✅ **macOS** with Messages.app signed in to iMessage
- ✅ **Node.js** v14+ (included with Clawdbot)
- ✅ **Full Disk Access** for Terminal/iTerm
- ✅ **Messages automation permission** (macOS prompts on first use)

### Dependencies

#### 1. imsg CLI
iMessage command-line tool by Peter Steinberger:

```bash
brew install steipete/tap/imsg
```

#### 2. OpenAI API Key
Add to your Clawdbot config (`~/.clawdbot/clawdbot.json`):

```json
{
  "skills": {
    "openai-whisper-api": {
      "apiKey": "sk-proj-YOUR_KEY_HERE"
    }
  }
}
```

🔗 Get your API key: https://platform.openai.com/api-keys

#### 3. macOS Permissions

Grant the following permissions:

1. **Full Disk Access** for Terminal/iTerm:
   - System Settings → Privacy & Security → Full Disk Access
   - Add Terminal.app or iTerm.app
   - Restart Terminal after granting

2. **Messages Automation** (macOS will prompt automatically on first send)

---

## 🚀 Quick Start

### Installation

#### Via ClawdHub (Recommended)

```bash
clawdhub install imsg-autoresponder
```

#### Manual Installation

1. Download `imsg-autoresponder.skill`
2. Install:
   ```bash
   clawdbot skill install imsg-autoresponder.skill
   ```

### Setup

#### 1️⃣ Add Your First Contact

**Via Telegram:**
```
/autorespond_add +15551234567 "Best Friend" "Reply with sarcastic humor"
```

**Or naturally:**
```
Add +15551234567 to the auto-responder with name "Best Friend" and prompt "Reply with sarcastic humor"
```

#### 2️⃣ Start the Watcher

```bash
cd ~/clawd/imsg-autoresponder/scripts
./launcher.sh start
```

**Or via Telegram:**
```
/autorespond_restart
```

#### 3️⃣ Test It!

Have the contact text you. The auto-responder will reply within 5 seconds (default polling interval).

---

## 💬 Usage

### Telegram Commands

All commands support **both space and underscore formats**:

#### Core Commands

| Command | Description |
|---------|-------------|
| `/autorespond list` or `/autorespond_list` | Show all contacts on watch list |
| `/autorespond status` or `/autorespond_status` | Show watcher process status |
| `/autorespond add` or `/autorespond_add <number> <name> <prompt>` | Add new contact |
| `/autorespond edit` or `/autorespond_edit <number> <prompt>` | Edit contact's prompt |
| `/autorespond remove` or `/autorespond_remove <number>` | Remove contact |
| `/autorespond delay` or `/autorespond_delay <number> <minutes>` | Set reply delay |
| `/autorespond history` or `/autorespond_history <number>` | Show response history |
| `/autorespond test` or `/autorespond_test <number> <message>` | Test without sending |
| `/autorespond toggle` or `/autorespond_toggle` | Enable/disable system |
| `/autorespond restart` or `/autorespond_restart` | Restart watcher process |

#### Bulk Operations

| Command | Description |
|---------|-------------|
| `/autorespond set-all-delays <minutes>` | Set delay for all contacts |
| `/autorespond enable-all` | Enable all contacts |
| `/autorespond disable-all` | Disable all contacts |

#### Time Windows

Control when auto-responses are active (per contact):

| Command | Description |
|---------|-------------|
| `/autorespond set-time-window <number> <start> <end>` | Add time window (e.g., 09:00 22:00) |
| `/autorespond clear-time-windows <number>` | Clear all time windows |

**Examples:**
```
/autorespond_set_time_window +15551234567 09:00 22:00
/autorespond_set_time_window +15551234567 08:00 12:00
/autorespond_clear_time_windows +15551234567
```

#### Keyword Triggers

Only respond when messages contain specific keywords (case-insensitive):

| Command | Description |
|---------|-------------|
| `/autorespond add-keyword <number> <keyword>` | Add keyword trigger |
| `/autorespond remove-keyword <number> <keyword>` | Remove keyword |
| `/autorespond clear-keywords <number>` | Clear all keywords |

**Examples:**
```
/autorespond_add_keyword +15551234567 urgent
/autorespond_add_keyword +15551234567 help
/autorespond_remove_keyword +15551234567 urgent
/autorespond_clear_keywords +15551234567
```

> **How it works:** If no keywords are set, responds to all messages (after rate limiting). If keywords exist, only responds when message contains at least one keyword.

#### Statistics & Limits

| Command | Description |
|---------|-------------|
| `/autorespond stats [<number>]` | View response statistics (all or specific contact) |
| `/autorespond set-daily-cap <number> <max>` | Set max replies per day (0 = unlimited) |

**Examples:**
```
/autorespond_stats
/autorespond_stats +15551234567
/autorespond_set_daily_cap +15551234567 10
```

### Natural Language Control

Just ask your Clawdbot agent naturally:

- _"Show me the auto-responder status"_
- _"Add my brother Scott to the watch list"_
- _"Change Mom's prompt to be more casual"_
- _"What has the auto-responder sent to Foxy?"_
- _"Disable auto-replies for Scott"_
- _"Only respond to Scott if he says 'help'"_

---

## ⚙️ Configuration

### Time Windows

Restrict auto-responses to specific hours per contact:

```bash
# Only respond between 9 AM and 10 PM
/autorespond_set_time_window +15551234567 09:00 22:00

# Add multiple windows (morning and evening)
/autorespond_set_time_window +15551234567 08:00 12:00
/autorespond_set_time_window +15551234567 17:00 23:00

# Remove all time windows (respond 24/7)
/autorespond_clear_time_windows +15551234567
```

**Use Cases:**
- ✅ Respect work hours (only respond 9-5)
- ✅ Avoid late night texts (cutoff at 10 PM)
- ✅ Weekend-only responses
- ✅ Different hours per contact

### Keyword Triggers

Only respond when messages contain specific keywords:

```bash
# Only respond if message contains "urgent" or "help"
/autorespond_add_keyword +15551234567 urgent
/autorespond_add_keyword +15551234567 help

# Add "emergency" keyword for Mom
/autorespond_add_keyword +19028778490 emergency

# Remove a keyword
/autorespond_remove_keyword +15551234567 urgent

# Clear all keywords (respond to all messages)
/autorespond_clear_keywords +15551234567
```

**Use Cases:**
- ✅ Only respond to urgent/important messages
- ✅ Filter casual vs. serious conversations
- ✅ Require explicit trigger words ("help", "question", "emergency")
- ✅ Different keywords per contact

### Statistics Tracking

View response statistics:

```bash
# All contacts
/autorespond_stats

# Specific contact
/autorespond_stats +15551234567
```

**Tracked Metrics:**
- Total responses (all-time)
- Daily count (today)
- Average responses per day
- First and last response dates

### Daily Reply Cap

Limit auto-replies per contact per day:

```bash
# Set max 10 replies per day for Scott
/autorespond_set_daily_cap +15551234567 10

# Remove limit (unlimited)
/autorespond_set_daily_cap +15551234567 0
```

**Use Cases:**
- ✅ Prevent spam if someone texts repeatedly
- ✅ Safety cap during testing
- ✅ Different limits per contact
- ✅ Protects against runaway conversations

### Prompt Engineering

Prompts define how the AI responds. Be specific!

**Good Examples:**

```
"Reply with only a middle finger emoji"

"You are Drac's AI. Match their energy - vulgar with vulgar, 
serious with serious. Keep it short and natural."

"Be polite and helpful. Keep responses under 160 characters."

"Respond as if you're my assistant. Professional but warm. 
If they ask about my availability, check my calendar."
```

### Rate Limiting

Control reply frequency:

```bash
# 30 minutes between replies
/autorespond_delay +15551234567 30

# Instant replies (testing only!)
/autorespond_delay +15551234567 0
```

---

## 🔧 Troubleshooting

### Watcher Not Responding

**Check status:**
```
/autorespond_status
```

**Restart if needed:**
```
/autorespond_restart
```

**Check logs:**
```bash
tail -f ~/clawd/logs/imsg-autoresponder.log
```

### Common Issues

#### ❌ "OPENAI_API_KEY not found"

**Solution:** Add API key to `~/.clawdbot/clawdbot.json`:
```json
{
  "skills": {
    "openai-whisper-api": {
      "apiKey": "sk-proj-YOUR_KEY_HERE"
    }
  }
}
```
Restart watcher after adding.

#### ❌ "Permission denied" / "Operation not permitted"

**Solution:** 
1. Grant Full Disk Access to Terminal
2. System Settings → Privacy & Security → Full Disk Access
3. Add Terminal.app
4. Restart Terminal

#### ❌ Messages not being detected

**Solution:**
1. Verify `imsg watch --json` works manually
2. Check Messages.app is signed in
3. Confirm contact is in watch list: `/autorespond_list`

#### ❌ Responses are too long/verbose

**Solution:** Edit prompt to emphasize brevity:
```
/autorespond_edit +15551234567 Keep replies to 1-2 sentences max. Be concise.
```

### Testing Without Sending

Preview real AI responses without sending:

```
/autorespond_test +15551234567 Hey what's up?
```

Perfect for testing new prompts before going live! ✅

---

## 🏗️ Architecture

### Components

- **`watcher.js`** - Background polling process that monitors iMessages
- **`manage.js`** - CLI for configuration management
- **`telegram-handler.js`** - Telegram command interface
- **`launcher.sh`** - Start/stop/status helper script

### How It Works

```mermaid
graph LR
    A[Polling Loop] --> B[Check New Messages]
    B --> C{On Watch List?}
    C -->|No| A
    C -->|Yes| D{Rate Limit OK?}
    D -->|No| A
    D -->|Yes| E{Keywords Match?}
    E -->|No| A
    E -->|Yes| F[Fetch History]
    F --> G[Generate AI Response]
    G --> H[Send Reply]
    H --> I[Update State]
    I --> A
```

1. **Polling loop** checks `imsg chats` every 5 seconds
2. **New messages** detected by comparing timestamps
3. **Rate limiting** prevents spam (configurable delay per contact)
4. **Processing lock** prevents duplicate responses during AI generation
5. **OpenAI API** generates contextual responses using conversation history
6. **Response sent** via `imsg send`
7. **State saved** (last response time, last checked time)

### Data Storage

| File | Purpose |
|------|---------|
| `~/clawd/imsg-autoresponder.json` | Configuration (watch list, prompts) |
| `~/clawd/data/imsg-autoresponder-state.json` | State tracking (last response times) |
| `~/clawd/logs/imsg-autoresponder.log` | Activity logs |
| `~/clawd/data/imsg-autoresponder.pid` | Process ID file |

---

## 🛡️ Safety Features

- ✅ **Rate limiting** prevents spam (configurable per contact)
- ✅ **Processing locks** prevent duplicate sends
- ✅ **Manual override** - disable any contact instantly
- ✅ **System toggle** - pause all auto-responses
- ✅ **Full logging** - review everything that was sent
- ✅ **Test mode** - preview responses before enabling
- ✅ **Daily caps** - limit max replies per day
- ✅ **Time windows** - only respond during specific hours
- ✅ **Keyword triggers** - require specific words to respond

---

## 🗺️ Roadmap

- [x] Time windows (only respond during certain hours) - **v1.1**
- [x] Keyword triggers (only respond if message contains specific words) - **v1.1**
- [x] Statistics tracking (response counts, daily totals) - **v1.1**
- [x] Max replies per day cap - **v1.1**
- [ ] Smart rate limiting based on conversation patterns
- [ ] Group chat support
- [ ] Web dashboard
- [ ] Voice message transcription support
- [ ] Multi-language support
- [ ] Sentiment analysis for better tone matching

---

## ⚠️ Privacy & Ethics

> **Important:** This tool sends messages on your behalf automatically. Use responsibly.

### Guidelines

- ✅ Only add contacts who know they're texting an AI or won't mind
- ✅ Be transparent about AI usage when appropriate
- ✅ Review responses regularly via `/autorespond_history`
- ❌ Don't use for deceptive purposes
- ❌ Don't impersonate someone without their knowledge
- ✅ Respect the boundaries of others
- ✅ Disable auto-responses for sensitive conversations

---

## 🤝 Contributing

Found a bug? Have a feature idea?

- **GitHub:** https://github.com/clawdbot/clawdbot
- **Discord:** https://discord.com/invite/clawd
- **ClawdHub:** https://clawdhub.com

---

## 📄 License

MIT License - see Clawdbot repository for full license text.

---

## 🙏 Credits

Built with ❤️ by the Clawdbot community.

**Powered by:**
- [imsg](https://imsg.to) by Peter Steinberger
- [OpenAI GPT-4](https://openai.com)
- [Clawdbot Agent Framework](https://clawd.bot)

---

<p align="center">
  <strong>Made with 🤖 + ☕ by the Clawdbot community</strong>
</p>


path: scripts/launcher.sh
content: #!/bin/bash

# iMessage Auto-Responder Launcher
# Simplified start/stop/status management

SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
WATCHER_SCRIPT="$SCRIPT_DIR/watcher.js"
PID_FILE="$HOME/clawd/data/imsg-autoresponder.pid"
LOG_FILE="$HOME/clawd/logs/imsg-autoresponder.log"

case "$1" in
  start)
    if [ -f "$PID_FILE" ] && kill -0 $(cat "$PID_FILE") 2>/dev/null; then
      echo "✗ Auto-responder is already running (PID $(cat "$PID_FILE"))"
      exit 1
    fi
    
    echo "Starting iMessage auto-responder..."
    nohup node "$WATCHER_SCRIPT" >> "$LOG_FILE" 2>&1 &
    echo $! > "$PID_FILE"
    echo "✓ Auto-responder started (PID $!)"
    echo "  Logs: $LOG_FILE"
    ;;
    
  stop)
    if [ ! -f "$PID_FILE" ]; then
      echo "✗ Auto-responder is not running (no PID file)"
      exit 1
    fi
    
    PID=$(cat "$PID_FILE")
    if ! kill -0 "$PID" 2>/dev/null; then
      echo "✗ Auto-responder is not running (stale PID file)"
      rm "$PID_FILE"
      exit 1
    fi
    
    echo "Stopping iMessage auto-responder (PID $PID)..."
    kill "$PID"
    rm "$PID_FILE"
    echo "✓ Auto-responder stopped"
    ;;
    
  restart)
    "$0" stop
    sleep 2
    "$0" start
    ;;
    
  status)
    if [ -f "$PID_FILE" ] && kill -0 $(cat "$PID_FILE") 2>/dev/null; then
      echo "✓ Auto-responder is RUNNING (PID $(cat "$PID_FILE"))"
      echo "  Logs: $LOG_FILE"
      echo ""
      echo "Recent activity:"
      tail -10 "$LOG_FILE" | sed 's/^/  /'
    else
      echo "✗ Auto-responder is NOT running"
      if [ -f "$PID_FILE" ]; then
        rm "$PID_FILE"
      fi
    fi
    ;;
    
  logs)
    if [ -f "$LOG_FILE" ]; then
      tail -f "$LOG_FILE"
    else
      echo "✗ No log file found at $LOG_FILE"
    fi
    ;;
    
  *)
    echo "Usage: $0 {start|stop|restart|status|logs}"
    exit 1
    ;;
esac


path: scripts/manage.js
content: #!/usr/bin/env node

/**
 * iMessage Auto-Responder Config Manager
 * CLI for managing watch list
 */

const fs = require('fs');
const path = require('path');
const os = require('os');

const WORKSPACE = process.env.CLAWD_WORKSPACE || path.join(os.homedir(), 'clawd');
const CONFIG_PATH = path.join(WORKSPACE, 'imsg-autoresponder.json');

function loadConfig() {
  if (!fs.existsSync(CONFIG_PATH)) {
    return {
      enabled: true,
      watchList: [],
      defaultMinMinutesBetweenReplies: 15
    };
  }
  return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
}

function saveConfig(config) {
  fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
  console.log('✓ Config saved to', CONFIG_PATH);
}

function showUsage() {
  console.log(`
iMessage Auto-Responder Manager

Usage:
  node manage.js list                           List all watch list entries
  node manage.js add <number> <prompt>          Add a contact to watch list
  node manage.js remove <number>                Remove a contact
  node manage.js enable <number>                Enable auto-responses for contact
  node manage.js disable <number>               Disable auto-responses for contact
  node manage.js set-delay <number> <minutes>   Set min minutes between replies
  node manage.js toggle                         Enable/disable entire system
  
  Bulk Operations:
  node manage.js set-all-delays <minutes>       Set delay for all contacts
  node manage.js enable-all                     Enable all contacts
  node manage.js disable-all                    Disable all contacts
  
  Time Windows:
  node manage.js set-time-window <number> <start> <end>   Add time window (HH:MM format)
  node manage.js clear-time-windows <number>              Remove all time windows
  
  Keyword Triggers:
  node manage.js add-keyword <number> <keyword>           Add keyword trigger
  node manage.js remove-keyword <number> <keyword>        Remove keyword trigger
  node manage.js clear-keywords <number>                  Remove all keyword triggers
  
  Daily Cap:
  node manage.js set-daily-cap <number> <max>             Set max replies per day (0 = unlimited)

Examples:
  node manage.js add "+15551234567" "Reply with a middle finger emoji"
  node manage.js add "+15559876543" "You are my helpful assistant. Reply warmly and briefly."
  node manage.js set-delay "+15551234567" 30
  node manage.js disable "+15551234567"
  node manage.js set-all-delays 30
  node manage.js disable-all
  node manage.js set-time-window "+15551234567" 09:00 22:00
  node manage.js clear-time-windows "+15551234567"
  node manage.js add-keyword "+15551234567" urgent
  node manage.js add-keyword "+15551234567" help
  node manage.js clear-keywords "+15551234567"
  `);
}

function list() {
  const config = loadConfig();
  
  console.log('\n=== Auto-Responder Status ===');
  console.log(`System: ${config.enabled ? '✓ ENABLED' : '✗ DISABLED'}`);
  console.log(`Default delay: ${config.defaultMinMinutesBetweenReplies} minutes\n`);
  
  if (config.watchList.length === 0) {
    console.log('No contacts in watch list.');
    return;
  }
  
  console.log('=== Watch List ===\n');
  config.watchList.forEach((contact, i) => {
    const status = contact.enabled === false ? '✗ DISABLED' : '✓ enabled';
    const delay = contact.minMinutesBetweenReplies || config.defaultMinMinutesBetweenReplies;
    
    console.log(`[${i + 1}] ${contact.identifier} (${status})`);
    if (contact.name) console.log(`    Name: ${contact.name}`);
    console.log(`    Delay: ${delay} minutes`);
    if (contact.maxRepliesPerDay) {
      console.log(`    Daily Cap: ${contact.maxRepliesPerDay} replies/day`);
    }
    if (contact.timeWindows && contact.timeWindows.length > 0) {
      console.log(`    Time Windows:`);
      contact.timeWindows.forEach(w => {
        console.log(`      ${w.start} - ${w.end}`);
      });
    }
    if (contact.keywords && contact.keywords.length > 0) {
      console.log(`    Keywords: ${contact.keywords.join(', ')}`);
    }
    console.log(`    Prompt: ${contact.prompt}`);
    console.log();
  });
}

function add(identifier, prompt, name) {
  const config = loadConfig();
  
  // Check if already exists
  const existing = config.watchList.find(c => c.identifier === identifier);
  if (existing) {
    console.log(`✗ Contact ${identifier} already exists. Use 'remove' first or edit the config manually.`);
    return;
  }
  
  config.watchList.push({
    identifier,
    name: name || '',
    prompt,
    enabled: true
  });
  
  saveConfig(config);
  console.log(`✓ Added ${identifier} to watch list`);
}

function remove(identifier) {
  const config = loadConfig();
  const index = config.watchList.findIndex(c => c.identifier === identifier);
  
  if (index === -1) {
    console.log(`✗ Contact ${identifier} not found in watch list`);
    return;
  }
  
  config.watchList.splice(index, 1);
  saveConfig(config);
  console.log(`✓ Removed ${identifier} from watch list`);
}

function setEnabled(identifier, enabled) {
  const config = loadConfig();
  const contact = config.watchList.find(c => c.identifier === identifier);
  
  if (!contact) {
    console.log(`✗ Contact ${identifier} not found in watch list`);
    return;
  }
  
  contact.enabled = enabled;
  saveConfig(config);
  console.log(`✓ ${enabled ? 'Enabled' : 'Disabled'} ${identifier}`);
}

function setDelay(identifier, minutes) {
  const config = loadConfig();
  const contact = config.watchList.find(c => c.identifier === identifier);
  
  if (!contact) {
    console.log(`✗ Contact ${identifier} not found in watch list`);
    return;
  }
  
  contact.minMinutesBetweenReplies = parseInt(minutes);
  saveConfig(config);
  console.log(`✓ Set delay to ${minutes} minutes for ${identifier}`);
}

function toggle() {
  const config = loadConfig();
  config.enabled = !config.enabled;
  saveConfig(config);
  console.log(`✓ Auto-responder ${config.enabled ? 'ENABLED' : 'DISABLED'}`);
}

function setAllDelays(minutes) {
  const config = loadConfig();
  
  if (config.watchList.length === 0) {
    console.log('✗ No contacts in watch list');
    return;
  }
  
  config.watchList.forEach(contact => {
    contact.minMinutesBetweenReplies = parseInt(minutes);
  });
  
  saveConfig(config);
  console.log(`✓ Set delay to ${minutes} minutes for all ${config.watchList.length} contacts`);
}

function enableAll() {
  const config = loadConfig();
  
  if (config.watchList.length === 0) {
    console.log('✗ No contacts in watch list');
    return;
  }
  
  config.watchList.forEach(contact => {
    contact.enabled = true;
  });
  
  saveConfig(config);
  console.log(`✓ Enabled all ${config.watchList.length} contacts`);
}

function disableAll() {
  const config = loadConfig();
  
  if (config.watchList.length === 0) {
    console.log('✗ No contacts in watch list');
    return;
  }
  
  config.watchList.forEach(contact => {
    contact.enabled = false;
  });
  
  saveConfig(config);
  console.log(`✓ Disabled all ${config.watchList.length} contacts`);
}

function setTimeWindow(identifier, start, end) {
  const config = loadConfig();
  const contact = config.watchList.find(c => c.identifier === identifier);
  
  if (!contact) {
    console.log(`✗ Contact ${identifier} not found in watch list`);
    return;
  }
  
  // Validate time format (HH:MM)
  const timeRegex = /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/;
  if (!timeRegex.test(start) || !timeRegex.test(end)) {
    console.log('✗ Invalid time format. Use HH:MM (e.g., 09:00, 22:30)');
    return;
  }
  
  if (!contact.timeWindows) {
    contact.timeWindows = [];
  }
  
  contact.timeWindows.push({ start, end });
  saveConfig(config);
  console.log(`✓ Added time window ${start} - ${end} for ${identifier}`);
}

function clearTimeWindows(identifier) {
  const config = loadConfig();
  const contact = config.watchList.find(c => c.identifier === identifier);
  
  if (!contact) {
    console.log(`✗ Contact ${identifier} not found in watch list`);
    return;
  }
  
  contact.timeWindows = [];
  saveConfig(config);
  console.log(`✓ Cleared all time windows for ${identifier}`);
}

function addKeyword(identifier, keyword) {
  const config = loadConfig();
  const contact = config.watchList.find(c => c.identifier === identifier);
  
  if (!contact) {
    console.log(`✗ Contact ${identifier} not found in watch list`);
    return;
  }
  
  if (!contact.keywords) {
    contact.keywords = [];
  }
  
  if (contact.keywords.includes(keyword)) {
    console.log(`✗ Keyword "${keyword}" already exists for ${identifier}`);
    return;
  }
  
  contact.keywords.push(keyword);
  saveConfig(config);
  console.log(`✓ Added keyword "${keyword}" for ${identifier}`);
}

function removeKeyword(identifier, keyword) {
  const config = loadConfig();
  const contact = config.watchList.find(c => c.identifier === identifier);
  
  if (!contact) {
    console.log(`✗ Contact ${identifier} not found in watch list`);
    return;
  }
  
  if (!contact.keywords || contact.keywords.length === 0) {
    console.log(`✗ No keywords configured for ${identifier}`);
    return;
  }
  
  const index = contact.keywords.indexOf(keyword);
  if (index === -1) {
    console.log(`✗ Keyword "${keyword}" not found for ${identifier}`);
    return;
  }
  
  contact.keywords.splice(index, 1);
  saveConfig(config);
  console.log(`✓ Removed keyword "${keyword}" from ${identifier}`);
}

function clearKeywords(identifier) {
  const config = loadConfig();
  const contact = config.watchList.find(c => c.identifier === identifier);
  
  if (!contact) {
    console.log(`✗ Contact ${identifier} not found in watch list`);
    return;
  }
  
  contact.keywords = [];
  saveConfig(config);
  console.log(`✓ Cleared all keywords for ${identifier}`);
}

function setDailyCap(identifier, maxReplies) {
  const config = loadConfig();
  const contact = config.watchList.find(c => c.identifier === identifier);
  
  if (!contact) {
    console.log(`✗ Contact ${identifier} not found in watch list`);
    return;
  }
  
  const max = parseInt(maxReplies);
  if (isNaN(max) || max < 0) {
    console.log(`✗ Invalid max replies value. Must be a positive number.`);
    return;
  }
  
  if (max === 0) {
    delete contact.maxRepliesPerDay;
    console.log(`✓ Removed daily cap for ${identifier} (unlimited)`);
  } else {
    contact.maxRepliesPerDay = max;
    console.log(`✓ Set daily cap to ${max} replies for ${identifier}`);
  }
  
  saveConfig(config);
}

// Parse command line
const [,, command, ...args] = process.argv;

switch (command) {
  case 'list':
    list();
    break;
    
  case 'add':
    if (args.length < 2) {
      console.log('✗ Usage: node manage.js add <number> "<prompt>" [name]');
      process.exit(1);
    }
    add(args[0], args[1], args[2]);
    break;
    
  case 'remove':
    if (args.length < 1) {
      console.log('✗ Usage: node manage.js remove <number>');
      process.exit(1);
    }
    remove(args[0]);
    break;
    
  case 'enable':
    if (args.length < 1) {
      console.log('✗ Usage: node manage.js enable <number>');
      process.exit(1);
    }
    setEnabled(args[0], true);
    break;
    
  case 'disable':
    if (args.length < 1) {
      console.log('✗ Usage: node manage.js disable <number>');
      process.exit(1);
    }
    setEnabled(args[0], false);
    break;
    
  case 'set-delay':
    if (args.length < 2) {
      console.log('✗ Usage: node manage.js set-delay <number> <minutes>');
      process.exit(1);
    }
    setDelay(args[0], args[1]);
    break;
    
  case 'toggle':
    toggle();
    break;
    
  case 'set-all-delays':
    if (args.length < 1) {
      console.log('✗ Usage: node manage.js set-all-delays <minutes>');
      process.exit(1);
    }
    setAllDelays(args[0]);
    break;
    
  case 'enable-all':
    enableAll();
    break;
    
  case 'disable-all':
    disableAll();
    break;
    
  case 'set-time-window':
    if (args.length < 3) {
      console.log('✗ Usage: node manage.js set-time-window <number> <start> <end>');
      console.log('  Example: node manage.js set-time-window "+15551234567" 09:00 22:00');
      process.exit(1);
    }
    setTimeWindow(args[0], args[1], args[2]);
    break;
    
  case 'clear-time-windows':
    if (args.length < 1) {
      console.log('✗ Usage: node manage.js clear-time-windows <number>');
      process.exit(1);
    }
    clearTimeWindows(args[0]);
    break;
    
  case 'add-keyword':
    if (args.length < 2) {
      console.log('✗ Usage: node manage.js add-keyword <number> <keyword>');
      console.log('  Example: node manage.js add-keyword "+15551234567" urgent');
      process.exit(1);
    }
    addKeyword(args[0], args[1]);
    break;
    
  case 'remove-keyword':
    if (args.length < 2) {
      console.log('✗ Usage: node manage.js remove-keyword <number> <keyword>');
      process.exit(1);
    }
    removeKeyword(args[0], args[1]);
    break;
    
  case 'clear-keywords':
    if (args.length < 1) {
      console.log('✗ Usage: node manage.js clear-keywords <number>');
      process.exit(1);
    }
    clearKeywords(args[0]);
    break;
    
  case 'set-daily-cap':
    if (args.length < 2) {
      console.log('✗ Usage: node manage.js set-daily-cap <number> <max_replies>');
      console.log('  Set to 0 for unlimited');
      process.exit(1);
    }
    setDailyCap(args[0], args[1]);
    break;
    
  default:
    showUsage();
    process.exit(command ? 1 : 0);
}


path: scripts/watcher.js
content: #!/usr/bin/env node

/**
 * iMessage Auto-Responder Watcher (Polling Version)
 * Polls for new messages instead of using `imsg watch`
 */

const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
const os = require('os');

// Paths
const WORKSPACE = process.env.CLAWD_WORKSPACE || path.join(os.homedir(), 'clawd');
const CONFIG_PATH = path.join(WORKSPACE, 'imsg-autoresponder.json');
const STATE_PATH = path.join(WORKSPACE, 'data', 'imsg-autoresponder-state.json');
const LOG_PATH = path.join(WORKSPACE, 'logs', 'imsg-autoresponder.log');

// Poll interval (milliseconds)
const POLL_INTERVAL = 5000; // 5 seconds

// Ensure directories exist
[path.dirname(STATE_PATH), path.dirname(LOG_PATH)].forEach(dir => {
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true });
  }
});

// Load or create config
function loadConfig() {
  if (!fs.existsSync(CONFIG_PATH)) {
    const defaultConfig = {
      enabled: true,
      watchList: [],
      defaultMinMinutesBetweenReplies: 15
    };
    fs.writeFileSync(CONFIG_PATH, JSON.stringify(defaultConfig, null, 2));
    log('Created default config at ' + CONFIG_PATH);
    return defaultConfig;
  }
  return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
}

// Load or create state
function loadState() {
  if (!fs.existsSync(STATE_PATH)) {
    return { 
      lastResponses: {}, 
      lastChecked: {}, 
      processing: {},
      stats: {}
    };
  }
  const state = JSON.parse(fs.readFileSync(STATE_PATH, 'utf8'));
  // Ensure required fields exist
  if (!state.processing) {
    state.processing = {};
  }
  if (!state.stats) {
    state.stats = {};
  }
  return state;
}

// Save state
function saveState(state) {
  fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
}

// Log function
function log(message) {
  const timestamp = new Date().toISOString();
  const logMessage = `[${timestamp}] ${message}\n`;
  // Only append to file, don't console.log (launcher already redirects stdout)
  fs.appendFileSync(LOG_PATH, logMessage);
}

// Get chat list
async function getChatList() {
  return new Promise((resolve, reject) => {
    const proc = spawn('imsg', ['chats', '--limit', '50', '--json']);
    let output = '';
    let errorOutput = '';

    proc.stdout.on('data', (data) => {
      output += data.toString();
    });

    proc.stderr.on('data', (data) => {
      errorOutput += data.toString();
    });

    proc.on('close', (code) => {
      if (code !== 0) {
        reject(new Error(`imsg chats failed: ${errorOutput}`));
        return;
      }
      
      const chats = output.trim().split('\n')
        .filter(line => line)
        .map(line => JSON.parse(line));
      resolve(chats);
    });
  });
}

// Get message history for a chat
async function getMessageHistory(chatId, limit = 20) {
  return new Promise((resolve, reject) => {
    const proc = spawn('imsg', ['history', '--chat-id', chatId, '--limit', limit, '--json']);
    let output = '';
    let errorOutput = '';

    proc.stdout.on('data', (data) => {
      output += data.toString();
    });

    proc.stderr.on('data', (data) => {
      errorOutput += data.toString();
    });

    proc.on('close', (code) => {
      if (code !== 0) {
        reject(new Error(`imsg history failed: ${errorOutput}`));
        return;
      }
      
      const messages = output.trim().split('\n')
        .filter(line => line)
        .map(line => JSON.parse(line));
      resolve(messages);
    });
  });
}

// Generate AI response using OpenAI API
async function generateResponse(contact, incomingMessage, messageHistory) {
  const historyText = messageHistory.slice(0, 5)
    .map(m => `${m.is_from_me ? 'Me' : contact.name || contact.identifier}: ${m.text || '[attachment]'}`)
    .join('\n');

  const prompt = `${contact.prompt}

Context - Recent message history (newest first):
${historyText}

Latest message from ${contact.name || contact.identifier}:
${incomingMessage.text || '[attachment]'}

Generate a response now:`;

  // Get OpenAI API key from Clawdbot config
  const configPath = path.join(os.homedir(), '.clawdbot', 'clawdbot.json');
  let [REDACTED_SECRET];
  
  if (!apiKey && fs.existsSync(configPath)) {
    try {
      const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
      [REDACTED_SECRET]'openai-whisper-api']?.apiKey || config.skills?.['openai-image-gen']?.apiKey;
    } catch (e) {
      // Ignore config parse errors
    }
  }

  if (!apiKey) {
    throw new Error('OPENAI_API_KEY not found');
  }

  const payload = JSON.stringify({
    model: 'gpt-4',
    messages: [{ role: 'user', content: prompt }],
    max_tokens: 150,
    temperature: 0.9
  });

  return new Promise((resolve, reject) => {
    const proc = spawn('curl', [
      '-s',
      'https://api.openai.com/v1/chat/completions',
      '-H', 'Content-Type: application/json',
      '-H', `Authorization: Bearer ${apiKey}`,
      '-d', payload
    ]);

    let output = '';

    proc.stdout.on('data', (data) => {
      output += data.toString();
    });

    proc.on('close', (code) => {
      if (code !== 0) {
        reject(new Error(`OpenAI API call failed`));
        return;
      }

      try {
        const response = JSON.parse(output);
        if (response.error) {
          reject(new Error(`OpenAI API error: ${response.error.message}`));
          return;
        }
        
        const text = response.choices[0].message.content.trim();
        resolve(text);
      } catch (e) {
        reject(new Error(`Failed to parse OpenAI response: ${e.message}`));
      }
    });
  });
}

// Send message via imsg
async function sendMessage(identifier, text) {
  return new Promise((resolve, reject) => {
    const proc = spawn('imsg', ['send', '--to', identifier, '--text', text]);
    let errorOutput = '';

    proc.stderr.on('data', (data) => {
      errorOutput += data.toString();
    });

    proc.on('close', (code) => {
      if (code !== 0) {
        reject(new Error(`imsg send failed: ${errorOutput}`));
        return;
      }
      resolve();
    });
  });
}

// Check if we should respond based on rate limiting
// Check if current time is within allowed time windows
function isWithinTimeWindow(contact) {
  // If no time windows configured, always allow
  if (!contact.timeWindows || contact.timeWindows.length === 0) {
    return true;
  }

  const now = new Date();
  const currentHour = now.getHours();
  const currentMinute = now.getMinutes();
  const currentTime = currentHour * 60 + currentMinute; // minutes since midnight

  // Check each time window
  for (const window of contact.timeWindows) {
    const [startHour, startMinute] = window.start.split(':').map(Number);
    const [endHour, endMinute] = window.end.split(':').map(Number);
    
    const startTime = startHour * 60 + startMinute;
    const endTime = endHour * 60 + endMinute;

    if (currentTime >= startTime && currentTime <= endTime) {
      return true; // Within an allowed window
    }
  }

  return false; // Outside all windows
}

// Check if message contains required keywords
function messageMatchesKeywords(contact, messageText) {
  // If no keywords configured, always allow
  if (!contact.keywords || contact.keywords.length === 0) {
    return true;
  }

  // Check if message contains any of the keywords (case-insensitive)
  const lowerText = (messageText || '').toLowerCase();
  
  for (const keyword of contact.keywords) {
    if (lowerText.includes(keyword.toLowerCase())) {
      return true; // Found a matching keyword
    }
  }

  return false; // No keywords matched
}

function shouldRespond(contact, state, config, messageText) {
  const now = Date.now();
  const identifier = contact.identifier;
  const minMs = (contact.minMinutesBetweenReplies !== undefined ? contact.minMinutesBetweenReplies : config.defaultMinMinutesBetweenReplies) * 60 * 1000;

  // Check time windows first
  if (!isWithinTimeWindow(contact)) {
    log(`Time window: Outside allowed hours for ${contact.name || identifier}`);
    return false;
  }

  // Check keyword triggers
  if (!messageMatchesKeywords(contact, messageText)) {
    log(`Keyword filter: Message doesn't contain required keywords for ${contact.name || identifier}`);
    return false;
  }

  // Check daily reply cap
  if (contact.maxRepliesPerDay && contact.maxRepliesPerDay > 0) {
    const stats = state.stats && state.stats[identifier];
    if (stats) {
      const today = new Date().toDateString();
      // Reset daily count if new day
      if (stats.dailyDate !== today) {
        stats.dailyCount = 0;
        stats.dailyDate = today;
      }
      
      if (stats.dailyCount >= contact.maxRepliesPerDay) {
        log(`Daily cap: Already sent ${stats.dailyCount} responses today for ${contact.name || identifier} (max: ${contact.maxRepliesPerDay})`);
        return false;
      }
    }
  }

  if (!state.lastResponses[identifier]) {
    return true;
  }

  const lastResponse = state.lastResponses[identifier];
  const elapsed = now - lastResponse;

  if (elapsed < minMs) {
    log(`Rate limit: Only ${Math.floor(elapsed / 1000 / 60)} minutes since last response to ${identifier} (need ${Math.floor(minMs / 1000 / 60)})`);
    return false;
  }

  return true;
}

// Check for new messages and respond
async function checkForNewMessages(config, state) {
  try {
    const chats = await getChatList();
    
    for (const chat of chats) {
      const contact = config.watchList.find(c => c.identifier === chat.identifier && c.enabled !== false);
      
      if (!contact) {
        continue; // Not in watch list
      }

      // Get recent messages
      const messages = await getMessageHistory(chat.id, 5);
      
      if (messages.length === 0) {
        continue;
      }

      // Get the most recent message
      const latestMessage = messages[0];
      
      // Skip if it's from us
      if (latestMessage.is_from_me) {
        continue;
      }

      // Check if we've already seen this message
      const lastCheckedTime = state.lastChecked[contact.identifier] || 0;
      const messageTime = new Date(latestMessage.created_at).getTime();
      
      if (messageTime <= lastCheckedTime) {
        continue; // Already seen this message
      }

      // Check if currently processing this contact (prevent race condition)
      if (state.processing[contact.identifier]) {
        log(`Skipping ${contact.name || contact.identifier} - already processing`);
        continue;
      }

      // Update last checked time and mark as processing
      state.lastChecked[contact.identifier] = messageTime;
      state.processing[contact.identifier] = true;
      saveState(state);

      log(`New message from ${contact.name || contact.identifier}: ${latestMessage.text || '[attachment]'}`);

      // Check rate limiting and conditions
      if (!shouldRespond(contact, state, config, latestMessage.text)) {
        state.processing[contact.identifier] = false;
        saveState(state);
        continue;
      }

      // Generate and send response
      try {
        log(`Generating response for ${contact.name || contact.identifier}...`);
        const response = await generateResponse(contact, latestMessage, messages);
        log(`Generated response: ${response}`);

        await sendMessage(contact.identifier, response);
        log(`✓ Sent response to ${contact.name || contact.identifier}`);

        // Update state
        state.lastResponses[contact.identifier] = Date.now();
        state.processing[contact.identifier] = false;
        
        // Update statistics
        if (!state.stats[contact.identifier]) {
          state.stats[contact.identifier] = {
            totalResponses: 0,
            firstResponse: Date.now(),
            lastResponse: Date.now(),
            dailyCount: 0,
            dailyDate: new Date().toDateString()
          };
        }
        
        const stats = state.stats[contact.identifier];
        const today = new Date().toDateString();
        
        // Reset daily count if it's a new day
        if (stats.dailyDate !== today) {
          stats.dailyCount = 0;
          stats.dailyDate = today;
        }
        
        stats.totalResponses++;
        stats.dailyCount++;
        stats.lastResponse = Date.now();
        
        saveState(state);

      } catch (error) {
        log(`ERROR generating/sending response: ${error.message}`);
        state.processing[contact.identifier] = false;
        saveState(state);
      }
    }

  } catch (error) {
    log(`ERROR checking messages: ${error.message}`);
  }
}

// Main polling loop
async function startPolling() {
  log('Starting iMessage auto-responder (polling mode)...');
  
  const config = loadConfig();
  const state = loadState();

  if (!config.enabled) {
    log('Auto-responder is DISABLED in config. Exiting.');
    return;
  }

  if (config.watchList.length === 0) {
    log('Watch list is empty. Add contacts to the config file.');
    log(`Config: ${CONFIG_PATH}`);
    return;
  }

  log(`Watching ${config.watchList.filter(c => c.enabled !== false).length} contacts (polling every ${POLL_INTERVAL/1000}s)`);

  // Clear all processing locks on startup (prevent stuck contacts from previous crashes)
  const stuckContacts = Object.keys(state.processing).filter(id => state.processing[id]);
  if (stuckContacts.length > 0) {
    log(`Clearing ${stuckContacts.length} stale processing locks from previous session`);
    state.processing = {};
  }

  // Initialize lastChecked for all contacts if not present
  config.watchList.forEach(contact => {
    if (!state.lastChecked[contact.identifier]) {
      state.lastChecked[contact.identifier] = Date.now();
    }
  });
  saveState(state);

  // Poll continuously
  setInterval(async () => {
    await checkForNewMessages(config, state);
  }, POLL_INTERVAL);

  // Also check immediately
  await checkForNewMessages(config, state);
}

// Start
startPolling();


path: scripts/watcher-old.js
content: #!/usr/bin/env node

/**
 * iMessage Auto-Responder Watcher
 * Monitors incoming messages and auto-responds based on watch list config
 */

const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
const os = require('os');

// Paths
const WORKSPACE = process.env.CLAWD_WORKSPACE || path.join(os.homedir(), 'clawd');
const CONFIG_PATH = path.join(WORKSPACE, 'imsg-autoresponder.json');
const STATE_PATH = path.join(WORKSPACE, 'data', 'imsg-autoresponder-state.json');
const LOG_PATH = path.join(WORKSPACE, 'logs', 'imsg-autoresponder.log');

// Ensure directories exist
[path.dirname(STATE_PATH), path.dirname(LOG_PATH)].forEach(dir => {
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true });
  }
});

// Load or create config
function loadConfig() {
  if (!fs.existsSync(CONFIG_PATH)) {
    const defaultConfig = {
      enabled: true,
      watchList: [],
      defaultMinMinutesBetweenReplies: 15
    };
    fs.writeFileSync(CONFIG_PATH, JSON.stringify(defaultConfig, null, 2));
    log('Created default config at ' + CONFIG_PATH);
    return defaultConfig;
  }
  return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
}

// Load or create state
function loadState() {
  if (!fs.existsSync(STATE_PATH)) {
    return { lastResponses: {} };
  }
  return JSON.parse(fs.readFileSync(STATE_PATH, 'utf8'));
}

// Save state
function saveState(state) {
  fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
}

// Log function
function log(message) {
  const timestamp = new Date().toISOString();
  const logMessage = `[${timestamp}] ${message}\n`;
  console.log(logMessage.trim());
  fs.appendFileSync(LOG_PATH, logMessage);
}

// Get message history for a chat
async function getMessageHistory(chatId, limit = 20) {
  return new Promise((resolve, reject) => {
    const proc = spawn('imsg', ['history', '--chat-id', chatId, '--limit', limit, '--json']);
    let output = '';
    let errorOutput = '';

    proc.stdout.on('data', (data) => {
      output += data.toString();
    });

    proc.stderr.on('data', (data) => {
      errorOutput += data.toString();
    });

    proc.on('close', (code) => {
      if (code !== 0) {
        reject(new Error(`imsg history failed: ${errorOutput}`));
        return;
      }
      
      const messages = output.trim().split('\n')
        .filter(line => line)
        .map(line => JSON.parse(line));
      resolve(messages);
    });
  });
}

// Generate AI response using Anthropic API via Clawdbot config
async function generateResponse(contact, incomingMessage, messageHistory) {
  // Build conversation history for context
  const historyText = messageHistory.slice(0, 5)
    .map(m => `${m.is_from_me ? 'Me' : contact.name || contact.identifier}: ${m.text || '[attachment]'}`)
    .join('\n');

  const prompt = `${contact.prompt}

Context - Recent message history (newest first):
${historyText}

Latest message from ${contact.name || contact.identifier}:
${incomingMessage.text || '[attachment]'}

Generate a response now:`;

  // Get Anthropic API key from Clawdbot config
  const configPath = path.join(os.homedir(), '.clawdbot', 'clawdbot.json');
  let [REDACTED_SECRET];
  
  if (!apiKey && fs.existsSync(configPath)) {
    try {
      const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
      [REDACTED_SECRET] || config.providers?.anthropic?.apiKey;
    } catch (e) {
      // Ignore config parse errors
    }
  }

  if (!apiKey) {
    throw new Error('ANTHROPIC_API_KEY not found. Set it in environment or ~/.clawdbot/clawdbot.json');
  }

  // Call Anthropic API
  const payload = JSON.stringify({
    model: 'claude-sonnet-4-5',
    max_tokens: 1024,
    messages: [
      { role: 'user', content: prompt }
    ]
  });

  return new Promise((resolve, reject) => {
    const proc = spawn('curl', [
      '-s',
      'https://api.anthropic.com/v1/messages',
      '-H', 'Content-Type: application/json',
      '-H', `x-[REDACTED_SECRET]`,
      '-H', 'anthropic-version: 2023-06-01',
      '-d', payload
    ]);

    let output = '';
    let errorOutput = '';

    proc.stdout.on('data', (data) => {
      output += data.toString();
    });

    proc.stderr.on('data', (data) => {
      errorOutput += data.toString();
    });

    proc.on('close', (code) => {
      if (code !== 0) {
        reject(new Error(`API call failed: ${errorOutput}`));
        return;
      }

      try {
        const response = JSON.parse(output);
        if (response.error) {
          reject(new Error(`API error: ${response.error.message}`));
          return;
        }
        
        const text = response.content[0].text;
        resolve(text);
      } catch (e) {
        reject(new Error(`Failed to parse API response: ${e.message}`));
      }
    });
  });
}

// Send message via imsg
async function sendMessage(identifier, text) {
  return new Promise((resolve, reject) => {
    const proc = spawn('imsg', ['send', '--to', identifier, '--text', text]);
    let errorOutput = '';

    proc.stderr.on('data', (data) => {
      errorOutput += data.toString();
    });

    proc.on('close', (code) => {
      if (code !== 0) {
        reject(new Error(`imsg send failed: ${errorOutput}`));
        return;
      }
      resolve();
    });
  });
}

// Check if we should respond based on rate limiting
function shouldRespond(contact, state) {
  const now = Date.now();
  const identifier = contact.identifier;
  const minMs = (contact.minMinutesBetweenReplies || contact.defaultMinMinutesBetweenReplies || 15) * 60 * 1000;

  if (!state.lastResponses[identifier]) {
    return true;
  }

  const lastResponse = state.lastResponses[identifier];
  const elapsed = now - lastResponse;

  if (elapsed < minMs) {
    log(`Rate limit: Only ${Math.floor(elapsed / 1000 / 60)} minutes since last response to ${identifier} (need ${Math.floor(minMs / 1000 / 60)})`);
    return false;
  }

  return true;
}

// Process a new message
async function processMessage(message, config, state) {
  const contact = config.watchList.find(c => 
    c.identifier === message.identifier && c.enabled !== false
  );

  if (!contact) {
    log(`Skipping message from ${message.identifier}: not in watch list or disabled`);
    return; // Not in watch list or disabled
  }

  if (message.is_from_me) {
    return; // Don't respond to our own messages
  }

  if (!shouldRespond(contact, state)) {
    return; // Rate limited
  }

  try {
    log(`Processing message from ${contact.name || contact.identifier}: ${message.text || '[attachment]'}`);

    // Get message history for context
    const history = await getMessageHistory(message.chat_id, 20);
    
    // Generate response
    const response = await generateResponse(contact, message, history);
    log(`Generated response: ${response}`);

    // Send response
    await sendMessage(message.identifier, response);
    log(`Sent response to ${contact.name || contact.identifier}`);

    // Update state
    state.lastResponses[contact.identifier] = Date.now();
    saveState(state);

  } catch (error) {
    log(`ERROR: ${error.message}`);
  }
}

// Main watch loop
async function watch() {
  log('Starting iMessage auto-responder watcher...');
  
  const config = loadConfig();
  const state = loadState();

  if (!config.enabled) {
    log('Auto-responder is DISABLED in config. Exiting.');
    return;
  }

  if (config.watchList.length === 0) {
    log('Watch list is empty. Add contacts to the config file.');
    log(`Config: ${CONFIG_PATH}`);
    return;
  }

  log(`Watching ${config.watchList.filter(c => c.enabled !== false).length} contacts`);

  // Start watching
  const proc = spawn('imsg', ['watch', '--json']);

  proc.stdout.on('data', (data) => {
    const lines = data.toString().split('\n').filter(line => line.trim());
    
    lines.forEach(line => {
      try {
        const message = JSON.parse(line);
        log(`Received message from ${message.identifier || 'unknown'}: ${message.text || '[attachment]'} (is_from_me: ${message.is_from_me})`);
        processMessage(message, config, state);
      } catch (error) {
        log(`Failed to parse message: ${error.message}`);
      }
    });
  });

  proc.stderr.on('data', (data) => {
    log(`imsg watch error: ${data.toString()}`);
  });

  proc.on('close', (code) => {
    log(`imsg watch exited with code ${code}`);
    // Restart after a delay
    setTimeout(() => {
      log('Restarting watcher...');
      watch();
    }, 5000);
  });

  // Reload config on SIGHUP
  process.on('SIGHUP', () => {
    log('Received SIGHUP, reloading config...');
    const newConfig = loadConfig();
    Object.assign(config, newConfig);
  });
}

// Start watching
watch();


path: scripts/telegram-handler.js
content: #!/usr/bin/env node

/**
 * Telegram Command Handler for iMessage Auto-Responder
 * Wraps manage.js operations for Telegram bot integration
 */

const { spawn, execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const os = require('os');

const WORKSPACE = process.env.CLAWD_WORKSPACE || path.join(os.homedir(), 'clawd');
const CONFIG_PATH = path.join(WORKSPACE, 'imsg-autoresponder.json');
const STATE_PATH = path.join(WORKSPACE, 'data', 'imsg-autoresponder-state.json');
const LOG_PATH = path.join(WORKSPACE, 'logs', 'imsg-autoresponder.log');
const MANAGE_SCRIPT = path.join(__dirname, 'manage.js');

function loadConfig() {
  if (!fs.existsSync(CONFIG_PATH)) {
    return { enabled: true, watchList: [], defaultMinMinutesBetweenReplies: 15 };
  }
  return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
}

function loadState() {
  if (!fs.existsSync(STATE_PATH)) {
    return { lastResponses: {}, lastChecked: {}, processing: {} };
  }
  return JSON.parse(fs.readFileSync(STATE_PATH, 'utf8'));
}

function getWatcherStatus() {
  const pidFile = path.join(WORKSPACE, 'data', 'imsg-autoresponder.pid');
  
  if (!fs.existsSync(pidFile)) {
    return { running: false };
  }
  
  const pid = fs.readFileSync(pidFile, 'utf8').trim();
  
  try {
    process.kill(parseInt(pid), 0); // Check if process exists
    return { running: true, pid: parseInt(pid) };
  } catch (e) {
    return { running: false, stale: true };
  }
}

function formatContact(contact, index, state) {
  const status = contact.enabled === false ? '❌' : '✅';
  const delay = contact.minMinutesBetweenReplies !== undefined 
    ? contact.minMinutesBetweenReplies 
    : 15;
  
  const lastResponse = state.lastResponses[contact.identifier];
  const lastResponseStr = lastResponse 
    ? `\n   Last response: ${new Date(lastResponse).toLocaleString()}`
    : '';
  
  // Statistics
  const stats = state.stats && state.stats[contact.identifier];
  const statsStr = stats 
    ? `\n   📊 Total: ${stats.totalResponses} | Today: ${stats.dailyCount}`
    : '';
  
  const promptPreview = contact.prompt.length > 80 
    ? contact.prompt.substring(0, 80) + '...'
    : contact.prompt;
  
  return `**[${index + 1}] ${contact.name || 'Unnamed'}** ${status}
   📞 \`${contact.identifier}\`
   ⏱️ Delay: ${delay} min
   💬 Prompt: _${promptPreview}_${lastResponseStr}${statsStr}`;
}

function handleList() {
  const config = loadConfig();
  const state = loadState();
  const watcherStatus = getWatcherStatus();
  
  const statusEmoji = config.enabled ? '✅' : '❌';
  const watcherEmoji = watcherStatus.running ? '🟢' : '🔴';
  
  let output = `**📱 iMessage Auto-Responder**\n\n`;
  output += `System: ${statusEmoji} ${config.enabled ? 'ENABLED' : 'DISABLED'}\n`;
  output += `Watcher: ${watcherEmoji} ${watcherStatus.running ? `Running (PID ${watcherStatus.pid})` : 'Stopped'}\n`;
  output += `Default delay: ${config.defaultMinMinutesBetweenReplies} minutes\n\n`;
  
  if (config.watchList.length === 0) {
    output += '📭 No contacts in watch list.';
  } else {
    output += `**👥 Watch List (${config.watchList.length})**\n\n`;
    config.watchList.forEach((contact, i) => {
      output += formatContact(contact, i, state) + '\n\n';
    });
  }
  
  return output;
}

function handleAdd(identifier, name, prompt) {
  execSync(`node "${MANAGE_SCRIPT}" add "${identifier}" "${prompt}" "${name}"`, {
    stdio: 'inherit'
  });
  return `✅ Added **${name}** (\`${identifier}\`) to watch list.\n\nRestart watcher to apply changes.`;
}

function handleRemove(identifier) {
  const config = loadConfig();
  const contact = config.watchList.find(c => c.identifier === identifier);
  const name = contact ? contact.name : identifier;
  
  execSync(`node "${MANAGE_SCRIPT}" remove "${identifier}"`, {
    stdio: 'inherit'
  });
  return `✅ Removed **${name}** (\`${identifier}\`) from watch list.\n\nRestart watcher to apply changes.`;
}

function handleEdit(identifier, newPrompt) {
  const config = loadConfig();
  const contact = config.watchList.find(c => c.identifier === identifier);
  
  if (!contact) {
    return `❌ Contact \`${identifier}\` not found in watch list.`;
  }
  
  contact.prompt = newPrompt;
  fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
  
  return `✅ Updated prompt for **${contact.name}** (\`${identifier}\`).\n\nRestart watcher to apply changes.`;
}

function handleDelay(identifier, minutes) {
  execSync(`node "${MANAGE_SCRIPT}" set-delay "${identifier}" ${minutes}`, {
    stdio: 'inherit'
  });
  
  const config = loadConfig();
  const contact = config.watchList.find(c => c.identifier === identifier);
  const name = contact ? contact.name : identifier;
  
  return `✅ Set delay to **${minutes} minutes** for **${name}** (\`${identifier}\`).\n\nRestart watcher to apply changes.`;
}

function handleToggle() {
  execSync(`node "${MANAGE_SCRIPT}" toggle`, {
    stdio: 'inherit'
  });
  
  const config = loadConfig();
  return config.enabled 
    ? '✅ Auto-responder **ENABLED**.\n\nRestart watcher to apply changes.'
    : '⏸️ Auto-responder **DISABLED**.\n\nRestart watcher to apply changes.';
}

function handleStatus() {
  const watcherStatus = getWatcherStatus();
  const config = loadConfig();
  
  let output = `**📊 Auto-Responder Status**\n\n`;
  
  if (watcherStatus.running) {
    output += `🟢 **Watcher is RUNNING** (PID ${watcherStatus.pid})\n\n`;
    
    // Get recent log entries
    if (fs.existsSync(LOG_PATH)) {
      const logs = execSync(`tail -10 "${LOG_PATH}"`, { encoding: 'utf8' });
      output += `**Recent Activity:**\n\`\`\`\n${logs}\`\`\``;
    }
  } else {
    output += `🔴 **Watcher is STOPPED**\n\n`;
    if (watcherStatus.stale) {
      output += `⚠️ Stale PID file detected. Run restart to clean up.`;
    }
  }
  
  return output;
}

function handleHistory(identifier, limit = 10) {
  const config = loadConfig();
  const contact = config.watchList.find(c => c.identifier === identifier);
  
  if (!contact) {
    return `❌ Contact \`${identifier}\` not found in watch list.`;
  }
  
  const name = contact.name || identifier;
  
  // Parse logs for this contact
  if (!fs.existsSync(LOG_PATH)) {
    return `📭 No history found for **${name}**.`;
  }
  
  const logs = fs.readFileSync(LOG_PATH, 'utf8');
  const lines = logs.split('\n').filter(line => 
    line.includes(identifier) || line.includes(name)
  ).slice(-limit * 3); // Get more lines, then filter
  
  const responses = [];
  for (let i = 0; i < lines.length; i++) {
    if (lines[i].includes('Generated response:')) {
      const response = lines[i].split('Generated response: ')[1];
      const timestamp = lines[i].match(/\[(.*?)\]/)?.[1];
      if (response) {
        responses.push({ timestamp, response });
      }
    }
  }
  
  let output = `**💬 Recent Responses to ${name}**\n\n`;
  
  if (responses.length === 0) {
    output += `📭 No responses found yet.`;
  } else {
    responses.slice(-limit).forEach(({ timestamp, response }) => {
      const date = new Date(timestamp).toLocaleString();
      output += `**${date}**\n${response}\n\n`;
    });
  }
  
  return output;
}

async function generateTestResponse(contact, testMessage) {
  const prompt = `${contact.prompt}

Context - Recent message history (newest first):
(No history - test mode)

Latest message from ${contact.name || contact.identifier}:
${testMessage}

Generate a response now:`;

  // Get OpenAI API key from Clawdbot config
  const configPath = path.join(os.homedir(), '.clawdbot', 'clawdbot.json');
  let [REDACTED_SECRET];
  
  if (!apiKey && fs.existsSync(configPath)) {
    try {
      const clawdbotConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
      [REDACTED_SECRET]'openai-whisper-api']?.apiKey || clawdbotConfig.skills?.['openai-image-gen']?.apiKey;
    } catch (e) {
      // Ignore config parse errors
    }
  }

  if (!apiKey) {
    throw new Error('OPENAI_API_KEY not found');
  }

  const payload = JSON.stringify({
    model: 'gpt-4',
    messages: [{ role: 'user', content: prompt }],
    max_tokens: 150,
    temperature: 0.9
  });

  return new Promise((resolve, reject) => {
    const proc = spawn('curl', [
      '-s',
      'https://api.openai.com/v1/chat/completions',
      '-H', 'Content-Type: application/json',
      '-H', `Authorization: Bearer ${apiKey}`,
      '-d', payload
    ]);

    let output = '';

    proc.stdout.on('data', (data) => {
      output += data.toString();
    });

    proc.on('close', (code) => {
      if (code !== 0) {
        reject(new Error(`OpenAI API call failed`));
        return;
      }

      try {
        const response = JSON.parse(output);
        if (response.error) {
          reject(new Error(`OpenAI API error: ${response.error.message}`));
          return;
        }
        
        const text = response.choices[0].message.content.trim();
        resolve(text);
      } catch (e) {
        reject(new Error(`Failed to parse OpenAI response: ${e.message}`));
      }
    });
  });
}

async function handleTest(identifier, testMessage) {
  const config = loadConfig();
  const contact = config.watchList.find(c => c.identifier === identifier);
  
  if (!contact) {
    return `❌ Contact \`${identifier}\` not found in watch list.`;
  }
  
  const name = contact.name || identifier;
  
  try {
    const response = await generateTestResponse(contact, testMessage);
    
    return `🧪 **Test Mode: ${name}**\n\n` +
      `**Test Message:**\n_"${testMessage}"_\n\n` +
      `**Generated Response:**\n${response}\n\n` +
      `✅ This response was **NOT sent**. This is preview only.`;
  } catch (error) {
    return `🧪 **Test Mode: ${name}**\n\n` +
      `**Test Message:**\n_"${testMessage}"_\n\n` +
      `❌ Failed to generate response:\n${error.message}`;
  }
}

function handleRestart() {
  const launcherPath = path.join(__dirname, 'launcher.sh');
  
  try {
    execSync(`"${launcherPath}" restart`, { encoding: 'utf8' });
    return '✅ Watcher restarted successfully.';
  } catch (e) {
    return `❌ Failed to restart watcher:\n\`\`\`\n${e.message}\`\`\``;
  }
}

function handleSetAllDelays(minutes) {
  try {
    execSync(`node "${MANAGE_SCRIPT}" set-all-delays ${minutes}`, { encoding: 'utf8' });
    return `✅ Set delay to **${minutes} minutes** for all contacts.\n\nRestart watcher to apply changes.`;
  } catch (e) {
    return `❌ Failed to set delays:\n${e.message}`;
  }
}

function handleEnableAll() {
  try {
    execSync(`node "${MANAGE_SCRIPT}" enable-all`, { encoding: 'utf8' });
    return `✅ Enabled all contacts.\n\nRestart watcher to apply changes.`;
  } catch (e) {
    return `❌ Failed to enable all:\n${e.message}`;
  }
}

function handleDisableAll() {
  try {
    execSync(`node "${MANAGE_SCRIPT}" disable-all`, { encoding: 'utf8' });
    return `✅ Disabled all contacts.\n\nRestart watcher to apply changes.`;
  } catch (e) {
    return `❌ Failed to disable all:\n${e.message}`;
  }
}

function handleSetTimeWindow(identifier, start, end) {
  try {
    execSync(`node "${MANAGE_SCRIPT}" set-time-window "${identifier}" "${start}" "${end}"`, { encoding: 'utf8' });
    const config = loadConfig();
    const contact = config.watchList.find(c => c.identifier === identifier);
    const name = contact ? contact.name || identifier : identifier;
    return `✅ Added time window **${start} - ${end}** for ${name}.\n\nRestart watcher to apply changes.`;
  } catch (e) {
    return `❌ Failed to set time window:\n${e.message}`;
  }
}

function handleClearTimeWindows(identifier) {
  try {
    execSync(`node "${MANAGE_SCRIPT}" clear-time-windows "${identifier}"`, { encoding: 'utf8' });
    const config = loadConfig();
    const contact = config.watchList.find(c => c.identifier === identifier);
    const name = contact ? contact.name || identifier : identifier;
    return `✅ Cleared all time windows for ${name}.\n\nRestart watcher to apply changes.`;
  } catch (e) {
    return `❌ Failed to clear time windows:\n${e.message}`;
  }
}

function handleAddKeyword(identifier, keyword) {
  try {
    execSync(`node "${MANAGE_SCRIPT}" add-keyword "${identifier}" "${keyword}"`, { encoding: 'utf8' });
    const config = loadConfig();
    const contact = config.watchList.find(c => c.identifier === identifier);
    const name = contact ? contact.name || identifier : identifier;
    return `✅ Added keyword **"${keyword}"** for ${name}.\n\nRestart watcher to apply changes.`;
  } catch (e) {
    return `❌ Failed to add keyword:\n${e.message}`;
  }
}

function handleRemoveKeyword(identifier, keyword) {
  try {
    execSync(`node "${MANAGE_SCRIPT}" remove-keyword "${identifier}" "${keyword}"`, { encoding: 'utf8' });
    const config = loadConfig();
    const contact = config.watchList.find(c => c.identifier === identifier);
    const name = contact ? contact.name || identifier : identifier;
    return `✅ Removed keyword **"${keyword}"** from ${name}.\n\nRestart watcher to apply changes.`;
  } catch (e) {
    return `❌ Failed to remove keyword:\n${e.message}`;
  }
}

function handleClearKeywords(identifier) {
  try {
    execSync(`node "${MANAGE_SCRIPT}" clear-keywords "${identifier}"`, { encoding: 'utf8' });
    const config = loadConfig();
    const contact = config.watchList.find(c => c.identifier === identifier);
    const name = contact ? contact.name || identifier : identifier;
    return `✅ Cleared all keywords for ${name}.\n\nRestart watcher to apply changes.`;
  } catch (e) {
    return `❌ Failed to clear keywords:\n${e.message}`;
  }
}

function handleStats(identifier) {
  const config = loadConfig();
  const state = loadState();
  
  if (identifier) {
    // Show stats for specific contact
    const contact = config.watchList.find(c => c.identifier === identifier);
    if (!contact) {
      return `❌ Contact \`${identifier}\` not found in watch list.`;
    }
    
    const name = contact.name || identifier;
    const stats = state.stats && state.stats[identifier];
    
    if (!stats || stats.totalResponses === 0) {
      return `📊 **Stats for ${name}**\n\nNo responses sent yet.`;
    }
    
    const firstDate = new Date(stats.firstResponse).toLocaleString();
    const lastDate = new Date(stats.lastResponse).toLocaleString();
    const daysSince = Math.floor((Date.now() - stats.firstResponse) / (1000 * 60 * 60 * 24));
    const avgPerDay = daysSince > 0 ? (stats.totalResponses / daysSince).toFixed(1) : stats.totalResponses;
    
    return `📊 **Stats for ${name}**\n\n` +
      `**Total Responses:** ${stats.totalResponses}\n` +
      `**Today:** ${stats.dailyCount}\n` +
      `**Average per day:** ${avgPerDay}\n` +
      `**First response:** ${firstDate}\n` +
      `**Last response:** ${lastDate}`;
  } else {
    // Show stats for all contacts
    let output = `📊 **Auto-Responder Statistics**\n\n`;
    
    let totalAll = 0;
    let todayAll = 0;
    
    config.watchList.forEach(contact => {
      const stats = state.stats && state.stats[contact.identifier];
      if (stats && stats.totalResponses > 0) {
        const name = contact.name || contact.identifier;
        output += `**${name}**\n`;
        output += `   Total: ${stats.totalResponses} | Today: ${stats.dailyCount}\n\n`;
        totalAll += stats.totalResponses;
        todayAll += stats.dailyCount;
      }
    });
    
    if (totalAll === 0) {
      output += `No responses sent yet.`;
    } else {
      output += `**Grand Total:** ${totalAll} responses\n`;
      output += `**Grand Today:** ${todayAll} responses`;
    }
    
    return output;
  }
}

function handleSetDailyCap(identifier, maxReplies) {
  try {
    execSync(`node "${MANAGE_SCRIPT}" set-daily-cap "${identifier}" ${maxReplies}`, { encoding: 'utf8' });
    const config = loadConfig();
    const contact = config.watchList.find(c => c.identifier === identifier);
    const name = contact ? contact.name || identifier : identifier;
    
    if (parseInt(maxReplies) === 0) {
      return `✅ Removed daily cap for ${name} (unlimited).\n\nRestart watcher to apply changes.`;
    } else {
      return `✅ Set daily cap to **${maxReplies} replies/day** for ${name}.\n\nRestart watcher to apply changes.`;
    }
  } catch (e) {
    return `❌ Failed to set daily cap:\n${e.message}`;
  }
}

// CLI interface
(async () => {
  const [,, command, ...args] = process.argv;

  try {
    let output = '';
    
    switch (command) {
      case 'list':
        output = handleList();
        break;
      
      case 'add':
        if (args.length < 3) {
          console.error('Usage: telegram-handler.js add <number> <name> <prompt>');
          process.exit(1);
        }
        output = handleAdd(args[0], args[1], args.slice(2).join(' '));
        break;
      
      case 'remove':
        if (args.length < 1) {
          console.error('Usage: telegram-handler.js remove <number>');
          process.exit(1);
        }
        output = handleRemove(args[0]);
        break;
      
      case 'edit':
        if (args.length < 2) {
          console.error('Usage: telegram-handler.js edit <number> <prompt>');
          process.exit(1);
        }
        output = handleEdit(args[0], args.slice(1).join(' '));
        break;
      
      case 'delay':
        if (args.length < 2) {
          console.error('Usage: telegram-handler.js delay <number> <minutes>');
          process.exit(1);
        }
        output = handleDelay(args[0], parseInt(args[1]));
        break;
      
      case 'toggle':
        output = handleToggle();
        break;
      
      case 'status':
        output = handleStatus();
        break;
      
      case 'history':
        if (args.length < 1) {
          console.error('Usage: telegram-handler.js history <number> [limit]');
          process.exit(1);
        }
        output = handleHistory(args[0], args[1] ? parseInt(args[1]) : 10);
        break;
      
      case 'test':
        if (args.length < 2) {
          console.error('Usage: telegram-handler.js test <number> <message>');
          process.exit(1);
        }
        output = await handleTest(args[0], args.slice(1).join(' '));
        break;
      
      case 'restart':
        output = handleRestart();
        break;
      
      case 'set-all-delays':
        if (args.length < 1) {
          console.error('Usage: telegram-handler.js set-all-delays <minutes>');
          process.exit(1);
        }
        output = handleSetAllDelays(parseInt(args[0]));
        break;
      
      case 'enable-all':
        output = handleEnableAll();
        break;
      
      case 'disable-all':
        output = handleDisableAll();
        break;
      
      case 'set-time-window':
        if (args.length < 3) {
          console.error('Usage: telegram-handler.js set-time-window <number> <start> <end>');
          process.exit(1);
        }
        output = handleSetTimeWindow(args[0], args[1], args[2]);
        break;
      
      case 'clear-time-windows':
        if (args.length < 1) {
          console.error('Usage: telegram-handler.js clear-time-windows <number>');
          process.exit(1);
        }
        output = handleClearTimeWindows(args[0]);
        break;
      
      case 'add-keyword':
        if (args.length < 2) {
          console.error('Usage: telegram-handler.js add-keyword <number> <keyword>');
          process.exit(1);
        }
        output = handleAddKeyword(args[0], args[1]);
        break;
      
      case 'remove-keyword':
        if (args.length < 2) {
          console.error('Usage: telegram-handler.js remove-keyword <number> <keyword>');
          process.exit(1);
        }
        output = handleRemoveKeyword(args[0], args[1]);
        break;
      
      case 'clear-keywords':
        if (args.length < 1) {
          console.error('Usage: telegram-handler.js clear-keywords <number>');
          process.exit(1);
        }
        output = handleClearKeywords(args[0]);
        break;
      
      case 'stats':
        output = handleStats(args[0]); // Optional contact identifier
        break;
      
      case 'set-daily-cap':
        if (args.length < 2) {
          console.error('Usage: telegram-handler.js set-daily-cap <number> <max_replies>');
          process.exit(1);
        }
        output = handleSetDailyCap(args[0], parseInt(args[1]));
        break;
      
      default:
        console.error(`Unknown command: ${command}`);
        process.exit(1);
    }
    
    console.log(output);
    
  } catch (error) {
    console.error(`Error: ${error.message}`);
    process.exit(1);
  }
})();


path: config/autoresponder.example.json
content: {
  "enabled": true,
  "defaultMinMinutesBetweenReplies": 15,
  "watchList": [
    {
      "identifier": "+15551234567",
      "name": "Example Contact",
      "prompt": "You are Drac's AI. You speak like Drac. Sound human, casual, confident, and natural. Be polite but blunt. Say the thing directly. Keep replies concise.",
      "minMinutesBetweenReplies": 10,
      "maxRepliesPerDay": 20,
      "timeWindows": [
        {"start": "09:00", "end": "22:00"}
      ],
      "keywords": ["urgent", "help", "emergency"],
      "enabled": true
    }
  ]
}


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: suspicious | SkillSpector: suspicious (CRITICAL)