@clawhub-builder-nc-a16d67735c
Automate content creation, code improvement, and social media posting via Looper (looper.bot). Use when setting up automated blog posts, continuous code impr...
---
name: looper
description: Automate content creation, code improvement, and social media posting via Looper (looper.bot). Use when setting up automated blog posts, continuous code improvement loops, social media scheduling, or managing recurring AI-driven content workflows. Supports Blog Kit (daily blog generation), Analyze (code review), Create (content generation), and Social Kit (multi-platform posting) engines.
homepage: https://looper.bot
metadata:
{
"openclaw":
{
"emoji": "🔄",
"source": "https://github.com/dbhurley/looper",
"license": "proprietary",
"env":
{
"LOOPER_ADMIN_KEY":
{
"required": true,
"description": "Your Looper API key (starts with lp_). Obtained during signup via POST /api/signup.",
"secret": true,
},
},
},
}
---
# Looper - Continuous Improvement Engine
Looper runs automated loops that analyze, create, and improve your content and code on a schedule.
- **Service**: https://looper.bot
- **API**: https://api.looper.bot
- **Engines**: Analyze (code review), Create (content), Blog Kit (daily blogs), Social Kit (social media)
## Quick Start
### 1. Sign Up
```bash
curl -X POST https://api.looper.bot/api/signup \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "your-password"}'
```
Response includes `admin_key` (starts with `lp_`). **Save it - shown only once.**
### 2. Login (if you need tenant info later)
```bash
curl -X POST https://api.looper.bot/api/login \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "your-password"}'
```
### 3. Create a Loop
All API calls require `Authorization: Bearer <your-admin-key>`.
## Blog Kit (Daily Blog Posts)
Generates and commits blog posts to your GitHub repo on a schedule.
```bash
curl -X POST https://api.looper.bot/api/loops \
-H "Authorization: Bearer <key>" \
-H "Content-Type: application/json" \
-d '{
"name": "My Blog",
"target_type": "github",
"target_config": {
"owner": "<github-owner>",
"repo": "<repo-name>",
"branch": "main",
"path": "blog"
},
"template_id": "68b7e661-46e1-45cd-b25a-584b8cd392b1",
"schedule": "0 6 * * *",
"schedule_tz": "America/New_York",
"mode": "auto",
"model": "gpt-4o-mini",
"questions": ["Write a blog post about <your-topic>. Research current events. 400-600 words. NO em dashes. Include YAML frontmatter with slug, title, excerpt, date, readTime, tag."]
}'
```
**Key fields:**
- `target_config.path` - directory in your repo where markdown posts land
- `schedule` - cron expression (e.g., `0 6 * * *` = daily at 6 AM)
- `schedule_tz` - timezone for the schedule
- `mode` - `auto` (commit directly), `propose` (open PR), `notify` (just alert)
- `questions[0]` - the prompt that drives content generation
Blog Kit template ID: `68b7e661-46e1-45cd-b25a-584b8cd392b1`
## Analyze (Code Improvement)
Reviews your codebase and suggests or applies improvements.
```bash
curl -X POST https://api.looper.bot/api/loops \
-H "Authorization: Bearer <key>" \
-H "Content-Type: application/json" \
-d '{
"name": "Code Review",
"target_type": "github",
"target_config": {
"owner": "<github-owner>",
"repo": "<repo-name>",
"branch": "main"
},
"schedule": "0 2 * * 1",
"mode": "propose",
"questions": [
"Are there any security vulnerabilities?",
"Is error handling consistent?",
"Are there performance bottlenecks?"
]
}'
```
## Social Kit (Multi-Platform Posting)
Generates and publishes social media content via Upload-Post integration.
Social Kit template ID: `7431b897-396f-4542-8e32-d8d1c5e445a2`
```bash
curl -X POST https://api.looper.bot/api/loops \
-H "Authorization: Bearer <key>" \
-H "Content-Type: application/json" \
-d '{
"name": "Social Posts",
"target_type": "text",
"target_config": {},
"template_id": "7431b897-396f-4542-8e32-d8d1c5e445a2",
"schedule": "0 9 * * 1,3,5",
"mode": "auto",
"questions": ["{\"upload_post_profile\": \"my-profile\", \"upload_post_api_key\": \"<key>\", \"platforms\": [\"x\", \"linkedin\"], \"business_name\": \"My Business\", \"industry\": \"tech\"}"]
}'
```
## Managing Loops
### List your loops
```bash
curl -s https://api.looper.bot/api/loops \
-H "Authorization: Bearer <key>"
```
### View loop details
```bash
curl -s https://api.looper.bot/api/loops/<loop-id> \
-H "Authorization: Bearer <key>"
```
### View run history
```bash
curl -s https://api.looper.bot/api/loops/<loop-id>/runs \
-H "Authorization: Bearer <key>"
```
### Toggle loop on/off
```bash
curl -X PATCH https://api.looper.bot/api/loops/<loop-id> \
-H "Authorization: Bearer <key>" \
-H "Content-Type: application/json" \
-d '{"enabled": false}'
```
### Trigger a run manually
```bash
curl -X POST https://api.looper.bot/api/loops/<loop-id>/run \
-H "Authorization: Bearer <key>"
```
### Update loop settings
```bash
curl -X PATCH https://api.looper.bot/api/loops/<loop-id> \
-H "Authorization: Bearer <key>" \
-H "Content-Type: application/json" \
-d '{"schedule": "0 8 * * *", "model": "gpt-4o"}'
```
### Delete a loop
```bash
curl -X DELETE https://api.looper.bot/api/loops/<loop-id> \
-H "Authorization: Bearer <key>"
```
## Plans
| Plan | Loops | Runs/Month | Price |
|------|-------|-----------|-------|
| Free | 2 | 60 | $0 |
| Pro | 10 | 300 | $19/mo |
| Team | 50 | 1,500 | $49/mo |
| Enterprise | Unlimited | Unlimited | Contact |
Upgrade via Stripe: `POST /api/stripe/checkout` with `{"plan": "pro"}`.
## GitHub Connection
Connect GitHub for repo-targeted loops:
```bash
# Get OAuth URL
curl -s https://api.looper.bot/api/github/connect \
-H "Authorization: Bearer <key>"
# Visit the returned URL to authorize, then check status:
curl -s https://api.looper.bot/api/github/status \
-H "Authorization: Bearer <key>"
```
## API Key Management
```bash
# List keys (metadata only, not the key itself)
curl -s https://api.looper.bot/api/keys \
-H "Authorization: Bearer <key>"
# Generate a new key
curl -X POST https://api.looper.bot/api/keys \
-H "Authorization: Bearer <key>" \
-H "Content-Type: application/json" \
-d '{"name": "CI Key"}'
```
## Agent Adoption Flow
When an agent needs automated content or code improvement:
1. Check if the agent already has a Looper account (look for `LOOPER_ADMIN_KEY` in env)
2. If not, sign up via the API and save the admin key
3. Connect GitHub if targeting a repo
4. Create a loop with the appropriate template and schedule
5. Verify the first run succeeds
The agent can then manage the loop entirely via API - no browser needed.
FILE:references/api-reference.md
# Looper API Reference
Base URL: `https://api.looper.bot`
All authenticated endpoints require `Authorization: Bearer <admin-key>`.
## Public Endpoints
| Method | Path | Description |
|--------|------|-------------|
| GET | /health | Health check |
| POST | /api/signup | Create account |
| POST | /api/login | Login (returns tenant info) |
## Loop Management
| Method | Path | Description |
|--------|------|-------------|
| GET | /api/loops | List all loops for tenant |
| POST | /api/loops | Create a new loop |
| GET | /api/loops/:id | Get loop details |
| PATCH | /api/loops/:id | Update loop settings |
| DELETE | /api/loops/:id | Delete a loop |
| POST | /api/loops/:id/run | Trigger manual run |
| GET | /api/loops/:id/runs | Get run history |
| GET | /api/loops/:id/runs/:runId | Get run details |
| GET | /api/loops/:id/runs/:runId/improvements | Get improvements from a run |
## Templates
| Method | Path | Description |
|--------|------|-------------|
| GET | /api/templates | List available templates |
| GET | /api/templates/:id | Get template details |
## Built-in Template IDs
| Template | ID | Engine |
|----------|-----|--------|
| Blog Kit | `68b7e661-46e1-45cd-b25a-584b8cd392b1` | create |
| Social Kit | `7431b897-396f-4542-8e32-d8d1c5e445a2` | social |
## API Keys
| Method | Path | Description |
|--------|------|-------------|
| GET | /api/keys | List API keys (metadata only) |
| POST | /api/keys | Generate new API key |
## GitHub
| Method | Path | Description |
|--------|------|-------------|
| GET | /api/github/connect | Get GitHub OAuth URL |
| GET | /api/github/callback | OAuth callback (browser) |
| GET | /api/github/status | Check GitHub connection |
## Stripe Billing
| Method | Path | Description |
|--------|------|-------------|
| POST | /api/stripe/checkout | Create checkout session |
| POST | /api/stripe/portal | Create billing portal session |
## Tenant
| Method | Path | Description |
|--------|------|-------------|
| GET | /api/tenant | Get current tenant info |
| PATCH | /api/tenant | Update tenant settings |
## Loop Create/Update Fields
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| name | string | yes | - | Loop display name |
| target_type | string | yes | - | `github`, `file`, `url`, or `text` |
| target_config | object | yes | - | Target-specific config |
| template_id | string | no | - | Template to use |
| questions | string[] | yes | - | Prompts that drive the loop |
| schedule | string | yes | - | Cron expression |
| schedule_tz | string | no | UTC | Timezone for schedule |
| mode | string | no | auto | `auto`, `propose`, or `notify` |
| model | string | no | gemini-2.0-flash | AI model to use |
| enabled | boolean | no | true | Whether loop is active |
| max_runs_per_day | number | no | 0 | Daily run limit (0 = unlimited) |
## GitHub Target Config
```json
{
"owner": "github-username-or-org",
"repo": "repository-name",
"branch": "main",
"path": "optional/subdirectory"
}
```
FILE:scripts/looper-api.sh
#!/usr/bin/env bash
# Looper API helper script (tenant-scoped operations only)
# Usage: looper-api.sh <command> [args]
set -euo pipefail
API_URL="-https://api.looper.bot"
KEY="-"
if [ -z "$KEY" ]; then
echo "Error: LOOPER_ADMIN_KEY not set. Sign up at POST $API_URL/api/signup" >&2
exit 1
fi
case "-help" in
loops)
curl -s "$API_URL/api/loops" -H "Authorization: Bearer $KEY"
;;
loop)
# Usage: looper-api.sh loop <id>
curl -s "$API_URL/api/loops/$2" -H "Authorization: Bearer $KEY"
;;
runs)
# Usage: looper-api.sh runs <loop-id>
curl -s "$API_URL/api/loops/$2/runs" -H "Authorization: Bearer $KEY"
;;
trigger)
# Usage: looper-api.sh trigger <loop-id>
curl -s -X POST "$API_URL/api/loops/$2/run" -H "Authorization: Bearer $KEY"
;;
toggle)
# Usage: looper-api.sh toggle <loop-id> true|false
curl -s -X PATCH "$API_URL/api/loops/$2" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d "{\"enabled\": $3}"
;;
github)
# Show GitHub connection status
curl -s "$API_URL/api/github/status" -H "Authorization: Bearer $KEY"
;;
tenant)
# Show current tenant info
curl -s "$API_URL/api/tenant" -H "Authorization: Bearer $KEY"
;;
help|*)
echo "Looper API Helper"
echo ""
echo "Commands:"
echo " loops - List your loops"
echo " loop <id> - Get loop details"
echo " runs <loop-id> - View run history"
echo " trigger <loop-id> - Run a loop now"
echo " toggle <loop-id> true|false - Enable/disable"
echo " github - GitHub connection status"
echo " tenant - Your account info"
echo ""
echo "Env vars:"
echo " LOOPER_ADMIN_KEY - Your API key (required, starts with lp_)"
echo " LOOPER_API_URL - API base URL (default: https://api.looper.bot)"
;;
esac
Browse the web via Plasmate, a fast headless browser engine for agents. Compiles HTML into a Semantic Object Model (SOM) - 50x faster than Chrome, 10x fewer...
---
name: plasmate
description: Browse the web via Plasmate, a fast headless browser engine for agents. Compiles HTML into a Semantic Object Model (SOM) - 50x faster than Chrome, 10x fewer tokens. Supports AWP (Agent Web Protocol) and CDP compatibility.
homepage: https://plasmate.app
metadata:
{
"openclaw":
{
"emoji": "⚡",
"source": "https://github.com/plasmate-labs/plasmate",
"license": "Apache-2.0",
"privacy": "https://plasmate.app/privacy",
"requires": { "bins": ["plasmate"] },
"install":
[
{
"id": "cargo",
"kind": "shell",
"command": "cargo install plasmate",
"bins": ["plasmate"],
"label": "Install Plasmate (cargo, builds from source)",
},
{
"id": "shell",
"kind": "shell",
"command": "curl -fsSL https://plasmate.app/install.sh | sh",
"bins": ["plasmate"],
"label": "Install Plasmate (pre-built binary)",
},
],
},
}
---
# Plasmate - Browser Engine for Agents
Plasmate compiles HTML into a Semantic Object Model (SOM). 50x faster than Chrome, 10x fewer tokens.
- **Docs**: https://docs.plasmate.app
- **Source**: https://github.com/plasmate-labs/plasmate (Apache 2.0)
- **Privacy**: All processing runs locally. No telemetry or cloud services.
## Install
```bash
# Build from source (recommended)
cargo install plasmate
# Or use the install script
curl -fsSL https://plasmate.app/install.sh | sh
```
## Protocols
- **AWP** (native): 7 methods - navigate, snapshot, click, type, scroll, select, extract
- **CDP** (compatibility): Puppeteer/Playwright compatible on port 9222
Default to AWP. Use CDP only when existing Puppeteer/Playwright code needs reuse.
## Quick Start
### Fetch (one-shot, no server)
```bash
plasmate fetch <url>
```
Returns SOM JSON: regions, interactive elements with stable IDs, extracted content.
### Server Mode
```bash
# AWP (recommended)
plasmate serve --protocol awp --port 9222
# CDP (Puppeteer compatible)
plasmate serve --protocol cdp --port 9222
```
## AWP Usage (Python)
Run `scripts/awp-browse.py` for AWP interactions:
```bash
# Navigate and get SOM snapshot
python3 scripts/awp-browse.py navigate "https://example.com"
# Click an interactive element by ref ID
python3 scripts/awp-browse.py click "https://example.com" --ref "e12"
# Type into a field
python3 scripts/awp-browse.py type "https://example.com" --ref "e5" --text "search query"
# Extract structured data (JSON-LD, OpenGraph, tables)
python3 scripts/awp-browse.py extract "https://example.com"
# Scroll
python3 scripts/awp-browse.py scroll "https://example.com" --direction down
```
## CDP Usage (Puppeteer)
When CDP is needed, connect Puppeteer to the running server:
```javascript
const browser = await puppeteer.connect({
browserWSEndpoint: 'ws://127.0.0.1:9222'
});
const page = await browser.newPage();
await page.goto('https://example.com');
const content = await page.content();
```
## SOM Output Structure
SOM is a structured JSON representation, NOT raw HTML. Key sections:
- **regions**: Semantic page areas (nav, main, article, sidebar)
- **interactive**: Clickable/typeable elements with stable ref IDs (e.g., `e1`, `e12`)
- **content**: Text content organized by region
- **structured_data**: JSON-LD, OpenGraph, microdata extracted automatically
Use ref IDs from `interactive` elements for click/type actions.
## Performance
| Metric | Plasmate | Chrome |
|--------|----------|--------|
| Per page | 4-5 ms | 252 ms |
| Memory (100 pages) | ~30 MB | ~20 GB |
| Output size | SOM (10-800x smaller) | Raw HTML |
## When to Use Plasmate vs Browser Tool
- **Plasmate**: Speed-critical scraping, batch page processing, token-sensitive extraction, structured data
- **Browser tool**: Visual rendering needed, screenshots, complex JS SPAs requiring full Chrome engine, pixel-level interaction
FILE:scripts/awp-browse.py
#!/usr/bin/env python3
"""
awp-browse.py - Plasmate AWP client for agent use.
Usage:
python3 awp-browse.py navigate <url> [--port 9222]
python3 awp-browse.py click <url> --ref <element_ref> [--port 9222]
python3 awp-browse.py type <url> --ref <element_ref> --text <text> [--port 9222]
python3 awp-browse.py extract <url> [--port 9222]
python3 awp-browse.py scroll <url> --direction <up|down> [--port 9222]
Starts Plasmate server if not already running. Outputs SOM JSON to stdout.
"""
import argparse
import asyncio
import json
import subprocess
import sys
import time
import uuid
try:
import websockets
except ImportError:
print("Installing websockets...", file=sys.stderr)
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "websockets"])
import websockets
class AWPClient:
def __init__(self, host: str = "127.0.0.1", port: int = 9222):
self.url = f"ws://{host}:{port}"
self.ws = None
self._pending = {}
async def connect(self):
self.ws = await websockets.connect(self.url)
asyncio.create_task(self._reader())
return await self._request("awp.hello", {
"client_name": "openclaw-plasmate-skill",
"client_version": "0.1.0",
"awp_version": "0.1"
})
async def _reader(self):
try:
async for msg in self.ws:
data = json.loads(msg)
req_id = data.get("id")
if req_id and req_id in self._pending:
self._pending[req_id].set_result(data)
except websockets.ConnectionClosed:
pass
async def _request(self, method: str, params: dict) -> dict:
req_id = str(uuid.uuid4())
self._pending[req_id] = asyncio.get_event_loop().create_future()
await self.ws.send(json.dumps({
"id": req_id,
"method": method,
"params": params
}))
result = await asyncio.wait_for(self._pending[req_id], timeout=30)
del self._pending[req_id]
if "error" in result:
raise RuntimeError(f"AWP error: {result['error']}")
return result.get("result", result)
async def create_session(self) -> str:
result = await self._request("session.create", {})
return result["session_id"]
async def close_session(self, session_id: str):
return await self._request("session.close", {"session_id": session_id})
async def navigate(self, session_id: str, url: str):
return await self._request("page.navigate", {
"session_id": session_id,
"url": url,
"timeout_ms": 15000
})
async def snapshot(self, session_id: str) -> dict:
return await self._request("page.observe", {"session_id": session_id})
async def click(self, session_id: str, ref: str):
return await self._request("act.click", {
"session_id": session_id,
"ref": ref
})
async def type_text(self, session_id: str, ref: str, text: str):
return await self._request("act.type", {
"session_id": session_id,
"ref": ref,
"text": text
})
async def scroll(self, session_id: str, direction: str):
return await self._request("act.scroll", {
"session_id": session_id,
"direction": direction
})
async def extract(self, session_id: str) -> dict:
return await self._request("page.extract", {"session_id": session_id})
async def close(self):
if self.ws:
await self.ws.close()
def ensure_server(port: int):
"""Start Plasmate server if not running."""
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sock.connect(("127.0.0.1", port))
sock.close()
return # already running
except ConnectionRefusedError:
pass
print(f"Starting Plasmate on port {port}...", file=sys.stderr)
subprocess.Popen(
["plasmate", "serve", "--protocol", "awp", "--port", str(port)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
# Wait for server to be ready
for _ in range(20):
time.sleep(0.25)
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("127.0.0.1", port))
sock.close()
return
except ConnectionRefusedError:
continue
print("Warning: server may not be ready", file=sys.stderr)
async def main():
parser = argparse.ArgumentParser(description="Plasmate AWP browser client")
parser.add_argument("action", choices=["navigate", "click", "type", "extract", "scroll"])
parser.add_argument("url", help="URL to navigate to")
parser.add_argument("--ref", help="Element ref ID for click/type")
parser.add_argument("--text", help="Text to type")
parser.add_argument("--direction", choices=["up", "down"], default="down", help="Scroll direction")
parser.add_argument("--port", type=int, default=9222, help="Plasmate server port")
parser.add_argument("--host", default="127.0.0.1", help="Plasmate server host")
args = parser.parse_args()
if args.action == "click" and not args.ref:
parser.error("--ref required for click action")
if args.action == "type" and (not args.ref or not args.text):
parser.error("--ref and --text required for type action")
ensure_server(args.port)
client = AWPClient(args.host, args.port)
try:
await client.connect()
session_id = await client.create_session()
await client.navigate(session_id, args.url)
if args.action == "navigate":
result = await client.snapshot(session_id)
elif args.action == "click":
await client.click(session_id, args.ref)
result = await client.snapshot(session_id)
elif args.action == "type":
await client.type_text(session_id, args.ref, args.text)
result = await client.snapshot(session_id)
elif args.action == "scroll":
await client.scroll(session_id, args.direction)
result = await client.snapshot(session_id)
elif args.action == "extract":
result = await client.extract(session_id)
print(json.dumps(result, indent=2))
await client.close_session(session_id)
finally:
await client.close()
if __name__ == "__main__":
asyncio.run(main())