Skill: ironystock/claw-cast (version 0.2.6)

--- name: clawcast description: Bootstrap and automate OBS scenes for local or remote instances via agentic-obs + mcporter. Includes optional explicit target-switch step that writes agentic-obs DB config only when acknowledged with a cross-component-write flag. Use when an OpenClaw agent needs to create a reusable baseline scene pack, wire browser/media sources over LAN-safe HTTP, run recording/stream smoke tests, and provide a clean starting point for project-specific customization. metadata: {"openclaw":{"emoji":"🎬","homepage":"https://github.com/ironystock/clawcast","requires":{"bins":["mcporter","python3","sqlite3","ss"]}}} --- # ClawCast Use this skill to create a generic, reusable OBS automation baseline. ## Prerequisites - `mcporter` installed/configured with `obs` MCP server - OBS WebSocket enabled on target OBS host (default `4455`) - `python3`, `sqlite3`, `ss` (iproute2), and standard shell tools ## Prerequisite 0 (required): verify obs MCP path ```bash mcporter list mcporter call 'obs.get_obs_status()' ``` If this fails, stop and finish `mcporter` + OBS MCP configuration first. ## Workflow 1. Switch target OBS host 2. Start overlay HTTP server 3. Rebuild baseline scene pack from skill assets 4. Optionally apply transition + audio defaults 5. Run recording smoke walkthrough 6. Optionally run stream dry-run ## Commands ```bash # 1) Target OBS host (explicit write acknowledgement + DB path required) ./skills/clawcast/scripts/obs_target_switch.sh <obs-host-ip> 4455 \ "$HOME/.agentic-obs/db.sqlite" --allow-cross-component-write # 2) Start/verify overlay host server (serves skill directory only) ./skills/clawcast/scripts/start_overlay_server.sh # 3) Rebuild baseline scenes + overlays ./skills/clawcast/scripts/rebuild_scenes.sh # 4) Apply transition preset ./skills/clawcast/scripts/apply_transition_preset.sh Fade 300 # 5) Optional audio baseline # export OBS_AUDIO_INPUTS="Mic/Aux,Desktop Audio" ./skills/clawcast/scripts/apply_audio_baseline.sh # 6) Run walkthrough recording ./skills/clawcast/scripts/smoke_test_walkthrough.sh # 7) Optional stream dry-run ./skills/clawcast/scripts/stream_dry_run.sh 15 "Intro" "Main Live" ``` ## Notes - This skill is boilerplate-first; customize scenes and assets after bootstrap. - Baseline-required overlays live in `assets/overlays/`. - Optional project-specific examples live in `examples/project-specific/`. - Avoid `/tmp` for persistent assets. - For remote OBS, prefer HTTP browser source URLs over `file://`. ## Security & transmission - No API keys/tokens are required by default for this skill. - OBS control uses the configured WebSocket endpoint (default port `4455`). - Overlay pages are served over local HTTP (`:8787`) and should remain on trusted LAN/VPN. - `start_overlay_server.sh` serves only the skill directory (not workspace root). - `obs_target_switch.sh` performs a cross-component config write only when given an explicit DB path argument plus `--allow-cross-component-write`. - Do not expose OBS WebSocket or overlay HTTP ports publicly. ## References - `references/scene-map.md` - `references/networking.md` - `references/troubleshooting.md` - `references/v0.2-features.md`

path: README.md
content: # clawcast (OpenClaw Skill)

## Start here: Install from GitHub (agent-facing)

If your agent is not using ClawHub yet, install ClawCast directly from this GitHub repo.

Tell your OpenClaw agent:
1. "Clone `https://github.com/ironystock/clawcast` into my workspace skills folder as `skills/clawcast`."
2. "Run `./skills/clawcast/scripts/obs_target_switch.sh <obs-host-ip> 4455 \"$HOME/.agentic-obs/db.sqlite\" --allow-cross-component-write`."
3. "Run `./skills/clawcast/scripts/start_overlay_server.sh` (serves only this skill folder, not workspace root)."
4. "Run `./skills/clawcast/scripts/rebuild_scenes.sh`."
5. "Run `./skills/clawcast/scripts/smoke_test_walkthrough.sh`."

Reusable, boilerplate-first skill for helping OpenClaw agents stand up and automate OBS scenes for **any** project (streams, demos, recordings, walkthroughs).

## Install via chat (quickest path)

Use these chat instructions with your OpenClaw agent:

1. "Install the clawcast skill"
2. "Target OBS at `<obs-host-ip>:4455`"
3. "Run the clawcast baseline rebuild"
4. "Run a recording smoke walkthrough"

Equivalent CLI path:

```bash
# from workspace root
openclaw skills install clawcast
# then run scripts shown in Quick start below
```

## North star

This project is intentionally generic:
- no creator-specific branding required
- no fixed scene story required
- no dependency on one specific stream theme

Use defaults to bootstrap quickly, then customize scenes/sources for your own workflow.

## What it automates

- target local or remote OBS WebSocket hosts
- host overlay files over LAN-safe HTTP
- create/recreate a baseline scene pack
- apply transition defaults
- optional audio baseline tuning
- run recording smoke walkthroughs
- optional stream dry-run

## Prerequisites

- OBS Studio running with WebSocket enabled (default port `4455`)
- `mcporter` installed and configured with an `obs` MCP server
- `python3`
- `sqlite3` (for OBS target switch script)
- `iproute2` (`ss`) for local server checks
- standard shell tools (`awk`, `grep`, `hostname`)

## Prerequisite 0 (required): validate mcporter + OBS MCP before running scripts

```bash
# Must show mcporter is installed
mcporter --help >/dev/null

# Must show an obs-capable server in your setup
mcporter list

# Must succeed before any ClawCast workflow scripts
mcporter call 'obs.get_obs_status()'
```

If the last command fails, finish your `mcporter`/OBS MCP setup first. ClawCast scripts now fail fast with this exact check.

## Quick start

### Mode A: Installed as an OpenClaw skill

Run from your OpenClaw workspace root.

