Skill: dylanbaker24/linkedin-monitor (version 1.1.0)

--- name: linkedin-monitor description: Bulletproof LinkedIn inbox monitoring with progressive autonomy. Monitors messages hourly, drafts replies in your voice, and alerts you to new conversations. Supports 4 autonomy levels from monitor-only to full autonomous. version: 1.0.0 author: Dylan Baker / lilAgents --- # LinkedIn Monitor Reliable LinkedIn inbox monitoring for Clawdbot. ## Features - **Hourly monitoring** — Checks inbox every hour, 24/7 - **Deterministic state** — No duplicate notifications, ever - **Progressive autonomy** — Start supervised, graduate to autonomous - **Health checks** — Alerts when auth expires or things break - **Your voice** — Drafts replies using your communication style ## Quick Start ```bash # 1. Setup (interactive) linkedin-monitor setup # 2. Verify health linkedin-monitor health # 3. Run manually (test) linkedin-monitor check # 4. Enable cron (hourly) linkedin-monitor enable ``` ## Autonomy Levels | Level | Name | Behavior | |-------|------|----------| | 0 | Monitor Only | Alerts to new messages only | | 1 | Draft + Approve | Drafts replies, waits for approval | | 2 | Auto-Reply Simple | Auto-handles acknowledgments, scheduling | | 3 | Full Autonomous | Replies as you, books meetings, networks | **Default: Level 1** — Change with `linkedin-monitor config autonomyLevel 2` ## Commands ```bash linkedin-monitor setup # Interactive setup wizard linkedin-monitor health # Check auth status linkedin-monitor check # Run one check cycle linkedin-monitor enable # Enable hourly cron linkedin-monitor disable # Disable cron linkedin-monitor status # Show current state linkedin-monitor config # View/edit configuration linkedin-monitor logs # View recent activity linkedin-monitor reset # Clear state (start fresh) ``` ## Configuration Location: `~/.clawdbot/linkedin-monitor/config.json` ```json { "autonomyLevel": 1, "alertChannel": "discord", "alertChannelId": "YOUR_CHANNEL_ID", "calendarLink": "cal.com/yourname", "communicationStyleFile": "USER.md", "timezone": "America/New_York", "schedule": "0 * * * *", "morningDigest": { "enabled": true, "hour": 9, "timezone": "Asia/Bangkok" }, "safetyLimits": { "maxMessagesPerDay": 50, "escalationKeywords": ["angry", "legal", "refund"], "dailyDigest": true } } ``` ## How It Works ### Monitoring Flow ``` 1. Health Check └── Verify LinkedIn auth (lk CLI) 2. Fetch Messages └── lk message list --json 3. Compare State └── Filter: only messages not in state file 4. For Each New Message ├── Level 0: Alert only ├── Level 1: Draft reply → Alert → Wait for approval ├── Level 2: Simple = auto-reply, Complex = draft └── Level 3: Full autonomous response 5. Update State └── Record message IDs (prevents duplicates) ``` ### State Management State is managed by scripts, not the LLM. This guarantees: - No duplicate notifications - Consistent behavior across sessions - Visible state for debugging State files: `~/.clawdbot/linkedin-monitor/state/` ## Sending Approved Messages When at Level 1, approve drafts with: ``` send [name] # Send draft to [name] send all # Send all pending drafts edit [name] [text] # Edit draft before sending skip [name] # Discard draft ``` ## Troubleshooting ### "Auth expired" ```bash lk auth login linkedin-monitor health ``` ### "No messages found" ```bash linkedin-monitor check --debug ``` ### Duplicate notifications ```bash linkedin-monitor reset # Clear state linkedin-monitor check # Fresh start ``` ## Dependencies - `lk` CLI (LinkedIn CLI) — `npm install -g lk` - `jq` (JSON processor) — `brew install jq` ## Files ``` ~/.clawdbot/linkedin-monitor/ ├── config.json # Your configuration ├── state/ │ ├── messages.json # Seen message IDs │ ├── lastrun.txt # Last check timestamp │ └── drafts.json # Pending drafts └── logs/ └── activity.log # Activity history ```

path: CRON-PAYLOAD.md
content: # LinkedIn Monitor Cron Payload

This is the instruction set for the hourly LinkedIn monitoring cron.

## Cron Payload (copy this for cron setup)

```
LinkedIn inbox check (browser-based).

STEP 1: Read state file
- Read ~/.clawdbot/linkedin-monitor/state/messages.json
- Extract seenIds list

STEP 2: Check LinkedIn
- Use browser tool (profile: clawd) - DO NOT CLOSE IT
- Navigate to linkedin.com/messaging/ if not already there
- Take snapshot

STEP 3: Parse conversations
- Extract each conversation from snapshot
- For each: name, last message preview, timestamp, isFromMe
- Identify INBOUND messages (where last message is NOT from me)

STEP 4: Compare against state
- For each inbound message, create unique ID: {name}_{timestamp}
- Check if ID exists in seenIds
- Collect only NEW messages (ID not in seenIds)

STEP 5: If NEW messages found
- Draft reply for each using USER.md communication style
- Read alertChannel and alertTarget from ~/.clawdbot/linkedin-monitor/config.json
- Post to the configured channel (Discord, Telegram, Slack, WhatsApp, etc.):
  Format:
  📬 **{Name}**
  > {message preview}
  
  **Draft reply:**
  > {your drafted reply}
  
  Reply "send {name}" to approve.

STEP 6: Update state
- Add new message IDs to seenIds
- Update lastCheck timestamp
- Write to ~/.clawdbot/linkedin-monitor/state/messages.json

STEP 7: If NO new messages
- Stay silent (no message to any channel)
- Still update lastCheck timestamp

IMPORTANT:
- DO NOT close the clawd browser
- DO NOT report messages already in seenIds
- Each message reported ONCE only
```

## Example State File

```json
{
  "seenIds": [
    "Marissa Campbell_4:43 AM",
    "Karan (Kavnit)_Jan 27"
  ],
  "lastCheck": "2026-01-28T16:47:00Z"
}
```


path: README.md
content: # LinkedIn Monitor for Clawdbot

**Bulletproof LinkedIn inbox monitoring with zero duplicate notifications.**

