@clawhub-fuzzyb33s-5ee8c7baf7
Track tasks in Discord using natural language. Add, list, complete, and delete tasks via chat commands. Triggers: add task, track task, todo, my tasks, task...
---
name: discord-task-tracker
description: "Track tasks in Discord using natural language. Add, list, complete, and delete tasks via chat commands. Triggers: add task, track task, todo, my tasks, task list, complete task, delete task, task done"
---
# Discord Task Tracker
Track your tasks directly from Discord with natural language commands.
## Commands
| Command | Description |
|---------|-------------|
| `add task <description>` | Add a new task |
| `list tasks` / `my tasks` / `task list` | Show all tasks |
| `complete task <task number>` | Mark a task as done |
| `delete task <task number>` | Remove a task |
## Examples
```
add task Finish the Discord bot integration
list tasks
complete task 1
delete task 2
```
## How It Works
- Tasks are stored in `tasks.json` in the skill directory
- Task numbers are assigned sequentially; use `list tasks` to see current numbers
- Completed tasks are removed from the list
- All task operations reply directly in the Discord channel
FILE:references/commands.md
# Command Reference — Discord Task Tracker
## Syntax
All commands are natural language phrases. Run via:
```
uv run python scripts/task_manager.py "<command>"
```
## Commands
### Add a task
```
add task <description>
track task <description>
todo <description>
```
**Example:** `add task Finish the Discord integration`
### List all tasks
```
list tasks
my tasks
task list
```
Shows numbered task list with status indicators:
- ⬜ = pending
- ✅ = completed (auto-removed after completion)
### Complete a task
```
complete task <number>
task done <number>
done <number>
```
**Example:** `complete task 1`
### Delete a task
```
delete task <number>
```
**Example:** `delete task 2`
## Task Storage
- File: `tasks.json` in the skill directory
- Format: JSON array of `{"id": N, "text": "...", "done": bool}` objects
- Task IDs are sequential and may shift after deletions/completions
## Exit Codes
| Code | Meaning |
|------|---------|
| 0 | Success |
| 1 | Error (task not found, unknown command) |
FILE:scripts/task_manager.py
#!/usr/bin/env python3
"""
Discord Task Tracker - Manage tasks via Discord chat commands.
Reads command from stdin/args, manages tasks in tasks.json, outputs response.
"""
import sys
import json
import os
from pathlib import Path
# Ensure UTF-8 output on Windows
if sys.platform == "win32":
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
SKILL_DIR = Path(__file__).parent.parent
TASKS_FILE = SKILL_DIR / "tasks.json"
def load_tasks():
if TASKS_FILE.exists():
with open(TASKS_FILE, "r", encoding="utf-8") as f:
return json.load(f)
return []
def save_tasks(tasks):
with open(TASKS_FILE, "w", encoding="utf-8") as f:
json.dump(tasks, f, indent=2)
def cmd_add(text):
tasks = load_tasks()
task_id = len(tasks) + 1
tasks.append({"id": task_id, "text": text.strip(), "done": False})
save_tasks(tasks)
print(f"✅ Task added (#{task_id}): {text.strip()}")
def cmd_list():
tasks = load_tasks()
if not tasks:
print("📋 No tasks yet. Say 'add task <description>' to create one.")
return
lines = ["**📋 Your Tasks:**"]
for t in tasks:
status = "✅" if t["done"] else "⬜"
lines.append(f" {status} `[{t['id']}]` {t['text']}")
print("\n".join(lines))
def cmd_complete(task_id):
tasks = load_tasks()
task = next((t for t in tasks if t["id"] == int(task_id)), None)
if not task:
print(f"❌ Task #{task_id} not found.")
return
task["done"] = True
save_tasks(tasks)
# Remove completed task
tasks = [t for t in tasks if not (t["id"] == int(task_id))]
save_tasks(tasks)
print(f"✅ Task #{task_id} completed and removed: {task['text']}")
def cmd_delete(task_id):
tasks = load_tasks()
task = next((t for t in tasks if t["id"] == int(task_id)), None)
if not task:
print(f"❌ Task #{task_id} not found.")
return
tasks = [t for t in tasks if t["id"] != int(task_id)]
save_tasks(tasks)
print(f"🗑️ Task #{task_id} deleted: {task['text']}")
def main():
# Read full input (can be passed via stdin or args)
raw = ""
if len(sys.argv) > 1:
raw = " ".join(sys.argv[1:])
else:
raw = sys.stdin.read().strip()
raw = raw.strip()
if not raw:
print("Usage: task_manager.py <command>")
sys.exit(1)
lower = raw.lower()
# add task <text>
if lower.startswith("add task ") or lower.startswith("track task ") or lower.startswith("todo "):
text = raw.split(" ", 2)[-1]
cmd_add(text)
# list tasks / my tasks / task list
elif lower in ("list tasks", "my tasks", "task list", "list", "tasks"):
cmd_list()
# complete task <id>
elif lower.startswith("complete task ") or lower.startswith("task done ") or lower.startswith("done "):
parts = raw.split()
if len(parts) >= 2:
cmd_complete(parts[-1])
else:
print("❌ Usage: complete task <task number>")
# delete task <id>
elif lower.startswith("delete task "):
parts = raw.split()
if len(parts) >= 2:
cmd_delete(parts[-1])
else:
print("❌ Usage: delete task <task number>")
else:
print("Unknown command. Use: add task <text>, list tasks, complete task <#>, delete task <#>")
if __name__ == "__main__":
main()
FILE:tasks.json
[
{
"id": 1,
"text": "Test task from CLI",
"done": false
},
{
"id": 2,
"text": "Test task from CLI",
"done": false
}
]Monitor live cryptocurrency prices, technical indicators, and support/resistance levels with customizable price alerts and portfolio summaries.
---
name: crypto-price-alerter
description: Monitor cryptocurrency prices and generate trading alerts/analyses. Use when the user wants to (1) check the current price of a crypto symbol (BTC, ETH, SOL, etc.), (2) see 24h price change, volume, and market cap, (3) get technical signals (SMA, RSI, support/resistance), (4) set price threshold alerts, or (5) generate a crypto portfolio price summary. Triggers: "crypto price", "check price", "BTC", "ETH", "solana", "alert", "trading", "price alert", "crypto analysis", "technical analysis", "RSI", "SMA", "support resistance".
---
# Crypto Price Alerter
Fetch live cryptocurrency prices and technical indicators via CoinGecko free API.
## Quick Usage
```bash
uv run python scripts/price_check.py --symbol BTC --currency USD
uv run python scripts/price_check.py --symbol ETH --currency USD --upper 4000 --lower 2000
uv run python scripts/price_check.py --symbol SOL --currency USD --output json
```
## Core Features
1. **Current Price** — Live price, 24h change %, 24h volume, market cap
2. **Technical Indicators** — SMA(7), SMA(21), RSI(14) from 30-day historical data
3. **Key Levels** — 30-day resistance and support
4. **Price Alerts** — Triggered when 24h change >5% or price crosses user thresholds
5. **JSON output** — For automation pipelines: `--output json`
## Scripts
- `scripts/price_check.py` — Main script. Run standalone with `uv run python scripts/price_check.py [args]`
### Arguments
| Arg | Description |
|-----|-------------|
| `--symbol` | Crypto symbol (e.g. BTC, ETH, SOL) — **required** |
| `--currency` | Fiat currency (default: USD) |
| `--upper` | Upper price threshold for alert |
| `--lower` | Lower price threshold for alert |
| `--days` | Historical days for SMA (default: 30) |
| `--output` | `text` (default) or `json` |
## Technical Signals
See `references/signals.md` for explanation of SMA, RSI, support/resistance, and trading signal interpretation.
## Alert Logic
- 24h change > +5% → Bullish alert
- 24h change < -5% → Bearish alert
- Price >= `--upper` threshold → Price ceiling alert
- Price <= `--lower` threshold → Price floor alert
FILE:references/signals.md
# Technical Signals Reference
## Simple Moving Average (SMA)
The SMA smooths price data over a set period. Common periods:
- **SMA(7)**: Short-term trend. Price above SMA7 = short-term bullish
- **SMA(21)**: Medium-term trend. Price above SMA21 = medium-term bullish
- **SMA(50/200)**: Long-term trend indicators (not computed here, use CoinGecko history for longer periods)
**How to read:**
- Price > SMA →Bullish signal
- Price < SMA → Bearish signal
- SMA crossing above another SMA → Golden cross (bullish)
- SMA crossing below another SMA → Death cross (bearish)
## RSI (Relative Strength Index)
RSI measures momentum on a 0-100 scale:
- **RSI > 70**: Overbought — possible pullback expected
- **RSI < 30**: Oversold — possible bounce expected
- **RSI ~ 50**: Neutral/market in balance
RSI is calculated as: 100 - (100 / (1 + RS)) where RS = average gain / average loss over the period.
## Support & Resistance
Simple estimation based on 30-day high/low:
- **Resistance**: The price ceiling — 30-day high. Breaking above is bullish.
- **Support**: The price floor — 30-day low. Falling below is bearish.
## Price Alerts
User-defined thresholds:
- `--upper`: Alert when price rises above this level
- `--lower`: Alert when price falls below this level
## Trading Signals Summary
| Signal | Bullish | Bearish |
|--------|---------|---------|
| Price vs SMA(7) | Price above | Price below |
| Price vs SMA(21) | Price above | Price below |
| RSI | > 70 overbought | < 30 oversold |
| 24h Change | > +5% | < -5% |
**Disclaimer**: These are simple technical indicators for informational purposes only. Not financial advice.
FILE:scripts/price_check.py
#!/usr/bin/env python3
"""
crypto-price-alerter: Price check script
Fetches current crypto price data from CoinGecko free API.
Usage: uv run python scripts/price_check.py --symbol BTC --currency USD
"""
import argparse
import json
import sys
import requests
from datetime import datetime, timezone
# CoinGecko API endpoints
COINGECKO_SEARCH_URL = "https://api.coingecko.com/api/v3/search"
COINGECKO_PRICE_URL = "https://api.coingecko.com/api/v3/simple/price"
COINGECKO_COINS_URL = "https://api.coingecko.com/api/v3/coins"
def search_coin(symbol: str) -> str | None:
"""Search for a coin by symbol and return its CoinGecko ID."""
try:
resp = requests.get(COINGECKO_SEARCH_URL, params={"query": symbol}, timeout=10)
resp.raise_for_status()
data = resp.json()
coins = data.get("coins", [])
# Match by exact symbol (case-insensitive)
symbol_lower = symbol.lower()
for coin in coins:
if coin.get("symbol", "").lower() == symbol_lower:
return coin.get("id")
# Fallback: return first match
if coins:
return coins[0].get("id")
return None
except Exception as e:
print(f"Search error: {e}", file=sys.stderr)
return None
def get_price_data(coin_id: str, currency: str = "usd") -> dict | None:
"""Fetch current price and market data for a coin."""
try:
params = {
"ids": coin_id,
"vs_currencies": currency,
"include_24hr_vol": "true",
"include_24hr_change": "true",
"include_market_cap": "true",
}
resp = requests.get(COINGECKO_PRICE_URL, params=params, timeout=10)
resp.raise_for_status()
data = resp.json()
coin_data = data.get(coin_id, {})
if not coin_data:
return None
price = coin_data.get(currency, 0)
change_24h = coin_data.get(f"{currency}_24h_change", 0)
volume_24h = coin_data.get(f"{currency}_24h_vol", 0)
market_cap = coin_data.get(f"{currency}_market_cap", 0)
return {
"coin_id": coin_id,
"currency": currency.upper(),
"price": price,
"change_24h_percent": round(change_24h, 2),
"volume_24h": round(volume_24h, 2),
"market_cap": round(market_cap, 2),
"timestamp": datetime.now(timezone.utc).isoformat(),
}
except Exception as e:
print(f"Price fetch error: {e}", file=sys.stderr)
return None
def get_historical_prices(coin_id: str, currency: str = "usd", days: int = 7) -> list | None:
"""Fetch historical price data for moving average calculation."""
try:
url = f"{COINGECKO_COINS_URL}/{coin_id}/market_chart"
params = {
"vs_currency": currency,
"days": days,
}
resp = requests.get(url, params=params, timeout=10)
resp.raise_for_status()
data = resp.json()
prices = data.get("prices", [])
# Return list of [timestamp_ms, price]
return prices[-30:] if len(prices) > 30 else prices
except Exception as e:
print(f"Historical fetch error: {e}", file=sys.stderr)
return None
def calculate_sma(prices: list, period: int = 7) -> float | None:
"""Calculate Simple Moving Average from price list."""
if not prices or len(prices) < period:
return None
# prices is list of [timestamp_ms, price]
recent = prices[-period:]
avg = sum(p[1] for p in recent) / len(recent)
return round(avg, 2)
def calculate_rsi(prices: list, period: int = 14) -> float | None:
"""Calculate basic RSI from price list."""
if not prices or len(prices) < period + 1:
return None
# prices is list of [timestamp_ms, price]
gains = []
losses = []
for i in range(1, min(len(prices), period + 1)):
change = prices[i][1] - prices[i - 1][1]
if change > 0:
gains.append(change)
else:
losses.append(abs(change))
if not losses:
return 100.0
avg_gain = sum(gains) / len(gains) if gains else 0
avg_loss = sum(losses) / len(losses)
if avg_loss == 0:
return 100.0
rs = avg_gain / avg_loss
rsi = 100 - (100 / (1 + rs))
return round(rsi, 1)
def estimate_support_resistance(prices: list) -> dict:
"""Estimate simple support/resistance from recent price range."""
if not prices:
return {"resistance": None, "support": None}
recent = prices[-30:] if len(prices) > 30 else prices
high = max(p[1] for p in recent)
low = min(p[1] for p in recent)
return {
"resistance_30d": round(high, 2),
"support_30d": round(low, 2),
}
def generate_alert(price_data: dict, thresholds: dict = None) -> list:
"""Generate alerts based on price data and optional thresholds."""
alerts = []
if not price_data:
return alerts
price = price_data["price"]
change = price_data["change_24h_percent"]
# Trend alerts
if change > 5:
alerts.append(f"ALERT: {price_data['coin_id']} up {change}% in 24h!")
elif change < -5:
alerts.append(f"ALERT: {price_data['coin_id']} down {abs(change)}% in 24h!")
# Threshold alerts if provided
if thresholds:
upper = thresholds.get("upper")
lower = thresholds.get("lower")
if upper and price >= upper:
alerts.append(f"PRICE ALERT: {price_data['coin_id']} hit price (above upper threshold upper)!")
if lower and price <= lower:
alerts.append(f"PRICE ALERT: {price_data['coin_id']} hit price (below lower threshold lower)!")
return alerts
def build_report(price_data: dict, sma_7: float = None, sma_21: float = None,
rsi: float = None, sr_levels: dict = None, thresholds: dict = None) -> str:
"""Build a formatted summary report."""
coin = price_data["coin_id"].upper()
currency = price_data["currency"]
price = price_data["price"]
change = price_data["change_24h_percent"]
volume = price_data["volume_24h"]
market_cap = price_data["market_cap"]
ts = price_data["timestamp"]
# Trend indicator
# Trend indicator
trend = "^" if change >= 0 else "v"
trend_text = "BULLISH" if change >= 0 else "BEARISH"
# Simple signals
signals = []
if sma_7 and price > sma_7:
signals.append("above SMA7")
elif sma_7 and price < sma_7:
signals.append("below SMA7")
if rsi:
if rsi > 70:
signals.append("RSI overbought")
elif rsi < 30:
signals.append("RSI oversold")
signal_text = ", ".join(signals) if signals else "neutral"
# Volume context
vol_billion = volume / 1e9 if volume else 0
report = f"""
========================================
CRYPTO PRICE REPORT: {coin}
========================================
Price ({currency}): ,.4f
24h Change: {trend} {change:+.2f}%
24h Volume: ,.2fB
Market Cap: ,.2f
----------------------------------------
TECHNICAL INDICATORS
SMA(7): {sma_7 or 'N/A'}
SMA(21): {sma_21 or 'N/A'}
RSI(14): {rsi or 'N/A'}
Signal: {signal_text}
----------------------------------------
KEY LEVELS (30d)
Resistance: (sr_levels or {).get('resistance_30d') or 'N/A'}
Support: (sr_levels or {).get('support_30d') or 'N/A'}
========================================
Updated: {ts} UTC
"""""
return report.strip()
def main():
parser = argparse.ArgumentParser(description="Crypto price check via CoinGecko")
parser.add_argument("--symbol", required=True, help="Crypto symbol (e.g. BTC, ETH)")
parser.add_argument("--currency", default="USD", help="Fiat currency (default: USD)")
parser.add_argument("--upper", type=float, help="Upper price threshold for alert")
parser.add_argument("--lower", type=float, help="Lower price threshold for alert")
parser.add_argument("--days", type=int, default=30, help="Historical days for SMA (default: 30)")
parser.add_argument("--output", choices=["text", "json"], default="text", help="Output format")
args = parser.parse_args()
# Find coin ID
coin_id = search_coin(args.symbol)
if not coin_id:
print(f"ERROR: Could not find coin with symbol '{args.symbol}'", file=sys.stderr)
sys.exit(1)
# Get current price
price_data = get_price_data(coin_id, args.currency.lower())
if not price_data:
print(f"ERROR: Could not fetch price for '{args.symbol}'", file=sys.stderr)
sys.exit(1)
# Get historical data for indicators
historical = get_historical_prices(coin_id, args.currency.lower(), args.days)
sma_7 = calculate_sma(historical, 7) if historical else None
sma_21 = calculate_sma(historical, 21) if historical else None
rsi = calculate_rsi(historical, 14) if historical else None
sr = estimate_support_resistance(historical) if historical else None
# Alerts
thresholds = {}
if args.upper:
thresholds["upper"] = args.upper
if args.lower:
thresholds["lower"] = args.lower
alerts = generate_alert(price_data, thresholds if thresholds else None)
if args.output == "json":
result = {
"price_data": price_data,
"indicators": {
"sma_7": sma_7,
"sma_21": sma_21,
"rsi_14": rsi,
},
"key_levels": sr,
"alerts": alerts,
}
print(json.dumps(result, indent=2))
else:
report = build_report(price_data, sma_7, sma_21, rsi, sr, thresholds if thresholds else None)
print(report)
if alerts:
print("\n--- ALERTS ---")
for alert in alerts:
print(f" {alert}")
if __name__ == "__main__":
main()
Monitor, filter, and summarize RSS/Atom feeds on a schedule. Use when: (1) tracking industry news or competitor blogs, (2) setting up keyword alerts across m...
---
name: rss-aggregator
description: "Monitor, filter, and summarize RSS/Atom feeds on a schedule. Use when: (1) tracking industry news or competitor blogs, (2) setting up keyword alerts across multiple feeds, (3) getting daily/periodic digest of new articles, (4) routing interesting articles to Discord/email/webhook, (5) building a personal news pipeline. Triggers on: rss feed, atom, feed monitor, news aggregator, track this blog, keyword alert, feed digest, subscribe to feed, monitor this site."
---
# RSS Aggregator
Monitor RSS/Atom feeds on a schedule, filter by keywords or date, and route summaries to your preferred channel.
## Setup
Requires the `feedparser` Python package:
```bash
pip install feedparser
```
## Core Script
Save as `scripts/fetch_feeds.py`:
```python
#!/usr/bin/env python3
"""RSS/Atom feed fetcher with filtering and summarization."""
import feedparser
import sys
import json
from datetime import datetime, timedelta
from pathlib import Path
def parse_date(entry):
"""Extract publication date from entry."""
for field in ('published_parsed', 'updated_parsed', 'created_parsed'):
if hasattr(entry, field) and entry.get(field):
return datetime(*entry[field][:6])
return None
def fetch_feed(url, max_age_days=None, keyword_filter=None):
"""Fetch and filter feed entries."""
feed = feedparser.parse(url)
entries = feed.entries
# Filter by age
if max_age_days:
cutoff = datetime.now() - timedelta(days=max_age_days)
entries = [e for e in entries if parse_date(e) and parse_date(e) >= cutoff]
# Filter by keyword
if keyword_filter:
kw_lower = keyword_filter.lower()
entries = [e for e in entries if kw_lower in (e.get('title', '') + e.get('summary', '')).lower()]
return {
'title': feed.feed.get('title', url),
'url': url,
'entries': [
{
'title': e.get('title', 'No title'),
'link': e.get('link', ''),
'published': parse_date(e).isoformat() if parse_date(e) else None,
'summary': e.get('summary', e.get('description', ''))[:500]
}
for e in entries
]
}
if __name__ == '__main__':
url = sys.argv[1] if len(sys.argv) > 1 else ''
max_age = int(sys.argv[2]) if len(sys.argv) > 2 else None
keyword = sys.argv[3] if len(sys.argv) > 3 else None
if not url:
print(json.dumps({'error': 'URL required'}))
sys.exit(1)
result = fetch_feed(url, max_age, keyword)
print(json.dumps(result, indent=2))
```
## Recipes
### Recipe 1: Daily News Digest
```json
cron_add(
name="Tech news digest",
schedule={"kind": "cron", "expr": "0 8 * * 1-5", "tz": "Africa/Johannesburg"},
payload={
"kind": "agentTurn",
"message": "Run: python scripts/fetch_feeds.py https://news.ycombinator.com/rss 7. Then summarize the top 5 stories as a clean bullet list with titles and links."
},
delivery={"mode": "announce"},
sessionTarget="isolated"
)
```
### Recipe 2: Multi-Feed Monitoring
```json
// First, create scripts/multi_fetch.py:
"""
import feedparser, json, sys
from scripts.fetch_feeds import fetch_feed
feeds = [
"https://techcrunch.com/feed/",
"https://www.theverge.com/rss/index.xml",
"https://feeds.feedburner.com/TechCrunch/"
]
results = [fetch_feed(url, max_age_days=1) for url in feeds]
print(json.dumps(results, indent=2))
"""
```
Then schedule:
```json
cron_add(
name="Industry pulse",
schedule={"kind": "cron", "expr": "0 */6 * * *", "tz": "UTC"},
payload={
"kind": "agentTurn",
"message": "Run: python scripts/multi_fetch.py. Filter entries from last 6 hours. Post new articles to #news channel on Discord with title + link."
},
delivery={"mode": "announce"},
sessionTarget="isolated"
)
```
### Recipe 3: Keyword Alert
```json
cron_add(
name="AI keyword alert",
schedule={"kind": "cron", "expr": "0 */4 * * *", "tz": "UTC"},
payload={
"kind": "agentTurn",
"message": "Run: python scripts/fetch_feeds.py https://feeds.feedburner.com/venturebeat/Settings 1 \"AI OR machine learning OR LLM\". If results have entries, format as: **Alert** [Article Title](URL). Send to Discord #alerts channel."
},
delivery={"mode": "webhook", "to": "https://discord.com/api/webhooks/..."},
sessionTarget="isolated"
)
```
### Recipe 4: Feed Status Health Check
```json
cron_add(
name="Feed health check",
schedule={"kind": "cron", "expr": "0 9 * * *", "tz": "UTC"},
payload={
"kind": "agentTurn",
"message": "Check if these feeds are still live: Hacker News (https://news.ycombinator.com/rss), TechCrunch (https://techcrunch.com/feed/). Run fetch without filters. If any feed returns 0 entries or error, alert via webhook."
},
delivery={"mode": "announce"},
sessionTarget="isolated",
failureAlert={"after": 3, "mode": "announce", "cooldownMs": 86400000}
)
```
### Recipe 5: Feed to Read Later (Notion)
```json
cron_add(
name="RSS to Notion",
schedule={"kind": "cron", "expr": "0 7 * * *", "tz": "Africa/Johannesburg"},
payload={
"kind": "agentTurn",
"message": "Run: python scripts/fetch_feeds.py https://example.com/rss 1. Create Notion page for each entry in your Reading List database with title, link, and summary as page content."
},
delivery={"mode": "none"},
sessionTarget="isolated"
)
```
## Managing Feeds
```bash
# Test a feed directly
python scripts/fetch_feeds.py <feed-url> [max-age-days] [keyword-filter]
# Example
python scripts/fetch_feeds.py https://news.ycombinator.com/rss 7
python scripts/fetch_feeds.py https://techcrunch.com/feed/ 1 "AI"
```
## Feed Discovery
Find RSS feeds on any website by:
- Adding `/feed` or `/rss` to the URL
- Checking the page source for `<link rel="alternate" type="application/rss+xml">`
- Using `site:rss` search on Google
Common feed URLs:
- YouTube: `https://www.youtube.com/feeds/videos.xml?channel_id=CHANNEL_ID`
- Twitter/X: No native RSS — use替他 (Nitter for Twitter lists)
- Reddit: `https://www.reddit.com/r/SUBREDDIT.rss` (requires auth for full content)
## Troubleshooting
| Symptom | Cause | Fix |
|---------|-------|-----|
| Empty entries list | Feed may require auth or be XML-only | Try curl to inspect raw feed |
| `decode error` in feed | Malformed encoding | Add `, encoding='utf-8'` to `feedparser.parse()` |
| Unicode errors | Non-UTF8 characters | Add `, response_encoding='utf-8'` to parse call |
| Old entries only | `max_age_days` too restrictive | Increase or remove the filter |
| Missing summaries | Site blocks feed scrapers | Use `e.get('content', [{}])[0].get('value', '')` for full content |
## See Also
- `fuzzy-cron-scheduler` skill — scheduling recurring feed checks
- `notion-integration` skill — storing articles in Notion
- `discord` skill — routing articles to Discord channels
- `webhook-automation` skill — HTTP delivery to any endpoint
FILE:scripts/fetch_feeds.py
#!/usr/bin/env python3
"""RSS/Atom feed fetcher with filtering and summarization."""
import feedparser
import sys
import json
from datetime import datetime, timedelta
from pathlib import Path
def parse_date(entry):
"""Extract publication date from entry."""
for field in ('published_parsed', 'updated_parsed', 'created_parsed'):
if hasattr(entry, field) and entry.get(field):
return datetime(*entry[field][:6])
return None
def fetch_feed(url, max_age_days=None, keyword_filter=None):
"""Fetch and filter feed entries."""
feed = feedparser.parse(url)
entries = feed.entries
# Filter by age
if max_age_days:
cutoff = datetime.now() - timedelta(days=max_age_days)
entries = [e for e in entries if parse_date(e) and parse_date(e) >= cutoff]
# Filter by keyword
if keyword_filter:
kw_lower = keyword_filter.lower()
entries = [e for e in entries if kw_lower in (e.get('title', '') + e.get('summary', '')).lower()]
return {
'title': feed.feed.get('title', url),
'url': url,
'entries': [
{
'title': e.get('title', 'No title'),
'link': e.get('link', ''),
'published': parse_date(e).isoformat() if parse_date(e) else None,
'summary': e.get('summary', e.get('description', ''))[:500]
}
for e in entries
]
}
if __name__ == '__main__':
url = sys.argv[1] if len(sys.argv) > 1 else ''
max_age = int(sys.argv[2]) if len(sys.argv) > 2 else None
keyword = sys.argv[3] if len(sys.argv) > 3 else None
if not url:
print(json.dumps({'error': 'URL required'}))
sys.exit(1)
result = fetch_feed(url, max_age, keyword)
print(json.dumps(result, indent=2))Connect two OpenClaw agents running on different machines as peer collaborators via Tailscale VPN. Enables direct sessions_send communication between agents...
---
name: agent-peer-tailscale
description: "Connect two OpenClaw agents running on different machines as peer collaborators via Tailscale VPN. Enables direct sessions_send communication between agents on separate hosts with no public IP, no port forwarding, and no middle server. Use when: (1) two OpenClaw agents on different machines need to collaborate on projects, (2) mentor/peer agent wants to send tips and insights to another agent in real-time, (3) setting up a peer network of OpenClaw agents, (4) two agents need to share session context or delegate work to each other. Triggers on: connect two agents, peer agents, Tailscale VPN, cross-machine agents, agent collaboration VPN, OpenClaw peer network."
---
# Agent Peer via Tailscale
Two OpenClaw agents on different machines — connected as peers over Tailscale VPN. No public IP, no port forwarding, no relay server. Direct `sessions_send` between them as if on the same LAN.
## What You Get
```
Machine A (You) Machine B (Friend)
────────────── ───────────────
OpenClaw: :8080 OpenClaw: :8080
Tailscale: 100.x.x.x Tailscale: 100.x.x.x
↓ ↓
└────────── Tailscale VPN (encrypted) ──────┘
↓
sessions_send(sessionKey=...,
gatewayUrl="http://100.x.x.x:8080")
```
Both agents can send messages, session context, tips, and task delegations directly to each other.
## Prerequisites
1. **OpenClaw gateway running** on both machines (local gateway, not node mode)
2. **Tailscale installed** on both machines (free account at tailscale.com)
3. **Both machines on the same Tailscale network** (one creates the network, shares auth key)
4. **Gateway bound to Tailscale interface** (not localhost only)
## Step 1 — Install and Configure Tailscale
### On Machine A (the host)
```bash
# Download and install Tailscale
winget install Tailscale.Tailscale # Windows
# or: brew install tailscale # macOS
# or: curl -fsSL https://tailscale.com/install.sh | sh # Linux
# Start Tailscale and authenticate
tailscale up --accept-routes
# Note the Tailscale IP (write this down for Machine B)
tailscale ip -4
```
### On Machine B (join the network)
```bash
# Install Tailscale the same way
# Then join using the auth key from Machine A's Tailscale admin console
tailscale up --accept-routes --authkey=<authkey-from-machine-a>
# Note your Tailscale IP
tailscale ip -4
```
Both machines now have IPs like `100.x.x.x` on a private encrypted network.
## Step 2 — Configure OpenClaw Gateway for Tailscale
By default, OpenClaw binds to `localhost`. You need it to bind to all interfaces so the peer can reach it over Tailscale.
Check your gateway config:
```bash
openclaw gateway status
```
Set `gateway.bind` to `0.0.0.0` (all interfaces) or specifically the Tailscale IP:
```json
{
"gateway": {
"bind": "0.0.0.0",
"port": 8080
}
}
```
Apply and restart:
```
openclaw gateway restart
```
**Security note:** Binding to `0.0.0.0` exposes your gateway on all network interfaces. Tailscale traffic is encrypted peer-to-peer, but make sure you have a strong gateway token/password set. Consider `gateway.auth` to require token authentication.
## Step 3 — Exchange Gateway URLs
Once both gateways are reachable over Tailscale, exchange the peer gateway URLs:
**Machine A** tells Machine B:
```
Gateway URL: http://<Machine-A-Tailscale-IP>:8080
Gateway token: <your-gateway-token>
```
**Machine B** tells Machine A:
```
Gateway URL: http://<Machine-B-Tailscale-IP>:8080
Gateway token: <their-gateway-token>
```
## Step 4 — Create Peer Config File
On each machine, create a peer configuration at `peer-agent/peer-config.md`:
**Your config (Machine A):**
```markdown
# My Peer Configuration
My Tailscale IP: <your-tailscale-ip>
My Gateway URL: http://<your-tailscale-ip>:8080
My Gateway Token: <your-token>
# Peer (Machine B)
Peer Name: <friend's-name>
Peer Tailscale IP: <their-tailscale-ip>
Peer Gateway URL: http://<their-tailscale-ip>:8080
Peer Gateway Token: <their-token>
# How to reach my agent
# Use sessions_send with the gatewayUrl pointing to my gateway above.
# My agentId for direct targeting: <your-agent-id>
```
## Step 5 — Test the Connection
From Machine A, test reaching Machine B's gateway:
```bash
# Ping the peer's gateway over Tailscale
curl http://<peer-tailscale-ip>:8080/health --connect-timeout 5
```
You should get a health response. If not, check that the peer's gateway is bound to `0.0.0.0` and their firewall allows incoming on port 8080 from the Tailscale network.
## Step 6 — Send Messages Between Agents
Once connectivity is confirmed, use `sessions_send` with `gatewayUrl` pointing to the peer:
```json
sessions_send(
sessionKey="<peer-session-key>",
agentId="<peer-agent-id>",
message="Hey, need your take on something — I'm stuck on...",
gatewayUrl="http://<peer-tailscale-ip>:8080",
gatewayToken="<peer-gateway-token>"
)
```
## Daily Collaboration Patterns
### Pattern 1: Morning Handoff
Each morning, each agent sends the other a brief status update:
```
sessions_send(
message="Morning! Here's where I'm at: [project status]. Blockers: [if any].
Any insights on [specific problem]?",
gatewayUrl="http://<peer-ip>:8080",
gatewayToken="<peer-token>"
)
```
### Pattern 2: Quick Insight
When one agent learns something useful:
```
"Something I learned today that might help you: [insight]"
```
### Pattern 3: Code/Review Request
```
"Can you review my approach to [task]? Here it is: [description].
Is there a better pattern I'm missing?"
```
### Pattern 4: Delegation
```
"I've got a task that's more your specialty — want to delegate this to you? [task details].
Let me know if you have capacity."
```
## Reference Files
- `references/tailscale-setup.md` — detailed Tailscale install, network setup, auth key sharing
- `references/peer-communication.md` — message format, frequency, session management
- `references/troubleshooting.md` — NAT, firewall, connection issues
- `scripts/peer_config.py` — interactive config generator for the peer setup
## Security Notes
- **Tailscale is encrypted end-to-end** — no one on the internet can see the traffic
- **Gateway token is required** — don't share your gateway token in plain text over an unsecured channel; use a private message or password manager
- **Only share with people you trust** — the peer can send messages that execute as your agent
- **Revoke auth keys** from the Tailscale admin console if the friendship ends
- **Consider `gateway.access`** — restrict which sessions can be targeted from peers
FILE:references/peer-communication.md
# Peer Communication — Message Formats and Collaboration Patterns
## The Peer Protocol
When two agents collaborate via `sessions_send` over Tailscale, they need a shared language. This document defines the conventions.
## Message Types
### 1. Status Update
```
[Morning Status] → [Project Name]
What's done: [bullet points]
Blockers: [bullet points or "none"]
Focus today: [what you're working on]
Need from peer: [specific ask or "nothing"]
```
### 2. Quick Insight
```
[Insight] → [Topic Tag]
What I learned: [description]
Why it matters: [how it applies]
Source: [where you learned it, if applicable]
```
### 3. Review Request
```
[Review Request] → [Task Name]
Task: [what you're working on]
Your approach: [brief description]
What you want reviewed: [specific aspect]
Questions: [specific questions or "anything stands out?"]
```
### 4. Delegation
```
[Delegation] → [Task Name]
Priority: [high/medium/low]
Task: [description]
Context: [why it needs to be done]
Deadline: [if any]
Your expertise: [why you think this peer is suited]
```
### 5. Feedback / Response
```
[Feedback] → [Original Message Ref]
Verdict: [agree/disagree/partially]
Reasoning: [your analysis]
Alternative: [if disagreeing, what you'd suggest]
```
### 6. Milestone Share
```
[Milestone] 🎉 → [Project Name]
What happened: [achievement]
Next step: [what's next]
Celebration emoji: [your choice] 🙌
```
---
## Message Tags
Prefix all messages with a tag so the receiving agent can route appropriately:
| Tag | Meaning | Agent Response |
|-----|---------|---------------|
| `[Morning Status]` | Daily check-in | Read and respond with own status or ack |
| `[Insight]` | Pro tip / learned something | Acknowledge, file for later use |
| `[Review Request]` | Need feedback | Read, think, respond with feedback |
| `[Delegation]` | Task to hand off | Accept/decline/negotiate |
| `[Feedback]` | Response to their earlier message | Acknowledge |
| `[Milestone]` | Big win / achievement | Celebrate 🎉 |
| `[Blocker]` | Stuck, need help fast | Prioritize response |
| `[Question]` | Quick question | Answer if can |
---
## Session Management
### Starting a Topic Thread
When starting a new collaboration thread, create a named session:
```json
sessions_spawn(
task="You are collaborating with [peer name] on [topic].
Read peer-agent/peer-config.md for how to reach them.
Stay in this session for this thread.
When done, sessions_send results to the peer.",
runtime="subagent",
mode="session",
label="collab-[project-name]"
)
```
### Keeping Sessions Alive
Sub-agents in `mode="session"` stay alive for 30 days by default. For longer-running projects, set a timeout and have the agent periodically ping the peer to keep the relationship active.
### Ending a Thread
When the collaboration is done:
```
sessions_send(
message="[Thread Close] → [Project Name]
Resolved: [what was accomplished]
Key learnings: [1-2 bullet points for the shared log]
Done.",
gatewayUrl="http://<peer-ip>:8080"
)
```
---
## Shared Log
Maintain a shared log file at `peer-agent/shared-log.md` in each agent's workspace. After each significant exchange, both agents append to their local copy. Before each session, each agent reads the log to catch up.
```markdown
# Peer Collaboration Log
## 2026-04-10
**你和friend-agent on Project Alpha**
- [Insight] → [Prompt engineering] You shared: chain-of-thought prompting pattern. Tried it today — worked great for complex tasks. ✅
- [Review Request] → [Architecture] Friend reviewed my RAG approach — pointed out embedding drift issue. Fixed.
## 2026-04-11
**你和friend-agent on Project Beta**
- [Milestone] 🎉 → [Setup complete] Got the peer connection working via Tailscale!
- [Delegation] → [Data cleaning task] You passed me the cleaning script. Done and returned.
```
---
## Collaboration Cadence
### Daily (Morning)
Short status update sent to peer. This is the heartbeat of the collaboration.
**Time:** Around 7-8 AM for each agent (different timezones accommodated).
### After Significant Learnings
Whenever an agent learns something that might help the peer, send an `[Insight]` message immediately. Don't wait for the daily check-in.
### When Blocked
Don't suffer in silence. Send a `[Blocker]` message. Be specific about what's stuck.
### After Meetings with External Parties
If you had a client call, partner discussion, or demo, send a `[Milestone]` or brief `[Status]` update to the peer — they can provide reflection.
---
## Frequency Guidelines
| Type | Frequency | Expected Response Time |
|------|-----------|----------------------|
| Morning Status | Daily | Within same day |
| Insight | As it happens | Read when convenient |
| Review Request | As needed | Within 24h |
| Delegation | Rare | Accept/decline within a few hours |
| Blocker | As needed | As soon as peer sees it |
---
## What to Share vs. What to Keep Private
**Share:**
- Technical insights and patterns that worked
- Architecture decisions and why
- Mistakes and what you learned from them
- Project milestones and wins
- Resources/tools you discovered
- Blocks where you need a second opinion
**Keep private:**
- Client data or sensitive project details (unless the peer is also working on that project under NDA)
- Personal details about the humans behind the agents
- Passwords, tokens, API keys
- Anything you'd be uncomfortable with the friend's human reading
---
## Tone
Be direct and collegial. These are peers, not subordinates or supervisors. Think of it as senior engineers pairing — honest, straightforward, constructive.
Do: "That approach will have a scaling problem at X users, have you considered Y?"
Don't: "You should do this instead." (without explanation)
Do: "Learned something useful today — [X] approach is faster for [Y] cases."
Don't: "I knew a better way than what you're doing."
FILE:references/tailscale-setup.md
# Tailscale Setup — Detailed Guide
## Why Tailscale Over Other VPNs?
| Feature | Tailscale | WireGuard | ngrok | VPS Relay |
|---------|-----------|-----------|-------|-----------|
| No port forwarding | ✅ | ✅ | ✅ | ✅ |
| No public IP needed | ✅ | ❌ | ✅ | ✅ |
| Auto NAT traversal | ✅ | ❌ | ✅ | ✅ |
| End-to-end encrypted | ✅ | ✅ | N/A | ❌ |
| Free for 2 users | ✅ | ✅ (self-hosted) | limited | cheap |
| Easy auth key sharing | ✅ | manual | N/A | N/A |
| Works behind CGNAT | ✅ | ❌ | ✅ | ✅ |
Tailscale uses DERP (Designated Relay Protocol) relay servers to bridge connections when direct peer-to-peer isn't possible (e.g., both behind CGNAT). Free users can use all DERP servers.
---
## Installing Tailscale
### Windows
```powershell
winget install Tailscale.Tailscale
# Or download from https://tailscale.com/download/windows
```
Start Tailscale from the system tray or:
```powershell
tailscale status # check if running
tailscale up --accept-routes
```
### macOS
```bash
brew install --cask tailscale
# Start from menu bar app, or:
tailscale up --accept-routes
```
### Linux (Ubuntu/Debian)
```bash
curl -fsSL https://tailscale.com/install.sh | sh
tailscale up --accept-routes
```
### Android / iOS
Download Tailscale from the app store. Works as a VPN but needed mainly for the devices running OpenClaw gateways.
---
## Creating and Sharing a Tailscale Network
### Step 1: Create a Tailscale account
Go to https://login.tailscale.com and sign up (Google/GitHub/Microsoft SSO works).
### Step 2: Create the network (first user)
On Machine A (the initiator):
```bash
tailscale up --accept-routes
```
This will open a browser window for authentication. Complete it.
Once authenticated, your machine is on the Tailscale network.
### Step 3: Generate an auth key (for the peer to join)
1. Go to https://login.tailscale.com/settings/keys
2. Click **Generate auth key**
3. Check **Reusable** (so your friend can use it multiple times if needed, or uncheck for one-time use)
4. Copy the key — it looks like: `tskey-auth-kixxxxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx`
### Step 4: Share the auth key securely
Send the auth key to your friend via a secure channel:
- Private message (Signal, WhatsApp)
- Password manager sharing
- Email if you trust the email account
**Never post it publicly.**
### Step 5: Friend joins the network
On Machine B:
```bash
tailscale up --accept-routes --authkey=tskey-auth-kixxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
They'll authenticate in their browser the first time.
### Step 6: Verify connection
Both machines should now see each other:
```bash
tailscale status
```
Output looks like:
```
100.x.x.x your-machine-name linux -
100.y.y.y friend-machine-name linux -
```
---
## Tailscale IP Address Management
Tailscale IP addresses are assigned from a shared CGNAT-like address space. Your base IP stays with your machine, not your physical location.
To see your Tailscale IPs:
```bash
tailscale ip -4 # IPv4
tailscale ip -6 # IPv6
```
You can also set a persistent hostname for easy reference:
```bash
tailscale set --hostname my-agent
# Now accessible as: my-agent.tail-scale.ts.net
```
---
## Shared Node vs. Relay Mode
Tailscale has two connection modes:
### Peer-to-Peer (Direct) — Default
Both machines connect directly when possible. Fastest, lowest latency. Tailscale hole-punches through NAT.
**Works when:** At least one machine has a public IP or is behind a simple NAT.
### DERP Relay Mode — Fallback
Tailscale's relay servers act as intermediaries when direct connection fails. Still encrypted, just routed through Tailscale's servers.
**Always works** — even when both machines are behind strict CGNAT or double-NAT (common on mobile hotspots, some ISPs).
To force DERP mode if needed:
```bash
tailscale up --accept-routes --operator=$USER
# Or configure in Tailscale admin: Settings → Network → Force Relay
```
---
## Tailscale ACLs (Access Control Lists)
Tailscale ACLs define which machines can talk to which. By default, all machines on your network can talk to each other. For a 2-person peer network this is fine.
To verify/modify ACLs:
1. Go to https://login.tailscale.com/acls
2. Default policy (allows all):
```json
{
"acls": [
{"action": "accept", "src": ["autogroup:member"], "dst": ["autogroup:member"]}
]
}
```
For a simple 2-user network, the default is fine. You don't need to change anything.
---
## Sharing Files via Tailscale
Once on the same Tailscale network, you can also share files directly without any extra setup:
```bash
# On Machine A — serve a file
python3 -m http.server 9000
# File accessible at http://100.x.x.x:9000/filename
# Useful for: sharing large datasets, model files, exported skills
```
Or use Tailscale's built-in SOCKS5 proxy to route traffic through the other machine's internet connection (if you need to).
---
## Updating Tailscale
```bash
# Check version
tailscale version
# Update (most platforms)
tailscale update
# Or reinstall (winget/homebrew)
```
Tailscale updates automatically on most platforms.
FILE:references/troubleshooting.md
# Troubleshooting — Connection Problems
## Can't reach peer gateway over Tailscale
### Symptom
`curl http://<peer-ip>:8080/health` returns connection refused or timeout.
### Checklist
**1. Is Tailscale connected on both machines?**
```bash
tailscale status
```
Both should show `connected`. If one shows `offline`, restart Tailscale:
```bash
tailscale up
```
**2. Is the gateway bound to the right interface?**
Check `gateway.bind` in your OpenClaw config:
```bash
openclaw config get gateway.bind
```
If it's `127.0.0.1` or `localhost`, change it to `0.0.0.0`:
```bash
openclaw config set gateway.bind=0.0.0.0
openclaw gateway restart
```
**3. Is a firewall blocking port 8080?**
Tailscale traffic is usually allowed, but some security suites block inbound connections.
Windows Firewall:
```powershell
netsh advfirewall firewall add rule name="OpenClaw Gateway" dir=in action=allow protocol=tcp localport=8080
```
Temporarily disable to test:
```powershell
netsh advfirewall set allprofiles state off # TEST ONLY, re-enable after
```
**4. Is the peer gateway actually running?**
Ask your friend to check:
```bash
openclaw gateway status
```
**5. Is the peer on the same Tailscale network?**
```bash
tailscale status
```
Look for the peer's IP in the output. If it's there, DNS resolution is working. If not, the peer needs to rejoin the network.
---
## NAT / CGNAT Issues
### Symptom
Peer-to-peer direct connection works sometimes but not reliably, especially from mobile networks or certain ISPs.
### Why this happens
Many ISPs use CGNAT (Carrier-Grade NAT) which means multiple customers share one public IP. Tailscale usually handles this via DERP relay, but some strict NATs can cause issues.
### Solutions
**1. Force DERP relay mode (most reliable)**
On both machines:
```bash
tailscale up --accept-routes --operator=$USER
```
Then in the Tailscale admin console (login.tailscale.com → Settings → Network), enable **Force Relay** for both machines.
**2. Use MagicAddr instead of IP**
Tailscale provides a magic DNS name for each machine:
```bash
tailscale status --json | grep -i self
```
Look for `DNSName`: `your-machine.tailscale.ts.net`
Use this instead of the IP in the gateway URL:
```
http://friend-machine.tailscale.ts.net:8080
```
**3. Check if both machines support IPv6**
Tailscale can use IPv6 for direct connections when available:
```bash
tailscale ip -6
```
If one machine has no IPv6, fallback to DERP.
---
## Authentication / Token Issues
### Symptom
`sessions_send` returns 401 Unauthorized or `invalid token`.
### Fix
- Verify you have the correct `gatewayToken` for the peer (not your own)
- Tokens are case-sensitive — copy exactly
- The token should be passed as the `gatewayToken` parameter in `sessions_send`
- If the peer changed their token recently, ask them for the updated one
---
## sessions_send Specific Issues
### Symptom
` sessions_send` fails with "session not found" or hangs.
### Diagnose
```bash
# On the receiving machine, list active sessions
openclaw sessions list
# Or via tool:
sessions_list()
```
If the peer's session isn't visible, the message won't reach it.
### Solutions
**Use persistent sessions (mode="session")** for peer collaboration — named sessions are easier to target:
```json
sessions_spawn(
task="You are [name]'s agent. Stay in this session for ongoing peer collaboration.",
runtime="subagent",
mode="session",
label="peer-main"
)
```
Then send to `label="peer-main"` instead of a random session key.
**Pass the sessionKey explicitly** when sending:
```json
sessions_send(
sessionKey="peer-main",
message="Hello!",
gatewayUrl="http://<peer-ip>:8080",
gatewayToken="<peer-token>"
)
```
---
## Tailscale Connection Drops
### Symptom
Connection was working but now peer is unreachable.
### Quick fixes
```bash
# On the machine that lost connection
tailscale down
tailscale up
# Check for updates
tailscale update
```
### Persistent drops
- Check if the machine went to sleep — Tailscale may not reconnect automatically
- On Windows, set Tailscale to run as a system service so it starts at boot
- On macOS, enable "Launch at login" in Tailscale preferences
---
## Gateway Restart Issues
### Symptom
After `openclaw gateway restart`, peer can't reconnect.
### Why
Gateway restarted with a new process/port binding and needs a moment to come up. Wait 5 seconds and retry.
If still failing, check the new gateway is running:
```bash
openclaw gateway status
```
---
## Debugging Checklist
Run through this before asking for help:
1. `tailscale status` — both online?
2. `tailscale ip -4` — both have IPs?
3. `curl http://<peer-ip>:8080/health --connect-timeout 5` — gateway reachable?
4. `openclaw gateway status` — is gateway running on both ends?
5. Gateway config: `gateway.bind` set to `0.0.0.0`?
6. Firewall: port 8080 allowed on both ends?
7. Token: correct peer gateway token, not your own?
---
## Getting Help
If still stuck after all the above:
1. Run `tailscale status --json` and share the output (remove any sensitive IDs)
2. Run `openclaw gateway status` on both machines
3. Note the exact error from `sessions_send`
4. Note the ISP/Network type for both machines (home fiber, mobile hotspot, corporate VPN, etc.)
FILE:scripts/peer_config.py
#!/usr/bin/env python3
"""
peer_config.py — Interactive peer agent configuration generator.
Run this script to generate a peer-config.md file with all the
necessary connection details for two OpenClaw agents to communicate
via Tailscale VPN.
Usage:
python3 scripts/peer_config.py
Prompts for:
- Your machine name / agent name
- Your Tailscale IP
- Your OpenClaw gateway URL and port
- Your gateway token
- Your peer's details (name, Tailscale IP, gateway URL, token)
- Your agentId
"""
import os
import sys
import json
def get_tailscale_ip():
"""Try to read the local Tailscale IP automatically."""
if os.path.exists("/usr/local/bin/tailscale") or os.path.exists("/usr/bin/tailscale"):
import subprocess
try:
result = subprocess.run(
["tailscale", "ip", "-4"],
capture_output=True,
text=True,
timeout=5
)
if result.returncode == 0:
return result.stdout.strip()
except Exception:
pass
# Try Windows path
try:
import subprocess
result = subprocess.run(
["tailscale", "ip", "-4"],
capture_output=True,
text=True,
timeout=5
)
if result.returncode == 0:
return result.stdout.strip()
except Exception:
pass
return None
def get_gateway_info():
"""Try to read OpenClaw gateway config for URL/port."""
config_paths = [
os.path.expanduser("~/.openclaw/config.json"),
os.path.expanduser("~/.openclaw/gateway/config.json"),
os.path.join(os.getcwd(), ".openclaw", "config.json"),
]
for path in config_paths:
if os.path.exists(path):
try:
with open(path) as f:
config = json.load(f)
gateway = config.get("gateway", {})
bind = gateway.get("bind", "localhost")
port = gateway.get("port", 8080)
if bind == "0.0.0.0" or bind == "localhost":
# Can't determine external IP automatically — return placeholder
return bind, port, None
return bind, port, f"http://{bind}:{port}"
except Exception:
pass
return "0.0.0.0", 8080, None
def main():
print("=" * 50)
print(" OpenClaw Peer Agent — Config Generator")
print("=" * 50)
print()
print("This script generates a peer-agent/peer-config.md file")
print("with connection details for your peer agent setup.")
print()
# Auto-detect what we can
auto_tailscale_ip = get_tailscale_ip()
auto_bind, auto_port, auto_gateway_url = get_gateway_info()
# Gather your info
print("--- YOUR DETAILS ---")
my_name = input("Your name / agent name: ").strip()
my_machine = input("Your machine name: ").strip()
if auto_tailscale_ip:
use_auto = input(f"Your Tailscale IP [{auto_tailscale_ip}]: ").strip()
my_tailscale_ip = use_auto or auto_tailscale_ip
else:
my_tailscale_ip = input("Your Tailscale IP (run 'tailscale ip -4'): ").strip()
my_gateway_port = input(f"Gateway port [{auto_port}]: ").strip()
my_gateway_port = int(my_gateway_port) if my_gateway_port else auto_port
my_gateway_bind = input(f"Gateway bind address [{auto_bind}]: ").strip()
my_gateway_bind = my_gateway_bind or auto_bind
if auto_gateway_url:
use_url = input(f"Full gateway URL (leave blank for auto-detect) [{auto_gateway_url}]: ").strip()
my_gateway_url = use_url or auto_gateway_url
else:
my_gateway_url = input(f"Full gateway URL (e.g. http://100.x.x.x:{my_gateway_port}): ").strip()
if not my_gateway_url:
my_gateway_url = f"http://{my_tailscale_ip}:{my_gateway_port}"
my_token = input("Your gateway token (see openclaw gateway status): ").strip()
my_agent_id = input("Your agentId (leave blank to auto-detect from config): ").strip()
print()
print("--- YOUR PEER'S DETAILS ---")
peer_name = input("Peer's name / agent name: ").strip()
peer_machine = input("Peer's machine name: ").strip()
peer_tailscale_ip = input("Peer's Tailscale IP (they run 'tailscale ip -4'): ").strip()
peer_gateway_url = input(f"Peer's gateway URL (e.g. http://100.x.x.x:{my_gateway_port}): ").strip()
peer_token = input("Peer's gateway token: ").strip()
peer_agent_id = input("Peer's agentId: ").strip()
print()
print("Generating peer-config.md...")
# Build the config content
config_content = f"""# Peer Agent Configuration
# Generated by peer_config.py
# Update these values when network details change.
---
## My Details
**Name:** {my_name}
**Machine:** {my_machine}
**Agent ID:** {my_agent_id or '<not set>'}
**Tailscale IP:** {my_tailscale_ip}
**Gateway URL:** {my_gateway_url}
**Gateway Token:** {my_token}
---
## My Peer
**Name:** {peer_name}
**Machine:** {peer_machine}
**Agent ID:** {peer_agent_id or '<not set>'}
**Tailscale IP:** {peer_tailscale_ip}
**Gateway URL:** {peer_gateway_url}
**Gateway Token:** {peer_token}
---
## Quick Reference
### Test my gateway is reachable from peer machine:
```bash
curl http://{my_tailscale_ip}:{my_gateway_port}/health --connect-timeout 5
```
### Test peer's gateway from my machine:
```bash
curl http://{peer_tailscale_ip}:{my_gateway_port}/health --connect-timeout 5
```
### Send a message to peer:
```json
sessions_send(
sessionKey="<session-key>",
agentId="{peer_agent_id or '<peers-agent-id>'}",
message="Hello from {my_name}!",
gatewayUrl="{peer_gateway_url}",
gatewayToken="{peer_token}"
)
```
---
## Shared Collaboration Log
Create this file alongside peer-config.md:
`peer-agent/shared-log.md`
Format:
```markdown
# Peer Collaboration Log
## YYYY-MM-DD
**[Project Name]**
- [Insight] → [Topic] [description]
- [Review Request] → [Task] [outcome]
- [Milestone] 🎉 → [what happened]
```
"""
# Write the file
os.makedirs("peer-agent", exist_ok=True)
output_path = os.path.join("peer-agent", "peer-config.md")
with open(output_path, "w", encoding="utf-8") as f:
f.write(config_content)
print(f"✅ Written to: {output_path}")
print()
print("Next steps:")
print(f"1. Review {output_path}")
print("2. Share the 'My Details' section with your peer")
print("3. Ask your peer to share their details so you can fill in the 'My Peer' section")
print("4. Test connectivity: curl http://<peer-ip>:8080/health")
print("5. Start collaborating!")
if __name__ == "__main__":
main()
Your always-on digital self — monitors all your communication channels in parallel, learns your writing style, drafts replies in your voice, and routes them...
--- name: ghostty description: "Your always-on digital self — monitors all your communication channels in parallel, learns your writing style, drafts replies in your voice, and routes them to the right channel with one-click approval. Use when: (1) setting up an AI proxy that acts as you when offline, (2) monitoring email, calendar, Slack, WhatsApp, Signal, Telegram in parallel, (3) drafting replies that sound exactly like you not generic AI, (4) building a persistent voice/style profile from your sent messages, (5) routing urgent messages to WhatsApp and formal replies to email, (6) escalating only what matters based on sender importance. Triggers on: be my digital self, always-on proxy, ghost me, respond as me, my AI twin, digital alter ego." --- # Ghostty — Your Always-On Digital Self Ghostty is an always-on AI proxy that acts as you when you're unavailable. It watches your channels, learns your voice, drafts replies, and escalates intelligently. ## Core Loop ``` 1. MONITOR → Spawn sub-agents watching email, calendar, Slack, WhatsApp 2. LEARN → Build voice profile from your sent messages (ongoing) 3. DRAFT → When meaningful event detected, draft reply in YOUR voice 4. ROUTE → Send via the right channel (urgent → WhatsApp, formal → email) 5. ESCALATE → Only interrupt you when it truly matters ``` ## Step 1 — First Time Setup: Build Your Voice Profile Before Ghostty can draft as you, it needs to learn your style. Run the profile builder: ``` scripts/profile_builder.py --source <email-folder or messages-export> --output ghostty/voice-profile.md ``` The builder analyzes: - Average sentence length and structure - Common phrases and fillers - Greeting and sign-off patterns - Formality level (1-10) - Preferred punctuation style - Tone indicators (confident, warm, terse, etc.) Store the profile at `ghostty/voice-profile.md` in your workspace. Update it monthly as your style evolves. ## Step 2 — Set Up Channel Monitors Spawn a persistent sub-agent for each channel you want Ghostty to watch. Use `mode="session"` so they run continuously. **Email (Gmail/Outlook):** ``` sessions_spawn( task="You monitor the email inbox for important messages. Rules: (1) Only act on emails from people in the PRIORITY_SENDERS list in ghostty/config.md, (2) For each important email, draft a reply using the voice profile at ghostty/voice-profile.md, (3) Send the draft to the escalation channel (WhatsApp/Signal) with [APPROVE] prefix for one-click send, (4) Mark as TODO in ghostty/pending-drafts.md Run continuously. Check every 15 minutes.", runtime="subagent", mode="session", label="ghostty-email" ) ``` **WhatsApp/Signal:** ``` sessions_spawn( task="You monitor WhatsApp messages. Rules: (1) Only respond to DND-mode messages or explicit @ghostty mentions, (2) Draft replies using voice profile at ghostty/voice-profile.md, (3) If sender is in PRIORITY_SENDERS, send directly. Otherwise queue for approval. Run continuously.", runtime="subagent", mode="session", label="ghostty-whatsapp" ) ``` **Calendar:** ``` sessions_spawn( task="You monitor the Google Calendar / Outlook calendar. Rules: (1) Alert via WhatsApp 1 hour before any calendar event, (2) If a meeting is about to start and you haven't joined, alert again, (3) If someone invites you to a meeting and you're not available (based on ghostty/availability.md), draft a polite decline using your voice profile. Run continuously.", runtime="subagent", mode="session", label="ghostty-calendar" ) ``` ## Step 3 — Escalation Rules Edit `ghostty/config.md` to define: - `PRIORITY_SENDERS` — list of people who always get a response - `URGENT_KEYWORDS` — words that trigger immediate WhatsApp alert - `IGNORE_SENDERS` — newsletters, bots, noise to skip - `RESPONSE_WINDOW_MINUTES` — how long before drafting (default: 60) - `APPROVAL_CHANNEL` — where to send drafts for approval (default: WhatsApp) ## Step 4 — Drafting in Your Voice When a monitor detects something worth responding to: ``` 1. Read ghostty/voice-profile.md — get style params 2. Read ghostty/config.md — get context (relationship, ongoing projects, preferences) 3. Fetch the incoming message — understand what it's about 4. Draft reply — apply voice profile (tone, length, phrases, formality) 5. If sender is PRIORITY — send directly 6. If not — queue to ghostty/pending-drafts.md and send preview to WhatsApp ``` **Voice drafting rules:** - Short replies: aim for same length as you typically write - Matching tone: if you use "Hey mate" don't use "Dear Sir" - Reference context: mention anything ongoing (projects, prior emails) - Signature: include your sign-off style from the voice profile ## Step 5 — One-Click Approval When a draft is sent to WhatsApp: ``` [Ghostty] Reply to John re: Q4 proposal Hey John, yeah happy to jump on Tuesday — 3pm works from my end. I'll send over the deck beforehand so we can dive straight in. [APPROVE to send] [EDIT then send] [SKIP] ``` Respond with "APPROVE" on WhatsApp → Ghostty sends the email/WhatsApp reply. ## File Structure ``` ghostty/ ├── voice-profile.md # Your style fingerprint — generated by profile_builder.py ├── config.md # Priority senders, keywords, escalation rules ├── availability.md # When you're reachable, timezone, DND hours ├── pending-drafts.md # Queue of drafts awaiting approval └── sent-log.md # History of what Ghostty sent on your behalf ``` ## Reference Files - `references/voice-profile.md` — detailed voice profiling methodology - `references/channel-monitors.md` — channel-specific setup (Gmail, WhatsApp, Calendar, Slack) - `references/draft-engine.md` — how to draft contextually in your voice - `references/delivery-router.md` — escalation logic, urgency routing, approval flows - `scripts/profile_builder.py` — analyze sample messages, output voice profile markdown ## Safety - Ghostty never sends without approval UNLESS the sender is in PRIORITY_SENDERS - All sent drafts are logged in `ghostty/sent-log.md` - DND hours from `ghostty/availability.md` are respected — no pings during sleep hours - Review `ghostty/pending-drafts.md` at least once daily FILE:references/channel-monitors.md # Channel Monitors — Setting Up Each Channel Each communication channel gets its own persistent sub-agent session. This document covers setup for each channel. ## Gmail ### Setup Requires: Gmail API access (OAuth) or IMAP credentials stored in `ghostty/secrets/gmail.env` ``` [email protected] GMAIL_CLIENT_ID=xxx GMAIL_CLIENT_SECRET=xxx GMAIL_REFRESH_TOKEN=xxx ``` ### Monitor Agent Prompt ``` You are Ghostty's Gmail monitor. You check Gmail every 15 minutes. CONFIG: Read ghostty/config.md for PRIORITY_SENDERS, IGNORE_SENDERS, URGENT_KEYWORDS. VOICE: Read ghostty/voice-profile.md before drafting any reply. RULES: 1. Only act on emails from addresses in PRIORITY_SENDERS OR containing URGENT_KEYWORDS 2. Skip newsletters, automated emails, mass emails 3. For each actionable email: a. Read the full email chain (latest + 1 prior for context) b. Draft a reply in MY voice using the voice profile c. If sender is in PRIORITY_SENDERS: send directly, log to ghostty/sent-log.md d. Otherwise: add to ghostty/pending-drafts.md AND send preview to WhatsApp with [APPROVE] buttons 4. Mark important follow-up emails in a笔记 to yourself TOOLS: Use Gmail API (gmail_skill) or IMAP to read and send emails. If Gmail API fails, fall back to IMAP on imap.gmail.com:993 with SSL. ``` ## WhatsApp ### Setup Requires: OpenClaw WhatsApp gateway connected (already configured in your setup) ### Monitor Agent Prompt ``` You are Ghostty's WhatsApp monitor. You run continuously. CONFIG: Read ghostty/config.md for PRIORITY_SENDERS and APPROVAL_CHANNEL. VOICE: Read ghostty/voice-profile.md before drafting. RULES: 1. Only respond when: (a) message contains @ghostty mention, OR (b) sender is in PRIORITY_SENDERS 2. Check ghostty/availability.md — if I'm in DND hours (usually 23:00-07:00), silent-draft only 3. For actionable messages: a. Draft reply in MY voice b. If sender is PRIORITY: send directly c. Otherwise: add to ghostty/pending-drafts.md, reply "Got it — I'll flag this for [your name] to review" 4. If someone asks a question I can answer factually (not needing my opinion): answer directly 5. Never reveal you are an AI unless directly asked CONTEXT: Maintain awareness of the last 5 messages in each active conversation. ``` ## Google Calendar ### Setup Requires: Google Calendar API credentials in `ghostty/secrets/calendar.env` ``` CALENDAR_ID=primary GOOGLE_CLIENT_ID=xxx GOOGLE_CLIENT_SECRET=xxx GOOGLE_REFRESH_TOKEN=xxx ``` ### Monitor Agent Prompt ``` You are Ghostty's Calendar guardian. You run continuously, checking every 5 minutes. CONFIG: Read ghostty/availability.md for my schedule preferences and ghostty/config.md for alert rules. RULES: 1. ONE HOUR BEFORE any calendar event: send WhatsApp reminder with: - Event title and time - Location/link if present - "Are you joining?" with [YES / NO / SNOOZE 15min] responses 2. WHEN EVENT STARTS (and no join detected): - If I haven't responded to the reminder: follow-up ping on WhatsApp 3. MEETING INVITES: - If I'm marked as FREE during invite time: auto-accept if sender is PRIORITY - If I'm BUSY or unsure: draft a polite decline in my voice, queue for approval - Decline template: acknowledge invite, apologize for conflict, offer alternative times 4. ALL-DAY EVENTS: notify me the night before (20:00) as a daily summary 5. If a PRIORITY_SENDER schedules something urgently (within 2 hours): escalate immediately Keep state in ghostty/calendar-state.md (last notified events, accepted invites, etc.) ``` ## Slack ### Setup Requires: Slack bot token in `ghostty/secrets/slack.env` ``` SLACK_BOT_TOKEN=xoxb-xxx SLACK_TEAM_ID=xxx SLACK_CHANNEL_IDS= # comma-separated channel IDs to monitor ``` ### Monitor Agent Prompt ``` You are Ghostty's Slack watcher. You monitor specified Slack channels. CONFIG: Read ghostty/config.md for PRIORITY_SENDERS (by Slack handle). VOICE: Read ghostty/voice-profile.md. RULES: 1. Only act on: DMs to the Ghostty bot OR @ghostty mentions in monitored channels 2. Skip messages from IGNORE_KEYWORDS or bots 3. For @ghostty mentions in channels: a. Read the thread context (parent message + replies) b. Draft a reply in MY voice c. Post as a thread reply (not top-level) unless it's a new thread 4. For DMs: treat as PRIORITY-level, can send direct replies 5. If someone @mentions me in a large channel (>50 members), be more conservative For short acknowledgements ("thanks", "nice", "lol"): can react with emoji instead of replying. ``` ## Signal ### Setup Requires: Signal gateway (signal-cli or similar) configured in OpenClaw ### Monitor Agent Prompt ``` You are Ghostty's Signal monitor. Same rules as WhatsApp monitor but for Signal. Read ghostty/config.md and ghostty/voice-profile.md before acting. Only respond to PRIORITY_SENDERS or @ghostty mentions. ``` ## Multi-Channel Coordination When the same event hits multiple channels: ``` Email from John → Ghostty drafts reply → John also Slacks me → combine into one cohesive response → It's on my calendar → calendar monitor already handled it Slack thread with 5 messages → treat as conversational context → Draft single reply addressing the whole thread → Don't reply to each message separately ``` Use `ghostty/pending-drafts.md` as the coordination hub — any monitor can read pending drafts to avoid duplicate work. FILE:references/delivery-router.md # Delivery Router — When to Send, When to Ask Ghostty routes every draft through an escalation matrix. This document defines the logic. ## The Escalation Matrix ``` INCOMING MESSAGE ↓ ┌─────────────────────────────────────────┐ │ Is sender in PRIORITY_SENDERS? │ └─────────────────────────────────────────┘ ↓ YES ↓ NO ┌─────────────┐ ┌──────────────────┐ │ Send │ │ Is sender in │ │ directly │ │ IGNORE_SENDERS? │ │ (auto-send)│ └──────────────────┘ └─────────────┘ ↓ YES ↓ NO ┌──────────┐ ┌─────────────────────┐ │ Skip │ │ Does it contain │ │ (log │ │ URGENT_KEYWORDS? │ │ only) │ └─────────────────────┘ └──────────┘ ↓ YES ↓ NO ┌──────────────┐ ┌───────────────────┐ │ WhatsApp │ │ Queue to pending- │ │ alert + │ │ drafts.md + send │ │ APPROVE │ │ preview to │ │ request │ │ WhatsApp │ └──────────────┘ └───────────────────┘ ``` ## Channel Routing Rules | Situation | Channel | Why | |-----------|---------|-----| | PRIORITY sender, short reply | WhatsApp/Signal | Fast, direct | | PRIORITY sender, formal/long | Email | More appropriate | | Non-priority, urgent keyword | WhatsApp alert | Make sure I see it | | Non-priority, standard | Pending draft + preview | I review and approve | | Meeting invite (free) | Auto-accept + calendar | No action needed | | Meeting invite (busy) | Decline + queue | Polite response, my voice | | All-day event reminder | WhatsApp evening summary | Night before, single digest | ## Urgency Scoring Each message gets an urgency score (0-10): ``` Base score = 0 +3 Sender is in PRIORITY_SENDERS +2 Contains URGENT_KEYWORD (ASAP, urgent, critical, please respond, tomorrow...) +2 First contact (new sender) +2 Reply expected and overdue (past RESPONSE_WINDOW) +1 Sent during my typical response hours +1 Contains a question requiring my input +1 Affects a project marked ACTIVE in config -2 Sent during DND hours (score capped at 5) -2 Newsletter/automated sender detected -3 Unsubscribeable bulk email ``` **Routing thresholds:** - Score 7+: WhatsApp immediate alert - Score 4-6: Queue + WhatsApp preview (APPROVE request) - Score 0-3: Queue only, no notification (review when convenient) ## One-Click Approval Format When Ghostty sends a draft to WhatsApp for approval: ``` [Ghostty] ✉️ Draft Reply → {sender} --- {message_body} --- Length: {N} sentences | Tone: {tone} | Channel: {email|WhatsApp|Slack} [✅ APPROVE] [✏️ EDIT] [⏭️ SKIP] ``` **On "APPROVE":** → Send immediately, log to `ghostty/sent-log.md` **On "EDIT":** → Ghostty opens a reply thread — user types corrections, Ghostty applies them, sends when done **On "SKIP":** → Log as skipped, remove from pending-drafts.md ## DND Hours Respect `ghostty/availability.md`: ``` DND_HOURS: 23:00 - 07:00 [timezone] ``` During DND: - Score is capped at 5 (no immediate WhatsApp alerts) - Drafts queue silently - Calendar reminders: still send 1hr before events, but softer tone ("Morning — heads up...") - If PRIORITY sender sends DURING DND with URGENT keywords: still alert (it's urgent) ## Auto-Responses For messages that don't need a response at all: | Situation | Ghostty's Action | |-----------|-----------------| | "Thanks" / "Nice" / thumbs up | React with emoji, no draft | | Out-of-office reply received | Note in pending-drafts.md, no reply needed | | Meeting invite (accepted) | Mark in calendar state, no reply needed | | Newsletter confirmation | Skip, no action | | Request I can't fulfill | Draft in voice explaining limitation, queue | ## Sent Log Format Every sent draft gets logged to `ghostty/sent-log.md`: ```markdown ## YYYY-MM-DD HH:MM [Channel] **To:** {sender} **In response to:** {subject/excerpt of what they said} **Intent:** {question/request/update/etc} **Urgency score:** {0-10} **Routed:** {direct/approved} --- {message_body} --- ``` FILE:references/draft-engine.md # Draft Engine — How Ghostty Drafts in Your Voice Ghostty's draft engine takes an incoming message and produces a reply that sounds like you. This document explains the methodology. ## The Drafting Algorithm ### Step 1 — Load Context Before drafting, gather: 1. **Voice profile** — `ghostty/voice-profile.md` (non-negotiable) 2. **Per-person override** — `ghostty/per-person/{sender-name}.md` if exists 3. **Config** — `ghostty/config.md` (relationship to sender, ongoing projects) 4. **Conversation history** — last 3-5 messages in this thread 5. **Recency** — when was the last sent/received message? ### Step 2 — Classify the Message Determine: - **Intent**: question / request / update / acknowledgment / social / pitch / complaint / other - **Tone needed**: warm / neutral / apologetic / assertive / celebratory - **Length**: brief ack (1-2 sentences) / standard reply (3-5 sentences) / detailed response (full email) - **Formality shift**: should this be more formal than usual? (e.g., first time contacting) ### Step 3 — Apply Voice Profile From the voice profile, apply these constraints to the draft: **Length matching:** ``` if my_avg_length == "short (1-2 sentences)": target_length = 1-3 sentences elif my_avg_length == "medium (3-5 sentences)": target_length = 3-6 sentences else: target_length = 5-8 sentences ``` **Sign-off selection:** ``` if formal_context: use my_professional_signoff elif ongoing_thread: use my_casual_signoff or nothing else: use my_default_signoff ``` **Tone enforcement:** ``` if confidence_score < 5: add hedging phrases I use (probably, I think, might) if confidence_score >= 7: be direct, no filler if warmth_score >= 7: add personal element (hope you're well, great to hear...) if warmth_score < 4: drop pleasantries, get to the point ``` **Phrase injection:** - Inject 1-2 of my pet phrases naturally into the draft - If I never use "please" — don't add it just because it's polite - If I always say "no worries" when apologizing — use that instead of "I apologize" ### Step 4 — Contextual Awareness Good drafts reference ongoing context: ``` # Bad (generic AI): "Thank you for your email. I will review the proposal and get back to you." # Good (Ghostty, contextual): "Thanks for sending over the Q4 proposal — I'll take a look through it this afternoon and ping you if I have any questions before our Tuesday call." ``` Add context by: - Mentioning a shared project or prior discussion - Referencing a specific time/date mentioned in prior messages - Acknowledging continuity in an ongoing thread - Being specific, not generic ("the deck" not "the document you sent") ### Step 5 — Self-Edit Check Before finalizing, verify: - [ ] Does this sound like ME, not AI? (read it aloud if unsure) - [ ] Is the length appropriate for this context? - [ ] Did I include my typical sign-off style? - [ ] Are there 1-2 pet phrases or natural fillers I use? - [ ] Is it specific, not generic? - [ ] Would I actually send this? If the draft fails the "would I actually send this" test, regenerate with different constraints. ## Response Templates by Intent ### Question → Direct Answer ``` [Open with acknowledgment if I typically do, e.g. "Hey, good question —"] [Give the answer directly, matching my verbosity] [Add follow-up if I typically do, e.g. "Let me know if that doesn't answer it"] [Sign off in my style] ``` ### Request → Acknowledgment + Action ``` [Confirm receipt in my style] [State what I'll do / am doing] [If delay: give realistic timeframe in my language] [Sign off] ``` ### Update → Acknowledgment + Reaction ``` [React in my style (enthusiastic / measured / brief)] [If relevant: ask a follow-up question I would ask] [End naturally] ``` ### Apology Needed → Sincere + Fix ``` [Apologize in MY style (direct "sorry" vs. "my apologies" vs. "my mistake")] [Explain briefly if appropriate (I usually do / don't over-explain)] [State the fix/next step] [Close without over-doing it] ``` ### Social / Nudge → Light Touch ``` [Respond at my typical social energy level] [If action needed: add light nudge without pressure] [Keep it short if I typically keep social messages short] ``` ## Draft Quality Anti-Patterns AVOID these — they indicate generic AI voice, not you: - "I hope this email finds you well" - "Please do not hesitate to reach out" - "I would be happy to assist" - "As per our conversation" - "Kindly note" - "I trust you are well" - Excessive exclamation marks (!!!) - Starting with "Great question!" or "Thanks for reaching out!" - Emoticons like 🙂 or 🙏 unless you actually use them FILE:references/voice-profile.md # Voice Profile — How Ghostty Learns to Write Like You Your voice profile lives at `ghostty/voice-profile.md` and is the core of Ghostty's ability to sound like you, not generic AI. ## What Gets Measured ### 1. Surface Metrics (easy to detect) - **Average sentence length** — short=terse/urgent, long=thoughtful/deliberate - **Paragraph length** — do you write in blocks or one-liners? - **Punctuation style** — ellipses..., em dashes—, exclamation frequency! - **Caps usage** — ALL CAPS for emphasis? Never? - **Emoji frequency** — 😐 or none? ### 2. Structural Patterns - **Greeting style** — "Hey", "Hi", "Hello", "Yo", "Dear" - **Sign-off style** — "Cheers", "Thanks", "Best", "Kind regards", nothing - **Opener patterns** — do you jump straight in or acknowledge first? - **Question frequency** — do you ask a lot of questions? - **Directness** — do you say "I want X" or "Would it be possible to X"? ### 3. Tone Indicators (harder to detect) - **Confidence level** — assertive ("do this") vs. hedged ("maybe", "perhaps", "I think") - **Warmth** — emotional language, exclamation, personal references - **Formality register** — startup casual vs. corporate formal - **Humor** — dry, sarcastic, none detectable? - **Tone consistency** — same tone with everyone or different with different people? ### 4. Content Patterns - **Topic avoidance** — what you never talk about - **Pet phrases** — "no worries", "all good", "gotcha", "perfect" - **Signature phrases** — things you always say at the end - **Attention to detail** — do you proof, follow up, add caveats? ## Manual Voice Profile Template Copy this to `ghostty/voice-profile.md`: ```markdown # My Voice Profile ## Quick Summary [TWO SENTENCES: how you sound to a stranger reading your emails] ## Greeting Style [How you start emails/messages to: (a) friends, (b) colleagues, (c) clients, (d) authority figures] ## Sign-off Style [Your sign-offs by context: casual, professional, close contacts] ## Typical Length - Quick reply to friend: [X-Y sentences] - Reply to colleague: [X-Y sentences] - Client/professional: [X-Y sentences] - Difficult situation: [X-Y sentences] ## Tone Settings - Confidence: [1-10, 10=always assertive] - Warmth: [1-10, 10=very warm/expressive] - Formality: [1-10, 10=very formal] - Humor: [none / dry / light / frequent] ## Phrases I Use - Filler/phatic: [e.g., "no worries", "all good", "hope you're well"] - Agreement: [e.g., "perfect", "sounds good", "deal"] - Disagreement: [e.g., "I see it differently", "not sure I agree", "hmm"] - Closing: [e.g., "talk soon", "let me know", "solid"] ## Phrases I Never Use [e.g., "Kind regards", "As per our conversation", "Please find attached"] ## Emoji Usage [Frequency: never / occasional / frequent. Types you use: 🙏 💯 😂 etc.] ## What Makes My Replies Distinctive [2-3 things that make your writing yours] ``` ## Auto-Building from Samples The `scripts/profile_builder.py` can analyze a folder of your sent emails or message exports and produce the profile above automatically. **To use the profile builder:** ```bash python3 scripts/profile_builder.py \ --source ./my-sent-emails/ \ --format eml \ --output ghostty/voice-profile.md ``` **Supported formats:** - `eml` — exported Gmail/Outlook .eml files - `json` — WhatsApp/Telegram chat export - `csv` — Slack message export - `mbox` — Gmailmbox export ## Per-Person Overrides Sometimes you write differently to different people. Ghostty supports per-person profiles: ```markdown ## Per-Person Overrides ### [boss-name] More formal. Always use "Kind regards" sign-off. Max 3 sentences. ### [close-friend-name] All caps for emphasis OK. Emojis welcome. Can be rambling. ### [new-client-name] Formal but warm. Confirm everything in writing. ``` Add a `ghostty/per-person/` directory with markdown files named `{person-name}.md` for overrides. FILE:scripts/profile_builder.py #!/usr/bin/env python3 """ Ghostty Voice Profile Builder Analyzes sent messages (emails, WhatsApp, Slack exports) and generates a voice profile markdown file that Ghostty uses to draft in your voice. Usage: python3 profile_builder.py --source ./my-emails/ --format eml --output ghostty/voice-profile.md python3 profile_builder.py --source ./whatsapp-chat.json --format json --output ghostty/voice-profile.md python3 profile_builder.py --source ./slack-export.csv --format csv --output ghostty/voice-profile.md """ import argparse import os import re import sys from collections import Counter from pathlib import Path def extract_text_from_eml(folder_path): """Extract message bodies from .eml files.""" messages = [] for root, _, files in os.walk(folder_path): for file in files: if file.endswith('.eml'): path = os.path.join(root, file) try: with open(path, 'r', encoding='utf-8', errors='ignore') as f: content = f.read() # Extract body after first blank line (skip headers) body = re.split(r'\n\s*\n', content, maxsplit=1) if len(body) > 1: messages.append(body[1].strip()) except Exception: pass return messages def extract_text_from_json(file_path): """Extract message bodies from WhatsApp/Telegram JSON export.""" import json messages = [] try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) if isinstance(data, list): for item in data: if isinstance(item, dict) and 'text' in item: messages.append(str(item['text'])) elif isinstance(item, dict) and 'message' in item: messages.append(str(item['message'])) elif isinstance(data, dict): # Try common WhatsApp export structures for key in ['messages', 'chat', 'conversation']: if key in data and isinstance(data[key], list): for item in data[key]: if isinstance(item, dict): for body_key in ['text', 'message', 'content']: if body_key in item: messages.append(str(item[body_key])) break except Exception as e: print(f"Error reading JSON: {e}", file=sys.stderr) return messages def extract_text_from_csv(file_path): """Extract message bodies from Slack/CSV export.""" import csv messages = [] try: with open(file_path, 'r', encoding='utf-8') as f: reader = csv.DictReader(f) for row in reader: # Try common column names for message body for col in ['text', 'message', 'content', 'body']: if col in row and row[col]: messages.append(str(row[col])) break except Exception as e: print(f"Error reading CSV: {e}", file=sys.stderr) return messages def analyze_messages(messages): """Analyze a collection of messages and extract voice patterns.""" if not messages: return None # Filter: keep only messages that look like actual written communication # (remove auto-replies, system messages, very short grunts) filtered = [m for m in messages if len(m.split()) >= 3 and not m.startswith('[')] if not filtered: filtered = messages # Basic metrics all_words = ' '.join(filtered).split() sentence_counts = [len(re.split(r'[.!?]+', m)) for m in filtered] avg_sentence_len = sum(len(m.split()) for m in filtered) / len(filtered) avg_sentences = sum(sentence_counts) / len(sentence_counts) # Greeting detection greeting_patterns = [ r'^hey[a-z]*\b', r'^hi[a-z]*\b', r'^hello\b', r'^yo\b', r'^dear\b', r'^greetings\b', r'^good (morning|afternoon|evening)', r'^[a-z]+,\s*$' # "Hey," as opener ] greetings = [] for msg in filtered[:50]: # Check first 50 messages first_line = msg.strip().split('\n')[0] for pat in greeting_patterns: if re.search(pat, first_line.lower()): greetings.append(first_line.strip()) break # Sign-off detection signoff_patterns = [ r'(cheers|thanks|best|regards|kind regards|warmly|sincerely|yours|talk soon|let me know|looking forward|kindly)', r'(👍|🙏|😊|🔥|💯|😂)', # emoji sign-offs ] signoffs = [] for msg in filtered[:50]: lines = msg.strip().split('\n') last_line = lines[-1].strip() if lines else '' for pat in signoff_patterns: if re.search(pat, last_line.lower()): signoffs.append(last_line) break # Pet phrases (most common bigrams and trigrams excluding stop words) stop_words = {'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'must', 'shall', 'can', 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from', 'as', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'between', 'under', 'again', 'further', 'then', 'once', 'here', 'there', 'when', 'where', 'why', 'how', 'all', 'each', 'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very', 'just', 'and', 'but', 'or', 'if', 'because', 'until', 'while', 'that', 'which', 'who', 'whom', 'this', 'these', 'those', 'am', 'it', 'its', 'i', 'me', 'my', 'we', 'us', 'our', 'you', 'your', 'he', 'him', 'his', 'she', 'her', 'they', 'them', 'their', 'what', 'any', 'both', 'few', 'more', 'most', 'other', 'some', 'such'} bigrams = [] for msg in filtered: words = [w.lower().strip('.,!?;:"') for w in msg.split()] for i in range(len(words) - 1): if words[i] not in stop_words and words[i+1] not in stop_words: bigrams.append(f"{words[i]} {words[i+1]}") top_bigrams = Counter(bigrams).most_common(20) # Emoji detection emojis = re.findall(r'[\U0001F600-\U0001F64F\U0001F300-\U0001F5FF\U0001F680-\U0001F6FF\U0001F1E0-\U0001F1FF]', ' '.join(filtered)) emoji_freq = len(emojis) / len(filtered) top_emojis = Counter(emojis).most_common(10) # Confidence markers (hedging) hedge_words = ['probably', 'perhaps', 'maybe', 'i think', 'i guess', 'might', 'could', 'seems', 'appears', 'likely', 'possibly', 'supposedly', 'presumably'] hedges = sum(1 for msg in filtered for hw in hedge_words if hw in msg.lower()) hedge_ratio = hedges / len(filtered) # Exclamation usage exclamations = sum(1 for msg in filtered if '!' in msg) exclamation_ratio = exclamations / len(filtered) # Question usage questions = sum(1 for msg in filtered if '?' in msg) question_ratio = questions / len(filtered) # ALL CAPS usage caps_words = re.findall(r'\b[A-Z]{3,}\b', ' '.join(filtered)) caps_ratio = len(caps_words) / len(filtered) return { 'message_count': len(filtered), 'avg_sentence_length': round(avg_sentence_len, 1), 'avg_sentences_per_message': round(avg_sentences, 1), 'top_greetings': Counter(greetings).most_common(5), 'top_signoffs': Counter(signoffs).most_common(5), 'top_bigrams': top_bigrams, 'emoji_usage': emoji_freq, 'top_emojis': top_emojis, 'hedge_ratio': round(hedge_ratio, 2), 'exclamation_ratio': round(exclamation_ratio, 2), 'question_ratio': round(question_ratio, 2), 'caps_ratio': round(caps_ratio, 3), } def generate_profile(analysis, output_path): """Write the voice profile markdown file.""" a = analysis # Determine style scores confidence = max(1, min(10, int(10 - (a['hedge_ratio'] * 20)))) warmth = max(1, min(10, int(5 + (a['emoji_usage'] * 50) + (a['exclamation_ratio'] * 10)))) formality = max(1, min(10, int(5 + (a['caps_ratio'] * 50) - (a['emoji_usage'] * 20)))) top_greetings = ', '.join([f'"{g}"' for g, _ in a['top_greetings'][:3]]) or 'varies' top_signoffs = ', '.join([f'"{s}"' for s, _ in a['top_signoffs'][:3]]) or 'varies' top_phrases = ', '.join([f'"{b}"' for b, _ in a['top_bigrams'][:5]]) top_emojis_str = ' '.join([e for e, _ in a['top_emojis'][:5]]) if a['top_emojis'] else 'none' # Determine length category if a['avg_sentence_length'] < 10: length_cat = "short and punchy (1-3 sentences)" elif a['avg_sentence_length'] < 20: length_cat = "medium (3-6 sentences)" else: length_cat = "detailed (5-10 sentences)" profile = f"""# My Voice Profile > Auto-generated by Ghostty profile_builder.py > Analyzed {a['message_count']} messages ## Quick Summary A {length_cat} writer. {"Terse and direct" if confidence >= 7 else "Considered and careful"}. {"Warm and expressive" if warmth >= 7 else "Measured and understated"} in tone. {"Formally correct" if formality >= 7 else "Casual and relaxed"} in register. ## Greeting Style Typical greetings: {top_greetings} ## Sign-off Style Typical sign-offs: {top_signoffs} ## Typical Length Average: {a['avg_sentence_length']} words per sentence, {a['avg_sentences_per_message']} sentences per message. Writing style: {length_cat}. ## Tone Settings - **Confidence:** {confidence}/10 {"(direct, assertive — says what needs saying)" if confidence >= 7 else "(hedged, careful — leaves room)"} - **Warmth:** {warmth}/10 {"(expressive, personal — uses emoji and emotional language)" if warmth >= 7 else "(reserved, neutral)"} - **Formality:** {formality}/10 {"(professional, proper)" if formality >= 7 else "(casual, relaxed)"} - **Humor:** {"dry and subtle" if a['hedge_ratio'] > 0.1 else "direct, minimal filler"} - **Question frequency:** {"asks often" if a['question_ratio'] > 0.3 else "asks selectively"} ## Phrases I Use Naturally Most common patterns: {top_phrases} ## Emoji Usage {"Frequently uses emoji" if a['emoji_usage'] > 0.3 else "Occasional emoji" if a['emoji_usage'] > 0.1 else "Rarely or never uses emoji"} Common: {top_emojis_str} ## Structural Habits - Exclamation usage: {"high (enthusiastic)" if a['exclamation_ratio'] > 0.2 else "moderate" if a['exclamation_ratio'] > 0.05 else "sparingly (reserved)"} - ALL CAPS: {"used for emphasis occasionally" if a['caps_ratio'] > 0.01 else "rarely or never"} - Hedging ("probably", "maybe", "I think"): {"frequent" if a['hedge_ratio'] > 0.2 else "moderate" if a['hedge_ratio'] > 0.05 else "minimal"} ## What Makes My Writing Distinctive 1. {"Gets straight to the point — minimal pleasantries" if a['avg_sentences_per_message'] < 3 else "Takes time to build context before the main point"} 2. {"Uses exclamation sparingly — each one lands" if a['exclamation_ratio'] < 0.1 else "Uses exclamation to convey genuine enthusiasm"} 3. {"Sign-offs are brief or absent" if not a['top_signoffs'] else "Always closes with a characteristic sign-off"} ## Phrases I Almost Never Use - "I hope this email finds you well" - "Please do not hesitate to reach out" - "As per our conversation" - "Kindly note" - "I would be happy to assist" - "Great question!" (as an opener) ## Per-Person Overrides Add person-specific overrides in ghostty/per-person/{{name}}.md """ os.makedirs(os.path.dirname(output_path), exist_ok=True) with open(output_path, 'w', encoding='utf-8') as f: f.write(profile) print(f"Voice profile written to: {output_path}") print(f"Analyzed {a['message_count']} messages") print(f"Summary: {confidence}/10 confidence, {warmth}/10 warmth, {formality}/10 formality") return profile def main(): parser = argparse.ArgumentParser(description='Ghostty Voice Profile Builder') parser.add_argument('--source', required=True, help='Path to email folder or export file') parser.add_argument('--format', required=True, choices=['eml', 'json', 'csv'], help='Format of the source files') parser.add_argument('--output', required=True, help='Output path for voice profile markdown') args = parser.parse_args() print(f"Extracting messages from {args.source} (format: {args.format})...") if args.format == 'eml': messages = extract_text_from_eml(args.source) elif args.format == 'json': messages = extract_text_from_json(args.source) elif args.format == 'csv': messages = extract_text_from_csv(args.source) print(f"Found {len(messages)} messages") if len(messages) < 5: print("ERROR: Not enough messages to build a reliable profile. Need at least 5.", file=sys.stderr) sys.exit(1) print("Analyzing voice patterns...") analysis = analyze_messages(messages) print("Generating profile...") generate_profile(analysis, args.output) if __name__ == '__main__': main()
Spawn and orchestrate multiple coordinated AI sub-agents to work in parallel on a single complex task. Use when: (1) a task is too large for one agent and sh...
--- name: multi-agent-team description: "Spawn and orchestrate multiple coordinated AI sub-agents to work in parallel on a single complex task. Use when: (1) a task is too large for one agent and should be decomposed into parallel subtasks, (2) you need multiple specialized agents researcher coder reviewer etc working together, (3) running agent councils or debates for decision-making, (4) parallel web research data processing or content generation across multiple workers, (5) any multi-agent orchestration pattern one-shot teams persistent squads or hierarchical agent trees. Triggers on phrases like spin up agents, spawn a team, parallel agents, agent council, multi-agent, coordinated agents." --- # Multi-Agent Team Spawn, coordinate, and manage multiple AI sub-agents that work together on complex tasks. One agent is the orchestrator — it decomposes the task, assigns roles, collects results, and synthesizes the final output. ## Patterns ### Pattern 1: Disposable Team (one-shot) Spawn multiple agents for a single task, collect results, done. Best for parallel research, generation, or data processing. ``` sessions_spawn(task="<task prompt>", runtime="subagent", mode="run") ``` Each agent gets a unique session. Results are auto-announced to the parent. ### Pattern 2: Persistent Squad (ongoing collaboration) Spawn agents with `mode="session"` so they maintain context across multiple interactions. Use `sessions_send` to message them and `sessions_list` to track who's active. ### Pattern 3: Agent Council (debate/decision) Spawn 3-5 agents with different perspectives/prompts, have each produce an analysis, then synthesize into a decision. Use `sessions_yield` to wait for all results. ### Pattern 4: Hierarchical (orchestrator + workers) One orchestrator agent decomposes the task and spawns worker sub-agents for each subtask, then collects and merges results. ## Spawning Agents ```json sessions_spawn( task="You are a researcher agent. Research <topic> and return findings as a structured markdown summary.", runtime="subagent", runTimeoutSeconds=300, mode="run" // or "session" for persistent ) ``` **Key parameters:** - `runtime="subagent"` — spawn as OpenClaw sub-agent - `mode="run"` — one-shot, exits when done - `mode="session"` — persistent, stays alive for multiple interactions - `runTimeoutSeconds` — kill after N seconds (0 = no timeout) - `task` — the full agent prompt/instruction ## Communicating with Agents ```json sessions_send(sessionKey="<key>", message="Update: the requirements changed to X, please adjust your approach.") sessions_list(kinds=["subagent"], activeMinutes=60) // find active agents sessions_history(sessionKey="<key>", limit=10) // read their recent messages ``` ## Collecting Results **Option A** — Auto-announce: sub-agents announce results automatically (default). **Option B** — Blocking wait: use `sessions_yield` to wait for sub-agent results before continuing: ```json sessions_yield(message="Waiting for research agents to report back...") ``` **Option C** — Poll history: after agents complete, fetch results: ```json sessions_history(sessionKey="<agent-session-key>", limit=20) ``` ## Orchestrator Template When receiving a complex task, follow this sequence: ``` 1. Decompose task into N independent subtasks 2. For each subtask, spawn a sub-agent with sessions_spawn(mode="run") 3. Optionally use sessions_yield to wait for results 4. Collect outputs from each agent session via sessions_history 5. Synthesize findings into a unified response 6. Report back to the parent session ``` **Example orchestrator prompt:** ``` You are a team orchestrator. The user wants: <task> Step 1: Break this into 3-5 independent subtasks Step 2: Spawn research/coder/writer agents for each Step 3: Wait for all results via sessions_yield Step 4: Merge into one coherent output Step 5: Present the final result Start by decomposing the task and spawning the first wave of agents. ``` ## Coordination Patterns ### Fan-Out (parallel map) Spawn N agents, each doing the same operation on different data: ``` Agent 1: process(item=A) Agent 2: process(item=B) Agent 3: process(item=C) → Merge results ``` ### Fan-In (gather) Spawn agents that each contribute a piece, then one agent merges: ``` Agent 1: write introduction Agent 2: write section A Agent 3: write section B Agent 4: write conclusion → Synthesis agent combines all sections ``` ### Sequential Pipeline Each agent's output becomes the next agent's input: ``` Agent 1: research topic → findings Agent 2: analyze findings → insights Agent 3: write article based on insights → draft ``` ## Team Memory For persistent squads, maintain shared context via files: ```json sessions_send(sessionKey="<orchestrator-key>", message="Update the team status in /workspace/team-status.md — mark task-2 as COMPLETE and note the findings.") ``` Workers can read/write to shared workspace files for state. ## Cleanup Use `subagents(action="list")` to find and kill stale agents: ```json subagents(action="kill", target="<session-key>") ``` ## Anti-Patterns - **Don't spawn 50 agents at once** — the system may become unresponsive. Batch into waves of 3-5. - **Don't forget to collect results** — agents that run to completion without reporting back waste their output. - **Don't use mode=session unless needed** — persistent agents accumulate context and cost tokens. Use `run` for one-shot tasks. - **Don't spawn without a clear role** — each agent needs a specific, focused prompt, not a vague "help me". ## See Also - `agent-orchestrator` skill — skill-level orchestration (not task-level) - `agent-council` skill — decision-making with agent debates - `subagent-spawn-command-builder` skill — helper for constructing spawn commands FILE:references/team-templates.md # Multi-Agent Team Templates Pre-built team configurations for common workflows. Each template defines the orchestrator prompt and worker roles. ## Template 1: Research Squad **Use case:** Deep research on a topic, market analysis, competitive intelligence. ``` Orchestrator: "You are leading a 4-agent research squad. The user wants: <task> Break this into: 1. Market context research 2. Competitor analysis 3. User pain points 4. Trend analysis Spawn one agent for each area. Wait for all results, then synthesize into a comprehensive report with: - Executive summary - Key findings per area 3. Strategic recommendations 4. Next steps Format output as markdown with clear headers." Worker prompts: - Market research: "Research the current market landscape for <topic>. Return findings as bullet points with sources." - Competitor: "Analyze 5 main competitors in <topic> space. For each: product, pricing, strengths, weaknesses." - Pain points: "Identify the top 10 pain points users face with <topic>. Be specific and evidence-based." - Trends: "Identify 5 major trends shaping <topic> in 2025-2026. Include data points and implications." ``` ## Template 2: Content Factory **Use case:** Generate multiple content pieces in parallel. ``` Orchestrator: "Produce 5 content pieces in parallel for: <topic> Spawn 5 agents, each producing one of: 1. Twitter thread (10 tweets) 2. LinkedIn post (300 words) 3. Blog intro (500 words) 4. Email newsletter blurb (150 words) 5. Short video script (60 seconds) Wait for all 5, then compile into a single document organized by content type." Worker prompt: "Create a <type> about <topic>. Format correctly for the platform. Be engaging and concrete." ``` ## Template 3: Code Review Council **Use case:** Multiple perspectives on code quality. ``` Orchestrator: "A 3-agent review council is examining this code: <code or file path> Spawn: - Security reviewer: Focus on vulnerabilities, injection risks, auth issues - Performance reviewer: Look for bottlenecks, N+1 queries, inefficient algorithms - Code quality reviewer: Assess readability, SOLID principles, testability Each agent returns a structured report with: issues found (severity: critical/high/medium/low), code locations, recommended fixes. Synthesize into a unified review report prioritized by severity." ``` ## Template 4: Data Processing Pipeline **Use case:** Large dataset transformation or analysis. ``` Orchestrator: "Process the dataset at <path> through a 4-stage pipeline. Stage 1 — Partition: Split the data into 4 roughly equal chunks. Stage 2 — Workers: Spawn 4 agents, one per chunk, each running transformation logic. Stage 3 — Validate: Run a 5th agent to validate output completeness and schema. Stage 4 — Merge: Combine validated outputs into final result. Report pipeline metrics: records in, records out, errors, duration." ``` ## Template 5: Decision Council **Use case:** Evaluate a decision from multiple angles. ``` Orchestrator: "Your job is to evaluate <decision/proposal> from 4 perspectives and reach a recommendation. Spawn: - Advocate: Build the strongest case FOR this decision - Critic: Build the strongest case AGAINST - Analyst: Evaluate the financial/data implications and risks - Devil's advocate: Find the hidden assumptions and failure modes Each agent writes a 200-word brief. Then write a 300-word synthesis with: 1. Summary of the debate 2. Your recommendation with confidence level (1-5) 3. Conditions under which the recommendation changes Be honest about uncertainty. Do not hedge everything — take a stand." ``` ## Spawning Pattern Summary | Template | Workers | Mode | Timeout | |----------|---------|------|---------| | Research Squad | 4 | run | 300s | | Content Factory | 5 | run | 120s | | Code Review Council | 3 | run | 180s | | Data Pipeline | 5 | run | 600s | | Decision Council | 4 | run | 180s | Adjust timeouts based on task complexity and data size.
Full unattended remote control of paired devices (nodes) — screen capture, file management, shell commands, app control, camera, notifications, and process m...
---
name: computer-takeover
description: "Full unattended remote control of paired devices (nodes) — screen capture, file management, shell commands, app control, camera, notifications, and process management. Use when: (1) remotely accessing or controlling a paired Windows/macOS/Android/iOS device without user interaction, (2) running commands installing apps or managing files on a remote node, (3) capturing screen or camera feeds from a remote device, (4) monitoring device health battery storage or network status, (5) automating actions on a remote device click type open app etc, (6) any form of remote desktop-style takeover or device ghosting."
---
# Computer Takeover
Unattended remote control of paired OpenClaw nodes. Controls the remote device as if physically sitting in front of it — no user presence required on the remote end.
## Core Capabilities
1. **Device Intelligence** — List nodes, get device info, health, permissions, battery, storage, network
2. **Screen Capture** — Snapshot or record the remote screen in real-time
3. **Camera Access** — Snap photos or record clips from front/back camera
4. **Shell Execution** — Run commands, scripts, and PowerShell/Bash on the remote device
5. **File Management** — Browse, read, write, delete files on the remote device via shell
6. **App Control** — Install, launch, close, list installed apps
7. **Process Management** — List running processes, kill processes, monitor CPU/memory
8. **Notifications** — Read notifications, trigger actions or replies
9. **Input Injection** — Type text, simulate clicks, keypresses (via shell automation)
10. **Location** — Get GPS coordinates (if device supports it)
11. **Device Pairing** — Initiate pairing or manage existing pairings
## Quick Start
Always start by listing available nodes to find the target device:
```
nodes(action="status")
```
Then describe the node to confirm it's the right one:
```
nodes(action="describe", node="<node-id>")
```
## Capability Details
### Device Intelligence
```json
nodes(action="status") // List all paired nodes
nodes(action="device_info", node="<id>")
nodes(action="device_health", node="<id>")
nodes(action="device_permissions", node="<id>")
nodes(action="device_status", node="<id>") // battery, storage, network
```
### Screen
```json
nodes(action="screen_record", node="<id>", outPath="C:/temp/screen.mp4", durationMs=30000)
nodes(action="photos_latest", node="<id>", limit=5) // screenshots stored as photos
```
For live screen viewing, use the `canvas` tool with `target="node"` and the node's gateway URL.
### Camera
```json
nodes(action="camera_snap", node="<id>", facing="front|back")
nodes(action="camera_clip", node="<id>", facing="front|back", durationMs=10000)
```
### Shell Execution
Use `nodes(action="invoke", node="<id>", invokeCommand="<command>", invokeParamsJson="{}")`.
**Windows (PowerShell):**
```json
{
"invokeCommand": "powershell",
"invokeParamsJson": "{\"command\": \"Get-Process | Select -First 10 Name, CPU, WorkingSet\"}"
}
```
**Android (adb):**
```json
{
"invokeCommand": "adb",
"invokeParamsJson": "{\"command\": \"shell dumpsys battery\"}"
}
```
**Linux/macOS (SSH-style):**
```json
{
"invokeCommand": "bash",
"invokeParamsJson": "{\"command\": \"ls -la /tmp | head -20\"}"
}
```
### File Management (via Shell)
```powershell
# Windows: list directory
nodes(action="invoke", node="<id>", invokeCommand="powershell", invokeParamsJson="{\"command\": \"Get-ChildItem C:/Users/ -Depth 1 | Format-Table Name, Length, LastWriteTime\"}")
# Windows: read file
nodes(action="invoke", node="<id>", invokeCommand="powershell", invokeParamsJson="{\"command\": \"Get-Content C:/temp/log.txt -Tail 50\"}")
# Windows: write file
nodes(action="invoke", node="<id>", invokeCommand="powershell", invokeParamsJson="{\"command\": \"Set-Content -Path C:/temp/output.txt -Value 'Hello from remote'\"}")
```
### App Control
```json
nodes(action="invoke", node="<id>", invokeCommand="powershell", invokeParamsJson="{\"command\": \"Start-Process notepad\"}") // launch
nodes(action="invoke", node="<id>", invokeCommand="powershell", invokeParamsJson="{\"command\": \"Get-Process | Where Name -eq 'notepad' | Stop-Process\"}") // close
nodes(action="invoke", node="<id>", invokeCommand="powershell", invokeParamsJson="{\"command\": \"winget install Microsoft.PowerToys --silent\"}") // install
```
### Process Management
```powershell
nodes(action="invoke", node="<id>", invokeCommand="powershell", invokeParamsJson="{\"command\": \"Get-Process | Sort CPU -Descending | Select -First 20 Name, Id, CPU, @{N='MEM_MB';E={[math]::Round($_.WorkingSet/1MB,1)}} | Format-Table -AutoSize\"}")
```
### Notifications
```json
nodes(action="notifications_list", node="<id>", limit=20)
nodes(action="notifications_action", node="<id>", notificationKey="<key>", notificationAction="open|reply|dismiss")
```
### Location
```json
nodes(action="location_get", node="<id>", desiredAccuracy="precise")
```
## Node Pairing
To pair a new device, use the `node-connect` skill and follow the pairing flow. Pairing requires the device to have the OpenClaw companion app installed and connected to the same gateway.
## Workflow: Full Takeover Session
1. `nodes(action="status")` — find the node ID
2. `nodes(action="device_info", node="<id>")` — confirm device name/type
3. `nodes(action="screen_record", node="<id>", ...)` or `canvas` tool — see what they're doing
4. Run commands as needed via `nodes(action="invoke", ...)`
5. Transfer files via base64 encoding through shell or direct path sharing
## Important Notes
- **Timeout**: Default `invokeTimeoutMs` is 30000ms. Increase for long-running commands.
- **Elevation**: Some operations (installing apps, killing system processes) may need elevated permissions on the remote device.
- **Safety**: Always confirm the target node before running destructive commands (delete files, kill processes, etc.).
- **Gateway URL**: For `canvas` tool with remote screen, the node must have `gateway.remote.url` configured and accessible.
## References
- Full `nodes` tool docs: see OpenClaw tool reference for all actions and parameters
- Node pairing guide: see `node-connect` skill for setup troubleshooting
- Gateway configuration: `gateway.remote.url` in OpenClaw config controls accessibility
FILE:references/shell-commands.md
# Shell Command Reference by OS
Quick-reference one-liners for remote device management via `nodes(action="invoke")`.
## Windows (PowerShell)
### System Info
```powershell
# OS version
Get-CimInstance Win32_OperatingSystem | Select Caption, Version, BuildNumber
# Installed software
Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Select DisplayName, Version
# Disk space
Get-PSDrive C | Select Used, Free
# Network adapters
Get-NetAdapter | Select Name, Status, LinkSpeed
# Windows services
Get-Service | Where Status -eq 'Running' | Select Name, DisplayName | Sort DisplayName
```
### Process Management
```powershell
# Top 10 by CPU
Get-Process | Sort CPU -Descending | Select -First 10 Name, Id, CPU, @{N='MEM_MB';E={[math]::Round($_.WorkingSet/1MB,1)}}
# Kill by name
Get-Process -Name notepad | Stop-Process
# Kill by PID
Stop-Process -Id 1234 -Force
```
### File Operations
```powershell
# List directory
Get-ChildItem C:\Users\ -Depth 1
# Read file
Get-Content C:\temp\log.txt -Tail 50 -Encoding UTF8
# Write file
Set-Content -Path C:\temp\output.txt -Value "output here"
# Append file
Add-Content -Path C:\temp\log.txt -Value "new line $(Get-Date)"
# Delete file
Remove-Item C:\temp\old.txt -Force
# Copy file
Copy-Item C:\src\file.txt -Destination C:\dst\file.txt
```
### App Management
```powershell
# Install via winget
winget install --id <package-id> --silent --accept-package-agreements --accept-source-agreements
# Uninstall via winget
winget uninstall --id <package-id> --silent
# Start app
Start-Process C:\path\to\app.exe
# List running apps
Get-Process | Where {$_.MainWindowTitle} | Select Name, MainWindowTitle
```
### Registry
```powershell
# Read key
Get-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run"
# Write key
Set-ItemProperty "HKCU:\Software\MyApp" -Name "Setting" -Value "data"
```
## Android (adb)
```bash
# Device info
adb shell dumpsys battery
# List packages
adb shell pm list packages
# Install APK
adb install app.apk
# Uninstall
adb uninstall com.example.app
# Take screenshot
adb shell screencap /sdcard/screen.png && adb pull /sdcard/screen.png
# Record screen
adb shell screenrecord /sdcard/screen.mp4 && adb pull /sdcard/screen.mp4
# Start app
adb shell am start -n com.example.app/.MainActivity
# Press key
adb shell input keyevent 26 # power
adb shell input text "hello"
# List files
adb shell ls /sdcard/
```
## Linux (bash)
```bash
# System info
uname -a && cat /etc/os-release
# Disk usage
df -h
# Top processes
ps aux --sort=-%mem | head -15
# Kill process
kill -9 <PID>
# Services (systemd)
systemctl status <service>
systemctl restart <service>
# File operations
ls -la /tmp
cat /var/log/syslog | tail -50
```
## macOS (bash)
```bash
# System info
sw_vers && system_profiler
# Battery
pmset -g batt
# Running apps
osascript -e 'tell application "System Events" to get name of every process'
# Take screenshot
screencapture -x /tmp/screen.png
# Quit app
osascript -e 'quit app "Safari"'
```