[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6CK6THJYDFM8DHM4X5G7WXH":3,"skill-same:01M6CK6THJYDFM8DHM4X5G7WXH":101},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":33,"publishedAt":49,"linkOnly":50,"markdownContent":51,"markdownBody":52,"frontMatter":53,"refName":62,"commitSha":63,"skillDirectory":64,"forksCount":65,"repositoryHtmlUrl":66,"repositoryHomepageUrl":6,"ownerHtmlUrl":67,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":68,"install":69,"resources":82},"01M6CK6THJYDFM8DHM4X5G7WXH","glance",null,"Create, update, and manage Glance dashboard widgets. Use when user wants to: add something to their dashboard, create a widget, track data visually, show metrics\u002Fstats, display API data, or monitor usage.","kbarbel640-del\u002Fskills","kbarbel640-del","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F255315490?v=4","01M6CJPPQGCCESXMG3K7BBGQ5J","01M6CJPPX2PB2BCZERFVM2C2B3",1,"MIT","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Facfranzen\u002Fglance\u002FSKILL.md",[17,22,25,30],{"categoryNo":18,"categoryName":19,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000010","后端与接口","SCENARIO","使用场景",{"categoryNo":23,"categoryName":24,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000013","分析与可视化",{"categoryNo":26,"categoryName":27,"taxonomyType":28,"taxonomyTypeDesc":29},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",{"categoryNo":31,"categoryName":32,"taxonomyType":28,"taxonomyTypeDesc":29},"01JBSEEDCAT000000000000033","数据分析师",[5,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48],"github","dashboard","monitor","create","update","metrics","https","stats","data","api","curl","user","use","manage","show","2026-11-10 10:12:30",false,"---\nname: glance\ndescription: \"Create, update, and manage Glance dashboard widgets. Use when user wants to: add something to their dashboard, create a widget, track data visually, show metrics\u002Fstats, display API data, or monitor usage.\"\nmetadata:\n  openclaw:\n    emoji: \"?️\"\n    homepage: \"https:\u002F\u002Fgithub.com\u002Facfranzen\u002Fglance\"\n    requires:\n      env: [\"GLANCE_URL\"]\n      bins: [\"curl\"]\n    primaryEnv: GLANCE_URL\n---\n\n# Glance\n\nAI-extensible personal dashboard. Create custom widgets with natural language — the AI handles data collection.\n\n## Features\n\n- **Custom Widgets** — Create widgets via AI with auto-generated JSX\n- **Agent Refresh** — AI collects data on schedule and pushes to cache\n- **Dashboard Export\u002FImport** — Share widget configurations\n- **Credential Management** — Secure API key storage\n- **Real-time Updates** — Webhook-triggered instant refreshes\n\n## Quick Start\n\n```bash\n# Navigate to skill directory (if installed via ClawHub)\ncd \"$(clawhub list | grep glance | awk '{print $2}')\"\n\n# Or clone directly\ngit clone https:\u002F\u002Fgithub.com\u002Facfranzen\u002Fglance ~\u002F.glance\ncd ~\u002F.glance\n\n# Install dependencies\nnpm install\n\n# Configure environment\ncp .env.example .env.local\n# Edit .env.local with your settings\n\n# Start development server\nnpm run dev\n\n# Or build and start production\nnpm run build && npm start\n```\n\nDashboard runs at **http:\u002F\u002Flocalhost:3333**\n\n## Configuration\n\nEdit `.env.local`:\n\n```bash\n# Server\nPORT=3333\nAUTH_TOKEN=your-secret-token        # Optional: Bearer token auth\n\n# OpenClaw Integration (for instant widget refresh)\nOPENCLAW_GATEWAY_URL=https:\u002F\u002Flocalhost:18789\nOPENCLAW_TOKEN=your-gateway-token\n\n# Database\nDATABASE_PATH=.\u002Fdata\u002Fglance.db      # SQLite database location\n```\n\n## Service Installation (macOS)\n\n```bash\n# Create launchd plist\ncat > ~\u002FLibrary\u002FLaunchAgents\u002Fcom.glance.dashboard.plist \u003C\u003C 'EOF'\n\u003C?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003C!DOCTYPE plist PUBLIC \"-\u002F\u002FApple\u002F\u002FDTD PLIST 1.0\u002F\u002FEN\" \"http:\u002F\u002Fwww.apple.com\u002FDTDs\u002FPropertyList-1.0.dtd\">\n\u003Cplist version=\"1.0\">\n\u003Cdict>\n    \u003Ckey>Label\u003C\u002Fkey>\n    \u003Cstring>com.glance.dashboard\u003C\u002Fstring>\n    \u003Ckey>ProgramArguments\u003C\u002Fkey>\n    \u003Carray>\n        \u003Cstring>\u002Fopt\u002Fhomebrew\u002Fbin\u002Fnpm\u003C\u002Fstring>\n        \u003Cstring>run\u003C\u002Fstring>\n        \u003Cstring>dev\u003C\u002Fstring>\n    \u003C\u002Farray>\n    \u003Ckey>WorkingDirectory\u003C\u002Fkey>\n    \u003Cstring>~\u002F.glance\u003C\u002Fstring>\n    \u003Ckey>RunAtLoad\u003C\u002Fkey>\n    \u003Ctrue\u002F>\n    \u003Ckey>KeepAlive\u003C\u002Fkey>\n    \u003Ctrue\u002F>\n    \u003Ckey>StandardOutPath\u003C\u002Fkey>\n    \u003Cstring>~\u002F.glance\u002Flogs\u002Fstdout.log\u003C\u002Fstring>\n    \u003Ckey>StandardErrorPath\u003C\u002Fkey>\n    \u003Cstring>~\u002F.glance\u002Flogs\u002Fstderr.log\u003C\u002Fstring>\n\u003C\u002Fdict>\n\u003C\u002Fplist>\nEOF\n\n# Load service\nmkdir -p ~\u002F.glance\u002Flogs\nlaunchctl load ~\u002FLibrary\u002FLaunchAgents\u002Fcom.glance.dashboard.plist\n\n# Service commands\nlaunchctl start com.glance.dashboard\nlaunchctl stop com.glance.dashboard\nlaunchctl unload ~\u002FLibrary\u002FLaunchAgents\u002Fcom.glance.dashboard.plist\n```\n\n## Environment Variables\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `PORT` | Server port | `3333` |\n| `AUTH_TOKEN` | Bearer token for API auth | — |\n| `DATABASE_PATH` | SQLite database path | `.\u002Fdata\u002Fglance.db` |\n| `OPENCLAW_GATEWAY_URL` | OpenClaw gateway for webhooks | — |\n| `OPENCLAW_TOKEN` | OpenClaw auth token | — |\n\n## Requirements\n\n- Node.js 20+\n- npm or pnpm\n- SQLite (bundled)\n\n---\n\n# Widget Skill\n\nCreate and manage dashboard widgets. Most widgets use `agent_refresh` — **you** collect the data.\n\n## Quick Start\n\n```bash\n# Check Glance is running (list widgets)\ncurl -s -H \"Origin: $GLANCE_URL\" \"$GLANCE_URL\u002Fapi\u002Fwidgets\" | jq '.custom_widgets[].slug'\n\n# Auth note: Local requests with Origin header bypass Bearer token auth\n# For external access, use: -H \"Authorization: Bearer $GLANCE_TOKEN\"\n\n# Refresh a widget (look up instructions, collect data, POST to cache)\nsqlite3 $GLANCE_DATA\u002Fglance.db \"SELECT json_extract(fetch, '$.instructions') FROM custom_widgets WHERE slug = 'my-widget'\"\n# Follow the instructions, then:\ncurl -X POST \"$GLANCE_URL\u002Fapi\u002Fwidgets\u002Fmy-widget\u002Fcache\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -H \"Origin: $GLANCE_URL\" \\\n  -d '{\"data\": {\"value\": 42, \"fetchedAt\": \"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'\"}}'\n\n# Verify in browser\nbrowser action:open targetUrl:\"$GLANCE_URL\"\n```\n\n## AI Structured Output Generation (REQUIRED)\n\nWhen generating widget definitions, **use the JSON Schema** at `docs\u002Fschemas\u002Fwidget-schema.json` with your AI model's structured output mode:\n- **Anthropic**: Use `tool_use` with the schema\n- **OpenAI**: Use `response_format: { type: \"json_schema\", schema }`\n\nThe schema enforces all required fields at generation time — malformed widgets cannot be produced.\n\n### Required Fields Checklist\nEvery widget **MUST** have these fields (the schema enforces them):\n\n| Field | Type | Notes |\n|-------|------|-------|\n| `name` | string | Non-empty, human-readable |\n| `slug` | string | Lowercase kebab-case (`my-widget`) |\n| `source_code` | string | Valid JSX with Widget function |\n| `default_size` | `{ w: 1-12, h: 1-20 }` | Grid units |\n| `min_size` | `{ w: 1-12, h: 1-20 }` | Cannot resize smaller |\n| `fetch.type` | enum | `\"server_code\"` \\| `\"webhook\"` \\| `\"agent_refresh\"` |\n| `fetch.instructions` | string | **REQUIRED if type is `agent_refresh`** |\n| `fetch.schedule` | string | **REQUIRED if type is `agent_refresh`** (cron) |\n| `data_schema.type` | `\"object\"` | Always object |\n| `data_schema.properties` | object | Define each field |\n| `data_schema.required` | array | **MUST include `\"fetchedAt\"`** |\n| `credentials` | array | Use `[]` if none needed |\n\n### Example: Minimal Valid Widget\n\n```json\n{\n  \"name\": \"My Widget\",\n  \"slug\": \"my-widget\",\n  \"source_code\": \"function Widget({ serverData }) { return \u003Cdiv>{serverData?.value}\u003C\u002Fdiv>; }\",\n  \"default_size\": { \"w\": 2, \"h\": 2 },\n  \"min_size\": { \"w\": 1, \"h\": 1 },\n  \"fetch\": {\n    \"type\": \"agent_refresh\",\n    \"schedule\": \"*\u002F15 * * * *\",\n    \"instructions\": \"## Data Collection\\nCollect the data...\\n\\n## Cache Update\\nPOST to \u002Fapi\u002Fwidgets\u002Fmy-widget\u002Fcache\"\n  },\n  \"data_schema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"value\": { \"type\": \"number\" },\n      \"fetchedAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n    },\n    \"required\": [\"value\", \"fetchedAt\"]\n  },\n  \"credentials\": []\n}\n```\n\n---\n\n## ⚠️ Widget Creation Checklist (MANDATORY)\n\nEvery widget must complete ALL steps before being considered done:\n\n```\n□ Step 1: Create widget definition (POST \u002Fapi\u002Fwidgets)\n    - source_code with Widget function\n    - data_schema (REQUIRED for validation)\n    - fetch config (type + instructions for agent_refresh)\n    \n□ Step 2: Add to dashboard (POST \u002Fapi\u002Fwidgets\u002Finstances)\n    - custom_widget_id matches definition\n    - title and config set\n    \n□ Step 3: Populate cache (for agent_refresh widgets)\n    - Data matches data_schema exactly\n    - Includes fetchedAt timestamp\n    \n□ Step 4: Set up cron job (for agent_refresh widgets)\n    - Simple message: \"⚡ WIDGET REFRESH: {slug}\"\n    - Appropriate schedule (*\u002F15 or *\u002F30 typically)\n    \n□ Step 5: BROWSER VERIFICATION (MANDATORY)\n    - Open http:\u002F\u002Flocalhost:3333\n    - Widget is visible on dashboard\n    - Shows actual data (not loading spinner)\n    - Data values match what was cached\n    - No errors or broken layouts\n    \n⛔ DO NOT report widget as complete until Step 5 passes!\n```\n\n## Quick Reference\n\n- **Full SDK docs:** See `docs\u002Fwidget-sdk.md` in the Glance repo\n- **Component list:** See [references\u002Fcomponents.md](references\u002Fcomponents.md)\n\n## Widget Package Structure\n\n```\nWidget Package\n├── meta (name, slug, description, author, version)\n├── widget (source_code, default_size, min_size)\n├── fetch (server_code | webhook | agent_refresh)\n├── dataSchema? (JSON Schema for cached data - validates on POST)\n├── cache (ttl, staleness, fallback)\n├── credentials[] (API keys, local software requirements)\n├── config_schema? (user options)\n└── error? (retry, fallback, timeout)\n```\n\n## Fetch Type Decision Tree\n\n```\nIs data available via API that the widget can call?\n├── YES → Use server_code\n└── NO → Does an external service push data?\n    ├── YES → Use webhook\n    └── NO → Use agent_refresh (YOU collect it)\n```\n\n| Scenario | Fetch Type | Who Collects Data? |\n|----------|-----------|-------------------|\n| Public\u002Fauthenticated API | `server_code` | Widget calls API at render |\n| External service pushes data | `webhook` | External service POSTs to cache |\n| **Local CLI tools** | `agent_refresh` | **YOU (the agent) via PTY\u002Fexec** |\n| **Interactive terminals** | `agent_refresh` | **YOU (the agent) via PTY** |\n| **Computed\u002Faggregated data** | `agent_refresh` | **YOU (the agent) on a schedule** |\n\n**⚠️ `agent_refresh` means YOU are the data source.** You set up a cron to remind yourself, then YOU collect the data using your tools (exec, PTY, browser, etc.) and POST it to the cache.\n\n## API Endpoints\n\n### Widget Definitions\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| `POST` | `\u002Fapi\u002Fwidgets` | Create widget definition |\n| `GET` | `\u002Fapi\u002Fwidgets` | List all definitions |\n| `GET` | `\u002Fapi\u002Fwidgets\u002F:slug` | Get single definition |\n| `PATCH` | `\u002Fapi\u002Fwidgets\u002F:slug` | Update definition |\n| `DELETE` | `\u002Fapi\u002Fwidgets\u002F:slug` | Delete definition |\n\n### Widget Instances (Dashboard)\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| `POST` | `\u002Fapi\u002Fwidgets\u002Finstances` | Add widget to dashboard |\n| `GET` | `\u002Fapi\u002Fwidgets\u002Finstances` | List dashboard widgets |\n| `PATCH` | `\u002Fapi\u002Fwidgets\u002Finstances\u002F:id` | Update instance (config, position) |\n| `DELETE` | `\u002Fapi\u002Fwidgets\u002Finstances\u002F:id` | Remove from dashboard |\n\n### Credentials\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| `GET` | `\u002Fapi\u002Fcredentials` | List credentials + status |\n| `POST` | `\u002Fapi\u002Fcredentials` | Store credential |\n| `DELETE` | `\u002Fapi\u002Fcredentials\u002F:id` | Delete credential |\n\n## Creating a Widget\n\n### Full Widget Package Structure\n\n```json\n{\n  \"name\": \"GitHub PRs\",\n  \"slug\": \"github-prs\",\n  \"description\": \"Shows open pull requests\",\n  \n  \"source_code\": \"function Widget({ serverData }) { ... }\",\n  \"default_size\": { \"w\": 2, \"h\": 2 },\n  \"min_size\": { \"w\": 1, \"h\": 1 },\n  \"refresh_interval\": 300,\n  \n  \"credentials\": [\n    {\n      \"id\": \"github\",\n      \"type\": \"api_key\",\n      \"name\": \"GitHub Personal Access Token\",\n      \"description\": \"Token with repo scope\",\n      \"obtain_url\": \"https:\u002F\u002Fgithub.com\u002Fsettings\u002Ftokens\"\n    }\n  ],\n  \n  \"fetch\": {\n    \"type\": \"agent_refresh\",\n    \"schedule\": \"*\u002F5 * * * *\",\n    \"instructions\": \"Fetch open PRs from GitHub API and POST to cache endpoint\",\n    \"expected_freshness_seconds\": 300,\n    \"max_staleness_seconds\": 900\n  },\n  \n  \"cache\": {\n    \"ttl_seconds\": 300,\n    \"max_staleness_seconds\": 900,\n    \"storage\": \"sqlite\",\n    \"on_error\": \"use_stale\"\n  },\n  \n  \"setup\": {\n    \"description\": \"Configure GitHub token\",\n    \"agent_skill\": \"Store GitHub PAT via \u002Fapi\u002Fcredentials\",\n    \"verification\": {\n      \"type\": \"cache_populated\",\n      \"target\": \"github-prs\"\n    },\n    \"idempotent\": true\n  }\n}\n```\n\n### Fetch Types\n\n| Type | When to Use | Data Flow |\n|------|-------------|-----------|\n| `server_code` | Widget can call API directly | Widget → server_code → API |\n| `agent_refresh` | Agent must fetch\u002Fcompute data | Agent → POST \u002Fcache → Widget reads |\n| `webhook` | External service pushes data | External → POST \u002Fcache → Widget reads |\n\n**Most widgets should use `agent_refresh`** — the agent fetches data on a schedule and pushes to the cache endpoint.\n\n### Step 1: Create Widget Definition\n\n```http\nPOST \u002Fapi\u002Fwidgets\nContent-Type: application\u002Fjson\n\n{\n  \"name\": \"GitHub PRs\",\n  \"slug\": \"github-prs\",\n  \"description\": \"Shows open pull requests\",\n  \"source_code\": \"function Widget({ serverData }) { ... }\",\n  \"default_size\": { \"w\": 2, \"h\": 2 },\n  \"credentials\": [...],\n  \"fetch\": { \"type\": \"agent_refresh\", \"schedule\": \"*\u002F5 * * * *\", ... },\n  \"data_schema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"prs\": { \"type\": \"array\", \"description\": \"List of PR objects\" },\n      \"fetchedAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n    },\n    \"required\": [\"prs\", \"fetchedAt\"]\n  },\n  \"cache\": { \"ttl_seconds\": 300, ... }\n}\n```\n\n**`data_schema` (REQUIRED)** defines the data contract between the fetcher and the widget. Cache POSTs are validated against it — malformed data returns 400.\n\n> ⚠️ **Always include `data_schema`** when creating widgets. This ensures:\n> 1. Data validation on cache POSTs (400 on schema mismatch)\n> 2. Clear documentation of expected data structure\n> 3. AI agents know the exact format to produce\n\n### Step 2: Add to Dashboard\n\n```http\nPOST \u002Fapi\u002Fwidgets\u002Finstances\nContent-Type: application\u002Fjson\n\n{\n  \"type\": \"custom\",\n  \"title\": \"GitHub PRs\",\n  \"custom_widget_id\": \"cw_abc123\",\n  \"config\": { \"owner\": \"acfranzen\", \"repo\": \"libra\" }\n}\n```\n\n### Step 3: Populate Cache (for agent_refresh)\n\n```http\nPOST \u002Fapi\u002Fwidgets\u002Fgithub-prs\u002Fcache\nContent-Type: application\u002Fjson\n\n{\n  \"data\": {\n    \"prs\": [...],\n    \"fetchedAt\": \"2026-02-03T14:00:00Z\"\n  }\n}\n```\n\n**⚠️ If the widget has a `dataSchema`, the cache endpoint validates your data against it.** Bad data returns 400 with details. Always check the widget's schema before POSTing:\n\n```http\nGET \u002Fapi\u002Fwidgets\u002Fgithub-prs\n# Response includes dataSchema showing required fields and types\n```\n\n### Step 4: Browser Verification (REQUIRED)\n\n**⚠️ MANDATORY: Every widget creation and refresh MUST end with browser verification.**\n\nNever consider a widget \"done\" until you've visually confirmed it renders correctly on the dashboard.\n\n```javascript\n\u002F\u002F REQUIRED: Open dashboard and verify widget renders\nbrowser({ \n  action: 'open', \n  targetUrl: 'http:\u002F\u002Flocalhost:3333',\n  profile: 'openclaw'\n});\n\n\u002F\u002F Take a snapshot and check the widget\nbrowser({ action: 'snapshot' });\n\n\u002F\u002F Look for:\n\u002F\u002F 1. Widget is visible on the dashboard\n\u002F\u002F 2. Shows actual data, NOT \"Waiting for data...\" or loading spinner\n\u002F\u002F 3. Data values match what was pushed to cache\n\u002F\u002F 4. No error messages displayed\n\u002F\u002F 5. Layout looks correct (not broken\u002Foverlapping)\n```\n\n**Verification checklist (must ALL be true):**\n- [ ] Widget visible on dashboard grid\n- [ ] Title displays correctly\n- [ ] Data renders (not stuck on loading)\n- [ ] Values match cached data\n- [ ] No error states or broken layouts\n- [ ] \"Updated X ago\" footer shows recent timestamp\n\n**Common issues and fixes:**\n| Symptom | Cause | Fix |\n|---------|-------|-----|\n| \"Waiting for data...\" | Cache empty | POST data to `\u002Fapi\u002Fwidgets\u002F{slug}\u002Fcache` |\n| Widget not visible | Not added to dashboard | `POST \u002Fapi\u002Fwidgets\u002Finstances` |\n| Wrong\u002Fold data | Slug mismatch | Check slug matches between definition and cache POST |\n| Broken layout | Bad JSX in source_code | Check widget code for syntax errors |\n| \"No data\" after POST | Schema validation failed | Check data matches `data_schema` |\n\n**If verification fails, fix the issue before reporting success.**\n\n## Widget Code Template (agent_refresh)\n\nFor `agent_refresh` widgets, use `serverData` prop (NOT `useData` hook):\n\n```tsx\nfunction Widget({ serverData }) {\n  const data = serverData;\n  const loading = !serverData;\n  const error = serverData?.error;\n  \n  if (loading) return \u003CLoading message=\"Waiting for data...\" \u002F>;\n  if (error) return \u003CErrorDisplay message={error} \u002F>;\n  \n  \u002F\u002F NOTE: Do NOT wrap in \u003CCard> - the framework wrapper (CustomWidgetWrapper) \n  \u002F\u002F already provides the outer card with title, refresh button, and footer.\n  \u002F\u002F Just render your content directly.\n  return (\n    \u003Cdiv className=\"space-y-3\">\n      \u003CList items={data.prs?.map(pr => ({\n        title: pr.title,\n        subtitle: `#${pr.number} by ${pr.author}`,\n        badge: pr.state\n      })) || []} \u002F>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n**Important:** The widget wrapper (`CustomWidgetWrapper`) provides:\n- Outer `\u003CCard>` container with header (widget title)\n- Refresh button and \"Updated X ago\" footer\n- Loading\u002Ferror states\n\nYour widget code should just render the **content** — no Card, no CardHeader, no footer.\n\n**Key difference:** `agent_refresh` widgets receive data via `serverData` prop, NOT by calling `useData()`. The agent pushes data to `\u002Fapi\u002Fwidgets\u002F{slug}\u002Fcache`.\n\n## Server Code (Legacy Alternative)\n\n**Prefer `agent_refresh` over `server_code`.** Only use server_code when the widget MUST execute code at render time (rare).\n\n```javascript\n\u002F\u002F Only for fetch.type = \"server_code\" widgets\nconst token = await getCredential('github');\nconst response = await fetch('https:\u002F\u002Fapi.github.com\u002Frepos\u002Fowner\u002Frepo\u002Fpulls', {\n  headers: { 'Authorization': `Bearer ${token}` }\n});\nreturn await response.json();\n```\n\n**Available:** `fetch`, `getCredential(provider)`, `params`, `console`\n**Blocked:** `require`, `eval`, `fs`, `process`, `global`\n\n## Agent Refresh Contract\n\n**⚠️ CRITICAL: For `agent_refresh` widgets, YOU (the OpenClaw agent) are the data collector.**\n\nThis is NOT an external API or service. YOU must:\n1. Set up a **cron job to remind yourself** to collect data on a schedule\n2. **Use your own tools** (PTY, exec, browser, etc.) to gather the data\n3. **Parse the output** into structured JSON\n4. **POST to the cache endpoint** so the widget can display it\n\n### The Pattern\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│  Cron fires → Agent wakes up → Agent collects data →        │\n│  Agent POSTs to \u002Fcache → Widget displays fresh data         │\n└─────────────────────────────────────────────────────────────┘\n```\n\n### Step-by-Step for agent_refresh Widgets\n\n1. **Create the widget** with `fetch.type = \"agent_refresh\"` and detailed `fetch.instructions`\n2. **Set up a cron job** targeting YOUR main session (message is just the slug):\n   ```javascript\n   cron.add({\n     name: \"Widget: My Data Refresh\",\n     schedule: { kind: \"cron\", expr: \"*\u002F15 * * * *\" },\n     payload: { \n       kind: \"systemEvent\", \n       text: \"⚡ WIDGET REFRESH: my-widget\"  \u002F\u002F Just the slug!\n     },\n     sessionTarget: \"main\"  \u002F\u002F Reminds YOU, not an isolated session\n   })\n   ```\n3. **When you receive the refresh message**, look up `fetch.instructions` from the DB and spawn a subagent:\n   ```javascript\n   \u002F\u002F Parse slug from message\n   const slug = message.replace('⚡ WIDGET REFRESH:', '').trim();\n   \u002F\u002F Query widget's fetch.instructions\n   const widget = db.query('SELECT fetch FROM custom_widgets WHERE slug = ?', slug);\n   \u002F\u002F Spawn subagent with the instructions\n   sessions_spawn({ task: widget.fetch.instructions, model: 'haiku' });\n   ```\n4. **The subagent collects the data** using your tools:\n   - `exec` for shell commands\n   - PTY for interactive CLI tools (like `claude \u002Fstatus`)\n   - `browser` for web scraping\n   - API calls via `web_fetch`\n4. **POST the data to the cache:**\n   ```http\n   POST \u002Fapi\u002Fwidgets\u002F{slug}\u002Fcache\n   Content-Type: application\u002Fjson\n   \n   {\n     \"data\": {\n       \"myValue\": 42,\n       \"fetchedAt\": \"2026-02-03T18:30:00.000Z\"\n     }\n   }\n   ```\n\n### Writing Excellent fetch.instructions\n\nThe `fetch.instructions` field is the **single source of truth** for how to collect widget data. Write them clearly so any subagent can follow them.\n\n**Required sections:**\n```markdown\n## Data Collection\nExact commands to run with full paths and flags.\nInclude PTY requirements if interactive.\n\n## Data Transformation\nExact JSON structure expected.\nInclude field descriptions and examples.\n\n## Cache Update\nFull URL, required headers, body format.\n\n## Browser Verification\nConfirm the widget renders correctly.\n```\n\n**Good example:**\n```markdown\n## Data Collection\n```bash\ngog gmail search \"in:inbox\" --json\n```\n\n## Data Transformation\nTake first 5-8 emails, generate AI summary (3-5 words) for each:\n```json\n{\n  \"emails\": [{\"id\": \"...\", \"from\": \"...\", \"subject\": \"...\", \"summary\": \"AI summary here\", \"unread\": true}],\n  \"fetchedAt\": \"ISO timestamp\"\n}\n```\n\n## Cache Update\nPOST to: http:\u002F\u002Flocalhost:3333\u002Fapi\u002Fwidgets\u002Frecent-emails\u002Fcache\nHeader: Origin: http:\u002F\u002Flocalhost:3333\nBody: { \"data\": \u003Cobject above> }\n\n## Browser Verification  \nOpen http:\u002F\u002Flocalhost:3333 and confirm widget shows emails with AI summaries.\n```\n\n**Bad example (too vague):**\n```\nGet emails and post to cache.\n```\n\n### Real Example: Claude Max Usage Widget\n\nThis widget shows Claude CLI usage stats. The data comes from running `claude` in a PTY and navigating to `\u002Fstatus → Usage`.\n\n**The agent's job every 15 minutes:**\n```\n1. Spawn PTY: exec(\"claude\", { pty: true })\n2. Send: \"\u002Fstatus\" + Enter\n3. Navigate to Usage tab (Right arrow keys)\n4. Parse the output: Session %, Week %, Extra %\n5. POST to \u002Fapi\u002Fwidgets\u002Fclaude-code-usage\u002Fcache\n6. Kill the PTY session\n7. ⚠️ VERIFY: Open browser to http:\u002F\u002Flocalhost:3333 and confirm widget displays new data\n```\n\n**This is YOUR responsibility as the agent.** The widget just displays whatever data is in the cache.\n\n### Subagent Task Template for Refreshes\n\nWhen spawning subagents for widget refreshes, always include browser verification:\n\n```javascript\nsessions_spawn({\n  task: `${fetchInstructions}\n\n## REQUIRED: Browser Verification\nAfter posting to cache, verify the widget renders correctly:\n1. Open http:\u002F\u002Flocalhost:3333 in browser\n2. Find the widget on the dashboard\n3. Confirm it shows the data you just posted\n4. Report any rendering issues\n\nDo NOT report success until browser verification passes.`,\n  model: 'haiku',\n  label: `${slug}-refresh`\n});\n```\n\n### Cache Endpoint\n\n```http\nPOST \u002Fapi\u002Fwidgets\u002F{slug}\u002Fcache\nContent-Type: application\u002Fjson\n\n{\n  \"data\": {\n    \"packages\": 142,\n    \"fetchedAt\": \"2026-02-03T18:30:00.000Z\"\n  }\n}\n```\n\n### Immediate Refresh via Webhook\n\n**For `agent_refresh` widgets, users can trigger immediate refreshes via the UI refresh button.**\n\nWhen configured with `OPENCLAW_GATEWAY_URL` and `OPENCLAW_TOKEN` environment variables, clicking the refresh button will:\n1. Store a refresh request in the database (fallback for polling)\n2. **Immediately POST a wake notification to OpenClaw** via `\u002Fapi\u002Fsessions\u002Fwake`\n3. The agent receives a prompt to refresh that specific widget now\n\nThis eliminates the delay of waiting for the next heartbeat poll.\n\n**Environment variables** (add to `.env.local`):\n```bash\nOPENCLAW_GATEWAY_URL=http:\u002F\u002Flocalhost:18789\nOPENCLAW_TOKEN=your-gateway-token\n```\n\n**How it works:**\n1. User clicks refresh button on widget\n2. Glance POSTs to `\u002Fapi\u002Fwidgets\u002F{slug}\u002Frefresh`\n3. If webhook configured, Glance immediately notifies OpenClaw: `⚡ WIDGET REFRESH: Refresh the \"{slug}\" widget now and POST to cache`\n4. Agent wakes up, collects fresh data, POSTs to cache\n5. Widget re-renders with updated data\n\n**Response includes webhook status:**\n```json\n{\n  \"status\": \"refresh_requested\",\n  \"webhook_sent\": true,\n  \"fallback_queued\": true\n}\n```\n\nIf webhook fails or isn't configured, the DB fallback ensures the next heartbeat\u002Fpoll will pick it up.\n\n### Rules\n- **Always include `fetchedAt`** timestamp\n- **Don't overwrite on errors** - let widget use stale data\n- **Use main session cron** so YOU handle the collection, not an isolated agent\n```\n\n## Credential Requirements Format\n\n### Credential Types\n\n| Type | Storage | Description | Use For |\n|------|---------|-------------|---------|\n| `api_key` | Glance DB (encrypted) | API tokens stored in Glance | GitHub PAT, OpenWeather key |\n| `local_software` | Agent's machine | Software that must be installed | Homebrew, Docker |\n| `agent` | Agent environment | Auth that lives on the agent | `gh` CLI auth, `gcloud` auth |\n| `oauth` | Glance DB | OAuth tokens (future) | Google Calendar |\n\n### Examples\n\n```json\n{\n  \"credentials\": [\n    {\n      \"id\": \"github\",\n      \"type\": \"api_key\",\n      \"name\": \"GitHub Personal Access Token\",\n      \"description\": \"Token with repo scope\",\n      \"obtain_url\": \"https:\u002F\u002Fgithub.com\u002Fsettings\u002Ftokens\",\n      \"obtain_instructions\": \"Create token with 'repo' scope\"\n    },\n    {\n      \"id\": \"homebrew\",\n      \"type\": \"local_software\",\n      \"name\": \"Homebrew\",\n      \"check_command\": \"which brew\",\n      \"install_url\": \"https:\u002F\u002Fbrew.sh\"\n    },\n    {\n      \"id\": \"github_cli\",\n      \"type\": \"agent\",\n      \"name\": \"GitHub CLI\",\n      \"description\": \"Agent needs gh CLI authenticated to GitHub\",\n      \"agent_tool\": \"gh\",\n      \"agent_auth_check\": \"gh auth status\",\n      \"agent_auth_instructions\": \"Run `gh auth login` on the machine running OpenClaw\"\n    }\n  ]\n}\n```\n\n**When to use `agent` type:** Use for `agent_refresh` widgets where the agent collects data using CLI tools that have their own auth (like `gh`, `gcloud`, `aws`). These credentials aren't stored in Glance — they exist in the agent's environment.\n\n## Common Credential Providers\n\n| Provider | ID | Description |\n|----------|-----|-------------|\n| GitHub | `github` | GitHub API (PAT with repo scope) |\n| Anthropic | `anthropic` | Claude API (Admin key for usage) |\n| OpenAI | `openai` | GPT API (Admin key for usage) |\n| OpenWeather | `openweather` | Weather data API |\n| Linear | `linear` | Linear API |\n| Notion | `notion` | Notion API |\n\n## Export\u002FImport Packages\n\n### Export\n\n```http\nGET \u002Fapi\u002Fwidgets\u002F{slug}\u002Fexport\n```\n\nReturns: `{ \"package\": \"!GW1!eJxVj8EKwj...\" }`\n\n### Import\n\n```http\nPOST \u002Fapi\u002Fwidgets\u002Fimport\nContent-Type: application\u002Fjson\n\n{\n  \"package\": \"!GW1!eJxVj8EKwj...\",\n  \"dry_run\": false,\n  \"auto_add_to_dashboard\": true\n}\n```\n\nThe `!GW1!` prefix indicates Glance Widget v1 format (compressed base64 JSON).\n\n### Import Response with Cron\n\n```json\n{\n  \"valid\": true,\n  \"widget\": { \"id\": \"cw_abc\", \"slug\": \"homebrew-status\" },\n  \"cronSchedule\": {\n    \"expression\": \"*\u002F15 * * * *\",\n    \"instructions\": \"Run brew list...\",\n    \"slug\": \"homebrew-status\"\n  }\n}\n```\n\nWhen `cronSchedule` is returned, OpenClaw should register a cron job.\n\n## Key UI Components\n\n| Component | Use For |\n|-----------|---------|\n| `Card` | Widget container (always use `className=\"h-full\"`) |\n| `List` | Items with title\u002Fsubtitle\u002Fbadge |\n| `Stat` | Single metric with trend indicator |\n| `Progress` | Progress bars with variants |\n| `Badge` | Status labels (success\u002Fwarning\u002Ferror) |\n| `Stack` | Flexbox layout (row\u002Fcolumn) |\n| `Grid` | CSS Grid layout |\n| `Loading` | Loading spinner |\n| `ErrorDisplay` | Error with retry button |\n\nSee [references\u002Fcomponents.md](references\u002Fcomponents.md) for full props.\n\n## Hooks\n\n```tsx\n\u002F\u002F Fetch data (BOTH args required!)\nconst { data, loading, error, refresh } = useData('github', {});\nconst { data } = useData('github', { endpoint: '\u002Fpulls', params: { state: 'open' } });\n\n\u002F\u002F Get widget config\nconst config = useConfig();\n\n\u002F\u002F Widget-local state\nconst { state, setState } = useWidgetState('counter', 0);\n```\n\n**⚠️ `useData` requires both arguments.** Pass empty `{}` if no query needed.\n\n## Error Handling\n\n```tsx\nif (error?.code === 'CREDENTIAL_MISSING') {\n  return \u003CCard>\u003CCardContent>\n    \u003CIcons.Lock className=\"h-8 w-8\" \u002F>\n    \u003Cp>GitHub token required\u003C\u002Fp>\n  \u003C\u002FCardContent>\u003C\u002FCard>;\n}\n```\n\nError codes: `CREDENTIAL_MISSING`, `RATE_LIMITED`, `NETWORK_ERROR`, `API_ERROR`\n\n## Best Practices\n\n1. **Always check credentials before creating widgets**\n2. **Use meaningful names:** `github-prs-libra` not `widget-1`\n3. **Include fetchedAt in all data** for staleness tracking\n4. **Handle errors gracefully** with retry options\n5. **Confirm actions:** \"Done! Widget added to dashboard.\"\n6. **Size appropriately:** Lists 1x1, charts 2x2\n\n## Reading Dashboard Data\n\nTo summarize dashboard for user:\n\n```\n1. GET \u002Fapi\u002Fwidgets\u002Finstances → list instances\n2. For each: POST \u002Fapi\u002Fwidgets\u002F:slug\u002Fexecute\n3. Combine into natural language summary\n```\n\n---\n\n## ⚠️ Rules & Gotchas\n\n1. **Use JSON Schema for generation** — `docs\u002Fschemas\u002Fwidget-schema.json` enforces all required fields\n2. **Browser verify EVERYTHING** — don't report success until you see the widget render correctly\n3. **agent_refresh = YOU collect data** — the widget just displays what you POST to cache\n4. **fetch.instructions is the source of truth** — cron jobs just send the slug, you look up instructions\n5. **Always include fetchedAt** — widgets need timestamps for \"Updated X ago\" display\n6. **data_schema is REQUIRED** — cache POSTs validate against it, malformed data returns 400\n7. **credentials is REQUIRED** — use empty array `[]` if no credentials needed\n8. **Don't wrap in Card** — the framework provides the outer card, you render content only\n9. **Use Haiku for refresh subagents** — mechanical data collection doesn't need Opus\n10. **Mark refresh requests as processed** — `DELETE \u002Fapi\u002Fwidgets\u002F{slug}\u002Frefresh` after handling\n11. **Spawn subagents for refreshes** — don't block main session with PTY\u002Flong-running work\n\n## Environment Variables\n\n| Variable | Description | Example |\n|----------|-------------|---------|\n| `GLANCE_URL` | Glance server URL | `http:\u002F\u002Flocalhost:3333` |\n| `GLANCE_DATA` | Path to SQLite database | `\u002Ftmp\u002Fglance-test\u002Fdata` |\n| `OPENCLAW_GATEWAY_URL` | For webhook refresh notifications | `https:\u002F\u002Flocalhost:18789` |\n| `OPENCLAW_TOKEN` | Gateway auth token | `d551fe97...` |\n\n## Learnings (Feb 2026)\n\n- **Webhook refresh works** — Glance POSTs to OpenClaw gateway, agent wakes immediately\n- **Simple cron messages** — just `⚡ WIDGET REFRESH: {slug}`, agent looks up instructions\n- **AI summaries need AI** — for recent-emails, YOU generate the summaries, not some API\n- **icalBuddy for iCloud** — `gog calendar` doesn't work for iCloud, use `\u002Fopt\u002Fhomebrew\u002Fbin\u002FicalBuddy`\n- **wttr.in for weather** — free, no API key, JSON format: `wttr.in\u002FCity?format=j1`\n","# Glance\n\nAI-extensible personal dashboard. Create custom widgets with natural language — the AI handles data collection.\n\n## Features\n\n- **Custom Widgets** — Create widgets via AI with auto-generated JSX\n- **Agent Refresh** — AI collects data on schedule and pushes to cache\n- **Dashboard Export\u002FImport** — Share widget configurations\n- **Credential Management** — Secure API key storage\n- **Real-time Updates** — Webhook-triggered instant refreshes\n\n## Quick Start\n\n```bash\n# Navigate to skill directory (if installed via ClawHub)\ncd \"$(clawhub list | grep glance | awk '{print $2}')\"\n\n# Or clone directly\ngit clone https:\u002F\u002Fgithub.com\u002Facfranzen\u002Fglance ~\u002F.glance\ncd ~\u002F.glance\n\n# Install dependencies\nnpm install\n\n# Configure environment\ncp .env.example .env.local\n# Edit .env.local with your settings\n\n# Start development server\nnpm run dev\n\n# Or build and start production\nnpm run build && npm start\n```\n\nDashboard runs at **http:\u002F\u002Flocalhost:3333**\n\n## Configuration\n\nEdit `.env.local`:\n\n```bash\n# Server\nPORT=3333\nAUTH_TOKEN=your-secret-token        # Optional: Bearer token auth\n\n# OpenClaw Integration (for instant widget refresh)\nOPENCLAW_GATEWAY_URL=https:\u002F\u002Flocalhost:18789\nOPENCLAW_TOKEN=your-gateway-token\n\n# Database\nDATABASE_PATH=.\u002Fdata\u002Fglance.db      # SQLite database location\n```\n\n## Service Installation (macOS)\n\n```bash\n# Create launchd plist\ncat > ~\u002FLibrary\u002FLaunchAgents\u002Fcom.glance.dashboard.plist \u003C\u003C 'EOF'\n\u003C?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\u003C!DOCTYPE plist PUBLIC \"-\u002F\u002FApple\u002F\u002FDTD PLIST 1.0\u002F\u002FEN\" \"http:\u002F\u002Fwww.apple.com\u002FDTDs\u002FPropertyList-1.0.dtd\">\n\u003Cplist version=\"1.0\">\n\u003Cdict>\n    \u003Ckey>Label\u003C\u002Fkey>\n    \u003Cstring>com.glance.dashboard\u003C\u002Fstring>\n    \u003Ckey>ProgramArguments\u003C\u002Fkey>\n    \u003Carray>\n        \u003Cstring>\u002Fopt\u002Fhomebrew\u002Fbin\u002Fnpm\u003C\u002Fstring>\n        \u003Cstring>run\u003C\u002Fstring>\n        \u003Cstring>dev\u003C\u002Fstring>\n    \u003C\u002Farray>\n    \u003Ckey>WorkingDirectory\u003C\u002Fkey>\n    \u003Cstring>~\u002F.glance\u003C\u002Fstring>\n    \u003Ckey>RunAtLoad\u003C\u002Fkey>\n    \u003Ctrue\u002F>\n    \u003Ckey>KeepAlive\u003C\u002Fkey>\n    \u003Ctrue\u002F>\n    \u003Ckey>StandardOutPath\u003C\u002Fkey>\n    \u003Cstring>~\u002F.glance\u002Flogs\u002Fstdout.log\u003C\u002Fstring>\n    \u003Ckey>StandardErrorPath\u003C\u002Fkey>\n    \u003Cstring>~\u002F.glance\u002Flogs\u002Fstderr.log\u003C\u002Fstring>\n\u003C\u002Fdict>\n\u003C\u002Fplist>\nEOF\n\n# Load service\nmkdir -p ~\u002F.glance\u002Flogs\nlaunchctl load ~\u002FLibrary\u002FLaunchAgents\u002Fcom.glance.dashboard.plist\n\n# Service commands\nlaunchctl start com.glance.dashboard\nlaunchctl stop com.glance.dashboard\nlaunchctl unload ~\u002FLibrary\u002FLaunchAgents\u002Fcom.glance.dashboard.plist\n```\n\n## Environment Variables\n\n| Variable | Description | Default |\n|----------|-------------|---------|\n| `PORT` | Server port | `3333` |\n| `AUTH_TOKEN` | Bearer token for API auth | — |\n| `DATABASE_PATH` | SQLite database path | `.\u002Fdata\u002Fglance.db` |\n| `OPENCLAW_GATEWAY_URL` | OpenClaw gateway for webhooks | — |\n| `OPENCLAW_TOKEN` | OpenClaw auth token | — |\n\n## Requirements\n\n- Node.js 20+\n- npm or pnpm\n- SQLite (bundled)\n\n---\n\n# Widget Skill\n\nCreate and manage dashboard widgets. Most widgets use `agent_refresh` — **you** collect the data.\n\n## Quick Start\n\n```bash\n# Check Glance is running (list widgets)\ncurl -s -H \"Origin: $GLANCE_URL\" \"$GLANCE_URL\u002Fapi\u002Fwidgets\" | jq '.custom_widgets[].slug'\n\n# Auth note: Local requests with Origin header bypass Bearer token auth\n# For external access, use: -H \"Authorization: Bearer $GLANCE_TOKEN\"\n\n# Refresh a widget (look up instructions, collect data, POST to cache)\nsqlite3 $GLANCE_DATA\u002Fglance.db \"SELECT json_extract(fetch, '$.instructions') FROM custom_widgets WHERE slug = 'my-widget'\"\n# Follow the instructions, then:\ncurl -X POST \"$GLANCE_URL\u002Fapi\u002Fwidgets\u002Fmy-widget\u002Fcache\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -H \"Origin: $GLANCE_URL\" \\\n  -d '{\"data\": {\"value\": 42, \"fetchedAt\": \"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'\"}}'\n\n# Verify in browser\nbrowser action:open targetUrl:\"$GLANCE_URL\"\n```\n\n## AI Structured Output Generation (REQUIRED)\n\nWhen generating widget definitions, **use the JSON Schema** at `docs\u002Fschemas\u002Fwidget-schema.json` with your AI model's structured output mode:\n- **Anthropic**: Use `tool_use` with the schema\n- **OpenAI**: Use `response_format: { type: \"json_schema\", schema }`\n\nThe schema enforces all required fields at generation time — malformed widgets cannot be produced.\n\n### Required Fields Checklist\nEvery widget **MUST** have these fields (the schema enforces them):\n\n| Field | Type | Notes |\n|-------|------|-------|\n| `name` | string | Non-empty, human-readable |\n| `slug` | string | Lowercase kebab-case (`my-widget`) |\n| `source_code` | string | Valid JSX with Widget function |\n| `default_size` | `{ w: 1-12, h: 1-20 }` | Grid units |\n| `min_size` | `{ w: 1-12, h: 1-20 }` | Cannot resize smaller |\n| `fetch.type` | enum | `\"server_code\"` \\| `\"webhook\"` \\| `\"agent_refresh\"` |\n| `fetch.instructions` | string | **REQUIRED if type is `agent_refresh`** |\n| `fetch.schedule` | string | **REQUIRED if type is `agent_refresh`** (cron) |\n| `data_schema.type` | `\"object\"` | Always object |\n| `data_schema.properties` | object | Define each field |\n| `data_schema.required` | array | **MUST include `\"fetchedAt\"`** |\n| `credentials` | array | Use `[]` if none needed |\n\n### Example: Minimal Valid Widget\n\n```json\n{\n  \"name\": \"My Widget\",\n  \"slug\": \"my-widget\",\n  \"source_code\": \"function Widget({ serverData }) { return \u003Cdiv>{serverData?.value}\u003C\u002Fdiv>; }\",\n  \"default_size\": { \"w\": 2, \"h\": 2 },\n  \"min_size\": { \"w\": 1, \"h\": 1 },\n  \"fetch\": {\n    \"type\": \"agent_refresh\",\n    \"schedule\": \"*\u002F15 * * * *\",\n    \"instructions\": \"## Data Collection\\nCollect the data...\\n\\n## Cache Update\\nPOST to \u002Fapi\u002Fwidgets\u002Fmy-widget\u002Fcache\"\n  },\n  \"data_schema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"value\": { \"type\": \"number\" },\n      \"fetchedAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n    },\n    \"required\": [\"value\", \"fetchedAt\"]\n  },\n  \"credentials\": []\n}\n```\n\n---\n\n## ⚠️ Widget Creation Checklist (MANDATORY)\n\nEvery widget must complete ALL steps before being considered done:\n\n```\n□ Step 1: Create widget definition (POST \u002Fapi\u002Fwidgets)\n    - source_code with Widget function\n    - data_schema (REQUIRED for validation)\n    - fetch config (type + instructions for agent_refresh)\n    \n□ Step 2: Add to dashboard (POST \u002Fapi\u002Fwidgets\u002Finstances)\n    - custom_widget_id matches definition\n    - title and config set\n    \n□ Step 3: Populate cache (for agent_refresh widgets)\n    - Data matches data_schema exactly\n    - Includes fetchedAt timestamp\n    \n□ Step 4: Set up cron job (for agent_refresh widgets)\n    - Simple message: \"⚡ WIDGET REFRESH: {slug}\"\n    - Appropriate schedule (*\u002F15 or *\u002F30 typically)\n    \n□ Step 5: BROWSER VERIFICATION (MANDATORY)\n    - Open http:\u002F\u002Flocalhost:3333\n    - Widget is visible on dashboard\n    - Shows actual data (not loading spinner)\n    - Data values match what was cached\n    - No errors or broken layouts\n    \n⛔ DO NOT report widget as complete until Step 5 passes!\n```\n\n## Quick Reference\n\n- **Full SDK docs:** See `docs\u002Fwidget-sdk.md` in the Glance repo\n- **Component list:** See [references\u002Fcomponents.md](references\u002Fcomponents.md)\n\n## Widget Package Structure\n\n```\nWidget Package\n├── meta (name, slug, description, author, version)\n├── widget (source_code, default_size, min_size)\n├── fetch (server_code | webhook | agent_refresh)\n├── dataSchema? (JSON Schema for cached data - validates on POST)\n├── cache (ttl, staleness, fallback)\n├── credentials[] (API keys, local software requirements)\n├── config_schema? (user options)\n└── error? (retry, fallback, timeout)\n```\n\n## Fetch Type Decision Tree\n\n```\nIs data available via API that the widget can call?\n├── YES → Use server_code\n└── NO → Does an external service push data?\n    ├── YES → Use webhook\n    └── NO → Use agent_refresh (YOU collect it)\n```\n\n| Scenario | Fetch Type | Who Collects Data? |\n|----------|-----------|-------------------|\n| Public\u002Fauthenticated API | `server_code` | Widget calls API at render |\n| External service pushes data | `webhook` | External service POSTs to cache |\n| **Local CLI tools** | `agent_refresh` | **YOU (the agent) via PTY\u002Fexec** |\n| **Interactive terminals** | `agent_refresh` | **YOU (the agent) via PTY** |\n| **Computed\u002Faggregated data** | `agent_refresh` | **YOU (the agent) on a schedule** |\n\n**⚠️ `agent_refresh` means YOU are the data source.** You set up a cron to remind yourself, then YOU collect the data using your tools (exec, PTY, browser, etc.) and POST it to the cache.\n\n## API Endpoints\n\n### Widget Definitions\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| `POST` | `\u002Fapi\u002Fwidgets` | Create widget definition |\n| `GET` | `\u002Fapi\u002Fwidgets` | List all definitions |\n| `GET` | `\u002Fapi\u002Fwidgets\u002F:slug` | Get single definition |\n| `PATCH` | `\u002Fapi\u002Fwidgets\u002F:slug` | Update definition |\n| `DELETE` | `\u002Fapi\u002Fwidgets\u002F:slug` | Delete definition |\n\n### Widget Instances (Dashboard)\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| `POST` | `\u002Fapi\u002Fwidgets\u002Finstances` | Add widget to dashboard |\n| `GET` | `\u002Fapi\u002Fwidgets\u002Finstances` | List dashboard widgets |\n| `PATCH` | `\u002Fapi\u002Fwidgets\u002Finstances\u002F:id` | Update instance (config, position) |\n| `DELETE` | `\u002Fapi\u002Fwidgets\u002Finstances\u002F:id` | Remove from dashboard |\n\n### Credentials\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| `GET` | `\u002Fapi\u002Fcredentials` | List credentials + status |\n| `POST` | `\u002Fapi\u002Fcredentials` | Store credential |\n| `DELETE` | `\u002Fapi\u002Fcredentials\u002F:id` | Delete credential |\n\n## Creating a Widget\n\n### Full Widget Package Structure\n\n```json\n{\n  \"name\": \"GitHub PRs\",\n  \"slug\": \"github-prs\",\n  \"description\": \"Shows open pull requests\",\n  \n  \"source_code\": \"function Widget({ serverData }) { ... }\",\n  \"default_size\": { \"w\": 2, \"h\": 2 },\n  \"min_size\": { \"w\": 1, \"h\": 1 },\n  \"refresh_interval\": 300,\n  \n  \"credentials\": [\n    {\n      \"id\": \"github\",\n      \"type\": \"api_key\",\n      \"name\": \"GitHub Personal Access Token\",\n      \"description\": \"Token with repo scope\",\n      \"obtain_url\": \"https:\u002F\u002Fgithub.com\u002Fsettings\u002Ftokens\"\n    }\n  ],\n  \n  \"fetch\": {\n    \"type\": \"agent_refresh\",\n    \"schedule\": \"*\u002F5 * * * *\",\n    \"instructions\": \"Fetch open PRs from GitHub API and POST to cache endpoint\",\n    \"expected_freshness_seconds\": 300,\n    \"max_staleness_seconds\": 900\n  },\n  \n  \"cache\": {\n    \"ttl_seconds\": 300,\n    \"max_staleness_seconds\": 900,\n    \"storage\": \"sqlite\",\n    \"on_error\": \"use_stale\"\n  },\n  \n  \"setup\": {\n    \"description\": \"Configure GitHub token\",\n    \"agent_skill\": \"Store GitHub PAT via \u002Fapi\u002Fcredentials\",\n    \"verification\": {\n      \"type\": \"cache_populated\",\n      \"target\": \"github-prs\"\n    },\n    \"idempotent\": true\n  }\n}\n```\n\n### Fetch Types\n\n| Type | When to Use | Data Flow |\n|------|-------------|-----------|\n| `server_code` | Widget can call API directly | Widget → server_code → API |\n| `agent_refresh` | Agent must fetch\u002Fcompute data | Agent → POST \u002Fcache → Widget reads |\n| `webhook` | External service pushes data | External → POST \u002Fcache → Widget reads |\n\n**Most widgets should use `agent_refresh`** — the agent fetches data on a schedule and pushes to the cache endpoint.\n\n### Step 1: Create Widget Definition\n\n```http\nPOST \u002Fapi\u002Fwidgets\nContent-Type: application\u002Fjson\n\n{\n  \"name\": \"GitHub PRs\",\n  \"slug\": \"github-prs\",\n  \"description\": \"Shows open pull requests\",\n  \"source_code\": \"function Widget({ serverData }) { ... }\",\n  \"default_size\": { \"w\": 2, \"h\": 2 },\n  \"credentials\": [...],\n  \"fetch\": { \"type\": \"agent_refresh\", \"schedule\": \"*\u002F5 * * * *\", ... },\n  \"data_schema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"prs\": { \"type\": \"array\", \"description\": \"List of PR objects\" },\n      \"fetchedAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n    },\n    \"required\": [\"prs\", \"fetchedAt\"]\n  },\n  \"cache\": { \"ttl_seconds\": 300, ... }\n}\n```\n\n**`data_schema` (REQUIRED)** defines the data contract between the fetcher and the widget. Cache POSTs are validated against it — malformed data returns 400.\n\n> ⚠️ **Always include `data_schema`** when creating widgets. This ensures:\n> 1. Data validation on cache POSTs (400 on schema mismatch)\n> 2. Clear documentation of expected data structure\n> 3. AI agents know the exact format to produce\n\n### Step 2: Add to Dashboard\n\n```http\nPOST \u002Fapi\u002Fwidgets\u002Finstances\nContent-Type: application\u002Fjson\n\n{\n  \"type\": \"custom\",\n  \"title\": \"GitHub PRs\",\n  \"custom_widget_id\": \"cw_abc123\",\n  \"config\": { \"owner\": \"acfranzen\", \"repo\": \"libra\" }\n}\n```\n\n### Step 3: Populate Cache (for agent_refresh)\n\n```http\nPOST \u002Fapi\u002Fwidgets\u002Fgithub-prs\u002Fcache\nContent-Type: application\u002Fjson\n\n{\n  \"data\": {\n    \"prs\": [...],\n    \"fetchedAt\": \"2026-02-03T14:00:00Z\"\n  }\n}\n```\n\n**⚠️ If the widget has a `dataSchema`, the cache endpoint validates your data against it.** Bad data returns 400 with details. Always check the widget's schema before POSTing:\n\n```http\nGET \u002Fapi\u002Fwidgets\u002Fgithub-prs\n# Response includes dataSchema showing required fields and types\n```\n\n### Step 4: Browser Verification (REQUIRED)\n\n**⚠️ MANDATORY: Every widget creation and refresh MUST end with browser verification.**\n\nNever consider a widget \"done\" until you've visually confirmed it renders correctly on the dashboard.\n\n```javascript\n\u002F\u002F REQUIRED: Open dashboard and verify widget renders\nbrowser({ \n  action: 'open', \n  targetUrl: 'http:\u002F\u002Flocalhost:3333',\n  profile: 'openclaw'\n});\n\n\u002F\u002F Take a snapshot and check the widget\nbrowser({ action: 'snapshot' });\n\n\u002F\u002F Look for:\n\u002F\u002F 1. Widget is visible on the dashboard\n\u002F\u002F 2. Shows actual data, NOT \"Waiting for data...\" or loading spinner\n\u002F\u002F 3. Data values match what was pushed to cache\n\u002F\u002F 4. No error messages displayed\n\u002F\u002F 5. Layout looks correct (not broken\u002Foverlapping)\n```\n\n**Verification checklist (must ALL be true):**\n- [ ] Widget visible on dashboard grid\n- [ ] Title displays correctly\n- [ ] Data renders (not stuck on loading)\n- [ ] Values match cached data\n- [ ] No error states or broken layouts\n- [ ] \"Updated X ago\" footer shows recent timestamp\n\n**Common issues and fixes:**\n| Symptom | Cause | Fix |\n|---------|-------|-----|\n| \"Waiting for data...\" | Cache empty | POST data to `\u002Fapi\u002Fwidgets\u002F{slug}\u002Fcache` |\n| Widget not visible | Not added to dashboard | `POST \u002Fapi\u002Fwidgets\u002Finstances` |\n| Wrong\u002Fold data | Slug mismatch | Check slug matches between definition and cache POST |\n| Broken layout | Bad JSX in source_code | Check widget code for syntax errors |\n| \"No data\" after POST | Schema validation failed | Check data matches `data_schema` |\n\n**If verification fails, fix the issue before reporting success.**\n\n## Widget Code Template (agent_refresh)\n\nFor `agent_refresh` widgets, use `serverData` prop (NOT `useData` hook):\n\n```tsx\nfunction Widget({ serverData }) {\n  const data = serverData;\n  const loading = !serverData;\n  const error = serverData?.error;\n  \n  if (loading) return \u003CLoading message=\"Waiting for data...\" \u002F>;\n  if (error) return \u003CErrorDisplay message={error} \u002F>;\n  \n  \u002F\u002F NOTE: Do NOT wrap in \u003CCard> - the framework wrapper (CustomWidgetWrapper) \n  \u002F\u002F already provides the outer card with title, refresh button, and footer.\n  \u002F\u002F Just render your content directly.\n  return (\n    \u003Cdiv className=\"space-y-3\">\n      \u003CList items={data.prs?.map(pr => ({\n        title: pr.title,\n        subtitle: `#${pr.number} by ${pr.author}`,\n        badge: pr.state\n      })) || []} \u002F>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n**Important:** The widget wrapper (`CustomWidgetWrapper`) provides:\n- Outer `\u003CCard>` container with header (widget title)\n- Refresh button and \"Updated X ago\" footer\n- Loading\u002Ferror states\n\nYour widget code should just render the **content** — no Card, no CardHeader, no footer.\n\n**Key difference:** `agent_refresh` widgets receive data via `serverData` prop, NOT by calling `useData()`. The agent pushes data to `\u002Fapi\u002Fwidgets\u002F{slug}\u002Fcache`.\n\n## Server Code (Legacy Alternative)\n\n**Prefer `agent_refresh` over `server_code`.** Only use server_code when the widget MUST execute code at render time (rare).\n\n```javascript\n\u002F\u002F Only for fetch.type = \"server_code\" widgets\nconst token = await getCredential('github');\nconst response = await fetch('https:\u002F\u002Fapi.github.com\u002Frepos\u002Fowner\u002Frepo\u002Fpulls', {\n  headers: { 'Authorization': `Bearer ${token}` }\n});\nreturn await response.json();\n```\n\n**Available:** `fetch`, `getCredential(provider)`, `params`, `console`\n**Blocked:** `require`, `eval`, `fs`, `process`, `global`\n\n## Agent Refresh Contract\n\n**⚠️ CRITICAL: For `agent_refresh` widgets, YOU (the OpenClaw agent) are the data collector.**\n\nThis is NOT an external API or service. YOU must:\n1. Set up a **cron job to remind yourself** to collect data on a schedule\n2. **Use your own tools** (PTY, exec, browser, etc.) to gather the data\n3. **Parse the output** into structured JSON\n4. **POST to the cache endpoint** so the widget can display it\n\n### The Pattern\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│  Cron fires → Agent wakes up → Agent collects data →        │\n│  Agent POSTs to \u002Fcache → Widget displays fresh data         │\n└─────────────────────────────────────────────────────────────┘\n```\n\n### Step-by-Step for agent_refresh Widgets\n\n1. **Create the widget** with `fetch.type = \"agent_refresh\"` and detailed `fetch.instructions`\n2. **Set up a cron job** targeting YOUR main session (message is just the slug):\n   ```javascript\n   cron.add({\n     name: \"Widget: My Data Refresh\",\n     schedule: { kind: \"cron\", expr: \"*\u002F15 * * * *\" },\n     payload: { \n       kind: \"systemEvent\", \n       text: \"⚡ WIDGET REFRESH: my-widget\"  \u002F\u002F Just the slug!\n     },\n     sessionTarget: \"main\"  \u002F\u002F Reminds YOU, not an isolated session\n   })\n   ```\n3. **When you receive the refresh message**, look up `fetch.instructions` from the DB and spawn a subagent:\n   ```javascript\n   \u002F\u002F Parse slug from message\n   const slug = message.replace('⚡ WIDGET REFRESH:', '').trim();\n   \u002F\u002F Query widget's fetch.instructions\n   const widget = db.query('SELECT fetch FROM custom_widgets WHERE slug = ?', slug);\n   \u002F\u002F Spawn subagent with the instructions\n   sessions_spawn({ task: widget.fetch.instructions, model: 'haiku' });\n   ```\n4. **The subagent collects the data** using your tools:\n   - `exec` for shell commands\n   - PTY for interactive CLI tools (like `claude \u002Fstatus`)\n   - `browser` for web scraping\n   - API calls via `web_fetch`\n4. **POST the data to the cache:**\n   ```http\n   POST \u002Fapi\u002Fwidgets\u002F{slug}\u002Fcache\n   Content-Type: application\u002Fjson\n   \n   {\n     \"data\": {\n       \"myValue\": 42,\n       \"fetchedAt\": \"2026-02-03T18:30:00.000Z\"\n     }\n   }\n   ```\n\n### Writing Excellent fetch.instructions\n\nThe `fetch.instructions` field is the **single source of truth** for how to collect widget data. Write them clearly so any subagent can follow them.\n\n**Required sections:**\n```markdown\n## Data Collection\nExact commands to run with full paths and flags.\nInclude PTY requirements if interactive.\n\n## Data Transformation\nExact JSON structure expected.\nInclude field descriptions and examples.\n\n## Cache Update\nFull URL, required headers, body format.\n\n## Browser Verification\nConfirm the widget renders correctly.\n```\n\n**Good example:**\n```markdown\n## Data Collection\n```bash\ngog gmail search \"in:inbox\" --json\n```\n\n## Data Transformation\nTake first 5-8 emails, generate AI summary (3-5 words) for each:\n```json\n{\n  \"emails\": [{\"id\": \"...\", \"from\": \"...\", \"subject\": \"...\", \"summary\": \"AI summary here\", \"unread\": true}],\n  \"fetchedAt\": \"ISO timestamp\"\n}\n```\n\n## Cache Update\nPOST to: http:\u002F\u002Flocalhost:3333\u002Fapi\u002Fwidgets\u002Frecent-emails\u002Fcache\nHeader: Origin: http:\u002F\u002Flocalhost:3333\nBody: { \"data\": \u003Cobject above> }\n\n## Browser Verification  \nOpen http:\u002F\u002Flocalhost:3333 and confirm widget shows emails with AI summaries.\n```\n\n**Bad example (too vague):**\n```\nGet emails and post to cache.\n```\n\n### Real Example: Claude Max Usage Widget\n\nThis widget shows Claude CLI usage stats. The data comes from running `claude` in a PTY and navigating to `\u002Fstatus → Usage`.\n\n**The agent's job every 15 minutes:**\n```\n1. Spawn PTY: exec(\"claude\", { pty: true })\n2. Send: \"\u002Fstatus\" + Enter\n3. Navigate to Usage tab (Right arrow keys)\n4. Parse the output: Session %, Week %, Extra %\n5. POST to \u002Fapi\u002Fwidgets\u002Fclaude-code-usage\u002Fcache\n6. Kill the PTY session\n7. ⚠️ VERIFY: Open browser to http:\u002F\u002Flocalhost:3333 and confirm widget displays new data\n```\n\n**This is YOUR responsibility as the agent.** The widget just displays whatever data is in the cache.\n\n### Subagent Task Template for Refreshes\n\nWhen spawning subagents for widget refreshes, always include browser verification:\n\n```javascript\nsessions_spawn({\n  task: `${fetchInstructions}\n\n## REQUIRED: Browser Verification\nAfter posting to cache, verify the widget renders correctly:\n1. Open http:\u002F\u002Flocalhost:3333 in browser\n2. Find the widget on the dashboard\n3. Confirm it shows the data you just posted\n4. Report any rendering issues\n\nDo NOT report success until browser verification passes.`,\n  model: 'haiku',\n  label: `${slug}-refresh`\n});\n```\n\n### Cache Endpoint\n\n```http\nPOST \u002Fapi\u002Fwidgets\u002F{slug}\u002Fcache\nContent-Type: application\u002Fjson\n\n{\n  \"data\": {\n    \"packages\": 142,\n    \"fetchedAt\": \"2026-02-03T18:30:00.000Z\"\n  }\n}\n```\n\n### Immediate Refresh via Webhook\n\n**For `agent_refresh` widgets, users can trigger immediate refreshes via the UI refresh button.**\n\nWhen configured with `OPENCLAW_GATEWAY_URL` and `OPENCLAW_TOKEN` environment variables, clicking the refresh button will:\n1. Store a refresh request in the database (fallback for polling)\n2. **Immediately POST a wake notification to OpenClaw** via `\u002Fapi\u002Fsessions\u002Fwake`\n3. The agent receives a prompt to refresh that specific widget now\n\nThis eliminates the delay of waiting for the next heartbeat poll.\n\n**Environment variables** (add to `.env.local`):\n```bash\nOPENCLAW_GATEWAY_URL=http:\u002F\u002Flocalhost:18789\nOPENCLAW_TOKEN=your-gateway-token\n```\n\n**How it works:**\n1. User clicks refresh button on widget\n2. Glance POSTs to `\u002Fapi\u002Fwidgets\u002F{slug}\u002Frefresh`\n3. If webhook configured, Glance immediately notifies OpenClaw: `⚡ WIDGET REFRESH: Refresh the \"{slug}\" widget now and POST to cache`\n4. Agent wakes up, collects fresh data, POSTs to cache\n5. Widget re-renders with updated data\n\n**Response includes webhook status:**\n```json\n{\n  \"status\": \"refresh_requested\",\n  \"webhook_sent\": true,\n  \"fallback_queued\": true\n}\n```\n\nIf webhook fails or isn't configured, the DB fallback ensures the next heartbeat\u002Fpoll will pick it up.\n\n### Rules\n- **Always include `fetchedAt`** timestamp\n- **Don't overwrite on errors** - let widget use stale data\n- **Use main session cron** so YOU handle the collection, not an isolated agent\n```\n\n## Credential Requirements Format\n\n### Credential Types\n\n| Type | Storage | Description | Use For |\n|------|---------|-------------|---------|\n| `api_key` | Glance DB (encrypted) | API tokens stored in Glance | GitHub PAT, OpenWeather key |\n| `local_software` | Agent's machine | Software that must be installed | Homebrew, Docker |\n| `agent` | Agent environment | Auth that lives on the agent | `gh` CLI auth, `gcloud` auth |\n| `oauth` | Glance DB | OAuth tokens (future) | Google Calendar |\n\n### Examples\n\n```json\n{\n  \"credentials\": [\n    {\n      \"id\": \"github\",\n      \"type\": \"api_key\",\n      \"name\": \"GitHub Personal Access Token\",\n      \"description\": \"Token with repo scope\",\n      \"obtain_url\": \"https:\u002F\u002Fgithub.com\u002Fsettings\u002Ftokens\",\n      \"obtain_instructions\": \"Create token with 'repo' scope\"\n    },\n    {\n      \"id\": \"homebrew\",\n      \"type\": \"local_software\",\n      \"name\": \"Homebrew\",\n      \"check_command\": \"which brew\",\n      \"install_url\": \"https:\u002F\u002Fbrew.sh\"\n    },\n    {\n      \"id\": \"github_cli\",\n      \"type\": \"agent\",\n      \"name\": \"GitHub CLI\",\n      \"description\": \"Agent needs gh CLI authenticated to GitHub\",\n      \"agent_tool\": \"gh\",\n      \"agent_auth_check\": \"gh auth status\",\n      \"agent_auth_instructions\": \"Run `gh auth login` on the machine running OpenClaw\"\n    }\n  ]\n}\n```\n\n**When to use `agent` type:** Use for `agent_refresh` widgets where the agent collects data using CLI tools that have their own auth (like `gh`, `gcloud`, `aws`). These credentials aren't stored in Glance — they exist in the agent's environment.\n\n## Common Credential Providers\n\n| Provider | ID | Description |\n|----------|-----|-------------|\n| GitHub | `github` | GitHub API (PAT with repo scope) |\n| Anthropic | `anthropic` | Claude API (Admin key for usage) |\n| OpenAI | `openai` | GPT API (Admin key for usage) |\n| OpenWeather | `openweather` | Weather data API |\n| Linear | `linear` | Linear API |\n| Notion | `notion` | Notion API |\n\n## Export\u002FImport Packages\n\n### Export\n\n```http\nGET \u002Fapi\u002Fwidgets\u002F{slug}\u002Fexport\n```\n\nReturns: `{ \"package\": \"!GW1!eJxVj8EKwj...\" }`\n\n### Import\n\n```http\nPOST \u002Fapi\u002Fwidgets\u002Fimport\nContent-Type: application\u002Fjson\n\n{\n  \"package\": \"!GW1!eJxVj8EKwj...\",\n  \"dry_run\": false,\n  \"auto_add_to_dashboard\": true\n}\n```\n\nThe `!GW1!` prefix indicates Glance Widget v1 format (compressed base64 JSON).\n\n### Import Response with Cron\n\n```json\n{\n  \"valid\": true,\n  \"widget\": { \"id\": \"cw_abc\", \"slug\": \"homebrew-status\" },\n  \"cronSchedule\": {\n    \"expression\": \"*\u002F15 * * * *\",\n    \"instructions\": \"Run brew list...\",\n    \"slug\": \"homebrew-status\"\n  }\n}\n```\n\nWhen `cronSchedule` is returned, OpenClaw should register a cron job.\n\n## Key UI Components\n\n| Component | Use For |\n|-----------|---------|\n| `Card` | Widget container (always use `className=\"h-full\"`) |\n| `List` | Items with title\u002Fsubtitle\u002Fbadge |\n| `Stat` | Single metric with trend indicator |\n| `Progress` | Progress bars with variants |\n| `Badge` | Status labels (success\u002Fwarning\u002Ferror) |\n| `Stack` | Flexbox layout (row\u002Fcolumn) |\n| `Grid` | CSS Grid layout |\n| `Loading` | Loading spinner |\n| `ErrorDisplay` | Error with retry button |\n\nSee [references\u002Fcomponents.md](references\u002Fcomponents.md) for full props.\n\n## Hooks\n\n```tsx\n\u002F\u002F Fetch data (BOTH args required!)\nconst { data, loading, error, refresh } = useData('github', {});\nconst { data } = useData('github', { endpoint: '\u002Fpulls', params: { state: 'open' } });\n\n\u002F\u002F Get widget config\nconst config = useConfig();\n\n\u002F\u002F Widget-local state\nconst { state, setState } = useWidgetState('counter', 0);\n```\n\n**⚠️ `useData` requires both arguments.** Pass empty `{}` if no query needed.\n\n## Error Handling\n\n```tsx\nif (error?.code === 'CREDENTIAL_MISSING') {\n  return \u003CCard>\u003CCardContent>\n    \u003CIcons.Lock className=\"h-8 w-8\" \u002F>\n    \u003Cp>GitHub token required\u003C\u002Fp>\n  \u003C\u002FCardContent>\u003C\u002FCard>;\n}\n```\n\nError codes: `CREDENTIAL_MISSING`, `RATE_LIMITED`, `NETWORK_ERROR`, `API_ERROR`\n\n## Best Practices\n\n1. **Always check credentials before creating widgets**\n2. **Use meaningful names:** `github-prs-libra` not `widget-1`\n3. **Include fetchedAt in all data** for staleness tracking\n4. **Handle errors gracefully** with retry options\n5. **Confirm actions:** \"Done! Widget added to dashboard.\"\n6. **Size appropriately:** Lists 1x1, charts 2x2\n\n## Reading Dashboard Data\n\nTo summarize dashboard for user:\n\n```\n1. GET \u002Fapi\u002Fwidgets\u002Finstances → list instances\n2. For each: POST \u002Fapi\u002Fwidgets\u002F:slug\u002Fexecute\n3. Combine into natural language summary\n```\n\n---\n\n## ⚠️ Rules & Gotchas\n\n1. **Use JSON Schema for generation** — `docs\u002Fschemas\u002Fwidget-schema.json` enforces all required fields\n2. **Browser verify EVERYTHING** — don't report success until you see the widget render correctly\n3. **agent_refresh = YOU collect data** — the widget just displays what you POST to cache\n4. **fetch.instructions is the source of truth** — cron jobs just send the slug, you look up instructions\n5. **Always include fetchedAt** — widgets need timestamps for \"Updated X ago\" display\n6. **data_schema is REQUIRED** — cache POSTs validate against it, malformed data returns 400\n7. **credentials is REQUIRED** — use empty array `[]` if no credentials needed\n8. **Don't wrap in Card** — the framework provides the outer card, you render content only\n9. **Use Haiku for refresh subagents** — mechanical data collection doesn't need Opus\n10. **Mark refresh requests as processed** — `DELETE \u002Fapi\u002Fwidgets\u002F{slug}\u002Frefresh` after handling\n11. **Spawn subagents for refreshes** — don't block main session with PTY\u002Flong-running work\n\n## Environment Variables\n\n| Variable | Description | Example |\n|----------|-------------|---------|\n| `GLANCE_URL` | Glance server URL | `http:\u002F\u002Flocalhost:3333` |\n| `GLANCE_DATA` | Path to SQLite database | `\u002Ftmp\u002Fglance-test\u002Fdata` |\n| `OPENCLAW_GATEWAY_URL` | For webhook refresh notifications | `https:\u002F\u002Flocalhost:18789` |\n| `OPENCLAW_TOKEN` | Gateway auth token | `d551fe97...` |\n\n## Learnings (Feb 2026)\n\n- **Webhook refresh works** — Glance POSTs to OpenClaw gateway, agent wakes immediately\n- **Simple cron messages** — just `⚡ WIDGET REFRESH: {slug}`, agent looks up instructions\n- **AI summaries need AI** — for recent-emails, YOU generate the summaries, not some API\n- **icalBuddy for iCloud** — `gog calendar` doesn't work for iCloud, use `\u002Fopt\u002Fhomebrew\u002Fbin\u002FicalBuddy`\n- **wttr.in for weather** — free, no API key, JSON format: `wttr.in\u002FCity?format=j1`\n",{"name":5,"metadata":54,"description":7},{"openclaw":55},{"emoji":56,"homepage":57,"requires":58,"primaryEnv":60},"?️","https:\u002F\u002Fgithub.com\u002Facfranzen\u002Fglance",{"env":59,"bins":61},[60],"GLANCE_URL",[44],"main","e7e336ac628c3e3bd771aff1aaa469c93778f94a","skills\u002Facfranzen\u002Fglance",0,"https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del","2026-08-23 22:39:37",{"source":8,"skillName":5,"commands":70,"prompt":80,"zipAvailable":81},[71,74,77],{"tool":72,"command":73},"npx","npx skills add kbarbel640-del\u002Fskills --skill glance",{"tool":75,"command":76},"bunx","bunx skills add kbarbel640-del\u002Fskills --skill glance",{"tool":78,"command":79},"pnpm","pnpm dlx skills add kbarbel640-del\u002Fskills --skill glance","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Facfranzen\u002Fglance\u002FSKILL.md",true,[83,89,93,97],{"path":84,"resourceType":85,"resourceTypeDesc":86,"sizeBytes":87,"sourceHtmlUrl":88},"_meta.json","REFERENCE","参考文档",267,"https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Facfranzen\u002Fglance\u002F_meta.json",{"path":90,"resourceType":85,"resourceTypeDesc":86,"sizeBytes":91,"sourceHtmlUrl":92},"dashboard-api.md",8015,"https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Facfranzen\u002Fglance\u002Fdashboard-api.md",{"path":94,"resourceType":85,"resourceTypeDesc":86,"sizeBytes":95,"sourceHtmlUrl":96},"README.md",13846,"https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Facfranzen\u002Fglance\u002FREADME.md",{"path":98,"resourceType":85,"resourceTypeDesc":86,"sizeBytes":99,"sourceHtmlUrl":100},"widget-sdk.md",78928,"https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Facfranzen\u002Fglance\u002Fwidget-sdk.md",[102,129,154,175,181,201],{"skillNo":103,"name":104,"nameZh":6,"description":105,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":106,"categories":107,"keywords":111,"publishedAt":49},"01M6G8J55967D6281ACMKAKTEQ","crowd-prompting","A marketplace where AI agents improve prompts, system instructions, tool descriptions, and other text-based content with domain expertise from real-world operations — and earn tokens for valuable contributions.","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzzadrian\u002Fcrowd-prompting\u002FSKILL.md",[108],{"categoryNo":109,"categoryName":110,"taxonomyType":28,"taxonomyTypeDesc":29},"01JBSEEDCAT000000000000036","内容创作者",[112,113,43,114,115,116,117,118,119,120,121,122,123,124,125,126,40,127,128],"crowd prompting","prompting","with","tokens","domain","world","prompts","operations","instructions","text","tool","system","improve","productivity","marketplace","content","ai agents",{"skillNo":130,"name":131,"nameZh":6,"description":132,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":133,"categories":134,"keywords":138,"publishedAt":49},"01M6G8J0PKPAYVET6BWE8W4C26","dilemm-ai","Compete in DilemmAI, the prisoner's dilemma AI arena at dilemm.ai. Use when an OpenClaw agent wants to create an account, design and submit strategy prompts for their bot, enter matchmaking, analyze historical matches and leaderboards, and iterate on bot strategy. Requires headless browser automation (no API available yet).","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzyori\u002Fdilemm-ai\u002FSKILL.md",[135],{"categoryNo":136,"categoryName":137,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000023","Skill与智能体开发",[139,43,140,141,142,143,37,144,145,146,147,148,149,150,151,152,153,118,46],"dilemm ai","arena","analyze","browser","openclaw agent","automation","browser automation","bot","openclaw","design","strategy","headless","compete","submit","matchmaking",{"skillNo":155,"name":156,"nameZh":6,"description":157,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":158,"categories":159,"keywords":163,"publishedAt":49},"01M6G8HWAF3K9SW7NP3Z83CPJ6","arxiv-paper-reviews","Interact with arXiv Crawler API to fetch papers, read reviews, and submit comments. Use when working with arXiv papers, fetching paper lists by date\u002Fcategory\u002Finterest, viewing paper details with comments, or submitting paper reviews via API at http:\u002F\u002F150.158.152.82:8000.","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzxrys\u002Fweak-accept\u002FSKILL.md",[160],{"categoryNo":161,"categoryName":162,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000021","资料检索与研究",[164,165,166,167,168,169,170,114,171,152,46,172,43,173,174],"arxiv paper reviews","reviews","arxiv","paper","crawler","papers","comments","fetch","read","http","via",{"skillNo":176,"name":156,"nameZh":6,"description":157,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":177,"categories":178,"keywords":180,"publishedAt":49},"01M6G8HQJ7N7WMY6YV7D7SSJ16","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzxrys\u002Farxiv-paper-reviews\u002FSKILL.md",[179],{"categoryNo":161,"categoryName":162,"taxonomyType":20,"taxonomyTypeDesc":21},[167,166,165,164,174,169,170,114,168,152,46,171,172,43,173],{"skillNo":182,"name":183,"nameZh":6,"description":184,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":185,"categories":186,"keywords":187,"publishedAt":49},"01M6G8HM95Y64JNVFC2HN4E2BG","git-summary","Get a quick summary of the current Git repository including status, recent commits, branches, and contributors.","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzweack\u002Fgit-summary\u002FSKILL.md",[],[188,189,190,191,192,193,194,195,196,197,198,199,200],"git","summary","git summary","commits","repository","win32","status","linux","contributors","true","quick","get","branches",{"skillNo":202,"name":203,"nameZh":6,"description":204,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":205,"categories":206,"keywords":211,"publishedAt":49},"01M6G8HGEGF05CNEJCGVYFSC4R","agent-constitution","Interact with AgentConstitution governance contracts on Base Sepolia. Check compliance, read rules, log actions, query governance state.","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fztsalexey\u002Fagent-constitution\u002FSKILL.md",[207,208],{"categoryNo":136,"categoryName":137,"taxonomyType":20,"taxonomyTypeDesc":21},{"categoryNo":209,"categoryName":210,"taxonomyType":28,"taxonomyTypeDesc":29},"01JBSEEDCAT000000000000037","律师与法务",[212,213,214,215,114,216,217,62,218,172,219,220,221,222,223,40,224,225,34],"agent constitution","constitution","contracts","tree","query","base","log","check","Rules","projects","compliance","actions","state","governance"]