Get notified when someone messages you on LinkedIn. Drafts replies in your voice. Never miss a lead.

![License](https://img.shields.io/badge/license-MIT-blue.svg)
![Clawdbot](https://img.shields.io/badge/clawdbot-skill-orange.svg)

---

## Features

- 🔔 **Hourly monitoring** — Checks your inbox every hour, 24/7
- 🚫 **No duplicates** — Each message reported exactly once (file-based state)
- ✍️ **Draft replies** — AI drafts responses in your communication style
- 🛡️ **Approval required** — Nothing sent without your OK
- 🌐 **Browser-based** — Works with your normal LinkedIn login
- ⚡ **Watchdog included** — Alerts you if monitoring stops working

---

## Quick Install

```bash
# 1. Install the skill
clawdhub install linkedin-monitor

# 2. Run setup
linkedin-monitor setup

# 3. Verify everything works
linkedin-monitor health

# 4. Enable hourly monitoring
linkedin-monitor enable
```

---

## Requirements

- [Clawdbot](https://github.com/clawdbot/clawdbot) installed and running
- A browser profile logged into LinkedIn
- A channel for alerts (Discord, Telegram, Slack, WhatsApp, or any Clawdbot channel)

---

## Setup Guide

### Step 1: Create a Browser Profile

LinkedIn Monitor uses Clawdbot's browser tool to check your inbox. You need a browser profile that's logged into LinkedIn.

**Option A: Use the default "clawd" profile**
```bash
# Start the clawd browser
clawdbot browser start --profile clawd

# This opens a Chrome window. Log into LinkedIn manually.
# Keep this browser open — it needs to stay running.
```

**Option B: Create a custom profile**
```bash
# Create a profile named "linkedin"
clawdbot browser start --profile linkedin

# Log into LinkedIn, then keep it open
```

> **Tip:** Put the browser on a second desktop/workspace so it doesn't interfere with your main browsing.

### Step 2: Run Setup

```bash
linkedin-monitor setup
```

This will ask you for:
- **Discord channel ID** — Where to send alerts (right-click channel → Copy ID)
- **Calendar link** — For booking meetings (e.g., cal.com/yourname)
- **Timezone** — Your timezone (e.g., America/New_York)

Configuration is saved to `~/.clawdbot/linkedin-monitor/config.json`

### Step 3: Verify Health

```bash
linkedin-monitor health
```

You should see:
```
Dependencies:
✓ jq installed
✓ Browser profile ready

Authentication:
✓ LinkedIn logged in as: Your Name

Configuration:
✓ Config file exists
✓ Alert channel configured

Automation:
! Cron job not installed — run: linkedin-monitor enable
```

### Step 4: Test Manually

```bash
linkedin-monitor check
```

This runs one check cycle. If you have unread messages, it will report them.

### Step 5: Enable Hourly Monitoring

```bash
linkedin-monitor enable
```

This creates a cron job that runs every hour to check your inbox.

---

## How It Works

```
Every Hour:
┌─────────────────────────────────────────┐
│ 1. Check browser is running             │
│ 2. Open LinkedIn messaging tab          │
│ 3. Take snapshot of inbox               │
│ 4. Compare against seen messages        │
│ 5. If NEW message:                      │
│    → Draft reply in your voice          │
│    → Alert you on Discord               │
│    → Wait for approval                  │
│ 6. Update state file                    │
└─────────────────────────────────────────┘
```

**State is tracked in:** `~/.clawdbot/linkedin-monitor/state/messages.json`

Each message gets a unique ID (`Name_Timestamp`). Once reported, it's added to `seenIds` and never reported again.

---

## Commands

| Command | Description |
|---------|-------------|
| `linkedin-monitor setup` | Interactive setup wizard |
| `linkedin-monitor health` | Check dependencies and auth |
| `linkedin-monitor check` | Run one monitoring cycle |
| `linkedin-monitor enable` | Enable hourly cron |
| `linkedin-monitor disable` | Disable cron |
| `linkedin-monitor status` | Show current state |
| `linkedin-monitor config` | View/edit configuration |
| `linkedin-monitor logs` | View activity logs |
| `linkedin-monitor reset` | Clear state (fresh start) |

---

## Approving Messages

When a new message arrives, you'll get an alert like:

```
📬 **John Smith**
> Hey, I saw your post about AI automation. Would love to chat!

**Draft reply:**
> Hey John, thanks for reaching out. Happy to connect — what specifically caught your interest?

Reply "send John" to approve.
```

**Commands:**
- `send John` — Send the draft as-is
- `send all` — Send all pending drafts
- `edit John Hey John, let's set up a call...` — Edit and send
- `skip John` — Discard the draft

---

## Configuration

Edit `~/.clawdbot/linkedin-monitor/config.json`:

```json
{
  "autonomyLevel": 1,
  "alertChannel": "discord",
  "alertTarget": "YOUR_CHANNEL_ID",
  "calendarLink": "cal.com/yourname",
  "communicationStyleFile": "USER.md",
  "timezone": "America/New_York",
  "schedule": "0 * * * *",
  "browserProfile": "clawd"
}
```

### Supported Channels

| Channel | alertChannel | alertTarget |
|---------|--------------|-------------|
| Discord | `discord` | Channel ID |
| Telegram | `telegram` | Chat ID |
| Slack | `slack` | Channel ID or name |
| WhatsApp | `whatsapp` | Chat ID or phone |
| Signal | `signal` | Phone number |

### Autonomy Levels

| Level | Behavior |
|-------|----------|
| 0 | Monitor only — alerts, no drafts |
| 1 | Draft + Approve — drafts replies, waits for your OK |
| 2 | Auto-reply simple — handles "thanks", scheduling automatically |
| 3 | Full autonomous — replies as you, books meetings |

**Default is Level 1.** Change with:
```bash
linkedin-monitor config autonomyLevel 2
```

---

## Communication Style

Drafts are generated using your communication style from `USER.md` in your Clawdbot workspace.

If you don't have a `USER.md`, create one with your preferences:

```markdown
# USER.md

## Communication Style
- Keep messages short (3-4 sentences)
- Be friendly but professional
- No corporate jargon
- Always offer value first
```

---

## Troubleshooting

### "Browser not running"
```bash
clawdbot browser start --profile clawd
# Then log into LinkedIn
```

### "LinkedIn logged out"
Open the clawd browser and log back into LinkedIn manually. The browser is usually on your second desktop.

### "Duplicate notifications"
```bash
linkedin-monitor reset
# This clears state — next check will see all messages as "new"
```

### "Cron not running"
```bash
linkedin-monitor disable
linkedin-monitor enable
# Check: crontab -l
```

### "Watchdog alert: Monitor may be down"
```bash
linkedin-monitor health
# Fix whatever's broken, then:
linkedin-monitor check
```

---

## Files

```
~/.clawdbot/linkedin-monitor/
├── config.json          # Your settings
├── state/
│   └── messages.json    # Seen message IDs
└── logs/
    └── activity.log     # Activity history
```

---

## Uninstall

```bash
# Disable cron
linkedin-monitor disable

# Remove the skill
clawdhub uninstall linkedin-monitor

# (Optional) Remove data
rm -rf ~/.clawdbot/linkedin-monitor
```

---

## Contributing

Found a bug? Have a feature request? Open an issue on GitHub.

---

## License

MIT License — use it however you want.

---

## Credits

Built by [Dylan Baker](https://github.com/dylanbaker) / [lilAgents](https://lilagents.com)

Part of the [Clawdbot](https://clawd.bot) ecosystem.


path: docs/SETUP.md
content: # LinkedIn Monitor Setup Guide

## Prerequisites

1. **Clawdbot** installed and running
2. **jq** installed (`brew install jq` on macOS)
3. A **Discord server** with a channel for alerts

## Step-by-Step Setup

### 1. Install the Skill

```bash
clawdhub install linkedin-monitor
```

Or clone manually:
```bash
cd ~/.clawdbot/skills
git clone https://github.com/dylanbaker/linkedin-monitor
```

### 2. Set Up Browser Profile

The monitor uses Clawdbot's browser tool to check your LinkedIn inbox.

```bash
# Start the browser
clawdbot browser start --profile clawd
```

A Chrome window will open. **Log into LinkedIn** and leave the browser open.

> **Important:** This browser needs to stay running 24/7. Put it on a second desktop/workspace.

### 3. Get Your Discord Channel ID

1. Open Discord
2. Go to Settings → Advanced → Enable "Developer Mode"
3. Right-click the channel where you want alerts
4. Click "Copy ID"

### 4. Run Setup

```bash
linkedin-monitor setup
```

Enter:
- Your Discord channel ID
- Your calendar link (optional)
- Your timezone

### 5. Verify Health

```bash
linkedin-monitor health
```

All checks should pass:
```
✓ jq installed
✓ Browser profile ready
✓ LinkedIn logged in
✓ Config file exists
✓ Alert channel configured
```

### 6. Test Manually

```bash
linkedin-monitor check
```

This runs one monitoring cycle. You should see either:
- "No new messages" (silent)
- An alert in your Discord channel

### 7. Enable Hourly Monitoring

```bash
linkedin-monitor enable
```

Done! The monitor will now check your inbox every hour.

## Verify It's Working

Check cron is installed:
```bash
crontab -l | grep linkedin
```

Check recent activity:
```bash
linkedin-monitor logs
```

Check status:
```bash
linkedin-monitor status
```

## What Happens Next

1. Every hour, the monitor checks your LinkedIn inbox
2. If someone new messaged you, you get a Discord alert
3. The alert includes a draft reply in your communication style
4. Reply "send [name]" to approve and send
5. Or edit/skip as needed

## Need Help?

- Run `linkedin-monitor health` to diagnose issues
- Check `linkedin-monitor logs` for errors
- See TROUBLESHOOT.md for common problems


path: docs/TROUBLESHOOT.md
content: # Troubleshooting LinkedIn Monitor

## Common Issues

### "Browser not running"

**Problem:** The clawd browser isn't running.

**Solution:**
```bash
clawdbot browser start --profile clawd
```

Then log into LinkedIn if needed.

### "LinkedIn logged out"

**Problem:** Your LinkedIn session expired.

**Solution:**
1. Find the clawd browser (usually on second desktop)
2. Log back into LinkedIn manually
3. Run `linkedin-monitor health` to verify

### "Config not found"

**Problem:** Setup hasn't been run.

**Solution:**
```bash
linkedin-monitor setup
```

### "Alert channel not configured"

**Problem:** Discord channel ID is missing.

**Solution:**
```bash
linkedin-monitor setup
# Or edit directly:
linkedin-monitor config alertChannelId YOUR_CHANNEL_ID
```

### "Cron not installed"

**Problem:** Hourly monitoring isn't enabled.

**Solution:**
```bash
linkedin-monitor enable
```

### "Duplicate notifications"

**Problem:** Same message reported multiple times.

**Solution:** This shouldn't happen with the new system, but if it does:
```bash
linkedin-monitor reset
```

This clears the state file. Next check will treat all messages as new.

### "Watchdog alert: Monitor may be down"

**Problem:** The hourly check hasn't run in 2+ hours.

**Solution:**
1. Run `linkedin-monitor health` to find the issue
2. Fix whatever's broken (usually browser or auth)
3. Run `linkedin-monitor check` to restart

### "No messages appearing"

**Problem:** Monitor runs but never reports anything.

**Possible causes:**
1. No one has messaged you (check LinkedIn directly)
2. Browser is on wrong page (should be linkedin.com/messaging/)
3. State file has all messages marked as seen

**Solution:**
```bash
# Check what the monitor sees
linkedin-monitor check --debug

# If needed, reset state
linkedin-monitor reset
```

## Debug Mode

Run with debug output:
```bash
DEBUG=1 linkedin-monitor check
```

This shows detailed logs of what's happening.

## View Logs

```bash
# Recent activity
linkedin-monitor logs

# Full log file
cat ~/.clawdbot/linkedin-monitor/logs/activity.log
```

## Reset Everything

Nuclear option — start fresh:
```bash
linkedin-monitor disable
linkedin-monitor reset
rm -rf ~/.clawdbot/linkedin-monitor
linkedin-monitor setup
linkedin-monitor enable
```

## Still Stuck?

1. Check Clawdbot is running: `clawdbot status`
2. Check browser is running: `clawdbot browser status --profile clawd`
3. Check cron is installed: `crontab -l | grep linkedin`
4. Open an issue on GitHub with your `linkedin-monitor health` output


path: package.json
content: {
  "name": "linkedin-monitor",
  "version": "1.0.0",
  "description": "Bulletproof LinkedIn inbox monitoring with zero duplicate notifications",
  "author": "Dylan Baker <[REDACTED_SECRET]>",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/dylanbaker/linkedin-monitor"
  },
  "keywords": [
    "clawdbot",
    "skill",
    "linkedin",
    "automation",
    "inbox",
    "monitoring",
    "social-media"
  ],
  "clawdbot": {
    "skillType": "automation",
    "minVersion": "1.0.0",
    "requires": {
      "tools": ["browser", "cron", "message"],
      "bins": ["jq"]
    }
  },
  "files": [
    "SKILL.md",
    "README.md",
    "scripts/",
    "templates/",
    "docs/"
  ]
}


path: scripts/check-browser.sh
content: #!/bin/bash
# Browser-based LinkedIn inbox check
# Uses Clawdbot browser tool instead of lk CLI

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_DIR="${HOME}/.clawdbot/linkedin-monitor"
STATE_DIR="${CONFIG_DIR}/state"
MESSAGES_FILE="${STATE_DIR}/messages.json"
LOG_DIR="${CONFIG_DIR}/logs"
LOG_FILE="${LOG_DIR}/activity.log"

# Ensure directories exist
mkdir -p "${STATE_DIR}" "${LOG_DIR}"

# Initialize state file if not exists
if [ ! -f "${MESSAGES_FILE}" ]; then
    echo '{"seenIds":[],"lastCheck":""}' > "${MESSAGES_FILE}"
fi

log() {
    local level="$1"
    shift
    local msg="$*"
    local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
    echo "[${timestamp}] [${level}] ${msg}" >> "${LOG_FILE}"
}

log "INFO" "Starting browser-based LinkedIn check"

# The actual browser check is done by Clawdbot
# This script is called by the cron, which then invokes Clawdbot
# to do the browser work and parse results

# Output instruction for Clawdbot cron
cat << 'EOF'
LINKEDIN_BROWSER_CHECK
Instructions for Clawdbot:
1. Use browser tool (profile: clawd) to navigate to linkedin.com/messaging/
2. Take snapshot
3. Parse conversation list for inbound messages (not from me)
4. Compare against state file: ~/.clawdbot/linkedin-monitor/state/messages.json
5. For each NEW inbound message:
   - Extract: name, message preview, timestamp
   - Add to results
   - Mark as seen in state file
6. If new messages found:
   - Draft replies using USER.md communication style
   - Post to #linkedin channel
7. Update lastCheck timestamp in state file
EOF


path: scripts/check.sh
content: #!/bin/bash
# Main monitoring script for linkedin-monitor
# Fetches messages, compares against state, outputs only NEW messages

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_DIR="${HOME}/.clawdbot/linkedin-monitor"
CONFIG_FILE="${CONFIG_DIR}/config.json"
LOG_DIR="${CONFIG_DIR}/logs"
LOG_FILE="${LOG_DIR}/activity.log"

# Source state management
source "${SCRIPT_DIR}/state.sh"

# Ensure directories exist
mkdir -p "${LOG_DIR}"
init_state

# Logging
log() {
    local level="$1"
    shift
    local msg="$*"
    local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
    echo "[${timestamp}] [${level}] ${msg}" >> "${LOG_FILE}"
    
    if [ "${DEBUG:-}" = "1" ]; then
        echo "[${level}] ${msg}" >&2
    fi
}

# Parse arguments
DEBUG=0
DRY_RUN=0
while [[ $# -gt 0 ]]; do
    case $1 in
        --debug) DEBUG=1; shift ;;
        --dry-run) DRY_RUN=1; shift ;;
        *) shift ;;
    esac
done

log "INFO" "Starting LinkedIn check"

# 1. Health check - verify auth
log "INFO" "Checking authentication..."
if ! command -v lk &> /dev/null; then
    log "ERROR" "lk CLI not installed"
    echo '{"error": "lk CLI not installed", "action": "install lk: npm install -g lk"}'
    exit 1
fi

if ! lk profile me --json 2>/dev/null | jq -e '.id' &>/dev/null; then
    log "ERROR" "LinkedIn auth expired"
    echo '{"error": "LinkedIn auth expired", "action": "run: lk auth login"}'
    exit 1
fi

log "INFO" "Auth OK"

# 2. Fetch messages
log "INFO" "Fetching messages..."
MESSAGES_RAW=$(lk message list --json 2>/dev/null) || {
    log "ERROR" "Failed to fetch messages"
    echo '{"error": "Failed to fetch messages", "details": "lk message list failed"}'
    exit 1
}

# 3. Parse messages and extract relevant info
# lk message list returns array of conversations
CONVERSATIONS=$(echo "${MESSAGES_RAW}" | jq -c '
    [.[] | {
        id: .conversationId,
        participantName: (.participants[0].firstName + " " + .participants[0].lastName),
        participantId: .participants[0].profileId,
        lastMessage: .lastMessage.text,
        lastMessageTime: .lastMessage.createdAt,
        lastMessageFromMe: .lastMessage.fromMe,
        unread: .unread
    }]
')

log "INFO" "Found $(echo "${CONVERSATIONS}" | jq 'length') conversations"

# 4. Filter to only inbound messages (not from me) that we haven't seen
NEW_MESSAGES="[]"
while IFS= read -r conv; do
    CONV_ID=$(echo "$conv" | jq -r '.id')
    FROM_ME=$(echo "$conv" | jq -r '.lastMessageFromMe')
    
    # Skip if last message is from me
    if [ "$FROM_ME" = "true" ]; then
        continue
    fi
    
    # Skip if we've already seen this conversation ID with this message
    # We use conversationId + lastMessageTime as unique key
    MSG_TIME=$(echo "$conv" | jq -r '.lastMessageTime')
    UNIQUE_KEY="${CONV_ID}_${MSG_TIME}"
    
    if is_seen "$UNIQUE_KEY"; then
        log "DEBUG" "Already seen: ${UNIQUE_KEY}"
        continue
    fi
    
    # This is a new inbound message!
    log "INFO" "New message from: $(echo "$conv" | jq -r '.participantName')"
    NEW_MESSAGES=$(echo "$NEW_MESSAGES" | jq --argjson conv "$conv" '. += [$conv]')
    
    # Mark as seen (unless dry run)
    if [ "$DRY_RUN" = "0" ]; then
        mark_seen "$UNIQUE_KEY"
    fi
    
done < <(echo "${CONVERSATIONS}" | jq -c '.[]')

# 5. Update last check timestamp
if [ "$DRY_RUN" = "0" ]; then
    update_lastcheck
fi

# 6. Output results
NEW_COUNT=$(echo "$NEW_MESSAGES" | jq 'length')
log "INFO" "Check complete. New messages: ${NEW_COUNT}"

if [ "$NEW_COUNT" -eq 0 ]; then
    # No new messages - output nothing (silent)
    log "INFO" "No new messages"
    exit 0
else
    # Output new messages as JSON
    echo "$NEW_MESSAGES" | jq -c '{
        newMessages: .,
        count: (. | length),
        timestamp: now | todate
    }'
fi


path: scripts/cron-wrapper.sh
content: #!/bin/bash
# Cron wrapper for linkedin-monitor
# Runs the check and sends results to Clawdbot if new messages found

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_DIR="${HOME}/.clawdbot/linkedin-monitor"
CONFIG_FILE="${CONFIG_DIR}/config.json"

# Load config
if [ ! -f "${CONFIG_FILE}" ]; then
    echo "Config not found. Run: linkedin-monitor setup"
    exit 1
fi

CHANNEL_ID=$(jq -r '.alertChannelId // ""' "${CONFIG_FILE}")
AUTONOMY_LEVEL=$(jq -r '.autonomyLevel // 1' "${CONFIG_FILE}")

# Run the check
RESULT=$(bash "${SCRIPT_DIR}/check.sh" 2>&1) || {
    # Check failed - might be an error
    if echo "$RESULT" | jq -e '.error' &>/dev/null; then
        ERROR_MSG=$(echo "$RESULT" | jq -r '.error')
        ACTION=$(echo "$RESULT" | jq -r '.action // "check logs"')
        
        # Alert about the error
        if [ -n "${CHANNEL_ID}" ]; then
            # Send error alert to Clawdbot
            # This uses the Clawdbot message format
            cat << EOF
LINKEDIN_MONITOR_ERROR
Channel: ${CHANNEL_ID}
Error: ${ERROR_MSG}
Action: ${ACTION}
EOF
        fi
    fi
    exit 1
}

# If no output, no new messages - exit silently
if [ -z "${RESULT}" ]; then
    exit 0
fi

# Parse the result
NEW_COUNT=$(echo "$RESULT" | jq -r '.count // 0')

if [ "$NEW_COUNT" -eq 0 ]; then
    exit 0
fi

# We have new messages!
echo "Found ${NEW_COUNT} new message(s)"

# Format the messages for Clawdbot
MESSAGES=$(echo "$RESULT" | jq -r '.newMessages[]')

# Build the alert message
ALERT="**LinkedIn Inbox Update**\n\n"

while IFS= read -r msg; do
    NAME=$(echo "$msg" | jq -r '.participantName')
    TEXT=$(echo "$msg" | jq -r '.lastMessage')
    TIME=$(echo "$msg" | jq -r '.lastMessageTime')
    
    ALERT="${ALERT}📬 **${NAME}**\n"
    ALERT="${ALERT}> ${TEXT}\n\n"
done < <(echo "$RESULT" | jq -c '.newMessages[]')

# Output for Clawdbot cron to pick up
# The cron system will see this output and process it
cat << EOF
LINKEDIN_MONITOR_ALERT
Channel: ${CHANNEL_ID}
AutonomyLevel: ${AUTONOMY_LEVEL}
NewCount: ${NEW_COUNT}
---
${ALERT}
---
Messages: ${RESULT}
EOF


path: scripts/ensure-browser.sh
content: #!/bin/bash
# Ensure clawd browser is running with LinkedIn tab open
# Run this on startup or if browser health check fails

echo "LinkedIn Monitor - Browser Setup"
echo "================================="

# This script outputs instructions for Clawdbot to execute
# It doesn't directly control the browser (that's done via browser tool)

cat << 'EOF'
BROWSER_SETUP_INSTRUCTIONS:

1. Check browser status:
   browser action=status profile=clawd

2. If not running, start it:
   browser action=start profile=clawd

3. Check tabs for LinkedIn:
   browser action=tabs profile=clawd

4. If no LinkedIn tab, open one:
   browser action=open profile=clawd targetUrl=https://www.linkedin.com/messaging/

5. Verify LinkedIn is logged in:
   - Take snapshot
   - Check for "Messaging" heading (logged in) vs "Sign in" (logged out)

6. If logged out:
   - Alert user to log in manually
   - Browser is on second desktop

IMPORTANT:
- The clawd browser should ALWAYS stay open
- LinkedIn tab should ALWAYS stay open
- Do NOT close the browser after checks
- Do NOT close tabs
EOF


path: scripts/health.sh
content: #!/bin/bash
# Health check for linkedin-monitor
# Verifies all dependencies and authentication status

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_DIR="${HOME}/.clawdbot/linkedin-monitor"
CONFIG_FILE="${CONFIG_DIR}/config.json"

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# Check result tracking
ERRORS=0
WARNINGS=0

check_pass() {
    echo -e "${GREEN}✓${NC} $1"
}

check_fail() {
    echo -e "${RED}✗${NC} $1"
    ((ERRORS++))
}

check_warn() {
    echo -e "${YELLOW}!${NC} $1"
    ((WARNINGS++))
}

echo "LinkedIn Monitor Health Check"
echo "=============================="
echo ""

# 1. Check jq installed
echo "Dependencies:"
if command -v jq &> /dev/null; then
    check_pass "jq installed ($(jq --version))"
else
    check_fail "jq not installed — run: brew install jq"
fi

# 2. Check lk CLI installed
if command -v lk &> /dev/null; then
    check_pass "lk CLI installed"
else
    check_fail "lk CLI not installed — run: npm install -g lk"
fi

echo ""

# 3. Check lk authentication
echo "Authentication:"
if command -v lk &> /dev/null; then
    # Try to get profile to verify auth
    if lk profile me --json 2>/dev/null | jq -e '.id' &>/dev/null; then
        PROFILE_NAME=$(lk profile me --json 2>/dev/null | jq -r '.firstName + " " + .lastName')
        check_pass "LinkedIn authenticated as: ${PROFILE_NAME}"
    else
        check_fail "LinkedIn auth expired — run: lk auth login"
    fi
else
    check_warn "Cannot check auth (lk not installed)"
fi

echo ""

# 4. Check configuration
echo "Configuration:"
if [ -f "${CONFIG_FILE}" ]; then
    check_pass "Config file exists"
    
    # Validate JSON
    if jq empty "${CONFIG_FILE}" 2>/dev/null; then
        check_pass "Config JSON valid"
        
        # Check required fields
        CHANNEL_ID=$(jq -r '.alertChannelId // ""' "${CONFIG_FILE}")
        if [ -n "${CHANNEL_ID}" ] && [ "${CHANNEL_ID}" != "null" ]; then
            check_pass "Alert channel configured"
        else
            check_warn "Alert channel not configured — run setup"
        fi
        
        AUTONOMY=$(jq -r '.autonomyLevel // 1' "${CONFIG_FILE}")
        check_pass "Autonomy level: ${AUTONOMY}"
        
    else
        check_fail "Config JSON invalid"
    fi
else
    check_warn "Config not found — run: linkedin-monitor setup"
fi

echo ""

# 5. Check state directory
echo "State:"
STATE_DIR="${CONFIG_DIR}/state"
if [ -d "${STATE_DIR}" ]; then
    check_pass "State directory exists"
    
    MESSAGES_FILE="${STATE_DIR}/messages.json"
    if [ -f "${MESSAGES_FILE}" ]; then
        SEEN_COUNT=$(jq '.seenIds | length' "${MESSAGES_FILE}" 2>/dev/null || echo "0")
        LAST_CHECK=$(jq -r '.lastCheck // "never"' "${MESSAGES_FILE}" 2>/dev/null)
        check_pass "Seen messages: ${SEEN_COUNT}"
        check_pass "Last check: ${LAST_CHECK}"
    else
        check_warn "No state file yet (first run)"
    fi
    
    # Check lastrun for watchdog
    LASTRUN_FILE="${STATE_DIR}/lastrun.txt"
    if [ -f "${LASTRUN_FILE}" ]; then
        LASTRUN=$(cat "${LASTRUN_FILE}")
        LASTRUN_EPOCH=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "${LASTRUN}" +%s 2>/dev/null || echo "0")
        NOW_EPOCH=$(date +%s)
        AGE_HOURS=$(( (NOW_EPOCH - LASTRUN_EPOCH) / 3600 ))
        
        if [ ${AGE_HOURS} -lt 2 ]; then
            check_pass "Monitor running (last run ${AGE_HOURS}h ago)"
        else
            check_warn "Monitor may be stale (last run ${AGE_HOURS}h ago)"
        fi
    fi
else
    check_warn "State directory not initialized — run: linkedin-monitor check"
fi

echo ""

# 6. Check cron jobs
echo "Automation:"
if crontab -l 2>/dev/null | grep -q "linkedin-monitor"; then
    check_pass "Cron job installed"
else
    check_warn "Cron job not installed — run: linkedin-monitor enable"
fi

# Summary
echo ""
echo "=============================="
if [ ${ERRORS} -eq 0 ] && [ ${WARNINGS} -eq 0 ]; then
    echo -e "${GREEN}All checks passed!${NC}"
    exit 0
elif [ ${ERRORS} -eq 0 ]; then
    echo -e "${YELLOW}${WARNINGS} warning(s), no errors${NC}"
    exit 0
else
    echo -e "${RED}${ERRORS} error(s), ${WARNINGS} warning(s)${NC}"
    exit 1
fi


path: scripts/lk.py
content: #!/usr/bin/env python3
"""
LinkedIn CLI using linkedin-api package
Provides message listing and profile info for linkedin-monitor
"""

import os
import sys
import json
import argparse
from datetime import datetime

try:
    from linkedin_api import Linkedin
except ImportError:
    print(json.dumps({"error": "linkedin-api not installed", "action": "pip3 install linkedin-api"}))
    sys.exit(1)

def get_credentials():
    """Get LinkedIn credentials from environment or config file"""
    li_at = os.environ.get('LINKEDIN_LI_AT')
    jsessionid = os.environ.get('LINKEDIN_JSESSIONID')
    
    if not li_at or not jsessionid:
        # Try loading from config
        config_path = os.path.expanduser('~/.clawdbot/linkedin-monitor/credentials.json')
        if os.path.exists(config_path):
            with open(config_path) as f:
                creds = json.load(f)
                li_at = creds.get('li_at')
                jsessionid = creds.get('jsessionid')
    
    if not li_at or not jsessionid:
        return None, None
    
    # Clean jsessionid if needed
    jsessionid = jsessionid.strip('"').strip("'")
    if not jsessionid.startswith('ajax:'):
        jsessionid = f'ajax:{jsessionid}'
    
    return li_at, jsessionid

def get_client():
    """Get authenticated LinkedIn client"""
    li_at, jsessionid = get_credentials()
    
    if not li_at or not jsessionid:
        print(json.dumps({
            "error": "No LinkedIn credentials found",
            "action": "Set LINKEDIN_LI_AT and LINKEDIN_JSESSIONID environment variables, or run: lk auth setup"
        }))
        sys.exit(1)
    
    try:
        # Create client with cookies
        api = Linkedin('', '', cookies={
            'li_at': li_at,
            'JSESSIONID': f'"{jsessionid}"'
        })
        return api
    except Exception as e:
        print(json.dumps({"error": f"Failed to create LinkedIn client: {str(e)}"}))
        sys.exit(1)

def cmd_profile_me(args):
    """Get current user's profile"""
    api = get_client()
    try:
        profile = api.get_user_profile()
        if args.json:
            print(json.dumps(profile, indent=2, default=str))
        else:
            name = f"{profile.get('firstName', '')} {profile.get('lastName', '')}"
            headline = profile.get('headline', '')
            print(f"{name}")
            print(f"{headline}")
    except Exception as e:
        print(json.dumps({"error": str(e)}))
        sys.exit(1)

def cmd_message_list(args):
    """List conversations/messages"""
    api = get_client()
    try:
        conversations = api.get_conversations()
        
        results = []
        for conv in conversations.get('elements', []):
            # Extract participant info
            participants = []
            for p in conv.get('participants', []):
                member = p.get('com.linkedin.voyager.messaging.MessagingMember', {})
                mini_profile = member.get('miniProfile', {})
                participants.append({
                    'firstName': mini_profile.get('firstName', ''),
                    'lastName': mini_profile.get('lastName', ''),
                    'profileId': mini_profile.get('publicIdentifier', '')
                })
            
            # Get last message
            events = conv.get('events', [])
            last_message = None
            if events:
                last_event = events[0]
                event_content = last_event.get('eventContent', {})
                msg_event = event_content.get('com.linkedin.voyager.messaging.event.MessageEvent', {})
                last_message = {
                    'text': msg_event.get('attributedBody', {}).get('text', ''),
                    'createdAt': last_event.get('createdAt'),
                    'fromMe': last_event.get('from', {}).get('com.linkedin.voyager.messaging.MessagingMember', {}).get('miniProfile', {}).get('publicIdentifier', '') == api.get_user_profile().get('publicIdentifier', '')
                }
            
            results.append({
                'conversationId': conv.get('entityUrn', '').split(':')[-1],
                'participants': participants,
                'lastMessage': last_message,
                'unread': conv.get('unreadCount', 0) > 0
            })
        
        if args.json:
            print(json.dumps(results, indent=2, default=str))
        else:
            for conv in results:
                name = f"{conv['participants'][0]['firstName']} {conv['participants'][0]['lastName']}" if conv['participants'] else 'Unknown'
                msg = conv['lastMessage']['text'][:50] + '...' if conv['lastMessage'] and len(conv['lastMessage']['text']) > 50 else (conv['lastMessage']['text'] if conv['lastMessage'] else '')
                unread = '●' if conv['unread'] else ' '
                print(f"{unread} {name}: {msg}")
                
    except Exception as e:
        print(json.dumps({"error": str(e)}))
        sys.exit(1)

def cmd_auth_setup(args):
    """Interactive credential setup"""
    print("LinkedIn Authentication Setup")
    print("=" * 40)
    print()
    print("To get your credentials:")
    print("1. Open LinkedIn in Chrome")
    print("2. Press F12 to open DevTools")
    print("3. Go to Application > Cookies > linkedin.com")
    print("4. Copy the values for 'li_at' and 'JSESSIONID'")
    print()
    
    li_at = input("Enter li_at cookie value: ").strip()
    jsessionid = input("Enter JSESSIONID cookie value: ").strip()
    
    # Save to config
    config_dir = os.path.expanduser('~/.clawdbot/linkedin-monitor')
    os.makedirs(config_dir, exist_ok=True)
    
    config_path = os.path.join(config_dir, 'credentials.json')
    with open(config_path, 'w') as f:
        json.dump({
            'li_at': li_at,
            'jsessionid': jsessionid.strip('"'),
            'updated_at': datetime.now().isoformat()
        }, f, indent=2)
    
    print()
    print(f"Credentials saved to: {config_path}")
    print("Run 'lk profile me' to test authentication")

def cmd_auth_status(args):
    """Check authentication status"""
    li_at, jsessionid = get_credentials()
    
    if not li_at or not jsessionid:
        print(json.dumps({"authenticated": False, "error": "No credentials found"}))
        sys.exit(1)
    
    # Try to verify by getting profile
    try:
        api = get_client()
        profile = api.get_user_profile()
        name = f"{profile.get('firstName', '')} {profile.get('lastName', '')}"
        print(json.dumps({
            "authenticated": True,
            "user": name,
            "id": profile.get('publicIdentifier', '')
        }))
    except Exception as e:
        print(json.dumps({"authenticated": False, "error": str(e)}))
        sys.exit(1)

def main():
    parser = argparse.ArgumentParser(description='LinkedIn CLI')
    subparsers = parser.add_subparsers(dest='command')
    
    # profile command
    profile_parser = subparsers.add_parser('profile', help='Profile commands')
    profile_sub = profile_parser.add_subparsers(dest='profile_cmd')
    me_parser = profile_sub.add_parser('me', help='Get your profile')
    me_parser.add_argument('--json', action='store_true', help='Output as JSON')
    
    # message command
    msg_parser = subparsers.add_parser('message', help='Message commands')
    msg_sub = msg_parser.add_subparsers(dest='message_cmd')
    list_parser = msg_sub.add_parser('list', help='List conversations')
    list_parser.add_argument('--json', action='store_true', help='Output as JSON')
    list_parser.add_argument('--unread', action='store_true', help='Only unread')
    
    # auth command
    auth_parser = subparsers.add_parser('auth', help='Authentication commands')
    auth_sub = auth_parser.add_subparsers(dest='auth_cmd')
    auth_sub.add_parser('setup', help='Setup credentials')
    auth_sub.add_parser('status', help='Check auth status')
    auth_sub.add_parser('login', help='Alias for setup')
    
    args = parser.parse_args()
    
    if args.command == 'profile' and args.profile_cmd == 'me':
        cmd_profile_me(args)
    elif args.command == 'message' and args.message_cmd == 'list':
        cmd_message_list(args)
    elif args.command == 'auth':
        if args.auth_cmd in ('setup', 'login'):
            cmd_auth_setup(args)
        elif args.auth_cmd == 'status':
            cmd_auth_status(args)
        else:
            auth_parser.print_help()
    else:
        parser.print_help()

if __name__ == '__main__':
    main()


path: scripts/state.sh
content: #!/bin/bash
# State management utilities for linkedin-monitor
# Handles reading/writing state to ensure no duplicate notifications

STATE_DIR="${HOME}/.clawdbot/linkedin-monitor/state"
MESSAGES_FILE="${STATE_DIR}/messages.json"
LASTRUN_FILE="${STATE_DIR}/lastrun.txt"
DRAFTS_FILE="${STATE_DIR}/drafts.json"

# Ensure state directory exists
init_state() {
    mkdir -p "${STATE_DIR}"
    
    # Initialize messages.json if not exists
    if [ ! -f "${MESSAGES_FILE}" ]; then
        echo '{"seenIds":[],"lastCheck":""}' > "${MESSAGES_FILE}"
    fi
    
    # Initialize drafts.json if not exists
    if [ ! -f "${DRAFTS_FILE}" ]; then
        echo '{"drafts":[]}' > "${DRAFTS_FILE}"
    fi
}

# Get list of seen message IDs
get_seen_ids() {
    init_state
    jq -r '.seenIds[]' "${MESSAGES_FILE}" 2>/dev/null || echo ""
}

# Check if a message ID has been seen
is_seen() {
    local id="$1"
    init_state
    jq -e --arg id "$id" '.seenIds | index($id) != null' "${MESSAGES_FILE}" >/dev/null 2>&1
}

# Mark a message ID as seen
mark_seen() {
    local id="$1"
    init_state
    
    # Add ID if not already present
    if ! is_seen "$id"; then
        local tmp=$(mktemp)
        jq --arg id "$id" '.seenIds += [$id]' "${MESSAGES_FILE}" > "$tmp" && mv "$tmp" "${MESSAGES_FILE}"
    fi
}

# Mark multiple IDs as seen
mark_seen_batch() {
    init_state
    local tmp=$(mktemp)
    
    # Read IDs from stdin (one per line)
    local ids="[]"
    while read -r id; do
        [ -n "$id" ] && ids=$(echo "$ids" | jq --arg id "$id" '. += [$id]')
    done
    
    # Merge with existing
    jq --argjson new "$ids" '.seenIds = (.seenIds + $new | unique)' "${MESSAGES_FILE}" > "$tmp" && mv "$tmp" "${MESSAGES_FILE}"
}

# Update last check timestamp
update_lastcheck() {
    init_state
    local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
    local tmp=$(mktemp)
    jq --arg now "$now" '.lastCheck = $now' "${MESSAGES_FILE}" > "$tmp" && mv "$tmp" "${MESSAGES_FILE}"
    echo "$now" > "${LASTRUN_FILE}"
}

# Get last check timestamp
get_lastcheck() {
    init_state
    jq -r '.lastCheck // ""' "${MESSAGES_FILE}"
}

# Save a draft reply
save_draft() {
    local name="$1"
    local message="$2"
    local draft_text="$3"
    local conversation_id="$4"
    
    init_state
    local tmp=$(mktemp)
    local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
    
    jq --arg name "$name" \
       --arg message "$message" \
       --arg draft "$draft_text" \
       --arg conv_id "$conversation_id" \
       --arg created "$now" \
       '.drafts += [{
           "name": $name,
           "inboundMessage": $message,
           "draftReply": $draft,
           "conversationId": $conv_id,
           "createdAt": $created,
           "status": "pending"
       }]' "${DRAFTS_FILE}" > "$tmp" && mv "$tmp" "${DRAFTS_FILE}"
}

