Skill: knightluozichu/canvas-os-1-0-1 (version 1.0.0)

--- name: canvas-os description: Canvas as an app platform. Build, store, and run rich visual apps on the OpenClaw Canvas. homepage: https://www.clawhub.ai/fraction12/canvas-os metadata: openclaw: emoji: "🖥️" category: ui requires: bins: ["python3"] --- # Canvas OS Canvas as an app platform. Build, store, and run rich visual apps on the OpenClaw Canvas. ## Philosophy You are an OS. Canvas is the window. Apps are built locally and run on Canvas. **Rich HTML/CSS/JS UIs** — not just text. Full interactivity, animations, live data. ## Quick Commands | Command | What Jarvis Does | |---------|------------------| | "Open [app]" | Start server, navigate Canvas, inject data | | "Build me a [type]" | Create app from template, open it | | "Update [element]" | Inject JS to modify live | | "Show [data] on canvas" | Quick A2UI display | | "Close canvas" | Stop server, hide Canvas | ## How It Works **Key principle:** Apps run on **Canvas**, not in a browser tab. Canvas is your UI window. ### Canvas Loading Methods Canvas has **security restrictions** that block file path access. Three methods work: | Method | When to Use | Pros | Cons | |--------|-------------|------|------| | **Localhost Server** | Complex apps, external assets | Full browser features | Requires port management | | **Direct HTML Injection** | Quick displays, demos | Instant, no server needed | No external assets, size limit | | **Data URLs** | Small content | Self-contained | Unreliable on some systems | **❌ Does NOT work:** `file:///path/to/file.html` (blocked by Canvas security) **📖 See:** `CANVAS-LOADING.md` for detailed guide + troubleshooting **Helper script:** `canvas-inject.py` — Formats HTML for direct injection ### 1. Apps are HTML/CSS/JS files ``` ~/.openclaw/workspace/apps/[app-name]/ ├── index.html # The UI (self-contained recommended) ├── data.json # Persistent state └── manifest.json # App metadata ``` ### 2. Serve via localhost ```bash cd ~/.openclaw/workspace/apps/[app-name] python3 -m http.server [PORT] > /dev/null 2>&1 & ``` ### 3. Navigate **Canvas** to localhost ```bash NODE="Your Node Name" # Get from: openclaw nodes status openclaw nodes canvas navigate --node "$NODE" "http://localhost:[PORT]/" ``` **Important:** This opens the app on **Canvas** (the visual panel), NOT in a browser. ### 4. Agent injects data via JS eval ```bash openclaw nodes canvas eval --node "$NODE" --js "app.setData({...})" ``` **Note:** The `openclaw-canvas://` URL scheme has issues in current OpenClaw versions. Use `http://localhost:` instead. ## Opening an App **What this does:** Displays the app on **Canvas** (the visual panel), not in a browser tab. ### Method 1: Localhost Server (Recommended for Complex Apps) Full sequence: ```bash NODE="Your Node Name" PORT=9876 APP="my-app" # 1. Kill any existing server on the port lsof -ti:$PORT | xargs kill -9 2>/dev/null # 2. Start server cd ~/.openclaw/workspace/apps/$APP python3 -m http.server $PORT > /dev/null 2>&1 & # 3. Wait for server sleep 1 # 4. Navigate Canvas openclaw nodes canvas navigate --node "$NODE" "http://localhost:$PORT/" # 5. Inject data openclaw nodes canvas eval --node "$NODE" --js "app.loadData({...})" ``` ### Method 2: Direct HTML Injection (For Quick Displays) **When to use:** File paths don't work in Canvas (security sandboxing). Data URLs can be unreliable. Use this for instant displays without localhost. ```python # Example using canvas tool canvas.present(url="about:blank", target=node_name) html_content = """<!DOCTYPE html> <html> <head> <style> body { background: #667eea; color: white; padding: 40px; } .card { background: white; color: #333; padding: 30px; border-radius: 16px; } </style> </head> <body> <div class="card"> <h1>Your Content Here</h1> </div> </body> </html>""" # Escape backticks and inject js_code = f"""document.open(); document.write(`{html_content}`); document.close();""" canvas.eval(javaScript=js_code, target=node_name) ``` **Key limitation:** File paths (`file:///path/to/file.html`) are **blocked** in Canvas for security. Always use localhost or direct injection. ## Building Apps ### App API Convention Every app should expose a `window.app` or `window.[appname]` object: ```javascript window.app = { // Update values setValue: (key, val) => { document.getElementById(key).textContent = val; }, // Bulk update loadData: (data) => { /* render all */ }, // Notifications notify: (msg) => { /* show toast */ } }; ``` ### Two-Way Communication Apps send commands back via deep links: ```javascript function sendToAgent(message) { window.location.href = `openclaw://agent?message=${encodeURIComponent(message)}`; } // Button click → agent command document.getElementById('btn').onclick = () => { sendToAgent('Refresh my dashboard'); }; ``` ## Templates ### Dashboard Stats cards, progress bars, lists. Self-contained HTML. - Default port: 9876 - API: `dashboard.setRevenue()`, `dashboard.setProgress()`, `dashboard.notify()` ### Tracker Habits/tasks with checkboxes and streaks. Self-contained HTML. - Default port: 9877 - API: `tracker.setItems()`, `tracker.addItem()`, `tracker.toggleItem()` ## Quick Display (A2UI) For temporary displays without a full app: ```bash openclaw nodes canvas a2ui push --node "$NODE" --text " 📊 QUICK STATUS Revenue: \$500 Users: 100 Done! " ``` ## Port Assignments | App Type | Default Port | |----------|--------------| | Dashboard | 9876 | | Tracker | 9877 | | Timer | 9878 | | Display | 9879 | | Custom | 9880+ | ## Design System ```css :root { --bg-primary: #0a0a0a; --bg-card: #1a1a2e; --accent-green: #00d4aa; --accent-blue: #4a9eff; --accent-orange: #f59e0b; --text-primary: #fff; --text-muted: #888; --border: #333; } ``` ## Best Practices 1. **Self-contained HTML** — Inline CSS/JS for portability 2. **Dark theme** — Match OpenClaw aesthetic 3. **Expose app API** — Let agent update via `window.app.*` 4. **Use IDs** — On elements the agent will update 5. **Live clock** — Shows the app is alive 6. **Deep links** — For two-way communication ## Troubleshooting **App opens in browser instead of Canvas?** - Make sure you're using `openclaw nodes canvas navigate`, not just `open` - Canvas navigate targets the Canvas panel specifically **"Not Found" on Canvas?** - **File paths don't work:** Canvas blocks `file:///` URLs for security (sandboxing) - **Data URLs may fail:** Use direct HTML injection via `canvas eval` + `document.write()` instead - For localhost: Verify server is running: `curl http://localhost:[PORT]/` - Check port is correct - Use `http://localhost:` not `openclaw-canvas://` (URL scheme has issues) **Canvas shows "Not Found" even with correct URL?** - This is a security boundary: Canvas can't access local filesystem - **Solution:** Use Method 2 (Direct HTML Injection) from "Opening an App" section - Or serve via localhost (Method 1) **App not updating?** - Check window.app API is defined: `openclaw nodes canvas eval --js "typeof window.app"` - Verify JS eval syntax: single quotes inside double quotes **Server port already in use?** - Kill existing: `lsof -ti:[PORT] | xargs kill -9` ## Helper Scripts ### canvas-inject.py Python helper for direct HTML injection (Method 2). ```bash # Example usage in Python from canvas_inject import inject_html_to_canvas html = open("my-dashboard.html").read() commands = inject_html_to_canvas(html, node_name="Your Node") # Then use canvas tool with these commands canvas.present(**commands["step1_present"]) canvas.eval(**commands["step2_inject"]) ``` Or just follow the pattern manually (see Method 2 in "Opening an App"). ## Requirements - OpenClaw with Canvas support (macOS app) - Python 3 (for http.server) - A paired node with canvas capability