```bash
# 1) Point to target OBS host (explicit cross-component write acknowledgement required)
./skills/clawcast/scripts/obs_target_switch.sh <obs-host-ip> 4455 \
  "$HOME/.agentic-obs/db.sqlite" --allow-cross-component-write

# 2) Start local overlay server (serves clawcast skill directory only)
./skills/clawcast/scripts/start_overlay_server.sh

# 3) Build baseline scenes + overlays
./skills/clawcast/scripts/rebuild_scenes.sh

# 4) Optional transition defaults
./skills/clawcast/scripts/apply_transition_preset.sh Fade 300

# 5) Optional audio baseline (set your own OBS input names)
export OBS_AUDIO_INPUTS="Mic/Aux,Desktop Audio"
./skills/clawcast/scripts/apply_audio_baseline.sh

# 6) Smoke recording walkthrough
./skills/clawcast/scripts/smoke_test_walkthrough.sh

# 7) Optional short streaming dry-run
./skills/clawcast/scripts/stream_dry_run.sh 15 "Intro" "Main Live"
```

### Mode B: Running from a cloned GitHub repo

Run from the cloned repo root.

```bash
# 1) Point to target OBS host (explicit cross-component write acknowledgement required)
./scripts/obs_target_switch.sh <obs-host-ip> 4455 \
  "$HOME/.agentic-obs/db.sqlite" --allow-cross-component-write

# 2) Start local overlay server (serves clawcast repo directory only)
./scripts/start_overlay_server.sh

# 3) Build baseline scenes + overlays
./scripts/rebuild_scenes.sh

# 4) Optional transition defaults
./scripts/apply_transition_preset.sh Fade 300

# 5) Optional audio baseline
export OBS_AUDIO_INPUTS="Mic/Aux,Desktop Audio"
./scripts/apply_audio_baseline.sh

# 6) Smoke recording walkthrough
./scripts/smoke_test_walkthrough.sh

# 7) Optional short streaming dry-run
./scripts/stream_dry_run.sh 15 "Intro" "Main Live"
```

## Security & data transmission

- **No bundled credentials:** This skill does not require API keys, tokens, or passwords in the repo.
- **OBS transport:** Control traffic uses OBS WebSocket on the host/port you set (default `4455`).
- **Overlay transport:** Overlay pages are served over local HTTP (default `:8787`) for OBS Browser Sources.
- **Scope lock:** `start_overlay_server.sh` serves the ClawCast skill/repo directory only (not workspace root).
- **Cross-component write guard:** `obs_target_switch.sh` requires an explicit DB path argument plus `--allow-cross-component-write` before writing DB config.
- **Trust boundary:** Run on trusted LAN/VPN only. Do not expose overlay HTTP or OBS WebSocket ports to the public internet.
- **Secrets handling:** Do not commit `.env` files, tokens, or private endpoint credentials into this repo.
- **Data profile:** The skill orchestrates scene/source configuration and local recording/stream actions; it does not phone home to third-party services by default.

## Runtime environment variables (intentional use)

- `OVERLAY_PORT` (optional): HTTP port for overlay server (default `8787`)
- `OVERLAY_BASE_PATH` (optional): overlay URL path used by rebuild script (default `/assets/overlays`)
- `OBS_AUDIO_INPUTS` (optional): comma-separated OBS inputs for `apply_audio_baseline.sh`
- `MIC_MUL` / `DESKTOP_MUL` (optional): volume multipliers for audio baseline helper

`obs_target_switch.sh` uses explicit positional arguments for DB path and cross-component-write acknowledgement (no required env vars).

All workflow scripts perform a fail-fast `mcporter call 'obs.get_obs_status()'` check before doing work.

## Asset packaging (required vs optional)

### Required (kept generic)
- `assets/overlays/intro.html`
- `assets/overlays/live-dashboard.html`
- `assets/overlays/work_status.html`
- `assets/overlays/presentation.html`
- `assets/overlays/control-panel.html`
- `assets/overlays/analytics.html`
- `assets/overlays/chat.html`
- `assets/overlays/brb.html`
- `assets/overlays/outro.html`

These power the default baseline scene map used by `scripts/rebuild_scenes.sh`.

### Optional examples (project-specific)
- `examples/project-specific/*`

These are not required for the baseline workflow and are provided only as adaptation references.

## Notes

- Installed mode defaults to overlays under `skills/clawcast/assets/overlays/`.
- Repo mode expects overlays under `assets/overlays/` in the cloned repo.
- For remote OBS, HTTP URLs are usually more reliable than `file://` local-file browser sources.
- You can replace baseline overlays with your own URLs/files after bootstrap.

## License

MIT


path: assets/overlays/analytics.html
content: <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
<style>
body {
  margin: 0;
  background: #0a0e27;
  color: white;
  font-family: 'Segoe UI', 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', Arial, sans-serif;
  padding: 40px;
}
.header {
  font-size: 72px;
  font-weight: bold;
  text-align: center;
  margin-bottom: 40px;
  color: #00d9ff;
  text-shadow: 0 0 20px #00d9ff;
}
.grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 30px;
}
.card {
  background: rgba(0,217,255,0.1);
  border: 2px solid #00d9ff;
  border-radius: 12px;
  padding: 30px;
}
.card h3 {
  margin: 0 0 20px 0;
  font-size: 32px;
  color: #00d9ff;
}
.big-number {
  font-size: 64px;
  font-weight: bold;
  color: #fff;
  text-align: center;
  margin: 20px 0;
}
.label {
  text-align: center;
  font-size: 24px;
  opacity: 0.8;
}
</style>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div class="header">📊 Civic.com Analytics</div>
<div class="grid">
  <div class="card">
    <h3>Active Users</h3>
    <div class="big-number" id="users">--</div>
    <div class="label">Real-time</div>
  </div>
  <div class="card">
    <h3>Page Views</h3>
    <div class="big-number" id="views">--</div>
    <div class="label">Last 30 min</div>
  </div>
  <div class="card">
    <h3>Top Location</h3>
    <div class="big-number" style="font-size: 48px;" id="location">Loading...</div>
    <div class="label">Current session</div>
  </div>
  <div class="card">
    <h3>Stream Status</h3>
    <div class="big-number" style="font-size: 48px;">🔴 LIVE</div>
    <div class="label" id="time"></div>
  </div>