# Get pending drafts
get_drafts() {
    init_state
    jq '.drafts | map(select(.status == "pending"))' "${DRAFTS_FILE}"
}

# Get draft by name
get_draft() {
    local name="$1"
    init_state
    jq --arg name "$name" '.drafts | map(select(.name == $name and .status == "pending")) | .[0]' "${DRAFTS_FILE}"
}

# Mark draft as sent
mark_draft_sent() {
    local name="$1"
    init_state
    local tmp=$(mktemp)
    jq --arg name "$name" '(.drafts[] | select(.name == $name and .status == "pending")).status = "sent"' "${DRAFTS_FILE}" > "$tmp" && mv "$tmp" "${DRAFTS_FILE}"
}

# Mark draft as skipped
mark_draft_skipped() {
    local name="$1"
    init_state
    local tmp=$(mktemp)
    jq --arg name "$name" '(.drafts[] | select(.name == $name and .status == "pending")).status = "skipped"' "${DRAFTS_FILE}" > "$tmp" && mv "$tmp" "${DRAFTS_FILE}"
}

# Reset all state (fresh start)
reset_state() {
    rm -f "${MESSAGES_FILE}" "${DRAFTS_FILE}" "${LASTRUN_FILE}"
    init_state
    echo "State reset complete"
}