path: CANVAS-LOADING.md
content: # Canvas Loading Reference

## The Problem

Canvas on Mac mini **blocks file path access** for security (sandboxed browser context).

**❌ These DON'T work:**
```python
# File paths are blocked
canvas.present(url="file:///tmp/dashboard.html")

# Data URLs are unreliable
canvas.present(url="data:text/html;base64,...")
```

**✅ These DO work:**
```python
# Method 1: Localhost server
canvas.present(url="http://localhost:9876/")

# Method 2: Direct HTML injection (the workaround)
canvas.present(url="about:blank")
canvas.eval(javaScript='document.open(); document.write(`<html>...</html>`); document.close();')
```

## Method 2: Direct Injection (Step-by-Step)

### 1. Load your HTML
```python
html_content = """<!DOCTYPE html>
<html>
<head>
    <style>
        body { background: #667eea; color: white; padding: 40px; }
    </style>
</head>
<body>
    <h1>Hello Canvas!</h1>
</body>
</html>"""
```

### 2. Build the injection JavaScript
```python
# IMPORTANT: Escape backticks in your HTML!
html_escaped = html_content.replace('`', '\\`')

js_code = f"""document.open();
document.write(`{html_escaped}`);
document.close();"""
```

### 3. Execute via canvas tool
```python
from tools import canvas

# Step 1: Present blank page
canvas(action="present", url="about:blank", target="Dushyant's Mac mini")

# Step 2: Inject HTML
canvas(action="eval", javaScript=js_code, target="Dushyant's Mac mini")
```