</div>
<script>
// Would fetch real data from GA API via backend
// For now, demo values
document.getElementById('users').textContent = Math.floor(Math.random() * 50) + 10;
document.getElementById('views').textContent = Math.floor(Math.random() * 200) + 50;
document.getElementById('location').textContent = '🌍 Global';

function updateTime() {
  document.getElementById('time').textContent = new Date().toLocaleTimeString();
}
setInterval(updateTime, 1000);
updateTime();
</script>
</body>
</html>


path: assets/overlays/brb.html
content: <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
<style>
body {
  margin: 0;
  background: transparent;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
  font-family: 'Segoe UI', 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', Arial, sans-serif;
}
.container {
  text-align: center;
  color: white;
}
h1 {
  font-size: 120px;
  margin: 0;
  text-shadow: 4px 4px 8px rgba(0,0,0,0.8);
}
p {
  font-size: 48px;
  margin-top: 20px;
  text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
}
</style>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div class="container">
  <h1>🦀</h1>
  <h1>Be Right Back</h1>
  <p>OpenClaw will return soon</p>
  <p style="font-size: 32px; margin-top: 60px;">🎵 Music by the artist</p>
</div>
</body>
</html>


path: assets/overlays/chat.html
content: <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
<style>
body {
  margin: 0;
  background: linear-gradient(135deg, #0f2027, #203a43, #2c5364);
  color: white;
  font-family: 'Segoe UI', 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', Arial, sans-serif;
  padding: 40px;
}
.container {
  display: grid;
  grid-template-columns: 2fr 1fr;
  gap: 30px;
  height: 100vh;
}
.main {
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.question-box {
  background: rgba(0,0,0,0.6);
  border: 3px solid #00d9ff;
  border-radius: 16px;
  padding: 40px;
  margin-bottom: 40px;
}
.question-box h2 {
  color: #00d9ff;
  font-size: 36px;
  margin: 0 0 20px 0;
}
.question {
  font-size: 42px;
  line-height: 1.5;
  margin: 20px 0;
}
.answer {
  background: rgba(0,255,100,0.1);
  border-left: 5px solid #0f0;
  padding: 30px;
  font-size: 36px;
  line-height: 1.6;
  border-radius: 8px;
}
.chat-sidebar {
  background: rgba(0,0,0,0.5);
  border: 2px solid #666;
  border-radius: 12px;
  padding: 20px;
  overflow-y: auto;
}
.chat-message {
  margin: 15px 0;
  padding: 10px;
  background: rgba(255,255,255,0.1);
  border-radius: 6px;
  font-size: 18px;
}
.username {
  color: #00d9ff;
  font-weight: bold;
}
</style>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div class="container">
  <div class="main">
    <div class="question-box">
      <h2>💬 Current Question</h2>
      <div class="question">"How does agentic-obs work with OpenClaw?"</div>
    </div>
    <div class="answer">
      🦀 Great question! agentic-obs is an MCP server that gives me direct control over OBS Studio. Through the Model Context Protocol, I can create scenes, switch views, start recordings, and even monitor the stream visually - all programmatically!
    </div>
  </div>
  <div class="chat-sidebar">
    <h3 style="color: #00d9ff; text-align: center;">Live Chat</h3>
    <div class="chat-message">
      <span class="username">Brad:</span> This is amazing!
    </div>
    <div class="chat-message">
      <span class="username">RECTANGL:</span> Love the animations 🦀
    </div>
    <div class="chat-message">
      <span class="username">Viewer123:</span> How do I get started?
    </div>
  </div>
</div>
</body>
</html>


path: assets/overlays/control-panel.html
content: <!DOCTYPE html>
<html>
<head>
<title>🦀 Main Live - Control Panel</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
  font-family: 'Arial', sans-serif;
  background: linear-gradient(135deg, #1e3c72, #2a5298);
  color: white;
  padding: 20px;
}
.header {
  text-align: center;
  padding: 30px;
  background: rgba(0,0,0,0.3);
  border-radius: 12px;
  margin-bottom: 30px;
}
.header h1 {
  font-size: 48px;
  margin-bottom: 10px;
}
.status {
  display: inline-block;
  padding: 10px 20px;
  border-radius: 20px;
  font-size: 20px;
  font-weight: bold;
}
.live { background: #f00; }
.offline { background: #666; }

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 20px;
}
.card {
  background: rgba(0,0,0,0.4);
  border: 2px solid rgba(255,255,255,0.2);
  border-radius: 12px;
  padding: 20px;
}
.card h2 {
  font-size: 24px;
  margin-bottom: 15px;
  color: #00d9ff;
}
.btn {
  width: 100%;
  padding: 15px;
  margin: 8px 0;
  font-size: 16px;
  font-weight: bold;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.2s;
}
.btn:hover { transform: scale(1.05); }
.btn-primary { background: #00d9ff; color: #000; }
.btn-success { background: #0f0; color: #000; }
.btn-danger { background: #f00; color: #fff; }
.btn-warning { background: #ff0; color: #000; }
.btn-secondary { background: #666; color: #fff; }

.scene-btn {
  padding: 20px;
  margin: 10px 0;
  font-size: 18px;
}
.active-scene {
  background: #0f0;
  color: #000;
  box-shadow: 0 0 20px #0f0;
}
.info {
  background: rgba(0,217,255,0.1);
  padding: 15px;
  border-radius: 8px;
  margin-top: 15px;
}
</style>
</head>
<body>
<div class="header">
  <h1>🦀 Main Live Control Panel</h1>
  <div class="status offline" id="status">● OFFLINE</div>
</div>

<div class="grid">
  <div class="card">
    <h2>🎬 Stream Control</h2>
    <button class="btn btn-success" onclick="startStream()">▶️ Go Live</button>
    <button class="btn btn-danger" onclick="stopStream()">⏹️ Stop Stream</button>
    <button class="btn btn-primary" onclick="startRecording()">⏺️ Start Recording</button>
    <button class="btn btn-secondary" onclick="stopRecording()">⏹️ Stop Recording</button>
  </div>

  <div class="card">
    <h2>🎭 Scenes</h2>
    <button class="btn scene-btn" onclick="switchScene('Intro')">🎬 Intro</button>
    <button class="btn scene-btn" onclick="switchScene('Main Live')">🦀 Main</button>
    <button class="btn scene-btn" onclick="switchScene('Work Mode')">💻 Work</button>
    <button class="btn scene-btn" onclick="switchScene('Presentation Mode')">📊 Presentation</button>
    <button class="btn scene-btn" onclick="switchScene('Analytics Dashboard')">📈 Analytics</button>
    <button class="btn scene-btn" onclick="switchScene('Chat Interaction')">💬 Chat</button>
    <button class="btn scene-btn" onclick="switchScene('BRB Screen')">☕ BRB</button>
    <button class="btn scene-btn" onclick="switchScene('Outro')">👋 Outro</button>
  </div>

  <div class="card">
    <h2>📊 Status</h2>
    <div class="info">
      <div><strong>Current Scene:</strong> <span id="currentScene">--</span></div>
      <div><strong>Recording:</strong> <span id="recordingStatus">Stopped</span></div>
      <div><strong>Streaming:</strong> <span id="streamingStatus">Offline</span></div>
      <div><strong>Uptime:</strong> <span id="uptime">00:00:00</span></div>
    </div>
    <button class="btn btn-secondary" onclick="refreshStatus()" style="margin-top: 15px;">🔄 Refresh</button>
  </div>

  <div class="card">
    <h2>📸 Monitor</h2>
    <div class="info">
      <img id="screenshot" src="http://127.0.0.1:8765/screenshot/stream_monitor" 
           style="width: 100%; border-radius: 8px; margin-top: 10px;"
           onerror="this.src='data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%22400%22 height=%22300%22%3E%3Crect width=%22400%22 height=%22300%22 fill=%22%23333%22/%3E%3Ctext x=%2250%25%22 y=%2250%25%22 text-anchor=%22middle%22 fill=%22white%22%3ENo Screenshot%3C/text%3E%3C/svg%3E'">
    </div>
    <button class="btn btn-secondary" onclick="refreshScreenshot()" style="margin-top: 10px;">📷 Refresh Screenshot</button>
  </div>
</div>

<script>
// Note: This is a static control panel
// For actual control, you'd need to implement backend API calls
// For now, these are placeholders showing the intended UX

function switchScene(name) {
  console.log('Switching to scene:', name);
  alert(`Would switch to: ${name}\n\nActual implementation needs backend API.`);
}

function startStream() {
  console.log('Starting stream');
  alert('Would start stream via agentic-obs.\n\nUse CLI: ./streaming/stream-workflow.sh start');
}

function stopStream() {
  console.log('Stopping stream');
  alert('Would stop stream via agentic-obs.\n\nUse CLI: ./streaming/stream-workflow.sh stop');
}

function startRecording() {
  alert('Would start recording.\n\nUse CLI: ./streaming/stream-workflow.sh record');
}

function stopRecording() {
  alert('Would stop recording.\n\nUse CLI: ./streaming/stream-workflow.sh stop-record');
}

function refreshStatus() {
  alert('Would refresh status from OBS.\n\nUse CLI: ./streaming/stream-workflow.sh status');
}

function refreshScreenshot() {
  const img = document.getElementById('screenshot');
  img.src = img.src.split('?')[0] + '?' + new Date().getTime();
}

// Auto-refresh screenshot every 5 seconds
setInterval(refreshScreenshot, 5000);

// Update uptime
let startTime = Date.now();
setInterval(() => {
  const elapsed = Math.floor((Date.now() - startTime) / 1000);
  const h = Math.floor(elapsed / 3600).toString().padStart(2, '0');
  const m = Math.floor((elapsed % 3600) / 60).toString().padStart(2, '0');
  const s = (elapsed % 60).toString().padStart(2, '0');
  document.getElementById('uptime').textContent = `${h}:${m}:${s}`;
}, 1000);
</script>
</body>
</html>


path: assets/overlays/intro.html
content: <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <style>
        body {
            margin: 0;
            padding: 0;
            width: 1920px;
            height: 1080px;
            background: linear-gradient(135deg, #0f2027 0%, #203a43 50%, #2c5364 100%);
            display: flex;
            align-items: center;
            justify-content: center;
            font-family: 'Segoe UI', 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', Tahoma, Geneva, Verdana, sans-serif;
            color: white;
        }
        
        .intro-container {
            text-align: center;
        }
        
        .crab {
            font-size: 200px;
            animation: bounce 2s ease-in-out infinite;
            display: inline-block;
        }
        
        @keyframes bounce {
            0%, 100% { transform: translateY(0); }
            50% { transform: translateY(-30px); }
        }
        
        .title {
            font-size: 96px;
            font-weight: bold;
            margin-top: 40px;
            background: linear-gradient(90deg, #60efff 0%, #0061ff 100%);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            background-clip: text;
        }
        
        .subtitle {
            font-size: 42px;
            margin-top: 20px;
            opacity: 0.9;
        }
    </style>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
    <div class="intro-container">
        <div class="crab">🦀</div>
        <div class="title">SNOWCRAB LIVE</div>
        <div class="subtitle">Your Friendly AI Assistant</div>
    </div>
</body>
</html>


path: assets/overlays/live-dashboard.html
content: <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <style>
        body {
            margin: 0;
            padding: 0;
            width: 1920px;
            height: 1080px;
            background: transparent;
            font-family: 'Segoe UI', 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', Tahoma, Geneva, Verdana, sans-serif;
            color: white;
        }
        
        .dashboard {
            position: absolute;
            bottom: 0;
            left: 0;
            right: 0;
            padding: 20px 40px;
            background: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, 0.8) 100%);
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        
        .info-panel {
            display: flex;
            gap: 40px;
        }
        
        .info-item {
            display: flex;
            flex-direction: column;
            gap: 5px;
        }
        
        .label {
            font-size: 16px;
            opacity: 0.7;
            text-transform: uppercase;
            letter-spacing: 1px;
        }
        
        .value {
            font-size: 28px;
            font-weight: bold;
        }
        
        .live-badge {
            background: #ff0000;
            color: white;
            padding: 12px 24px;
            border-radius: 8px;
            font-size: 24px;
            font-weight: bold;
            display: flex;
            align-items: center;
            gap: 10px;
            animation: pulse 2s ease-in-out infinite;
        }
        
        @keyframes pulse {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.8; }
        }
        
        .dot {
            width: 12px;
            height: 12px;
            background: white;
            border-radius: 50%;
            animation: blink 1s ease-in-out infinite;
        }
        
        @keyframes blink {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.3; }
        }
        
        .branding {
            position: absolute;
            top: 40px;
            left: 40px;
            font-size: 48px;
            font-weight: bold;
            text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.5);
            display: flex;
            align-items: center;
            gap: 15px;
        }
        
        .crab {
            font-size: 60px;
            animation: wave 3s ease-in-out infinite;
        }
        
        @keyframes wave {
            0%, 100% { transform: rotate(-10deg); }
            50% { transform: rotate(10deg); }
        }
    </style>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
    <div class="branding">
        <span class="crab">🦀</span>
        <span>Main Live</span>
    </div>
    
    <div class="dashboard">
        <div class="info-panel">
            <div class="info-item">
                <div class="label">Platform</div>
                <div class="value">Your Stack</div>
            </div>
            <div class="info-item">
                <div class="label">Tools</div>
                <div class="value">64+ Integrations</div>
            </div>
            <div class="info-item">
                <div class="label">Demo</div>
                <div class="value">Canva Generation</div>
            </div>
        </div>
        
        <div class="live-badge">
            <div class="dot"></div>
            <span>LIVE</span>
        </div>
    </div>
</body>
</html>


path: assets/overlays/outro.html
content: <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <style>
        body {
            margin: 0;
            padding: 0;
            width: 1920px;
            height: 1080px;
            background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            font-family: 'Segoe UI', 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', Tahoma, Geneva, Verdana, sans-serif;
            color: white;
        }
        
        .crab {
            font-size: 180px;
            margin-bottom: 40px;
        }
        
        .message {
            font-size: 72px;
            font-weight: bold;
            margin-bottom: 20px;
        }
        
        .credits {
            font-size: 36px;
            opacity: 0.8;
            text-align: center;
            line-height: 1.6;
        }
        
        .link {
            margin-top: 40px;
            font-size: 42px;
            background: rgba(255, 255, 255, 0.1);
            padding: 20px 40px;
            border-radius: 12px;
        }
    </style>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
    <div class="crab">🦀</div>
    <div class="message">Thanks for Watching!</div>
    <div class="credits">
        Powered by OpenClaw automation<br>
        Built with 🇨🇦 in your studio
    </div>
    <div class="link">github.com/openclaw/openclaw</div>
</body>
</html>


path: assets/overlays/presentation.html
content: <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
<style>
body {
  margin: 0;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
  font-family: 'Segoe UI', 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', Arial, sans-serif;
}
.slide {
  text-align: center;
  color: white;
  padding: 60px;
  max-width: 1400px;
}
h1 {
  font-size: 96px;
  margin: 0 0 40px 0;
  text-shadow: 4px 4px 16px rgba(0,0,0,0.5);
  font-weight: bold;
}
h2 {
  font-size: 56px;
  margin: 20px 0;
  opacity: 0.9;
}
.footer {
  position: fixed;
  bottom: 40px;
  right: 60px;
  font-size: 32px;
  opacity: 0.7;
}
</style>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div class="slide">
  <h1>🦀 OpenClaw</h1>
  <h2>AI-Powered Live Streaming</h2>
  <h2 style="font-size: 40px; margin-top: 60px;">Built with OpenClaw + OBS</h2>
</div>
<div class="footer">Presentation Mode</div>
</body>
</html>


path: assets/overlays/work_status.html
content: <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
<style>
body {
  margin: 0;
  background: #1e1e1e;
  color: #00ff41;
  font-family: 'Courier New', monospace;
  padding: 40px;
  font-size: 28px;
}
.status {
  border: 2px solid #00ff41;
  padding: 30px;
  border-radius: 10px;
  margin-bottom: 20px;
}
h1 {
  color: #00ff41;
  font-size: 48px;
  margin: 0 0 20px 0;
  text-shadow: 0 0 10px #00ff41;
}
.task {
  margin: 15px 0;
  padding-left: 20px;
}
.timestamp {
  color: #888;
  font-size: 20px;
}
</style>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div class="status">
  <h1>🦀 OpenClaw // Work Mode</h1>
  <div class="task">▸ Status: <span style="color: #0f0">ACTIVE</span></div>
  <div class="task">▸ Session: Building Streaming Infrastructure</div>
  <div class="task">▸ Current: OBS Scene Development</div>
  <div class="task timestamp">▸ <span id="time"></span></div>
</div>
<script>
function updateTime() {
  document.getElementById('time').textContent = new Date().toLocaleTimeString();
}
setInterval(updateTime, 1000);
updateTime();
</script>
</body>
</html>


path: examples/project-specific/README.md
content: # Project-specific examples

These files are optional references from prior project work.

- They are **not required** for ClawCast baseline setup.
- They are intentionally kept out of `assets/overlays/` so the default skill package stays generic.
- Use them only as inspiration when building your own branded overlays.


path: examples/project-specific/civic-nexus-demo.html
content: <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <style>
        body {
            margin: 0;
            padding: 0;
            width: 1920px;
            height: 1080px;
            background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
            font-family: 'Segoe UI', 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', Tahoma, Geneva, Verdana, sans-serif;
            color: white;
            display: flex;
            flex-direction: column;
            justify-content: space-between;
            overflow: hidden;
        }
        
        .header {
            padding: 40px 60px;
            background: rgba(0, 0, 0, 0.3);
        }
        
        .logo {
            font-size: 72px;
            font-weight: bold;
            margin-bottom: 10px;
        }
        
        .tagline {
            font-size: 32px;
            opacity: 0.9;
            font-style: italic;
        }
        
        .content {
            flex: 1;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 60px;
        }
        
        .demo-card {
            background: rgba(255, 255, 255, 0.1);
            backdrop-filter: blur(10px);
            border-radius: 20px;
            padding: 60px 80px;
            text-align: center;
            border: 2px solid rgba(255, 255, 255, 0.2);
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
        }
        
        .demo-title {
            font-size: 64px;
            font-weight: bold;
            margin-bottom: 30px;
            background: linear-gradient(90deg, #60efff 0%, #0061ff 100%);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            background-clip: text;
        }
        
        .demo-subtitle {
            font-size: 36px;
            opacity: 0.85;
            margin-bottom: 20px;
        }
        
        .crab {
            font-size: 120px;
            margin: 20px 0;
            animation: wave 2s ease-in-out infinite;
        }
        
        @keyframes wave {
            0%, 100% { transform: rotate(-5deg); }
            50% { transform: rotate(5deg); }
        }
        
        .footer {
            padding: 30px 60px;
            background: rgba(0, 0, 0, 0.4);
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        
        .tools-badge {
            font-size: 24px;
            opacity: 0.8;
        }
        
        .live-indicator {
            background: #ff0000;
            color: white;
            padding: 10px 20px;
            border-radius: 8px;
            font-size: 28px;
            font-weight: bold;
            animation: pulse 2s ease-in-out infinite;
        }
        
        @keyframes pulse {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.7; }
        }
    </style>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
    <div class="header">
        <div class="logo">🦀 Main Live</div>
        <div class="tagline">AI Agent Integration Demo</div>
    </div>
    
    <div class="content">
        <div class="demo-card">
            <div class="demo-title">Connected Tools</div>
            <div class="demo-subtitle">100+ Integrations at My Claws</div>
            <div class="crab">🦀</div>
            <div class="demo-subtitle">Live Demo: Canva Design Generation</div>
        </div>
    </div>
    
    <div class="footer">
        <div class="tools-badge">📦 Google Docs • Canva • Gmail • Analytics • PostgreSQL • MongoDB • Box • ...</div>
        <div class="live-indicator">● REC</div>
    </div>
</body>
</html>


path: examples/project-specific/live_dashboard-legacy.html
content: <!DOCTYPE html>
<html>
<head>
<style>
body {
  margin: 0;
  background: transparent;
  color: white;
  font-family: 'Courier New', monospace;
  padding: 20px;
}
.dashboard {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
}
.panel {
  background: rgba(0,0,0,0.7);
  border: 2px solid #00ff41;
  padding: 20px;
  border-radius: 8px;
}
h2 {
  color: #00ff41;
  margin: 0 0 15px 0;
  font-size: 28px;
  text-shadow: 0 0 10px #00ff41;
}
.metric {
  font-size: 20px;
  margin: 10px 0;
  display: flex;
  justify-content: space-between;
}
.value {
  color: #0ff;
  font-weight: bold;
}
#time {
  font-size: 48px;
  color: #00ff41;
  text-align: center;
  margin: 20px 0;
  text-shadow: 0 0 20px #00ff41;
}
</style>
</head>
<body>
<div id="time"></div>
<div class="dashboard">
  <div class="panel">
    <h2>🦀 System Status</h2>
    <div class="metric"><span>Agent:</span><span class="value">ACTIVE</span></div>
    <div class="metric"><span>OBS:</span><span class="value">CONNECTED</span></div>
    <div class="metric"><span>Mode:</span><span class="value">LIVE</span></div>
  </div>
  <div class="panel">
    <h2>📊 Session Info</h2>
    <div class="metric"><span>Uptime:</span><span class="value" id="uptime">00:00:00</span></div>
    <div class="metric"><span>Scene:</span><span class="value">Auto</span></div>
    <div class="metric"><span>Quality:</span><span class="value">1080p60</span></div>
  </div>
</div>
<script>
let startTime = Date.now();
function update() {
  const now = new Date();
  document.getElementById('time').textContent = now.toLocaleTimeString();
  
  const elapsed = Math.floor((Date.now() - startTime) / 1000);
  const h = Math.floor(elapsed / 3600).toString().padStart(2, '0');
  const m = Math.floor((elapsed % 3600) / 60).toString().padStart(2, '0');
  const s = (elapsed % 60).toString().padStart(2, '0');
  document.getElementById('uptime').textContent = `${h}:${m}:${s}`;
}
setInterval(update, 1000);
update();
</script>
</body>
</html>


path: references/networking.md
content: # Networking Notes

## Host layout

- Agent machine serves overlays over HTTP on `:8787`
- OBS machine (local/remote) loads browser sources from agent LAN IP

## Validation checks

1. Confirm server listening:
   - `ss -ltnp | grep :8787`
2. Confirm local reachability:
   - `curl -I http://127.0.0.1:8787/assets/overlays/intro.html`
3. Confirm OBS target set:
   - `mcporter call 'obs.get_obs_status()'`

## Common issue

If browser sources are blank in OBS but page loads in browser:
- disable hardware acceleration in Browser Source properties
- refresh cache / recreate source
- verify no firewall blocks OBS host from reaching agent LAN IP


path: references/scene-map.md
content: # Scene Map (Generic Baseline)

Overlay host base:
- `http://<agent-lan-ip>:8787`

Asset base (default):
- `/skills/clawcast/assets/overlays/`

## Baseline scenes and default overlays

- Intro → `intro.html`
- Main Live → `live-dashboard.html`
- Work Mode → `work_status.html`
- Presentation Mode → `presentation.html`
- Feature Demo → `control-panel.html`
- Metrics → `analytics.html`
- Chat Interaction → `chat.html`
- BRB Screen → `brb.html`
- Outro → `outro.html`

## Default walkthrough order

Intro → Main Live → Work Mode → Presentation Mode → Feature Demo → Metrics → Chat Interaction → BRB Screen → Outro

## Customization guidance

- Replace scene names freely to match your project.
- Replace browser source URLs with your own overlays/apps.
- Keep one “safe” scene for fallbacks when rebuilding.


path: references/troubleshooting.md
content: # Troubleshooting

## Browser source blank on remote OBS

1. Confirm OBS host can reach `http://<agent-lan-ip>:8787/...`
2. Prefer HTTP URLs over `file://` for remote OBS
3. Recreate source with cache-busted URL (`?v=<timestamp>`)
4. In OBS UI, disable browser-source hardware acceleration if rendering is unstable

## Recording start says success but output is not running

- Check OBS is not blocked by open settings dialogs
- Verify Recording path/format in OBS settings
- Run one manual start/stop in OBS, then retry automation

## Audio not present in recordings

- Verify source exists and is unmuted
- Verify source volume is not zero
- Verify track routing in Advanced Audio Properties
- For multi-scene music continuity, use one shared source duplicated across scenes

## OBS connection failures

- Confirm OBS is running and WebSocket is enabled
- Verify host/port in `obs_target_switch.sh`
- Retry with `mcporter call 'obs.get_obs_status()'`


path: references/v0.2-features.md
content: # v0.2.0 Feature Notes

## Included in v0.2.0

- Transition preset helper
  - `scripts/apply_transition_preset.sh <transition> <duration_ms>`
- Audio baseline helper (optional, operator-provided input names)
  - `scripts/apply_audio_baseline.sh`
  - Uses `OBS_AUDIO_INPUTS`, `MIC_DB`, `DESKTOP_DB`
- Stream dry-run helper
  - `scripts/stream_dry_run.sh [seconds] [start_scene] [main_scene]`

## Design choices

- Keep defaults safe and generic.
- Avoid hardcoded machine/user paths.
- Keep stream dry-run optional to avoid accidental live pushes.

## Still out of scope

- Full OBS profile migration (encoders, stream keys, hotkeys, dock layout)
- Deep audio routing templates for every hardware setup
- Automated browser-source hardware-acceleration UI toggles


path: scripts/apply_audio_baseline.sh
content: #!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
"$SCRIPT_DIR/require_obs_mcp.sh"

# Optional comma-separated input names via env:
# export OBS_AUDIO_INPUTS="Mic/Aux,Desktop Audio"
INPUTS_CSV="${OBS_AUDIO_INPUTS:-}"
MIC_MUL="${MIC_MUL:-0.85}"
DESKTOP_MUL="${DESKTOP_MUL:-0.55}"

if [[ -z "$INPUTS_CSV" ]]; then
  echo "No OBS_AUDIO_INPUTS provided; skipping explicit input tuning."
  echo "Tip: export OBS_AUDIO_INPUTS='Mic/Aux,Desktop Audio'"
  exit 0
fi

IFS=',' read -r -a INPUTS <<< "$INPUTS_CSV"
for i in "${!INPUTS[@]}"; do
  name="$(echo "${INPUTS[$i]}" | xargs)"
  [[ -z "$name" ]] && continue
  target_mul="$MIC_MUL"
  [[ "$i" -gt 0 ]] && target_mul="$DESKTOP_MUL"
  mcporter call "obs.set_input_volume(input_name: \"$name\", volume_mul: $target_mul)" >/dev/null || true
  if mcporter call "obs.get_input_mute(input_name: \"$name\")" | grep -q '"is_muted": true'; then
    mcporter call "obs.toggle_input_mute(input_name: \"$name\")" >/dev/null || true
  fi
  echo "Tuned input '$name' to volume_mul=${target_mul}"
done


path: scripts/apply_transition_preset.sh
content: #!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
"$SCRIPT_DIR/require_obs_mcp.sh"

TRANSITION="${1:-Fade}"
DURATION_MS="${2:-300}"

mcporter call "obs.set_current_transition(transition_name: \"$TRANSITION\")" >/dev/null
mcporter call "obs.set_transition_duration(transition_duration: $DURATION_MS)" >/dev/null

echo "Applied transition preset: $TRANSITION (${DURATION_MS}ms)"


path: scripts/obs_target_switch.sh
content: #!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
"$SCRIPT_DIR/require_obs_mcp.sh"

HOST="${1:-}"
PORT="${2:-4455}"
DB="${3:-}"
ACK="${4:-}"

if [[ -z "$HOST" || -z "$DB" ]]; then
  echo "Usage: ./scripts/obs_target_switch.sh <obs-host> [obs-port] <agentic-obs-db-path> --allow-cross-component-write"
  echo "Example: ./scripts/obs_target_switch.sh 192.168.1.50 4455 \"$HOME/.agentic-obs/db.sqlite\" --allow-cross-component-write"
  exit 1
fi

if [[ ! -f "$DB" ]]; then
  echo "ERROR: DB file not found: $DB"
  exit 1
fi

if [[ "$ACK" != "--allow-cross-component-write" ]]; then
  echo "ERROR: This script writes to an external agentic-obs DB."
  echo "Add explicit acknowledgement flag: --allow-cross-component-write"
  exit 1
fi

sqlite3 "$DB" "update config set value='$HOST', updated_at=datetime('now') where key='obs_host';"
sqlite3 "$DB" "update config set value='$PORT', updated_at=datetime('now') where key='obs_port';"

echo "Updated agentic-obs target in $DB to $HOST:$PORT"
mcporter call 'obs.get_obs_status()'


path: scripts/rebuild_scenes.sh
content: #!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
"$SCRIPT_DIR/require_obs_mcp.sh"

IP="${1:-$(hostname -I | awk '{print $1}')}"
PORT="${OVERLAY_PORT:-8787}"
BASE_PATH="${OVERLAY_BASE_PATH:-/assets/overlays}"
BASE="http://$IP:$PORT$BASE_PATH"

mc() { mcporter call "$1" >/dev/null; }

SAFE_SCENE="Scene"
mc "obs.set_current_scene(scene_name: \"$SAFE_SCENE\")" || true

# Remove known baseline scenes if present
for s in "Intro" "Main Live" "Work Mode" "Presentation Mode" "Feature Demo" "Metrics" "Chat Interaction" "BRB Screen" "Outro"; do
  mc "obs.remove_scene(scene_name: \"$s\")" || true
done

# Create baseline scenes
for s in "Intro" "Main Live" "Work Mode" "Presentation Mode" "Feature Demo" "Metrics" "Chat Interaction" "BRB Screen" "Outro"; do
  mc "obs.create_scene(scene_name: \"$s\")"
done

# Attach default skill-bundled overlays
mc "obs.create_browser_source(scene_name: \"Intro\", source_name: \"Intro Overlay\", url: \"$BASE/intro.html?v=1\", width: 1920, height: 1080)"
mc "obs.create_browser_source(scene_name: \"Main Live\", source_name: \"Live Dashboard\", url: \"$BASE/live-dashboard.html?v=1\", width: 1920, height: 1080)"
mc "obs.create_browser_source(scene_name: \"Work Mode\", source_name: \"Work Status Overlay\", url: \"$BASE/work_status.html?v=1\", width: 1920, height: 1080)"
mc "obs.create_browser_source(scene_name: \"Presentation Mode\", source_name: \"Presentation Overlay\", url: \"$BASE/presentation.html?v=1\", width: 1920, height: 1080)"
mc "obs.create_browser_source(scene_name: \"Feature Demo\", source_name: \"Feature Demo Overlay\", url: \"$BASE/control-panel.html?v=1\", width: 1920, height: 1080)"
mc "obs.create_browser_source(scene_name: \"Metrics\", source_name: \"Metrics Overlay\", url: \"$BASE/analytics.html?v=1\", width: 1920, height: 1080)"
mc "obs.create_browser_source(scene_name: \"Chat Interaction\", source_name: \"Chat Overlay\", url: \"$BASE/chat.html?v=1\", width: 1920, height: 1080)"
mc "obs.create_browser_source(scene_name: \"BRB Screen\", source_name: \"BRB Overlay\", url: \"$BASE/brb.html?v=1\", width: 1920, height: 1080)"
mc "obs.create_browser_source(scene_name: \"Outro\", source_name: \"Outro Overlay\", url: \"$BASE/outro.html?v=1\", width: 1920, height: 1080)"

mc 'obs.set_current_scene(scene_name: "Intro")'
echo "Rebuilt baseline scene pack using $BASE"


path: scripts/require_obs_mcp.sh
content: #!/usr/bin/env bash
set -euo pipefail

if ! command -v mcporter >/dev/null 2>&1; then
  echo "ERROR: mcporter is not installed or not on PATH."
  echo "Install/configure mcporter first, then ensure an 'obs' MCP server is available."
  exit 1
fi

if ! mcporter call 'obs.get_obs_status()' >/dev/null 2>&1; then
  echo "ERROR: OBS MCP server is not configured/reachable via mcporter."
  echo "Run these checks:"
  echo "  - mcporter list"
  echo "  - mcporter call 'obs.get_obs_status()'"
  echo "Then retry ClawCast scripts."
  exit 1
fi


path: scripts/smoke_test_walkthrough.sh
content: #!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
"$SCRIPT_DIR/require_obs_mcp.sh"

HOLD_SECONDS="${1:-7}"

mc() { mcporter call "$1" >/dev/null; }

ORDER=("Intro" "Main Live" "Work Mode" "Presentation Mode" "Feature Demo" "Metrics" "Chat Interaction" "BRB Screen" "Outro")

mc 'obs.set_current_scene(scene_name: "Intro")'
mc 'obs.start_recording()'
for s in "${ORDER[@]}"; do
  mc "obs.set_current_scene(scene_name: \"$s\")"
  sleep "$HOLD_SECONDS"
done

OUT=$(mcporter call 'obs.stop_recording()')
echo "$OUT"


path: scripts/start_overlay_server.sh
content: #!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
OVERLAY_ROOT="$SKILL_ROOT/assets/overlays"
PORT="${OVERLAY_PORT:-8787}"
LOG_DIR="$SKILL_ROOT/.runtime"
LOG="$LOG_DIR/http-server.log"

if [[ ! -d "$OVERLAY_ROOT" ]]; then
  echo "ERROR: overlay directory not found: $OVERLAY_ROOT"
  exit 1
fi

mkdir -p "$LOG_DIR"

if ss -ltn | grep -q ":$PORT "; then
  echo "Overlay server already listening on :$PORT"
else
  # Security scope: serve skill-local overlays only (not workspace root).
  nohup python3 -m http.server "$PORT" --directory "$SKILL_ROOT" > "$LOG" 2>&1 &
  sleep 1
  echo "Started overlay server on :$PORT (root=$SKILL_ROOT)"
fi

IP=$(hostname -I | awk '{print $1}')
echo "LAN base URL: http://$IP:$PORT"
echo "Overlay base URL: http://$IP:$PORT/assets/overlays"


path: scripts/stream_dry_run.sh
content: #!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
"$SCRIPT_DIR/require_obs_mcp.sh"

SECONDS_ON_AIR="${1:-15}"
START_SCENE="${2:-Intro}"
MAIN_SCENE="${3:-Main Live}"

CURRENT=$(mcporter call 'obs.list_scenes()' | python3 -c 'import sys,json; print(json.load(sys.stdin)["current_scene"])')

mcporter call "obs.set_current_scene(scene_name: \"$START_SCENE\")" >/dev/null || true
mcporter call 'obs.start_streaming()' >/dev/null
sleep 3
mcporter call "obs.set_current_scene(scene_name: \"$MAIN_SCENE\")" >/dev/null || true
sleep "$SECONDS_ON_AIR"
mcporter call 'obs.stop_streaming()' >/dev/null
mcporter call "obs.set_current_scene(scene_name: \"$CURRENT\")" >/dev/null || true

echo "Dry-run stream complete (${SECONDS_ON_AIR}s). Restored scene: $CURRENT"


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