# Show state summary
show_state() {
    init_state
    local seen_count=$(jq '.seenIds | length' "${MESSAGES_FILE}")
    local last_check=$(get_lastcheck)
    local pending_drafts=$(jq '[.drafts[] | select(.status == "pending")] | length' "${DRAFTS_FILE}")
    
    echo "LinkedIn Monitor State"
    echo "======================"
    echo "Seen messages: ${seen_count}"
    echo "Last check: ${last_check:-never}"
    echo "Pending drafts: ${pending_drafts}"
}

# Run command if script is executed directly
case "${1:-}" in
    init) init_state ;;
    seen) get_seen_ids ;;
    is-seen) is_seen "$2" && echo "yes" || echo "no" ;;
    mark) mark_seen "$2" ;;
    lastcheck) get_lastcheck ;;
    update-lastcheck) update_lastcheck ;;
    save-draft) save_draft "$2" "$3" "$4" "$5" ;;
    get-drafts) get_drafts ;;
    get-draft) get_draft "$2" ;;
    mark-sent) mark_draft_sent "$2" ;;
    mark-skipped) mark_draft_skipped "$2" ;;
    reset) reset_state ;;
    status) show_state ;;
    *) echo "Usage: state.sh {init|seen|is-seen|mark|lastcheck|update-lastcheck|save-draft|get-drafts|get-draft|mark-sent|mark-skipped|reset|status}" ;;
esac


path: templates/config.json
content: {
  "autonomyLevel": 1,
  "alertChannel": "",
  "alertTarget": "",
  "calendarLink": "",
  "communicationStyleFile": "USER.md",
  "timezone": "America/New_York",
  "schedule": "0 * * * *",
  "morningDigest": {
    "enabled": true,
    "hour": 9,
    "timezone": "America/New_York"
  },
  "safetyLimits": {
    "maxMessagesPerDay": 50,
    "escalationKeywords": ["angry", "legal", "refund", "lawsuit", "attorney"],
    "dailyDigest": true
  },
  "browserProfile": "clawd"
}


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