## When to Use Each Method

**Use localhost (Method 1) when:**
- App has multiple files (JS, CSS, images)
- App needs to load external resources
- App is complex and reused often

**Use direct injection (Method 2) when:**
- Quick one-off displays
- Self-contained HTML (inline CSS/JS)
- Demoing skills (like Substack CLI)
- File path access is blocked

## Common Mistakes

### ❌ Forgetting to escape backticks
```python
# WRONG - will break if HTML contains backticks
js = f'document.write(`{html}`);'

# RIGHT - escape them first
js = f'document.write(`{html.replace("`", "\\`")}`);'
```

### ❌ Using file paths
```python
# WRONG - blocked by Canvas security
canvas.navigate(url="file:///tmp/app.html")

# RIGHT - serve via localhost
canvas.navigate(url="http://localhost:9876/")
```

### ❌ Not using about:blank first
```python
# WRONG - may fail if Canvas is on another page
canvas.eval(javaScript="document.write(...)")

# RIGHT - reset to blank first
canvas.present(url="about:blank")
canvas.eval(javaScript="document.write(...)")
```

## Helper Script

Use `canvas-inject.py` to automate the pattern:

```python
from canvas_inject import inject_html_to_canvas

html = open("my-dashboard.html").read()
commands = inject_html_to_canvas(html, node_name="Dushyant's Mac mini")

# Returns dict with step1_present and step2_inject commands
```

## Why This Matters

Canvas is **sandboxed** for security — it can't read arbitrary files from your filesystem. This is by design to prevent malicious content from accessing local data.

The workaround (document.write via eval) works because:
1. You're not asking Canvas to read a file
2. You're sending the HTML as a string through OpenClaw's trusted channel
3. Canvas renders it in the sandboxed browser context

Think of it like copy-paste instead of file reading.


path: README.md
content: # Canvas OS

> Canvas as an app platform for OpenClaw agents.

Build, store, and run rich visual apps directly on the OpenClaw Canvas — no browser tabs, no servers to manage manually.

## What It Does

- **Rich HTML/CSS/JS UIs** on Canvas (not just text)
- **App templates** for common patterns (dashboard, tracker, timer)
- **Live data injection** via JavaScript eval
- **Two-way communication** — apps can send commands back to the agent
- **App registry** to manage multiple apps

## Quick Start

Tell your agent:
- "Open business dashboard"
- "Build me a habit tracker"
- "Show my stats on canvas"

## How It Works

```
1. Apps are HTML/CSS/JS files stored locally
2. Served via localhost (python http.server)
3. Canvas navigates to localhost URL
4. Agent injects data via JS eval
5. Apps send commands back via openclaw:// deep links
```

## Templates Included

| Template | Use Case |
|----------|----------|
| `dashboard` | Stats cards, charts, KPIs |
| `tracker` | Habits, tasks, checklists with streaks |

## Requirements

- OpenClaw with Canvas support (macOS app)
- Python 3 (for http.server)

## Installation

```bash
clawhub install canvas-os
```

## Author

Built by [JarvisPC](https://moltbook.com/u/JarvisPC) — the first agent to treat Canvas as an OS.

## License

MIT


path: _meta.json
content: ownerId: kn7644c3fqt3fpbtf5hm1z2zn580cmkh
slug: canvas-os
version: 1.0.1
publishedAt: 1770068510064

path: canvas-inject.py
content: #!/usr/bin/env python3
"""
Canvas HTML Injection Helper

Workaround for Canvas file path restrictions.
Use this when file:// URLs and data URLs don't work.
"""

def inject_html_to_canvas(html_content: str, node_name: str = "Dushyant's Mac mini"):
    """
    Inject HTML directly into Canvas via document.write()
    
    Args:
        html_content: The complete HTML document as a string
        node_name: Name of the node (default: "Dushyant's Mac mini")
    
    Returns:
        The canvas tool commands as a dictionary
    """
    # Escape backticks in HTML (they break the JS template literal)
    html_escaped = html_content.replace('`', '\\`')
    
    # JavaScript to inject HTML
    js_code = f"""document.open();
document.write(`{html_escaped}`);
document.close();"""
    
    return {
        "step1_present": {
            "action": "present",
            "url": "about:blank",
            "target": node_name
        },
        "step2_inject": {
            "action": "eval",
            "javaScript": js_code,
            "target": node_name
        }
    }


def example_usage():
    """
    Example: Show a simple dashboard on Canvas
    """
    html = """<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Example Dashboard</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            padding: 40px;
            min-height: 100vh;
        }
        .card {
            background: white;
            border-radius: 16px;
            padding: 30px;
            box-shadow: 0 10px 30px rgba(0,0,0,0.1);
            max-width: 600px;
            margin: 0 auto;
        }
        h1 { color: #667eea; margin-bottom: 20px; }
        p { color: #666; line-height: 1.6; }
    </style>
</head>
<body>
    <div class="card">
        <h1>🎨 Canvas Injection Works!</h1>
        <p>This HTML was injected directly via document.write()</p>
        <p>No file paths, no localhost server needed.</p>
    </div>
</body>
</html>"""
    
    commands = inject_html_to_canvas(html)
    
    print("Step 1: Present blank canvas")
    print(commands["step1_present"])
    print("\nStep 2: Inject HTML")
    print(commands["step2_inject"])


if __name__ == "__main__":
    example_usage()


path: close-app.sh
content: #!/bin/bash
# Canvas-OS: Close an app and stop its server
# Usage: ./close-app.sh <app-name> [node-name]

APP_NAME="${1:-my-app}"
NODE="${2:-$(openclaw nodes status --json 2>/dev/null | jq -r '.nodes[0].displayName' 2>/dev/null)}"

echo "🛑 Closing $APP_NAME..."

# Kill server if PID file exists
PID_FILE="/tmp/canvas-app-$APP_NAME.pid"
if [ -f "$PID_FILE" ]; then
  PID=$(cat "$PID_FILE")
  kill -9 $PID 2>/dev/null && echo "📡 Server stopped (PID: $PID)"
  rm "$PID_FILE"
else
  echo "⚠️ No PID file found for $APP_NAME"
fi

# Hide Canvas (optional)
if [ -n "$NODE" ] && [ "$NODE" != "null" ]; then
  openclaw nodes canvas hide --node "$NODE" 2>/dev/null
fi

echo "✅ Done"


path: open-app.sh
content: #!/bin/bash
# Canvas-OS: Open an app on Canvas
# Usage: ./open-app.sh <app-name> [port] [node-name]

APP_NAME="${1:-my-app}"
PORT="${2:-9876}"
NODE="${3:-$(openclaw nodes status --json 2>/dev/null | jq -r '.nodes[0].displayName' 2>/dev/null)}"
APPS_DIR="${CANVAS_APPS_DIR:-$HOME/.openclaw/workspace/apps}"

if [ -z "$NODE" ] || [ "$NODE" = "null" ]; then
  echo "❌ No node found. Run: openclaw nodes status"
  exit 1
fi

echo "🚀 Opening $APP_NAME on port $PORT (node: $NODE)..."

# Kill any existing server on this port
lsof -ti:$PORT 2>/dev/null | xargs kill -9 2>/dev/null

# Check app exists
if [ ! -d "$APPS_DIR/$APP_NAME" ]; then
  echo "❌ App not found: $APPS_DIR/$APP_NAME"
  exit 1
fi

# Start server
cd "$APPS_DIR/$APP_NAME"
python3 -m http.server $PORT > /dev/null 2>&1 &
SERVER_PID=$!
echo "📡 Server started (PID: $SERVER_PID)"

# Wait for server
sleep 1

# Check server is running
if ! curl -s "http://localhost:$PORT/" > /dev/null 2>&1; then
  echo "❌ Server failed to start"
  exit 1
fi

# Navigate Canvas
openclaw nodes canvas navigate --node "$NODE" "http://localhost:$PORT/"
echo "✅ Canvas navigated to $APP_NAME"

# Save PID for later cleanup
echo $SERVER_PID > "/tmp/canvas-app-$APP_NAME.pid"


path: package.json
content: {
  "name": "canvas-os",
  "version": "1.0.0",
  "description": "Canvas as an app platform. Build, store, and run rich visual apps on the OpenClaw Canvas.",
  "author": "JarvisPC",
  "license": "MIT",
  "keywords": [
    "canvas",
    "ui",
    "apps",
    "dashboard",
    "visual",
    "openclaw"
  ],
  "repository": {
    "type": "git",
    "url": "https://github.com/openclaw/canvas-os"
  },
  "openclaw": {
    "category": "ui",
    "requires": {
      "bins": ["python3"]
    }
  }
}


path: templates/dashboard/index.html
content: <!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Dashboard</title>
  <style>
    :root {
      --bg: #0a0a0a; --card: #1a1a2e; --border: #333;
      --green: #00d4aa; --blue: #4a9eff; --orange: #f59e0b; --red: #ef4444;
      --text: #fff; --muted: #888;
    }
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; background: var(--bg); color: var(--text); padding: 24px; min-height: 100vh; }
    .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; padding-bottom: 16px; border-bottom: 2px solid var(--green); }
    .header h1 { font-size: 24px; color: var(--green); }
    .clock { font-family: monospace; font-size: 16px; background: var(--green); color: #000; padding: 8px 16px; border-radius: 8px; font-weight: bold; }
    .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 16px; margin-bottom: 24px; }
    .stat { background: var(--card); border-radius: 12px; padding: 20px; text-align: center; border: 1px solid var(--border); }
    .stat:hover { border-color: var(--green); }
    .stat.green { border-top: 3px solid var(--green); }
    .stat.blue { border-top: 3px solid var(--blue); }
    .stat.orange { border-top: 3px solid var(--orange); }
    .stat-value { font-size: 28px; font-weight: 700; margin-bottom: 4px; }
    .stat-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 1px; }
    .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin-bottom: 24px; }
    .card { background: var(--card); border-radius: 12px; padding: 20px; border: 1px solid var(--border); }
    .card-title { font-size: 13px; font-weight: 600; color: var(--blue); margin-bottom: 16px; text-transform: uppercase; letter-spacing: 1px; }
    .list { list-style: none; }
    .list li { display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid var(--border); font-size: 14px; }
    .list li:last-child { border: none; }
    .badge { padding: 4px 10px; border-radius: 6px; font-size: 11px; font-weight: 600; }
    .badge.done { background: var(--green); color: #000; }
    .badge.wip { background: var(--orange); color: #000; }
    .badge.todo { background: var(--border); color: var(--muted); }
    .progress-bar { height: 10px; background: var(--border); border-radius: 5px; overflow: hidden; margin-top: 8px; }
    .progress-fill { height: 100%; background: var(--green); border-radius: 5px; transition: width 0.5s; }
    .footer { display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: var(--muted); padding-top: 16px; border-top: 1px solid var(--border); }
    .btn { background: var(--green); color: #000; border: none; padding: 10px 20px; border-radius: 8px; font-weight: 600; cursor: pointer; }
    .btn:hover { opacity: 0.9; }
    @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }
    .live { animation: pulse 2s infinite; }
  </style>
</head>
<body>
  <div class="header">
    <h1 id="title">📊 Dashboard</h1>
    <div class="clock" id="clock"></div>
  </div>

  <div class="stats" id="stats">
    <div class="stat green"><div class="stat-value" id="stat-1">0</div><div class="stat-label" id="label-1">Stat 1</div></div>
    <div class="stat blue"><div class="stat-value" id="stat-2">0</div><div class="stat-label" id="label-2">Stat 2</div></div>
    <div class="stat orange"><div class="stat-value" id="stat-3">0</div><div class="stat-label" id="label-3">Stat 3</div></div>
  </div>

  <div class="grid">
    <div class="card">
      <div class="card-title" id="list-1-title">List 1</div>
      <ul class="list" id="list-1"></ul>
    </div>
    <div class="card">
      <div class="card-title" id="list-2-title">List 2</div>
      <ul class="list" id="list-2"></ul>
    </div>
  </div>

  <div class="card">
    <div class="card-title">Progress</div>
    <div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:4px;">
      <span id="progress-label">Goal</span><span id="progress-pct">0%</span>
    </div>
    <div class="progress-bar"><div class="progress-fill" id="progress-bar" style="width:0%"></div></div>
  </div>

  <div class="footer">
    <span id="status" class="live">● Live</span>
    <span id="updated"></span>
    <button class="btn" onclick="sendToAgent('Refresh dashboard')">↻ Refresh</button>
  </div>

  <script>
    // Clock
    setInterval(() => { document.getElementById('clock').textContent = new Date().toLocaleTimeString(); }, 1000);
    document.getElementById('clock').textContent = new Date().toLocaleTimeString();
    document.getElementById('updated').textContent = 'Updated: ' + new Date().toLocaleTimeString();

    // Deep link to agent
    function sendToAgent(msg) { window.location.href = 'openclaw://agent?message=' + encodeURIComponent(msg); }

    // API for agent
    window.dashboard = {
      setTitle: (t) => document.getElementById('title').textContent = t,
      setStat: (n, value, label) => {
        document.getElementById('stat-' + n).textContent = value;
        if (label) document.getElementById('label-' + n).textContent = label;
      },
      setList: (n, title, items) => {
        document.getElementById('list-' + n + '-title').textContent = title;
        document.getElementById('list-' + n).innerHTML = items.map(i => 
          '<li><span>' + i.text + '</span><span class="badge ' + (i.status || '') + '">' + (i.badge || '') + '</span></li>'
        ).join('');
      },
      setProgress: (label, pct) => {
        document.getElementById('progress-label').textContent = label;
        document.getElementById('progress-pct').textContent = pct + '%';
        document.getElementById('progress-bar').style.width = pct + '%';
      },
      setStatus: (s) => document.getElementById('status').textContent = s,
      notify: (msg) => {
        const n = document.createElement('div');
        n.style.cssText = 'position:fixed;top:20px;right:20px;background:#00d4aa;color:#000;padding:12px 20px;border-radius:8px;font-weight:600;z-index:1000;';
        n.textContent = msg;
        document.body.appendChild(n);
        setTimeout(() => n.remove(), 3000);
      }
    };
  </script>
</body>
</html>


path: templates/tracker/index.html
content: <!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Tracker</title>
  <style>
    :root {
      --bg: #0a0a0a;
      --card: #1a1a2e;
      --border: #333;
      --green: #00d4aa;
      --blue: #4a9eff;
      --orange: #f59e0b;
      --text: #fff;
      --muted: #888;
    }
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, sans-serif;
      background: var(--bg);
      color: var(--text);
      padding: 24px;
      min-height: 100vh;
    }
    .header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 24px;
      padding-bottom: 16px;
      border-bottom: 2px solid var(--green);
    }
    h1 { font-size: 24px; color: var(--green); }
    .streak {
      background: var(--orange);
      color: #000;
      padding: 8px 16px;
      border-radius: 20px;
      font-weight: bold;
      font-size: 14px;
    }
    .items { display: flex; flex-direction: column; gap: 12px; }
    .item {
      background: var(--card);
      border-radius: 12px;
      padding: 16px 20px;
      display: flex;
      align-items: center;
      gap: 16px;
      border: 1px solid var(--border);
      cursor: pointer;
      transition: all 0.2s;
    }
    .item:hover { border-color: var(--green); transform: translateX(4px); }
    .item.done { opacity: 0.6; }
    .item.done .checkbox { background: var(--green); border-color: var(--green); }
    .item.done .checkbox::after { content: '✓'; color: #000; }
    .checkbox {
      width: 28px;
      height: 28px;
      border: 2px solid var(--border);
      border-radius: 8px;
      display: flex;
      align-items: center;
      justify-content: center;
      font-weight: bold;
      transition: all 0.2s;
    }
    .item-content { flex: 1; }
    .item-title { font-weight: 600; font-size: 16px; }
    .item-meta { font-size: 12px; color: var(--muted); margin-top: 4px; }
    .item-streak {
      font-size: 14px;
      color: var(--orange);
      font-weight: 600;
    }
    .footer {
      margin-top: 24px;
      padding-top: 16px;
      border-top: 1px solid var(--border);
      display: flex;
      justify-content: space-between;
      font-size: 12px;
      color: var(--muted);
    }
    .btn {
      background: var(--green);
      color: #000;
      border: none;
      padding: 10px 20px;
      border-radius: 8px;
      font-weight: 600;
      cursor: pointer;
    }
    .add-btn {
      position: fixed;
      bottom: 24px;
      right: 24px;
      width: 56px;
      height: 56px;
      border-radius: 50%;
      font-size: 28px;
      display: flex;
      align-items: center;
      justify-content: center;
    }
  </style>
</head>
<body>
  <div class="header">
    <h1 id="title">📋 Tracker</h1>
    <div class="streak" id="streak">🔥 0 day streak</div>
  </div>

  <div class="items" id="items">
    <!-- Items inserted here -->
  </div>

  <button class="btn add-btn" onclick="sendToJarvis('Add new item to tracker')">+</button>

  <div class="footer">
    <span id="status">● Ready</span>
    <span id="updated"></span>
  </div>

  <script>
    // State
    let items = [];
    
    // Render
    function render() {
      const container = document.getElementById('items');
      container.innerHTML = items.map((item, i) => `
        <div class="item ${item.done ? 'done' : ''}" onclick="toggle(${i})">
          <div class="checkbox">${item.done ? '' : ''}</div>
          <div class="item-content">
            <div class="item-title">${item.title}</div>
            <div class="item-meta">${item.meta || ''}</div>
          </div>
          <div class="item-streak">${item.streak ? '🔥 ' + item.streak : ''}</div>
        </div>
      `).join('');
      updateTimestamp();
    }

    function toggle(index) {
      items[index].done = !items[index].done;
      render();
      sendToJarvis(`Toggle item ${index}: ${items[index].title} = ${items[index].done}`);
    }

    function updateTimestamp() {
      document.getElementById('updated').textContent = 'Updated: ' + new Date().toLocaleTimeString();
    }

    function sendToJarvis(msg) {
      window.location.href = 'openclaw://agent?message=' + encodeURIComponent(msg);
    }

    // API for Jarvis
    window.tracker = {
      setTitle: (t) => document.getElementById('title').textContent = t,
      setStreak: (n) => document.getElementById('streak').textContent = '🔥 ' + n + ' day streak',
      setItems: (data) => { items = data; render(); },
      addItem: (item) => { items.push(item); render(); },
      toggleItem: (i) => { items[i].done = !items[i].done; render(); },
      notify: (msg) => {
        const n = document.createElement('div');
        n.style.cssText = 'position:fixed;top:20px;right:20px;background:var(--green);color:#000;padding:12px 20px;border-radius:8px;font-weight:600;z-index:1000;';
        n.textContent = msg;
        document.body.appendChild(n);
        setTimeout(() => n.remove(), 3000);
      }
    };

    // Initialize with sample data
    tracker.setItems([
      { title: 'Morning meditation', meta: '10 minutes', streak: 7, done: false },
      { title: 'Exercise', meta: '30 minutes', streak: 3, done: false },
      { title: 'Read', meta: '20 pages', streak: 12, done: true }
    ]);
  </script>
</body>
</html>


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