Spaces:
Sleeping
Sleeping
Upload 351 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +15 -0
- assets/avatar-placeholder.svg +19 -0
- assets/chrome-extension/README.md +22 -0
- assets/chrome-extension/background.js +438 -0
- assets/chrome-extension/icons/icon128.png +0 -0
- assets/chrome-extension/icons/icon16.png +0 -0
- assets/chrome-extension/icons/icon32.png +0 -0
- assets/chrome-extension/icons/icon48.png +0 -0
- assets/chrome-extension/manifest.json +25 -0
- assets/chrome-extension/options.html +196 -0
- assets/chrome-extension/options.js +59 -0
- assets/dmg-background-small.png +3 -0
- assets/dmg-background.png +3 -0
- docs/.i18n/README.md +31 -0
- docs/.i18n/glossary.zh-CN.json +82 -0
- docs/.i18n/zh-CN.tm.jsonl +0 -0
- docs/CNAME +1 -0
- docs/_config.yml +53 -0
- docs/_layouts/default.html +145 -0
- docs/assets/markdown.css +179 -0
- docs/assets/openclaw-logo-text-dark.png +3 -0
- docs/assets/openclaw-logo-text.png +0 -0
- docs/assets/pixel-lobster.svg +60 -0
- docs/assets/showcase/agents-ui.jpg +3 -0
- docs/assets/showcase/bambu-cli.png +3 -0
- docs/assets/showcase/codexmonitor.png +3 -0
- docs/assets/showcase/gohome-grafana.png +3 -0
- docs/assets/showcase/ios-testflight.jpg +3 -0
- docs/assets/showcase/oura-health.png +3 -0
- docs/assets/showcase/padel-cli.svg +11 -0
- docs/assets/showcase/padel-screenshot.jpg +0 -0
- docs/assets/showcase/papla-tts.jpg +0 -0
- docs/assets/showcase/pr-review-telegram.jpg +3 -0
- docs/assets/showcase/roborock-screenshot.jpg +0 -0
- docs/assets/showcase/roborock-status.svg +13 -0
- docs/assets/showcase/roof-camera-sky.jpg +3 -0
- docs/assets/showcase/snag.png +3 -0
- docs/assets/showcase/tesco-shop.jpg +0 -0
- docs/assets/showcase/wienerlinien.png +3 -0
- docs/assets/showcase/wine-cellar-skill.jpg +0 -0
- docs/assets/showcase/winix-air-purifier.jpg +3 -0
- docs/assets/showcase/xuezh-pronunciation.jpeg +0 -0
- docs/assets/terminal.css +473 -0
- docs/assets/theme.js +55 -0
- docs/automation/auth-monitoring.md +44 -0
- docs/automation/cron-jobs.md +444 -0
- docs/automation/cron-vs-heartbeat.md +281 -0
- docs/automation/gmail-pubsub.md +256 -0
- docs/automation/poll.md +69 -0
- docs/automation/webhook.md +163 -0
.gitattributes
CHANGED
|
@@ -5,3 +5,18 @@ apps/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png filter=l
|
|
| 5 |
apps/ios/Sources/Assets.xcassets/AppIcon.appiconset/icon-1024.png filter=lfs diff=lfs merge=lfs -text
|
| 6 |
apps/macos/Icon.icon/Assets/openclaw-mac.png filter=lfs diff=lfs merge=lfs -text
|
| 7 |
apps/macos/Sources/OpenClaw/Resources/OpenClaw.icns filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
apps/ios/Sources/Assets.xcassets/AppIcon.appiconset/icon-1024.png filter=lfs diff=lfs merge=lfs -text
|
| 6 |
apps/macos/Icon.icon/Assets/openclaw-mac.png filter=lfs diff=lfs merge=lfs -text
|
| 7 |
apps/macos/Sources/OpenClaw/Resources/OpenClaw.icns filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
assets/dmg-background-small.png filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
assets/dmg-background.png filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
docs/assets/openclaw-logo-text-dark.png filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
docs/assets/showcase/agents-ui.jpg filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
docs/assets/showcase/bambu-cli.png filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
docs/assets/showcase/codexmonitor.png filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
docs/assets/showcase/gohome-grafana.png filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
docs/assets/showcase/ios-testflight.jpg filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
docs/assets/showcase/oura-health.png filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
docs/assets/showcase/pr-review-telegram.jpg filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
docs/assets/showcase/roof-camera-sky.jpg filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
docs/assets/showcase/snag.png filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
docs/assets/showcase/wienerlinien.png filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
docs/assets/showcase/winix-air-purifier.jpg filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
docs/images/mobile-ui-screenshot.png filter=lfs diff=lfs merge=lfs -text
|
assets/avatar-placeholder.svg
ADDED
|
|
assets/chrome-extension/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenClaw Chrome Extension (Browser Relay)
|
| 2 |
+
|
| 3 |
+
Purpose: attach OpenClaw to an existing Chrome tab so the Gateway can automate it (via the local CDP relay server).
|
| 4 |
+
|
| 5 |
+
## Dev / load unpacked
|
| 6 |
+
|
| 7 |
+
1. Build/run OpenClaw Gateway with browser control enabled.
|
| 8 |
+
2. Ensure the relay server is reachable at `http://127.0.0.1:18792/` (default).
|
| 9 |
+
3. Install the extension to a stable path:
|
| 10 |
+
|
| 11 |
+
```bash
|
| 12 |
+
openclaw browser extension install
|
| 13 |
+
openclaw browser extension path
|
| 14 |
+
```
|
| 15 |
+
|
| 16 |
+
4. Chrome → `chrome://extensions` → enable “Developer mode”.
|
| 17 |
+
5. “Load unpacked” → select the path printed above.
|
| 18 |
+
6. Pin the extension. Click the icon on a tab to attach/detach.
|
| 19 |
+
|
| 20 |
+
## Options
|
| 21 |
+
|
| 22 |
+
- `Relay port`: defaults to `18792`.
|
assets/chrome-extension/background.js
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const DEFAULT_PORT = 18792
|
| 2 |
+
|
| 3 |
+
const BADGE = {
|
| 4 |
+
on: { text: 'ON', color: '#FF5A36' },
|
| 5 |
+
off: { text: '', color: '#000000' },
|
| 6 |
+
connecting: { text: '…', color: '#F59E0B' },
|
| 7 |
+
error: { text: '!', color: '#B91C1C' },
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
/** @type {WebSocket|null} */
|
| 11 |
+
let relayWs = null
|
| 12 |
+
/** @type {Promise<void>|null} */
|
| 13 |
+
let relayConnectPromise = null
|
| 14 |
+
|
| 15 |
+
let debuggerListenersInstalled = false
|
| 16 |
+
|
| 17 |
+
let nextSession = 1
|
| 18 |
+
|
| 19 |
+
/** @type {Map<number, {state:'connecting'|'connected', sessionId?:string, targetId?:string, attachOrder?:number}>} */
|
| 20 |
+
const tabs = new Map()
|
| 21 |
+
/** @type {Map<string, number>} */
|
| 22 |
+
const tabBySession = new Map()
|
| 23 |
+
/** @type {Map<string, number>} */
|
| 24 |
+
const childSessionToTab = new Map()
|
| 25 |
+
|
| 26 |
+
/** @type {Map<number, {resolve:(v:any)=>void, reject:(e:Error)=>void}>} */
|
| 27 |
+
const pending = new Map()
|
| 28 |
+
|
| 29 |
+
function nowStack() {
|
| 30 |
+
try {
|
| 31 |
+
return new Error().stack || ''
|
| 32 |
+
} catch {
|
| 33 |
+
return ''
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
async function getRelayPort() {
|
| 38 |
+
const stored = await chrome.storage.local.get(['relayPort'])
|
| 39 |
+
const raw = stored.relayPort
|
| 40 |
+
const n = Number.parseInt(String(raw || ''), 10)
|
| 41 |
+
if (!Number.isFinite(n) || n <= 0 || n > 65535) return DEFAULT_PORT
|
| 42 |
+
return n
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
function setBadge(tabId, kind) {
|
| 46 |
+
const cfg = BADGE[kind]
|
| 47 |
+
void chrome.action.setBadgeText({ tabId, text: cfg.text })
|
| 48 |
+
void chrome.action.setBadgeBackgroundColor({ tabId, color: cfg.color })
|
| 49 |
+
void chrome.action.setBadgeTextColor({ tabId, color: '#FFFFFF' }).catch(() => {})
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
async function ensureRelayConnection() {
|
| 53 |
+
if (relayWs && relayWs.readyState === WebSocket.OPEN) return
|
| 54 |
+
if (relayConnectPromise) return await relayConnectPromise
|
| 55 |
+
|
| 56 |
+
relayConnectPromise = (async () => {
|
| 57 |
+
const port = await getRelayPort()
|
| 58 |
+
const httpBase = `http://127.0.0.1:${port}`
|
| 59 |
+
const wsUrl = `ws://127.0.0.1:${port}/extension`
|
| 60 |
+
|
| 61 |
+
// Fast preflight: is the relay server up?
|
| 62 |
+
try {
|
| 63 |
+
await fetch(`${httpBase}/`, { method: 'HEAD', signal: AbortSignal.timeout(2000) })
|
| 64 |
+
} catch (err) {
|
| 65 |
+
throw new Error(`Relay server not reachable at ${httpBase} (${String(err)})`)
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
const ws = new WebSocket(wsUrl)
|
| 69 |
+
relayWs = ws
|
| 70 |
+
|
| 71 |
+
await new Promise((resolve, reject) => {
|
| 72 |
+
const t = setTimeout(() => reject(new Error('WebSocket connect timeout')), 5000)
|
| 73 |
+
ws.onopen = () => {
|
| 74 |
+
clearTimeout(t)
|
| 75 |
+
resolve()
|
| 76 |
+
}
|
| 77 |
+
ws.onerror = () => {
|
| 78 |
+
clearTimeout(t)
|
| 79 |
+
reject(new Error('WebSocket connect failed'))
|
| 80 |
+
}
|
| 81 |
+
ws.onclose = (ev) => {
|
| 82 |
+
clearTimeout(t)
|
| 83 |
+
reject(new Error(`WebSocket closed (${ev.code} ${ev.reason || 'no reason'})`))
|
| 84 |
+
}
|
| 85 |
+
})
|
| 86 |
+
|
| 87 |
+
ws.onmessage = (event) => void onRelayMessage(String(event.data || ''))
|
| 88 |
+
ws.onclose = () => onRelayClosed('closed')
|
| 89 |
+
ws.onerror = () => onRelayClosed('error')
|
| 90 |
+
|
| 91 |
+
if (!debuggerListenersInstalled) {
|
| 92 |
+
debuggerListenersInstalled = true
|
| 93 |
+
chrome.debugger.onEvent.addListener(onDebuggerEvent)
|
| 94 |
+
chrome.debugger.onDetach.addListener(onDebuggerDetach)
|
| 95 |
+
}
|
| 96 |
+
})()
|
| 97 |
+
|
| 98 |
+
try {
|
| 99 |
+
await relayConnectPromise
|
| 100 |
+
} finally {
|
| 101 |
+
relayConnectPromise = null
|
| 102 |
+
}
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
function onRelayClosed(reason) {
|
| 106 |
+
relayWs = null
|
| 107 |
+
for (const [id, p] of pending.entries()) {
|
| 108 |
+
pending.delete(id)
|
| 109 |
+
p.reject(new Error(`Relay disconnected (${reason})`))
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
for (const tabId of tabs.keys()) {
|
| 113 |
+
void chrome.debugger.detach({ tabId }).catch(() => {})
|
| 114 |
+
setBadge(tabId, 'connecting')
|
| 115 |
+
void chrome.action.setTitle({
|
| 116 |
+
tabId,
|
| 117 |
+
title: 'OpenClaw Browser Relay: disconnected (click to re-attach)',
|
| 118 |
+
})
|
| 119 |
+
}
|
| 120 |
+
tabs.clear()
|
| 121 |
+
tabBySession.clear()
|
| 122 |
+
childSessionToTab.clear()
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
function sendToRelay(payload) {
|
| 126 |
+
const ws = relayWs
|
| 127 |
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
| 128 |
+
throw new Error('Relay not connected')
|
| 129 |
+
}
|
| 130 |
+
ws.send(JSON.stringify(payload))
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
async function maybeOpenHelpOnce() {
|
| 134 |
+
try {
|
| 135 |
+
const stored = await chrome.storage.local.get(['helpOnErrorShown'])
|
| 136 |
+
if (stored.helpOnErrorShown === true) return
|
| 137 |
+
await chrome.storage.local.set({ helpOnErrorShown: true })
|
| 138 |
+
await chrome.runtime.openOptionsPage()
|
| 139 |
+
} catch {
|
| 140 |
+
// ignore
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
function requestFromRelay(command) {
|
| 145 |
+
const id = command.id
|
| 146 |
+
return new Promise((resolve, reject) => {
|
| 147 |
+
pending.set(id, { resolve, reject })
|
| 148 |
+
try {
|
| 149 |
+
sendToRelay(command)
|
| 150 |
+
} catch (err) {
|
| 151 |
+
pending.delete(id)
|
| 152 |
+
reject(err instanceof Error ? err : new Error(String(err)))
|
| 153 |
+
}
|
| 154 |
+
})
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
async function onRelayMessage(text) {
|
| 158 |
+
/** @type {any} */
|
| 159 |
+
let msg
|
| 160 |
+
try {
|
| 161 |
+
msg = JSON.parse(text)
|
| 162 |
+
} catch {
|
| 163 |
+
return
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
if (msg && msg.method === 'ping') {
|
| 167 |
+
try {
|
| 168 |
+
sendToRelay({ method: 'pong' })
|
| 169 |
+
} catch {
|
| 170 |
+
// ignore
|
| 171 |
+
}
|
| 172 |
+
return
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
if (msg && typeof msg.id === 'number' && (msg.result !== undefined || msg.error !== undefined)) {
|
| 176 |
+
const p = pending.get(msg.id)
|
| 177 |
+
if (!p) return
|
| 178 |
+
pending.delete(msg.id)
|
| 179 |
+
if (msg.error) p.reject(new Error(String(msg.error)))
|
| 180 |
+
else p.resolve(msg.result)
|
| 181 |
+
return
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
if (msg && typeof msg.id === 'number' && msg.method === 'forwardCDPCommand') {
|
| 185 |
+
try {
|
| 186 |
+
const result = await handleForwardCdpCommand(msg)
|
| 187 |
+
sendToRelay({ id: msg.id, result })
|
| 188 |
+
} catch (err) {
|
| 189 |
+
sendToRelay({ id: msg.id, error: err instanceof Error ? err.message : String(err) })
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
function getTabBySessionId(sessionId) {
|
| 195 |
+
const direct = tabBySession.get(sessionId)
|
| 196 |
+
if (direct) return { tabId: direct, kind: 'main' }
|
| 197 |
+
const child = childSessionToTab.get(sessionId)
|
| 198 |
+
if (child) return { tabId: child, kind: 'child' }
|
| 199 |
+
return null
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
function getTabByTargetId(targetId) {
|
| 203 |
+
for (const [tabId, tab] of tabs.entries()) {
|
| 204 |
+
if (tab.targetId === targetId) return tabId
|
| 205 |
+
}
|
| 206 |
+
return null
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
async function attachTab(tabId, opts = {}) {
|
| 210 |
+
const debuggee = { tabId }
|
| 211 |
+
await chrome.debugger.attach(debuggee, '1.3')
|
| 212 |
+
await chrome.debugger.sendCommand(debuggee, 'Page.enable').catch(() => {})
|
| 213 |
+
|
| 214 |
+
const info = /** @type {any} */ (await chrome.debugger.sendCommand(debuggee, 'Target.getTargetInfo'))
|
| 215 |
+
const targetInfo = info?.targetInfo
|
| 216 |
+
const targetId = String(targetInfo?.targetId || '').trim()
|
| 217 |
+
if (!targetId) {
|
| 218 |
+
throw new Error('Target.getTargetInfo returned no targetId')
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
const sessionId = `cb-tab-${nextSession++}`
|
| 222 |
+
const attachOrder = nextSession
|
| 223 |
+
|
| 224 |
+
tabs.set(tabId, { state: 'connected', sessionId, targetId, attachOrder })
|
| 225 |
+
tabBySession.set(sessionId, tabId)
|
| 226 |
+
void chrome.action.setTitle({
|
| 227 |
+
tabId,
|
| 228 |
+
title: 'OpenClaw Browser Relay: attached (click to detach)',
|
| 229 |
+
})
|
| 230 |
+
|
| 231 |
+
if (!opts.skipAttachedEvent) {
|
| 232 |
+
sendToRelay({
|
| 233 |
+
method: 'forwardCDPEvent',
|
| 234 |
+
params: {
|
| 235 |
+
method: 'Target.attachedToTarget',
|
| 236 |
+
params: {
|
| 237 |
+
sessionId,
|
| 238 |
+
targetInfo: { ...targetInfo, attached: true },
|
| 239 |
+
waitingForDebugger: false,
|
| 240 |
+
},
|
| 241 |
+
},
|
| 242 |
+
})
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
setBadge(tabId, 'on')
|
| 246 |
+
return { sessionId, targetId }
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
async function detachTab(tabId, reason) {
|
| 250 |
+
const tab = tabs.get(tabId)
|
| 251 |
+
if (tab?.sessionId && tab?.targetId) {
|
| 252 |
+
try {
|
| 253 |
+
sendToRelay({
|
| 254 |
+
method: 'forwardCDPEvent',
|
| 255 |
+
params: {
|
| 256 |
+
method: 'Target.detachedFromTarget',
|
| 257 |
+
params: { sessionId: tab.sessionId, targetId: tab.targetId, reason },
|
| 258 |
+
},
|
| 259 |
+
})
|
| 260 |
+
} catch {
|
| 261 |
+
// ignore
|
| 262 |
+
}
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
if (tab?.sessionId) tabBySession.delete(tab.sessionId)
|
| 266 |
+
tabs.delete(tabId)
|
| 267 |
+
|
| 268 |
+
for (const [childSessionId, parentTabId] of childSessionToTab.entries()) {
|
| 269 |
+
if (parentTabId === tabId) childSessionToTab.delete(childSessionId)
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
try {
|
| 273 |
+
await chrome.debugger.detach({ tabId })
|
| 274 |
+
} catch {
|
| 275 |
+
// ignore
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
setBadge(tabId, 'off')
|
| 279 |
+
void chrome.action.setTitle({
|
| 280 |
+
tabId,
|
| 281 |
+
title: 'OpenClaw Browser Relay (click to attach/detach)',
|
| 282 |
+
})
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
async function connectOrToggleForActiveTab() {
|
| 286 |
+
const [active] = await chrome.tabs.query({ active: true, currentWindow: true })
|
| 287 |
+
const tabId = active?.id
|
| 288 |
+
if (!tabId) return
|
| 289 |
+
|
| 290 |
+
const existing = tabs.get(tabId)
|
| 291 |
+
if (existing?.state === 'connected') {
|
| 292 |
+
await detachTab(tabId, 'toggle')
|
| 293 |
+
return
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
tabs.set(tabId, { state: 'connecting' })
|
| 297 |
+
setBadge(tabId, 'connecting')
|
| 298 |
+
void chrome.action.setTitle({
|
| 299 |
+
tabId,
|
| 300 |
+
title: 'OpenClaw Browser Relay: connecting to local relay…',
|
| 301 |
+
})
|
| 302 |
+
|
| 303 |
+
try {
|
| 304 |
+
await ensureRelayConnection()
|
| 305 |
+
await attachTab(tabId)
|
| 306 |
+
} catch (err) {
|
| 307 |
+
tabs.delete(tabId)
|
| 308 |
+
setBadge(tabId, 'error')
|
| 309 |
+
void chrome.action.setTitle({
|
| 310 |
+
tabId,
|
| 311 |
+
title: 'OpenClaw Browser Relay: relay not running (open options for setup)',
|
| 312 |
+
})
|
| 313 |
+
void maybeOpenHelpOnce()
|
| 314 |
+
// Extra breadcrumbs in chrome://extensions service worker logs.
|
| 315 |
+
const message = err instanceof Error ? err.message : String(err)
|
| 316 |
+
console.warn('attach failed', message, nowStack())
|
| 317 |
+
}
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
async function handleForwardCdpCommand(msg) {
|
| 321 |
+
const method = String(msg?.params?.method || '').trim()
|
| 322 |
+
const params = msg?.params?.params || undefined
|
| 323 |
+
const sessionId = typeof msg?.params?.sessionId === 'string' ? msg.params.sessionId : undefined
|
| 324 |
+
|
| 325 |
+
// Map command to tab
|
| 326 |
+
const bySession = sessionId ? getTabBySessionId(sessionId) : null
|
| 327 |
+
const targetId = typeof params?.targetId === 'string' ? params.targetId : undefined
|
| 328 |
+
const tabId =
|
| 329 |
+
bySession?.tabId ||
|
| 330 |
+
(targetId ? getTabByTargetId(targetId) : null) ||
|
| 331 |
+
(() => {
|
| 332 |
+
// No sessionId: pick the first connected tab (stable-ish).
|
| 333 |
+
for (const [id, tab] of tabs.entries()) {
|
| 334 |
+
if (tab.state === 'connected') return id
|
| 335 |
+
}
|
| 336 |
+
return null
|
| 337 |
+
})()
|
| 338 |
+
|
| 339 |
+
if (!tabId) throw new Error(`No attached tab for method ${method}`)
|
| 340 |
+
|
| 341 |
+
/** @type {chrome.debugger.DebuggerSession} */
|
| 342 |
+
const debuggee = { tabId }
|
| 343 |
+
|
| 344 |
+
if (method === 'Runtime.enable') {
|
| 345 |
+
try {
|
| 346 |
+
await chrome.debugger.sendCommand(debuggee, 'Runtime.disable')
|
| 347 |
+
await new Promise((r) => setTimeout(r, 50))
|
| 348 |
+
} catch {
|
| 349 |
+
// ignore
|
| 350 |
+
}
|
| 351 |
+
return await chrome.debugger.sendCommand(debuggee, 'Runtime.enable', params)
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
if (method === 'Target.createTarget') {
|
| 355 |
+
const url = typeof params?.url === 'string' ? params.url : 'about:blank'
|
| 356 |
+
const tab = await chrome.tabs.create({ url, active: false })
|
| 357 |
+
if (!tab.id) throw new Error('Failed to create tab')
|
| 358 |
+
await new Promise((r) => setTimeout(r, 100))
|
| 359 |
+
const attached = await attachTab(tab.id)
|
| 360 |
+
return { targetId: attached.targetId }
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
if (method === 'Target.closeTarget') {
|
| 364 |
+
const target = typeof params?.targetId === 'string' ? params.targetId : ''
|
| 365 |
+
const toClose = target ? getTabByTargetId(target) : tabId
|
| 366 |
+
if (!toClose) return { success: false }
|
| 367 |
+
try {
|
| 368 |
+
await chrome.tabs.remove(toClose)
|
| 369 |
+
} catch {
|
| 370 |
+
return { success: false }
|
| 371 |
+
}
|
| 372 |
+
return { success: true }
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
if (method === 'Target.activateTarget') {
|
| 376 |
+
const target = typeof params?.targetId === 'string' ? params.targetId : ''
|
| 377 |
+
const toActivate = target ? getTabByTargetId(target) : tabId
|
| 378 |
+
if (!toActivate) return {}
|
| 379 |
+
const tab = await chrome.tabs.get(toActivate).catch(() => null)
|
| 380 |
+
if (!tab) return {}
|
| 381 |
+
if (tab.windowId) {
|
| 382 |
+
await chrome.windows.update(tab.windowId, { focused: true }).catch(() => {})
|
| 383 |
+
}
|
| 384 |
+
await chrome.tabs.update(toActivate, { active: true }).catch(() => {})
|
| 385 |
+
return {}
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
const tabState = tabs.get(tabId)
|
| 389 |
+
const mainSessionId = tabState?.sessionId
|
| 390 |
+
const debuggerSession =
|
| 391 |
+
sessionId && mainSessionId && sessionId !== mainSessionId
|
| 392 |
+
? { ...debuggee, sessionId }
|
| 393 |
+
: debuggee
|
| 394 |
+
|
| 395 |
+
return await chrome.debugger.sendCommand(debuggerSession, method, params)
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
function onDebuggerEvent(source, method, params) {
|
| 399 |
+
const tabId = source.tabId
|
| 400 |
+
if (!tabId) return
|
| 401 |
+
const tab = tabs.get(tabId)
|
| 402 |
+
if (!tab?.sessionId) return
|
| 403 |
+
|
| 404 |
+
if (method === 'Target.attachedToTarget' && params?.sessionId) {
|
| 405 |
+
childSessionToTab.set(String(params.sessionId), tabId)
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
if (method === 'Target.detachedFromTarget' && params?.sessionId) {
|
| 409 |
+
childSessionToTab.delete(String(params.sessionId))
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
try {
|
| 413 |
+
sendToRelay({
|
| 414 |
+
method: 'forwardCDPEvent',
|
| 415 |
+
params: {
|
| 416 |
+
sessionId: source.sessionId || tab.sessionId,
|
| 417 |
+
method,
|
| 418 |
+
params,
|
| 419 |
+
},
|
| 420 |
+
})
|
| 421 |
+
} catch {
|
| 422 |
+
// ignore
|
| 423 |
+
}
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
function onDebuggerDetach(source, reason) {
|
| 427 |
+
const tabId = source.tabId
|
| 428 |
+
if (!tabId) return
|
| 429 |
+
if (!tabs.has(tabId)) return
|
| 430 |
+
void detachTab(tabId, reason)
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
chrome.action.onClicked.addListener(() => void connectOrToggleForActiveTab())
|
| 434 |
+
|
| 435 |
+
chrome.runtime.onInstalled.addListener(() => {
|
| 436 |
+
// Useful: first-time instructions.
|
| 437 |
+
void chrome.runtime.openOptionsPage()
|
| 438 |
+
})
|
assets/chrome-extension/icons/icon128.png
ADDED
|
|
assets/chrome-extension/icons/icon16.png
ADDED
|
|
assets/chrome-extension/icons/icon32.png
ADDED
|
|
assets/chrome-extension/icons/icon48.png
ADDED
|
|
assets/chrome-extension/manifest.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"manifest_version": 3,
|
| 3 |
+
"name": "OpenClaw Browser Relay",
|
| 4 |
+
"version": "0.1.0",
|
| 5 |
+
"description": "Attach OpenClaw to your existing Chrome tab via a local CDP relay server.",
|
| 6 |
+
"icons": {
|
| 7 |
+
"16": "icons/icon16.png",
|
| 8 |
+
"32": "icons/icon32.png",
|
| 9 |
+
"48": "icons/icon48.png",
|
| 10 |
+
"128": "icons/icon128.png"
|
| 11 |
+
},
|
| 12 |
+
"permissions": ["debugger", "tabs", "activeTab", "storage"],
|
| 13 |
+
"host_permissions": ["http://127.0.0.1/*", "http://localhost/*"],
|
| 14 |
+
"background": { "service_worker": "background.js", "type": "module" },
|
| 15 |
+
"action": {
|
| 16 |
+
"default_title": "OpenClaw Browser Relay (click to attach/detach)",
|
| 17 |
+
"default_icon": {
|
| 18 |
+
"16": "icons/icon16.png",
|
| 19 |
+
"32": "icons/icon32.png",
|
| 20 |
+
"48": "icons/icon48.png",
|
| 21 |
+
"128": "icons/icon128.png"
|
| 22 |
+
}
|
| 23 |
+
},
|
| 24 |
+
"options_ui": { "page": "options.html", "open_in_tab": true }
|
| 25 |
+
}
|
assets/chrome-extension/options.html
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<title>OpenClaw Browser Relay</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root {
|
| 9 |
+
color-scheme: light dark;
|
| 10 |
+
--accent: #ff5a36;
|
| 11 |
+
--panel: color-mix(in oklab, canvas 92%, canvasText 8%);
|
| 12 |
+
--border: color-mix(in oklab, canvasText 18%, transparent);
|
| 13 |
+
--muted: color-mix(in oklab, canvasText 70%, transparent);
|
| 14 |
+
--shadow: 0 10px 30px color-mix(in oklab, canvasText 18%, transparent);
|
| 15 |
+
font-family: ui-rounded, system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Rounded",
|
| 16 |
+
"SF Pro Display", "Segoe UI", sans-serif;
|
| 17 |
+
line-height: 1.4;
|
| 18 |
+
}
|
| 19 |
+
body {
|
| 20 |
+
margin: 0;
|
| 21 |
+
min-height: 100vh;
|
| 22 |
+
background:
|
| 23 |
+
radial-gradient(1000px 500px at 10% 0%, color-mix(in oklab, var(--accent) 30%, transparent), transparent 70%),
|
| 24 |
+
radial-gradient(900px 450px at 90% 0%, color-mix(in oklab, var(--accent) 18%, transparent), transparent 75%),
|
| 25 |
+
canvas;
|
| 26 |
+
color: canvasText;
|
| 27 |
+
}
|
| 28 |
+
.wrap {
|
| 29 |
+
max-width: 820px;
|
| 30 |
+
margin: 36px auto;
|
| 31 |
+
padding: 0 24px 48px 24px;
|
| 32 |
+
}
|
| 33 |
+
header {
|
| 34 |
+
display: flex;
|
| 35 |
+
align-items: center;
|
| 36 |
+
gap: 12px;
|
| 37 |
+
margin-bottom: 18px;
|
| 38 |
+
}
|
| 39 |
+
.logo {
|
| 40 |
+
width: 44px;
|
| 41 |
+
height: 44px;
|
| 42 |
+
border-radius: 14px;
|
| 43 |
+
background: color-mix(in oklab, var(--accent) 18%, transparent);
|
| 44 |
+
border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent);
|
| 45 |
+
box-shadow: var(--shadow);
|
| 46 |
+
display: grid;
|
| 47 |
+
place-items: center;
|
| 48 |
+
}
|
| 49 |
+
.logo img {
|
| 50 |
+
width: 28px;
|
| 51 |
+
height: 28px;
|
| 52 |
+
image-rendering: pixelated;
|
| 53 |
+
}
|
| 54 |
+
h1 {
|
| 55 |
+
font-size: 20px;
|
| 56 |
+
margin: 0;
|
| 57 |
+
letter-spacing: -0.01em;
|
| 58 |
+
}
|
| 59 |
+
.subtitle {
|
| 60 |
+
margin: 2px 0 0 0;
|
| 61 |
+
color: var(--muted);
|
| 62 |
+
font-size: 13px;
|
| 63 |
+
}
|
| 64 |
+
.grid {
|
| 65 |
+
display: grid;
|
| 66 |
+
grid-template-columns: 1fr;
|
| 67 |
+
gap: 14px;
|
| 68 |
+
}
|
| 69 |
+
.card {
|
| 70 |
+
background: var(--panel);
|
| 71 |
+
border: 1px solid var(--border);
|
| 72 |
+
border-radius: 16px;
|
| 73 |
+
padding: 16px;
|
| 74 |
+
box-shadow: var(--shadow);
|
| 75 |
+
}
|
| 76 |
+
.card h2 {
|
| 77 |
+
margin: 0 0 10px 0;
|
| 78 |
+
font-size: 14px;
|
| 79 |
+
letter-spacing: 0.01em;
|
| 80 |
+
}
|
| 81 |
+
.card p {
|
| 82 |
+
margin: 8px 0 0 0;
|
| 83 |
+
color: var(--muted);
|
| 84 |
+
font-size: 13px;
|
| 85 |
+
}
|
| 86 |
+
.row {
|
| 87 |
+
display: flex;
|
| 88 |
+
align-items: center;
|
| 89 |
+
gap: 8px;
|
| 90 |
+
flex-wrap: wrap;
|
| 91 |
+
}
|
| 92 |
+
label {
|
| 93 |
+
display: block;
|
| 94 |
+
font-size: 12px;
|
| 95 |
+
color: var(--muted);
|
| 96 |
+
margin-bottom: 6px;
|
| 97 |
+
}
|
| 98 |
+
input {
|
| 99 |
+
width: 160px;
|
| 100 |
+
padding: 10px 12px;
|
| 101 |
+
border-radius: 12px;
|
| 102 |
+
border: 1px solid var(--border);
|
| 103 |
+
background: color-mix(in oklab, canvas 92%, canvasText 8%);
|
| 104 |
+
color: canvasText;
|
| 105 |
+
outline: none;
|
| 106 |
+
}
|
| 107 |
+
input:focus {
|
| 108 |
+
border-color: color-mix(in oklab, var(--accent) 70%, transparent);
|
| 109 |
+
box-shadow: 0 0 0 4px color-mix(in oklab, var(--accent) 20%, transparent);
|
| 110 |
+
}
|
| 111 |
+
button {
|
| 112 |
+
padding: 10px 14px;
|
| 113 |
+
border-radius: 12px;
|
| 114 |
+
border: 1px solid color-mix(in oklab, var(--accent) 55%, transparent);
|
| 115 |
+
background: linear-gradient(
|
| 116 |
+
180deg,
|
| 117 |
+
color-mix(in oklab, var(--accent) 80%, white 20%),
|
| 118 |
+
var(--accent)
|
| 119 |
+
);
|
| 120 |
+
color: white;
|
| 121 |
+
font-weight: 650;
|
| 122 |
+
letter-spacing: 0.01em;
|
| 123 |
+
cursor: pointer;
|
| 124 |
+
}
|
| 125 |
+
button:active {
|
| 126 |
+
transform: translateY(1px);
|
| 127 |
+
}
|
| 128 |
+
.hint {
|
| 129 |
+
margin-top: 10px;
|
| 130 |
+
font-size: 12px;
|
| 131 |
+
color: var(--muted);
|
| 132 |
+
}
|
| 133 |
+
code {
|
| 134 |
+
font-family: ui-monospace, Menlo, Monaco, Consolas, "SF Mono", monospace;
|
| 135 |
+
font-size: 12px;
|
| 136 |
+
}
|
| 137 |
+
a {
|
| 138 |
+
color: color-mix(in oklab, var(--accent) 85%, canvasText 15%);
|
| 139 |
+
}
|
| 140 |
+
.status {
|
| 141 |
+
margin-top: 10px;
|
| 142 |
+
font-size: 12px;
|
| 143 |
+
color: color-mix(in oklab, var(--accent) 70%, canvasText 30%);
|
| 144 |
+
min-height: 16px;
|
| 145 |
+
}
|
| 146 |
+
.status[data-kind='ok'] {
|
| 147 |
+
color: color-mix(in oklab, #16a34a 75%, canvasText 25%);
|
| 148 |
+
}
|
| 149 |
+
.status[data-kind='error'] {
|
| 150 |
+
color: color-mix(in oklab, #ef4444 75%, canvasText 25%);
|
| 151 |
+
}
|
| 152 |
+
</style>
|
| 153 |
+
</head>
|
| 154 |
+
<body>
|
| 155 |
+
<div class="wrap">
|
| 156 |
+
<header>
|
| 157 |
+
<div class="logo" aria-hidden="true">
|
| 158 |
+
<img src="icons/icon128.png" alt="" />
|
| 159 |
+
</div>
|
| 160 |
+
<div>
|
| 161 |
+
<h1>OpenClaw Browser Relay</h1>
|
| 162 |
+
<p class="subtitle">Click the toolbar button on a tab to attach / detach.</p>
|
| 163 |
+
</div>
|
| 164 |
+
</header>
|
| 165 |
+
|
| 166 |
+
<div class="grid">
|
| 167 |
+
<div class="card">
|
| 168 |
+
<h2>Getting started</h2>
|
| 169 |
+
<p>
|
| 170 |
+
If you see a red <code>!</code> badge on the extension icon, the relay server is not reachable.
|
| 171 |
+
Start OpenClaw’s browser relay on this machine (Gateway or node host), then click the toolbar button again.
|
| 172 |
+
</p>
|
| 173 |
+
<p>
|
| 174 |
+
Full guide (install, remote Gateway, security): <a href="https://docs.openclaw.ai/tools/chrome-extension" target="_blank" rel="noreferrer">docs.openclaw.ai/tools/chrome-extension</a>
|
| 175 |
+
</p>
|
| 176 |
+
</div>
|
| 177 |
+
|
| 178 |
+
<div class="card">
|
| 179 |
+
<h2>Relay port</h2>
|
| 180 |
+
<label for="port">Port</label>
|
| 181 |
+
<div class="row">
|
| 182 |
+
<input id="port" inputmode="numeric" pattern="[0-9]*" />
|
| 183 |
+
<button id="save" type="button">Save</button>
|
| 184 |
+
</div>
|
| 185 |
+
<div class="hint">
|
| 186 |
+
Default: <code>18792</code>. Extension connects to: <code id="relay-url">http://127.0.0.1:<port>/</code>.
|
| 187 |
+
Only change this if your OpenClaw profile uses a different <code>cdpUrl</code> port.
|
| 188 |
+
</div>
|
| 189 |
+
<div class="status" id="status"></div>
|
| 190 |
+
</div>
|
| 191 |
+
</div>
|
| 192 |
+
|
| 193 |
+
<script type="module" src="options.js"></script>
|
| 194 |
+
</div>
|
| 195 |
+
</body>
|
| 196 |
+
</html>
|
assets/chrome-extension/options.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const DEFAULT_PORT = 18792
|
| 2 |
+
|
| 3 |
+
function clampPort(value) {
|
| 4 |
+
const n = Number.parseInt(String(value || ''), 10)
|
| 5 |
+
if (!Number.isFinite(n)) return DEFAULT_PORT
|
| 6 |
+
if (n <= 0 || n > 65535) return DEFAULT_PORT
|
| 7 |
+
return n
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
function updateRelayUrl(port) {
|
| 11 |
+
const el = document.getElementById('relay-url')
|
| 12 |
+
if (!el) return
|
| 13 |
+
el.textContent = `http://127.0.0.1:${port}/`
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
function setStatus(kind, message) {
|
| 17 |
+
const status = document.getElementById('status')
|
| 18 |
+
if (!status) return
|
| 19 |
+
status.dataset.kind = kind || ''
|
| 20 |
+
status.textContent = message || ''
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
async function checkRelayReachable(port) {
|
| 24 |
+
const url = `http://127.0.0.1:${port}/`
|
| 25 |
+
const ctrl = new AbortController()
|
| 26 |
+
const t = setTimeout(() => ctrl.abort(), 900)
|
| 27 |
+
try {
|
| 28 |
+
const res = await fetch(url, { method: 'HEAD', signal: ctrl.signal })
|
| 29 |
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
| 30 |
+
setStatus('ok', `Relay reachable at ${url}`)
|
| 31 |
+
} catch {
|
| 32 |
+
setStatus(
|
| 33 |
+
'error',
|
| 34 |
+
`Relay not reachable at ${url}. Start OpenClaw’s browser relay on this machine, then click the toolbar button again.`,
|
| 35 |
+
)
|
| 36 |
+
} finally {
|
| 37 |
+
clearTimeout(t)
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
async function load() {
|
| 42 |
+
const stored = await chrome.storage.local.get(['relayPort'])
|
| 43 |
+
const port = clampPort(stored.relayPort)
|
| 44 |
+
document.getElementById('port').value = String(port)
|
| 45 |
+
updateRelayUrl(port)
|
| 46 |
+
await checkRelayReachable(port)
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
async function save() {
|
| 50 |
+
const input = document.getElementById('port')
|
| 51 |
+
const port = clampPort(input.value)
|
| 52 |
+
await chrome.storage.local.set({ relayPort: port })
|
| 53 |
+
input.value = String(port)
|
| 54 |
+
updateRelayUrl(port)
|
| 55 |
+
await checkRelayReachable(port)
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
document.getElementById('save').addEventListener('click', () => void save())
|
| 59 |
+
void load()
|
assets/dmg-background-small.png
ADDED
|
Git LFS Details
|
assets/dmg-background.png
ADDED
|
Git LFS Details
|
docs/.i18n/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenClaw docs i18n assets
|
| 2 |
+
|
| 3 |
+
This folder stores **generated** and **config** files for documentation translations.
|
| 4 |
+
|
| 5 |
+
## Files
|
| 6 |
+
|
| 7 |
+
- `glossary.<lang>.json` — preferred term mappings (used in prompt guidance).
|
| 8 |
+
- `<lang>.tm.jsonl` — translation memory (cache) keyed by workflow + model + text hash.
|
| 9 |
+
|
| 10 |
+
## Glossary format
|
| 11 |
+
|
| 12 |
+
`glossary.<lang>.json` is an array of entries:
|
| 13 |
+
|
| 14 |
+
```json
|
| 15 |
+
{
|
| 16 |
+
"source": "troubleshooting",
|
| 17 |
+
"target": "故障排除",
|
| 18 |
+
"ignore_case": true,
|
| 19 |
+
"whole_word": false
|
| 20 |
+
}
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
Fields:
|
| 24 |
+
|
| 25 |
+
- `source`: English (or source) phrase to prefer.
|
| 26 |
+
- `target`: preferred translation output.
|
| 27 |
+
|
| 28 |
+
## Notes
|
| 29 |
+
|
| 30 |
+
- Glossary entries are passed to the model as **prompt guidance** (no deterministic rewrites).
|
| 31 |
+
- The translation memory is updated by `scripts/docs-i18n`.
|
docs/.i18n/glossary.zh-CN.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"source": "OpenClaw",
|
| 4 |
+
"target": "OpenClaw"
|
| 5 |
+
},
|
| 6 |
+
{
|
| 7 |
+
"source": "Gateway",
|
| 8 |
+
"target": "Gateway"
|
| 9 |
+
},
|
| 10 |
+
{
|
| 11 |
+
"source": "Pi",
|
| 12 |
+
"target": "Pi"
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"source": "agent",
|
| 16 |
+
"target": "智能体"
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"source": "channel",
|
| 20 |
+
"target": "渠道"
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"source": "session",
|
| 24 |
+
"target": "会话"
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"source": "provider",
|
| 28 |
+
"target": "提供商"
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"source": "model",
|
| 32 |
+
"target": "模型"
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"source": "tool",
|
| 36 |
+
"target": "工具"
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"source": "CLI",
|
| 40 |
+
"target": "CLI"
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"source": "install sanity",
|
| 44 |
+
"target": "安装完整性检查"
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"source": "get unstuck",
|
| 48 |
+
"target": "快速排障"
|
| 49 |
+
},
|
| 50 |
+
{
|
| 51 |
+
"source": "troubleshooting",
|
| 52 |
+
"target": "故障排除"
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
"source": "FAQ",
|
| 56 |
+
"target": "常见问题"
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"source": "onboarding",
|
| 60 |
+
"target": "上手引导"
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"source": "wizard",
|
| 64 |
+
"target": "向导"
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
"source": "environment variables",
|
| 68 |
+
"target": "环境变量"
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
"source": "environment variable",
|
| 72 |
+
"target": "环境变量"
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"source": "env vars",
|
| 76 |
+
"target": "环境变量"
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
"source": "env var",
|
| 80 |
+
"target": "环境变量"
|
| 81 |
+
}
|
| 82 |
+
]
|
docs/.i18n/zh-CN.tm.jsonl
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
docs/CNAME
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
docs.openclaw.ai
|
docs/_config.yml
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
title: "OpenClaw Docs"
|
| 2 |
+
description: "A TypeScript/Node gateway + macOS/iOS/Android companions for WhatsApp (web) and Telegram (bot)."
|
| 3 |
+
markdown: kramdown
|
| 4 |
+
highlighter: rouge
|
| 5 |
+
|
| 6 |
+
# Keep GitHub Pages' default page URLs (e.g. /gateway.html). Many docs links
|
| 7 |
+
# are written as relative *.md links and are rewritten during the build.
|
| 8 |
+
|
| 9 |
+
plugins:
|
| 10 |
+
- jekyll-relative-links
|
| 11 |
+
|
| 12 |
+
relative_links:
|
| 13 |
+
enabled: true
|
| 14 |
+
collections: true
|
| 15 |
+
|
| 16 |
+
defaults:
|
| 17 |
+
- scope:
|
| 18 |
+
path: ""
|
| 19 |
+
values:
|
| 20 |
+
layout: default
|
| 21 |
+
|
| 22 |
+
nav:
|
| 23 |
+
- title: "Home"
|
| 24 |
+
url: "/"
|
| 25 |
+
- title: "OpenClaw Assistant"
|
| 26 |
+
url: "/start/openclaw/"
|
| 27 |
+
- title: "Gateway"
|
| 28 |
+
url: "/gateway/"
|
| 29 |
+
- title: "Remote"
|
| 30 |
+
url: "/gateway/remote/"
|
| 31 |
+
- title: "Discovery"
|
| 32 |
+
url: "/gateway/discovery/"
|
| 33 |
+
- title: "Configuration"
|
| 34 |
+
url: "/gateway/configuration/"
|
| 35 |
+
- title: "WebChat"
|
| 36 |
+
url: "/web/webchat/"
|
| 37 |
+
- title: "macOS App"
|
| 38 |
+
url: "/platforms/macos/"
|
| 39 |
+
- title: "iOS App"
|
| 40 |
+
url: "/platforms/ios/"
|
| 41 |
+
- title: "Android App"
|
| 42 |
+
url: "/platforms/android/"
|
| 43 |
+
- title: "Telegram"
|
| 44 |
+
url: "/channels/telegram/"
|
| 45 |
+
- title: "Security"
|
| 46 |
+
url: "/gateway/security/"
|
| 47 |
+
- title: "Troubleshooting"
|
| 48 |
+
url: "/gateway/troubleshooting/"
|
| 49 |
+
|
| 50 |
+
kramdown:
|
| 51 |
+
input: GFM
|
| 52 |
+
hard_wrap: false
|
| 53 |
+
syntax_highlighter: rouge
|
docs/_layouts/default.html
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en" data-theme="auto">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<meta name="color-scheme" content="light dark" />
|
| 7 |
+
<title>
|
| 8 |
+
{% if page.url == "/" %}{{ site.title }}{% else %}{{ page.title | default: page.path | split: "/" | last | replace: ".md", "" }} · {{ site.title }}{% endif %}
|
| 9 |
+
</title>
|
| 10 |
+
|
| 11 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 12 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 13 |
+
<link
|
| 14 |
+
href="https://fonts.googleapis.com/css2?family=Pixelify+Sans:wght@400..700&family=Fragment+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap"
|
| 15 |
+
rel="stylesheet"
|
| 16 |
+
/>
|
| 17 |
+
<script>
|
| 18 |
+
(() => {
|
| 19 |
+
try {
|
| 20 |
+
const stored = localStorage.getItem("openclaw:theme");
|
| 21 |
+
if (stored === "light" || stored === "dark") document.documentElement.dataset.theme = stored;
|
| 22 |
+
} catch {
|
| 23 |
+
// ignore
|
| 24 |
+
}
|
| 25 |
+
})();
|
| 26 |
+
</script>
|
| 27 |
+
<link rel="stylesheet" href="{{ "/assets/terminal.css" | relative_url }}" />
|
| 28 |
+
<link rel="stylesheet" href="{{ "/assets/markdown.css" | relative_url }}" />
|
| 29 |
+
<script defer src="{{ "/assets/theme.js" | relative_url }}"></script>
|
| 30 |
+
</head>
|
| 31 |
+
|
| 32 |
+
<body>
|
| 33 |
+
<a class="skip-link" href="#content">Skip to content</a>
|
| 34 |
+
|
| 35 |
+
<header class="shell">
|
| 36 |
+
<div class="shell__frame" role="banner">
|
| 37 |
+
<div class="shell__titlebar">
|
| 38 |
+
<div class="brand" aria-label="OpenClaw docs terminal">
|
| 39 |
+
<img
|
| 40 |
+
class="brand__logo"
|
| 41 |
+
src="{{ "/assets/pixel-lobster.svg" | relative_url }}"
|
| 42 |
+
alt=""
|
| 43 |
+
width="40"
|
| 44 |
+
height="40"
|
| 45 |
+
decoding="async"
|
| 46 |
+
/>
|
| 47 |
+
<div class="brand__text">
|
| 48 |
+
<div class="brand__name">OpenClaw</div>
|
| 49 |
+
<div class="brand__hint">docs // lobster terminal</div>
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
| 52 |
+
|
| 53 |
+
<div class="titlebar__actions">
|
| 54 |
+
<a class="titlebar__cta" href="https://github.com/openclaw/openclaw">
|
| 55 |
+
<span class="titlebar__cta-label">GitHub</span>
|
| 56 |
+
<span class="titlebar__cta-meta">repo</span>
|
| 57 |
+
</a>
|
| 58 |
+
<a class="titlebar__cta titlebar__cta--accent" href="https://github.com/openclaw/openclaw/releases/latest">
|
| 59 |
+
<span class="titlebar__cta-label">Download</span>
|
| 60 |
+
<span class="titlebar__cta-meta">latest</span>
|
| 61 |
+
</a>
|
| 62 |
+
</div>
|
| 63 |
+
</div>
|
| 64 |
+
|
| 65 |
+
<div class="shell__nav" aria-label="Primary">
|
| 66 |
+
<nav class="nav">
|
| 67 |
+
{% assign nav = site.nav | default: empty %}
|
| 68 |
+
{% for item in nav %}
|
| 69 |
+
{% assign item_url = item.url | relative_url %}
|
| 70 |
+
<a class="nav__link" href="{{ item_url }}">
|
| 71 |
+
<span class="nav__chev">›</span>{{ item.title }}
|
| 72 |
+
</a>
|
| 73 |
+
{% endfor %}
|
| 74 |
+
</nav>
|
| 75 |
+
|
| 76 |
+
<div class="shell__status" aria-hidden="true">
|
| 77 |
+
<span class="status__dot"></span>
|
| 78 |
+
<span class="status__text">
|
| 79 |
+
{% if page.url == "/" %}
|
| 80 |
+
ready
|
| 81 |
+
{% else %}
|
| 82 |
+
viewing: {{ page.path }}
|
| 83 |
+
{% endif %}
|
| 84 |
+
</span>
|
| 85 |
+
</div>
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
</header>
|
| 89 |
+
|
| 90 |
+
<main id="content" class="content" role="main">
|
| 91 |
+
<div class="terminal">
|
| 92 |
+
<div class="terminal__prompt" aria-hidden="true">
|
| 93 |
+
<span class="prompt__user">openclaw</span>@<span class="prompt__host">openclaw</span>:<span class="prompt__path">~/docs</span>$<span class="prompt__cmd">
|
| 94 |
+
{% if page.url == "/" %}cat index.md{% else %}less {{ page.path }}{% endif %}
|
| 95 |
+
</span>
|
| 96 |
+
</div>
|
| 97 |
+
|
| 98 |
+
{% if page.summary %}
|
| 99 |
+
<p class="terminal__summary">{{ page.summary }}</p>
|
| 100 |
+
{% endif %}
|
| 101 |
+
|
| 102 |
+
{% if page.read_when %}
|
| 103 |
+
<details class="terminal__meta">
|
| 104 |
+
<summary>Read when…</summary>
|
| 105 |
+
<ul>
|
| 106 |
+
{% for hint in page.read_when %}
|
| 107 |
+
<li>{{ hint }}</li>
|
| 108 |
+
{% endfor %}
|
| 109 |
+
</ul>
|
| 110 |
+
</details>
|
| 111 |
+
{% endif %}
|
| 112 |
+
|
| 113 |
+
<article class="markdown">
|
| 114 |
+
{{ content }}
|
| 115 |
+
</article>
|
| 116 |
+
|
| 117 |
+
<footer class="terminal__footer" role="contentinfo">
|
| 118 |
+
<div class="footer__line">
|
| 119 |
+
<span class="footer__sig">openclaw.ai</span>
|
| 120 |
+
<span class="footer__sep">·</span>
|
| 121 |
+
<a href="https://github.com/openclaw/openclaw">source</a>
|
| 122 |
+
<span class="footer__sep">·</span>
|
| 123 |
+
<a href="https://github.com/openclaw/openclaw/releases">releases</a>
|
| 124 |
+
</div>
|
| 125 |
+
<div class="footer__hint" aria-hidden="true">
|
| 126 |
+
tip: press <kbd>F2</kbd> (Mac: <kbd>fn</kbd>+<kbd>F2</kbd>) to flip
|
| 127 |
+
the universe
|
| 128 |
+
</div>
|
| 129 |
+
<div class="footer__actions">
|
| 130 |
+
<button
|
| 131 |
+
class="theme-toggle"
|
| 132 |
+
type="button"
|
| 133 |
+
data-theme-toggle
|
| 134 |
+
aria-label="Toggle theme (F2; on Mac: fn+F2)"
|
| 135 |
+
aria-pressed="false"
|
| 136 |
+
>
|
| 137 |
+
<span class="theme-toggle__key">F2</span>
|
| 138 |
+
<span class="theme-toggle__label" data-theme-label>theme</span>
|
| 139 |
+
</button>
|
| 140 |
+
</div>
|
| 141 |
+
</footer>
|
| 142 |
+
</div>
|
| 143 |
+
</main>
|
| 144 |
+
</body>
|
| 145 |
+
</html>
|
docs/assets/markdown.css
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.markdown {
|
| 2 |
+
margin-top: 18px;
|
| 3 |
+
line-height: 1.7;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
.mdx-content > h1:first-of-type {
|
| 7 |
+
display: none;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
.markdown h1,
|
| 11 |
+
.markdown h2,
|
| 12 |
+
.markdown h3,
|
| 13 |
+
.markdown h4 {
|
| 14 |
+
font-family: var(--font-pixel);
|
| 15 |
+
letter-spacing: 0.04em;
|
| 16 |
+
line-height: 1.15;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
.markdown h1 {
|
| 20 |
+
font-size: clamp(28px, 4vw, 44px);
|
| 21 |
+
margin: 26px 0 10px;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.markdown h2 {
|
| 25 |
+
font-size: 22px;
|
| 26 |
+
margin: 26px 0 10px;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
.markdown h3 {
|
| 30 |
+
font-size: 18px;
|
| 31 |
+
margin: 22px 0 8px;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
.markdown p {
|
| 35 |
+
margin: 0 0 14px;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.markdown a {
|
| 39 |
+
color: var(--link);
|
| 40 |
+
text-decoration: none;
|
| 41 |
+
border-bottom: 1px dotted color-mix(in oklab, var(--link) 65%, transparent);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.markdown a:hover {
|
| 45 |
+
color: var(--link2);
|
| 46 |
+
border-bottom-color: color-mix(in oklab, var(--link2) 75%, transparent);
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.markdown hr {
|
| 50 |
+
border: 0;
|
| 51 |
+
height: 1px;
|
| 52 |
+
background: linear-gradient(
|
| 53 |
+
90deg,
|
| 54 |
+
transparent,
|
| 55 |
+
color-mix(in oklab, var(--frame-border) 30%, transparent),
|
| 56 |
+
transparent
|
| 57 |
+
);
|
| 58 |
+
margin: 26px 0;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.markdown blockquote {
|
| 62 |
+
margin: 18px 0;
|
| 63 |
+
padding: 14px 14px;
|
| 64 |
+
border-radius: var(--radius-sm);
|
| 65 |
+
background: color-mix(in oklab, var(--panel) 70%, transparent);
|
| 66 |
+
border-left: 6px solid color-mix(in oklab, var(--accent) 60%, transparent);
|
| 67 |
+
color: var(--muted);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.markdown ul,
|
| 71 |
+
.markdown ol {
|
| 72 |
+
margin: 0 0 14px 22px;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.markdown li {
|
| 76 |
+
margin: 4px 0;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.markdown img {
|
| 80 |
+
max-width: 100%;
|
| 81 |
+
height: auto;
|
| 82 |
+
border-radius: 12px;
|
| 83 |
+
border: 1px solid color-mix(in oklab, var(--frame-border) 20%, transparent);
|
| 84 |
+
box-shadow: 0 12px 0 -8px rgba(0, 0, 0, 0.18);
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.showcase-link {
|
| 88 |
+
position: relative;
|
| 89 |
+
display: inline-flex;
|
| 90 |
+
align-items: center;
|
| 91 |
+
gap: 6px;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.showcase-preview {
|
| 95 |
+
position: absolute;
|
| 96 |
+
left: 50%;
|
| 97 |
+
top: 100%;
|
| 98 |
+
width: min(420px, 80vw);
|
| 99 |
+
padding: 8px;
|
| 100 |
+
border-radius: 14px;
|
| 101 |
+
background: color-mix(in oklab, var(--panel) 92%, transparent);
|
| 102 |
+
border: 1px solid color-mix(in oklab, var(--frame-border) 30%, transparent);
|
| 103 |
+
box-shadow: 0 18px 40px -18px rgba(0, 0, 0, 0.55);
|
| 104 |
+
transform: translate(-50%, 10px) scale(0.98);
|
| 105 |
+
opacity: 0;
|
| 106 |
+
visibility: hidden;
|
| 107 |
+
pointer-events: none;
|
| 108 |
+
z-index: 20;
|
| 109 |
+
transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s ease;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.showcase-preview img {
|
| 113 |
+
width: 100%;
|
| 114 |
+
height: auto;
|
| 115 |
+
border-radius: 10px;
|
| 116 |
+
border: 1px solid color-mix(in oklab, var(--frame-border) 25%, transparent);
|
| 117 |
+
box-shadow: none;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.showcase-link:hover .showcase-preview,
|
| 121 |
+
.showcase-link:focus-within .showcase-preview {
|
| 122 |
+
opacity: 1;
|
| 123 |
+
visibility: visible;
|
| 124 |
+
transform: translate(-50%, 6px) scale(1);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
@media (hover: none) {
|
| 128 |
+
.showcase-preview {
|
| 129 |
+
display: none;
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.markdown code {
|
| 134 |
+
font-family: var(--font-body);
|
| 135 |
+
font-size: 0.95em;
|
| 136 |
+
padding: 0.15em 0.35em;
|
| 137 |
+
border-radius: 8px;
|
| 138 |
+
background: color-mix(in oklab, var(--panel) 70%, var(--code-bg));
|
| 139 |
+
border: 1px solid color-mix(in oklab, var(--frame-border) 18%, transparent);
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
.markdown pre {
|
| 143 |
+
background: var(--code-bg);
|
| 144 |
+
color: var(--code-fg);
|
| 145 |
+
padding: 14px 14px;
|
| 146 |
+
border-radius: var(--radius-sm);
|
| 147 |
+
border: 1px solid color-mix(in oklab, var(--frame-border) 18%, transparent);
|
| 148 |
+
overflow-x: auto;
|
| 149 |
+
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--code-accent) 12%, transparent);
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.markdown pre code {
|
| 153 |
+
background: transparent;
|
| 154 |
+
border: 0;
|
| 155 |
+
padding: 0;
|
| 156 |
+
color: inherit;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.markdown table {
|
| 160 |
+
width: 100%;
|
| 161 |
+
border-collapse: collapse;
|
| 162 |
+
margin: 16px 0 22px;
|
| 163 |
+
border: 1px solid color-mix(in oklab, var(--frame-border) 20%, transparent);
|
| 164 |
+
border-radius: var(--radius-sm);
|
| 165 |
+
overflow: hidden;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.markdown th,
|
| 169 |
+
.markdown td {
|
| 170 |
+
padding: 10px 10px;
|
| 171 |
+
border-bottom: 1px solid color-mix(in oklab, var(--frame-border) 15%, transparent);
|
| 172 |
+
vertical-align: top;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.markdown th {
|
| 176 |
+
background: color-mix(in oklab, var(--panel2) 85%, transparent);
|
| 177 |
+
font-family: var(--font-pixel);
|
| 178 |
+
letter-spacing: 0.06em;
|
| 179 |
+
}
|
docs/assets/openclaw-logo-text-dark.png
ADDED
|
Git LFS Details
|
docs/assets/openclaw-logo-text.png
ADDED
|
docs/assets/pixel-lobster.svg
ADDED
|
|
docs/assets/showcase/agents-ui.jpg
ADDED
|
Git LFS Details
|
docs/assets/showcase/bambu-cli.png
ADDED
|
Git LFS Details
|
docs/assets/showcase/codexmonitor.png
ADDED
|
Git LFS Details
|
docs/assets/showcase/gohome-grafana.png
ADDED
|
Git LFS Details
|
docs/assets/showcase/ios-testflight.jpg
ADDED
|
Git LFS Details
|
docs/assets/showcase/oura-health.png
ADDED
|
Git LFS Details
|
docs/assets/showcase/padel-cli.svg
ADDED
|
|
docs/assets/showcase/padel-screenshot.jpg
ADDED
|
docs/assets/showcase/papla-tts.jpg
ADDED
|
docs/assets/showcase/pr-review-telegram.jpg
ADDED
|
Git LFS Details
|
docs/assets/showcase/roborock-screenshot.jpg
ADDED
|
docs/assets/showcase/roborock-status.svg
ADDED
|
|
docs/assets/showcase/roof-camera-sky.jpg
ADDED
|
Git LFS Details
|
docs/assets/showcase/snag.png
ADDED
|
Git LFS Details
|
docs/assets/showcase/tesco-shop.jpg
ADDED
|
docs/assets/showcase/wienerlinien.png
ADDED
|
Git LFS Details
|
docs/assets/showcase/wine-cellar-skill.jpg
ADDED
|
docs/assets/showcase/winix-air-purifier.jpg
ADDED
|
Git LFS Details
|
docs/assets/showcase/xuezh-pronunciation.jpeg
ADDED
|
docs/assets/terminal.css
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
--font-body: "Fragment Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
| 3 |
+
--font-pixel: "Pixelify Sans", system-ui, sans-serif;
|
| 4 |
+
--radius: 14px;
|
| 5 |
+
--radius-sm: 10px;
|
| 6 |
+
--border: 2px;
|
| 7 |
+
--shadow-px: 0 0 0 var(--border) var(--frame-border), 0 12px 0 -4px rgba(0, 0, 0, 0.25);
|
| 8 |
+
--scanline-size: 6px;
|
| 9 |
+
--scanline-opacity: 0.08;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
html[data-theme="light"],
|
| 13 |
+
html[data-theme="auto"] {
|
| 14 |
+
--bg0: #fbf4e7;
|
| 15 |
+
--bg1: #fffaf0;
|
| 16 |
+
--panel: #fffdf8;
|
| 17 |
+
--panel2: #fff6e5;
|
| 18 |
+
--text: #10221c;
|
| 19 |
+
--muted: #3e5a50;
|
| 20 |
+
--faint: #6b7f77;
|
| 21 |
+
--link: #0f6b4c;
|
| 22 |
+
--link2: #ff4f40;
|
| 23 |
+
--accent: #ff4f40;
|
| 24 |
+
--accent2: #00b88a;
|
| 25 |
+
--frame-border: #1b2e27;
|
| 26 |
+
--code-bg: #0b1713;
|
| 27 |
+
--code-fg: #eafff6;
|
| 28 |
+
--code-accent: #67ff9b;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
html[data-theme="dark"] {
|
| 32 |
+
--bg0: #0b1a22;
|
| 33 |
+
--bg1: #0a1720;
|
| 34 |
+
--panel: #0e231f;
|
| 35 |
+
--panel2: #102a24;
|
| 36 |
+
--text: #c9eadc;
|
| 37 |
+
--muted: #8ab8aa;
|
| 38 |
+
--faint: #699b8d;
|
| 39 |
+
--link: #6fe8c7;
|
| 40 |
+
--link2: #ff7b63;
|
| 41 |
+
--accent: #ff4f40;
|
| 42 |
+
--accent2: #5fdfa2;
|
| 43 |
+
--frame-border: #6fbfa8;
|
| 44 |
+
--code-bg: #091814;
|
| 45 |
+
--code-fg: #d7f5e8;
|
| 46 |
+
--code-accent: #5fdfa2;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
@media (prefers-color-scheme: dark) {
|
| 50 |
+
html[data-theme="auto"] {
|
| 51 |
+
--bg0: #0b1a22;
|
| 52 |
+
--bg1: #0a1720;
|
| 53 |
+
--panel: #0e231f;
|
| 54 |
+
--panel2: #102a24;
|
| 55 |
+
--text: #c9eadc;
|
| 56 |
+
--muted: #8ab8aa;
|
| 57 |
+
--faint: #699b8d;
|
| 58 |
+
--link: #6fe8c7;
|
| 59 |
+
--link2: #ff7b63;
|
| 60 |
+
--accent: #ff4f40;
|
| 61 |
+
--accent2: #5fdfa2;
|
| 62 |
+
--frame-border: #6fbfa8;
|
| 63 |
+
--code-bg: #091814;
|
| 64 |
+
--code-fg: #d7f5e8;
|
| 65 |
+
--code-accent: #5fdfa2;
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
* {
|
| 70 |
+
box-sizing: border-box;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
html {
|
| 74 |
+
height: 100%;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
body {
|
| 78 |
+
min-height: 100%;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
body {
|
| 82 |
+
margin: 0;
|
| 83 |
+
font-family: var(--font-body);
|
| 84 |
+
color: var(--text);
|
| 85 |
+
background:
|
| 86 |
+
radial-gradient(1100px 700px at 20% -10%, color-mix(in oklab, var(--accent) 18%, transparent), transparent 55%),
|
| 87 |
+
radial-gradient(900px 600px at 95% 10%, color-mix(in oklab, var(--accent2) 14%, transparent), transparent 60%),
|
| 88 |
+
radial-gradient(900px 600px at 50% 120%, color-mix(in oklab, var(--link) 10%, transparent), transparent 55%),
|
| 89 |
+
linear-gradient(180deg, var(--bg0), var(--bg1));
|
| 90 |
+
overflow-x: hidden;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
body::before,
|
| 94 |
+
body::after {
|
| 95 |
+
display: none;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.skip-link {
|
| 99 |
+
position: absolute;
|
| 100 |
+
top: 10px;
|
| 101 |
+
left: 10px;
|
| 102 |
+
z-index: 10;
|
| 103 |
+
padding: 10px 12px;
|
| 104 |
+
border-radius: 999px;
|
| 105 |
+
background: var(--panel);
|
| 106 |
+
color: var(--text);
|
| 107 |
+
text-decoration: none;
|
| 108 |
+
transform: translateY(-130%);
|
| 109 |
+
box-shadow: var(--shadow-px);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.skip-link:focus {
|
| 113 |
+
transform: translateY(0);
|
| 114 |
+
outline: none;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.shell {
|
| 118 |
+
position: sticky;
|
| 119 |
+
top: 0;
|
| 120 |
+
z-index: 100;
|
| 121 |
+
padding: 22px 16px 10px;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.shell__frame {
|
| 125 |
+
max-width: 1120px;
|
| 126 |
+
margin: 0 auto;
|
| 127 |
+
border-radius: var(--radius);
|
| 128 |
+
background:
|
| 129 |
+
linear-gradient(180deg, color-mix(in oklab, var(--panel2) 88%, transparent), color-mix(in oklab, var(--panel) 92%, transparent));
|
| 130 |
+
box-shadow: var(--shadow-px);
|
| 131 |
+
border: var(--border) solid var(--frame-border);
|
| 132 |
+
overflow: hidden;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.shell__titlebar {
|
| 136 |
+
display: flex;
|
| 137 |
+
align-items: center;
|
| 138 |
+
justify-content: space-between;
|
| 139 |
+
gap: 12px;
|
| 140 |
+
padding: 14px 14px 12px;
|
| 141 |
+
background:
|
| 142 |
+
linear-gradient(90deg, color-mix(in oklab, var(--accent) 26%, transparent), transparent 42%),
|
| 143 |
+
linear-gradient(180deg, color-mix(in oklab, var(--panel) 90%, #000 10%), color-mix(in oklab, var(--panel2) 90%, #000 10%));
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.brand {
|
| 147 |
+
display: flex;
|
| 148 |
+
align-items: center;
|
| 149 |
+
gap: 12px;
|
| 150 |
+
min-width: 0;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.brand__logo {
|
| 154 |
+
flex: 0 0 auto;
|
| 155 |
+
width: 40px;
|
| 156 |
+
height: 40px;
|
| 157 |
+
image-rendering: pixelated;
|
| 158 |
+
filter: drop-shadow(0 2px 0 color-mix(in oklab, var(--frame-border) 80%, transparent));
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
.brand__text {
|
| 162 |
+
min-width: 0;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
.brand__name {
|
| 166 |
+
font-family: var(--font-pixel);
|
| 167 |
+
letter-spacing: 0.14em;
|
| 168 |
+
font-weight: 700;
|
| 169 |
+
font-size: 18px;
|
| 170 |
+
line-height: 1.1;
|
| 171 |
+
text-shadow: 0 1px 0 color-mix(in oklab, var(--frame-border) 55%, transparent);
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.brand__hint {
|
| 175 |
+
margin-top: 2px;
|
| 176 |
+
font-size: 12px;
|
| 177 |
+
color: var(--muted);
|
| 178 |
+
white-space: nowrap;
|
| 179 |
+
overflow: hidden;
|
| 180 |
+
text-overflow: ellipsis;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
.titlebar__actions {
|
| 184 |
+
display: flex;
|
| 185 |
+
align-items: center;
|
| 186 |
+
gap: 10px;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
.titlebar__cta {
|
| 190 |
+
display: inline-flex;
|
| 191 |
+
align-items: center;
|
| 192 |
+
gap: 8px;
|
| 193 |
+
padding: 8px 12px 8px 10px;
|
| 194 |
+
border-radius: 12px;
|
| 195 |
+
background:
|
| 196 |
+
linear-gradient(140deg, color-mix(in oklab, var(--accent) 10%, transparent), transparent 60%),
|
| 197 |
+
color-mix(in oklab, var(--panel) 92%, transparent);
|
| 198 |
+
border: var(--border) solid color-mix(in oklab, var(--frame-border) 80%, transparent);
|
| 199 |
+
color: var(--text);
|
| 200 |
+
text-decoration: none;
|
| 201 |
+
box-shadow:
|
| 202 |
+
0 6px 0 -3px rgba(0, 0, 0, 0.25),
|
| 203 |
+
inset 0 0 0 1px color-mix(in oklab, var(--panel2) 55%, transparent);
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.titlebar__cta:hover {
|
| 207 |
+
border-color: color-mix(in oklab, var(--accent2) 45%, transparent);
|
| 208 |
+
box-shadow:
|
| 209 |
+
0 0 0 2px color-mix(in oklab, var(--accent2) 30%, transparent),
|
| 210 |
+
0 6px 0 -3px rgba(0, 0, 0, 0.25);
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
.titlebar__cta:active {
|
| 214 |
+
transform: translateY(1px);
|
| 215 |
+
box-shadow: 0 4px 0 -3px rgba(0, 0, 0, 0.25);
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
.titlebar__cta:focus-visible {
|
| 219 |
+
outline: 3px solid color-mix(in oklab, var(--accent2) 60%, transparent);
|
| 220 |
+
outline-offset: 2px;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.titlebar__cta--accent {
|
| 224 |
+
background:
|
| 225 |
+
linear-gradient(120deg, color-mix(in oklab, var(--accent) 22%, transparent), transparent 70%),
|
| 226 |
+
color-mix(in oklab, var(--panel) 88%, transparent);
|
| 227 |
+
border-color: color-mix(in oklab, var(--accent) 60%, var(--frame-border));
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
.titlebar__cta-label {
|
| 231 |
+
font-family: var(--font-pixel);
|
| 232 |
+
letter-spacing: 0.12em;
|
| 233 |
+
font-size: 12px;
|
| 234 |
+
text-transform: uppercase;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.titlebar__cta-meta {
|
| 238 |
+
display: inline-flex;
|
| 239 |
+
align-items: center;
|
| 240 |
+
justify-content: center;
|
| 241 |
+
height: 20px;
|
| 242 |
+
padding: 0 8px;
|
| 243 |
+
border-radius: 999px;
|
| 244 |
+
font-size: 11px;
|
| 245 |
+
text-transform: uppercase;
|
| 246 |
+
letter-spacing: 0.08em;
|
| 247 |
+
background: var(--code-bg);
|
| 248 |
+
color: var(--code-accent);
|
| 249 |
+
border: 1px solid color-mix(in oklab, var(--code-accent) 30%, transparent);
|
| 250 |
+
box-shadow: inset 0 0 12px color-mix(in oklab, var(--code-accent) 25%, transparent);
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
.theme-toggle {
|
| 254 |
+
display: inline-flex;
|
| 255 |
+
align-items: center;
|
| 256 |
+
gap: 10px;
|
| 257 |
+
padding: 9px 10px;
|
| 258 |
+
border-radius: 12px;
|
| 259 |
+
background: color-mix(in oklab, var(--panel) 92%, transparent);
|
| 260 |
+
border: var(--border) solid var(--frame-border);
|
| 261 |
+
color: var(--text);
|
| 262 |
+
cursor: pointer;
|
| 263 |
+
box-shadow: 0 6px 0 -3px rgba(0, 0, 0, 0.25);
|
| 264 |
+
user-select: none;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.theme-toggle:active {
|
| 268 |
+
transform: translateY(1px);
|
| 269 |
+
box-shadow: 0 4px 0 -3px rgba(0, 0, 0, 0.25);
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.theme-toggle__key {
|
| 273 |
+
display: inline-flex;
|
| 274 |
+
align-items: center;
|
| 275 |
+
justify-content: center;
|
| 276 |
+
width: 36px;
|
| 277 |
+
height: 28px;
|
| 278 |
+
border-radius: 9px;
|
| 279 |
+
background: var(--code-bg);
|
| 280 |
+
color: var(--code-accent);
|
| 281 |
+
border: 1px solid color-mix(in oklab, var(--code-accent) 30%, transparent);
|
| 282 |
+
font-weight: 700;
|
| 283 |
+
font-size: 12px;
|
| 284 |
+
letter-spacing: 0.06em;
|
| 285 |
+
text-shadow: 0 0 14px color-mix(in oklab, var(--code-accent) 55%, transparent);
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
.theme-toggle__label {
|
| 289 |
+
font-family: var(--font-pixel);
|
| 290 |
+
letter-spacing: 0.12em;
|
| 291 |
+
font-size: 12px;
|
| 292 |
+
text-transform: uppercase;
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
.theme-toggle:focus-visible {
|
| 296 |
+
outline: 3px solid color-mix(in oklab, var(--accent2) 60%, transparent);
|
| 297 |
+
outline-offset: 2px;
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
.shell__nav {
|
| 301 |
+
display: flex;
|
| 302 |
+
align-items: center;
|
| 303 |
+
justify-content: space-between;
|
| 304 |
+
gap: 10px;
|
| 305 |
+
padding: 10px 14px 12px;
|
| 306 |
+
border-top: 1px solid color-mix(in oklab, var(--frame-border) 25%, transparent);
|
| 307 |
+
background: linear-gradient(180deg, transparent, color-mix(in oklab, var(--panel2) 78%, transparent));
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
.nav {
|
| 311 |
+
display: flex;
|
| 312 |
+
flex-wrap: wrap;
|
| 313 |
+
gap: 12px;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
.nav__link {
|
| 317 |
+
display: inline-flex;
|
| 318 |
+
align-items: center;
|
| 319 |
+
gap: 8px;
|
| 320 |
+
padding: 6px 10px;
|
| 321 |
+
border-radius: 999px;
|
| 322 |
+
text-decoration: none;
|
| 323 |
+
color: var(--text);
|
| 324 |
+
background: color-mix(in oklab, var(--panel) 85%, transparent);
|
| 325 |
+
border: 1px solid color-mix(in oklab, var(--frame-border) 20%, transparent);
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
.nav__link:hover {
|
| 329 |
+
border-color: color-mix(in oklab, var(--accent2) 45%, transparent);
|
| 330 |
+
box-shadow: 0 0 0 2px color-mix(in oklab, var(--accent2) 30%, transparent);
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.nav__chev {
|
| 334 |
+
color: var(--accent);
|
| 335 |
+
font-weight: 700;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.shell__status {
|
| 339 |
+
display: inline-flex;
|
| 340 |
+
align-items: center;
|
| 341 |
+
gap: 10px;
|
| 342 |
+
color: var(--muted);
|
| 343 |
+
font-size: 12px;
|
| 344 |
+
white-space: nowrap;
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
.status__dot {
|
| 348 |
+
width: 10px;
|
| 349 |
+
height: 10px;
|
| 350 |
+
border-radius: 999px;
|
| 351 |
+
background: radial-gradient(circle at 30% 30%, var(--accent2), color-mix(in oklab, var(--accent2) 30%, #000));
|
| 352 |
+
box-shadow: 0 0 0 2px color-mix(in oklab, var(--accent2) 18%, transparent), 0 0 18px color-mix(in oklab, var(--accent2) 50%, transparent);
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
.content {
|
| 356 |
+
padding: 18px 16px 48px;
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
.terminal {
|
| 360 |
+
position: relative;
|
| 361 |
+
max-width: 1120px;
|
| 362 |
+
margin: 0 auto;
|
| 363 |
+
border-radius: var(--radius);
|
| 364 |
+
border: var(--border) solid var(--frame-border);
|
| 365 |
+
background: linear-gradient(180deg, color-mix(in oklab, var(--panel) 92%, transparent), color-mix(in oklab, var(--panel2) 86%, transparent));
|
| 366 |
+
box-shadow: var(--shadow-px);
|
| 367 |
+
padding: 18px 16px 16px;
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
.terminal__prompt {
|
| 371 |
+
display: block;
|
| 372 |
+
padding: 10px 12px;
|
| 373 |
+
border-radius: var(--radius-sm);
|
| 374 |
+
background: var(--code-bg);
|
| 375 |
+
color: var(--code-fg);
|
| 376 |
+
border: 1px solid color-mix(in oklab, var(--frame-border) 18%, transparent);
|
| 377 |
+
overflow-x: auto;
|
| 378 |
+
white-space: nowrap;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
.prompt__user {
|
| 382 |
+
color: var(--code-accent);
|
| 383 |
+
text-shadow: 0 0 16px color-mix(in oklab, var(--code-accent) 52%, transparent);
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
.prompt__host {
|
| 387 |
+
color: color-mix(in oklab, var(--code-fg) 92%, var(--accent2));
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
.prompt__path {
|
| 391 |
+
color: color-mix(in oklab, var(--code-fg) 78%, var(--link));
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
.prompt__cmd {
|
| 395 |
+
margin-left: 8px;
|
| 396 |
+
color: color-mix(in oklab, var(--code-fg) 90%, var(--accent));
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
.terminal__summary {
|
| 400 |
+
margin: 12px 2px 0;
|
| 401 |
+
color: var(--muted);
|
| 402 |
+
font-size: 14px;
|
| 403 |
+
line-height: 1.5;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
.terminal__meta {
|
| 407 |
+
margin: 12px 2px 0;
|
| 408 |
+
padding: 12px 12px;
|
| 409 |
+
border-radius: var(--radius-sm);
|
| 410 |
+
border: 1px dashed color-mix(in oklab, var(--frame-border) 25%, transparent);
|
| 411 |
+
background: color-mix(in oklab, var(--panel) 76%, transparent);
|
| 412 |
+
color: var(--faint);
|
| 413 |
+
font-size: 13px;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
.terminal__meta summary {
|
| 417 |
+
cursor: pointer;
|
| 418 |
+
font-family: var(--font-pixel);
|
| 419 |
+
letter-spacing: 0.08em;
|
| 420 |
+
text-transform: uppercase;
|
| 421 |
+
color: var(--text);
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
.terminal__meta ul {
|
| 425 |
+
margin: 10px 0 0 18px;
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
.terminal__footer {
|
| 429 |
+
margin-top: 22px;
|
| 430 |
+
padding-top: 16px;
|
| 431 |
+
border-top: 1px solid color-mix(in oklab, var(--frame-border) 20%, transparent);
|
| 432 |
+
color: var(--muted);
|
| 433 |
+
font-size: 13px;
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
.footer__actions {
|
| 437 |
+
margin-top: 14px;
|
| 438 |
+
display: flex;
|
| 439 |
+
justify-content: flex-end;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
.terminal__footer a {
|
| 443 |
+
color: var(--link);
|
| 444 |
+
text-decoration: none;
|
| 445 |
+
border-bottom: 1px dotted color-mix(in oklab, var(--link) 55%, transparent);
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
.terminal__footer a:hover {
|
| 449 |
+
color: var(--link2);
|
| 450 |
+
border-bottom-color: color-mix(in oklab, var(--link2) 75%, transparent);
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
.footer__hint {
|
| 454 |
+
margin-top: 8px;
|
| 455 |
+
color: var(--faint);
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
kbd {
|
| 459 |
+
font-family: var(--font-body);
|
| 460 |
+
font-size: 0.9em;
|
| 461 |
+
padding: 2px 6px;
|
| 462 |
+
border-radius: 8px;
|
| 463 |
+
border: 1px solid color-mix(in oklab, var(--frame-border) 18%, transparent);
|
| 464 |
+
background: color-mix(in oklab, var(--panel) 65%, transparent);
|
| 465 |
+
box-shadow: 0 6px 0 -4px rgba(0, 0, 0, 0.18);
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
@supports not (color: color-mix(in oklab, black, white)) {
|
| 469 |
+
body::before,
|
| 470 |
+
body::after {
|
| 471 |
+
opacity: 0.2;
|
| 472 |
+
}
|
| 473 |
+
}
|
docs/assets/theme.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const THEME_STORAGE_KEY = "openclaw:theme";
|
| 2 |
+
|
| 3 |
+
function safeGet(key) {
|
| 4 |
+
try {
|
| 5 |
+
return localStorage.getItem(key);
|
| 6 |
+
} catch {
|
| 7 |
+
return null;
|
| 8 |
+
}
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
function safeSet(key, value) {
|
| 12 |
+
try {
|
| 13 |
+
localStorage.setItem(key, value);
|
| 14 |
+
} catch {
|
| 15 |
+
// ignore
|
| 16 |
+
}
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
function preferredTheme() {
|
| 20 |
+
const stored = safeGet(THEME_STORAGE_KEY);
|
| 21 |
+
if (stored === "light" || stored === "dark") return stored;
|
| 22 |
+
return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
function applyTheme(theme) {
|
| 26 |
+
document.documentElement.dataset.theme = theme;
|
| 27 |
+
|
| 28 |
+
const toggle = document.querySelector("[data-theme-toggle]");
|
| 29 |
+
const label = document.querySelector("[data-theme-label]");
|
| 30 |
+
|
| 31 |
+
if (toggle instanceof HTMLButtonElement) toggle.setAttribute("aria-pressed", theme === "dark" ? "true" : "false");
|
| 32 |
+
if (label) label.textContent = theme === "dark" ? "dark" : "light";
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
function toggleTheme() {
|
| 36 |
+
const current = document.documentElement.dataset.theme === "dark" ? "dark" : "light";
|
| 37 |
+
const next = current === "dark" ? "light" : "dark";
|
| 38 |
+
safeSet(THEME_STORAGE_KEY, next);
|
| 39 |
+
applyTheme(next);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
applyTheme(preferredTheme());
|
| 43 |
+
|
| 44 |
+
document.addEventListener("click", (event) => {
|
| 45 |
+
const target = event.target;
|
| 46 |
+
const button = target instanceof Element ? target.closest("[data-theme-toggle]") : null;
|
| 47 |
+
if (button) toggleTheme();
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
document.addEventListener("keydown", (event) => {
|
| 51 |
+
if (event.key === "F2") {
|
| 52 |
+
event.preventDefault();
|
| 53 |
+
toggleTheme();
|
| 54 |
+
}
|
| 55 |
+
});
|
docs/automation/auth-monitoring.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
summary: "Monitor OAuth expiry for model providers"
|
| 3 |
+
read_when:
|
| 4 |
+
- Setting up auth expiry monitoring or alerts
|
| 5 |
+
- Automating Claude Code / Codex OAuth refresh checks
|
| 6 |
+
title: "Auth Monitoring"
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
# Auth monitoring
|
| 10 |
+
|
| 11 |
+
OpenClaw exposes OAuth expiry health via `openclaw models status`. Use that for
|
| 12 |
+
automation and alerting; scripts are optional extras for phone workflows.
|
| 13 |
+
|
| 14 |
+
## Preferred: CLI check (portable)
|
| 15 |
+
|
| 16 |
+
```bash
|
| 17 |
+
openclaw models status --check
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
Exit codes:
|
| 21 |
+
|
| 22 |
+
- `0`: OK
|
| 23 |
+
- `1`: expired or missing credentials
|
| 24 |
+
- `2`: expiring soon (within 24h)
|
| 25 |
+
|
| 26 |
+
This works in cron/systemd and requires no extra scripts.
|
| 27 |
+
|
| 28 |
+
## Optional scripts (ops / phone workflows)
|
| 29 |
+
|
| 30 |
+
These live under `scripts/` and are **optional**. They assume SSH access to the
|
| 31 |
+
gateway host and are tuned for systemd + Termux.
|
| 32 |
+
|
| 33 |
+
- `scripts/claude-auth-status.sh` now uses `openclaw models status --json` as the
|
| 34 |
+
source of truth (falling back to direct file reads if the CLI is unavailable),
|
| 35 |
+
so keep `openclaw` on `PATH` for timers.
|
| 36 |
+
- `scripts/auth-monitor.sh`: cron/systemd timer target; sends alerts (ntfy or phone).
|
| 37 |
+
- `scripts/systemd/openclaw-auth-monitor.{service,timer}`: systemd user timer.
|
| 38 |
+
- `scripts/claude-auth-status.sh`: Claude Code + OpenClaw auth checker (full/json/simple).
|
| 39 |
+
- `scripts/mobile-reauth.sh`: guided re‑auth flow over SSH.
|
| 40 |
+
- `scripts/termux-quick-auth.sh`: one‑tap widget status + open auth URL.
|
| 41 |
+
- `scripts/termux-auth-widget.sh`: full guided widget flow.
|
| 42 |
+
- `scripts/termux-sync-widget.sh`: sync Claude Code creds → OpenClaw.
|
| 43 |
+
|
| 44 |
+
If you don’t need phone automation or systemd timers, skip these scripts.
|
docs/automation/cron-jobs.md
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
summary: "Cron jobs + wakeups for the Gateway scheduler"
|
| 3 |
+
read_when:
|
| 4 |
+
- Scheduling background jobs or wakeups
|
| 5 |
+
- Wiring automation that should run with or alongside heartbeats
|
| 6 |
+
- Deciding between heartbeat and cron for scheduled tasks
|
| 7 |
+
title: "Cron Jobs"
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# Cron jobs (Gateway scheduler)
|
| 11 |
+
|
| 12 |
+
> **Cron vs Heartbeat?** See [Cron vs Heartbeat](/automation/cron-vs-heartbeat) for guidance on when to use each.
|
| 13 |
+
|
| 14 |
+
Cron is the Gateway’s built-in scheduler. It persists jobs, wakes the agent at
|
| 15 |
+
the right time, and can optionally deliver output back to a chat.
|
| 16 |
+
|
| 17 |
+
If you want _“run this every morning”_ or _“poke the agent in 20 minutes”_,
|
| 18 |
+
cron is the mechanism.
|
| 19 |
+
|
| 20 |
+
## TL;DR
|
| 21 |
+
|
| 22 |
+
- Cron runs **inside the Gateway** (not inside the model).
|
| 23 |
+
- Jobs persist under `~/.openclaw/cron/` so restarts don’t lose schedules.
|
| 24 |
+
- Two execution styles:
|
| 25 |
+
- **Main session**: enqueue a system event, then run on the next heartbeat.
|
| 26 |
+
- **Isolated**: run a dedicated agent turn in `cron:<jobId>`, optionally deliver output.
|
| 27 |
+
- Wakeups are first-class: a job can request “wake now” vs “next heartbeat”.
|
| 28 |
+
|
| 29 |
+
## Quick start (actionable)
|
| 30 |
+
|
| 31 |
+
Create a one-shot reminder, verify it exists, and run it immediately:
|
| 32 |
+
|
| 33 |
+
```bash
|
| 34 |
+
openclaw cron add \
|
| 35 |
+
--name "Reminder" \
|
| 36 |
+
--at "2026-02-01T16:00:00Z" \
|
| 37 |
+
--session main \
|
| 38 |
+
--system-event "Reminder: check the cron docs draft" \
|
| 39 |
+
--wake now \
|
| 40 |
+
--delete-after-run
|
| 41 |
+
|
| 42 |
+
openclaw cron list
|
| 43 |
+
openclaw cron run <job-id> --force
|
| 44 |
+
openclaw cron runs --id <job-id>
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
Schedule a recurring isolated job with delivery:
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
openclaw cron add \
|
| 51 |
+
--name "Morning brief" \
|
| 52 |
+
--cron "0 7 * * *" \
|
| 53 |
+
--tz "America/Los_Angeles" \
|
| 54 |
+
--session isolated \
|
| 55 |
+
--message "Summarize overnight updates." \
|
| 56 |
+
--deliver \
|
| 57 |
+
--channel slack \
|
| 58 |
+
--to "channel:C1234567890"
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
## Tool-call equivalents (Gateway cron tool)
|
| 62 |
+
|
| 63 |
+
For the canonical JSON shapes and examples, see [JSON schema for tool calls](/automation/cron-jobs#json-schema-for-tool-calls).
|
| 64 |
+
|
| 65 |
+
## Where cron jobs are stored
|
| 66 |
+
|
| 67 |
+
Cron jobs are persisted on the Gateway host at `~/.openclaw/cron/jobs.json` by default.
|
| 68 |
+
The Gateway loads the file into memory and writes it back on changes, so manual edits
|
| 69 |
+
are only safe when the Gateway is stopped. Prefer `openclaw cron add/edit` or the cron
|
| 70 |
+
tool call API for changes.
|
| 71 |
+
|
| 72 |
+
## Beginner-friendly overview
|
| 73 |
+
|
| 74 |
+
Think of a cron job as: **when** to run + **what** to do.
|
| 75 |
+
|
| 76 |
+
1. **Choose a schedule**
|
| 77 |
+
- One-shot reminder → `schedule.kind = "at"` (CLI: `--at`)
|
| 78 |
+
- Repeating job → `schedule.kind = "every"` or `schedule.kind = "cron"`
|
| 79 |
+
- If your ISO timestamp omits a timezone, it is treated as **UTC**.
|
| 80 |
+
|
| 81 |
+
2. **Choose where it runs**
|
| 82 |
+
- `sessionTarget: "main"` → run during the next heartbeat with main context.
|
| 83 |
+
- `sessionTarget: "isolated"` → run a dedicated agent turn in `cron:<jobId>`.
|
| 84 |
+
|
| 85 |
+
3. **Choose the payload**
|
| 86 |
+
- Main session → `payload.kind = "systemEvent"`
|
| 87 |
+
- Isolated session → `payload.kind = "agentTurn"`
|
| 88 |
+
|
| 89 |
+
Optional: `deleteAfterRun: true` removes successful one-shot jobs from the store.
|
| 90 |
+
|
| 91 |
+
## Concepts
|
| 92 |
+
|
| 93 |
+
### Jobs
|
| 94 |
+
|
| 95 |
+
A cron job is a stored record with:
|
| 96 |
+
|
| 97 |
+
- a **schedule** (when it should run),
|
| 98 |
+
- a **payload** (what it should do),
|
| 99 |
+
- optional **delivery** (where output should be sent).
|
| 100 |
+
- optional **agent binding** (`agentId`): run the job under a specific agent; if
|
| 101 |
+
missing or unknown, the gateway falls back to the default agent.
|
| 102 |
+
|
| 103 |
+
Jobs are identified by a stable `jobId` (used by CLI/Gateway APIs).
|
| 104 |
+
In agent tool calls, `jobId` is canonical; legacy `id` is accepted for compatibility.
|
| 105 |
+
Jobs can optionally auto-delete after a successful one-shot run via `deleteAfterRun: true`.
|
| 106 |
+
|
| 107 |
+
### Schedules
|
| 108 |
+
|
| 109 |
+
Cron supports three schedule kinds:
|
| 110 |
+
|
| 111 |
+
- `at`: one-shot timestamp (ms since epoch). Gateway accepts ISO 8601 and coerces to UTC.
|
| 112 |
+
- `every`: fixed interval (ms).
|
| 113 |
+
- `cron`: 5-field cron expression with optional IANA timezone.
|
| 114 |
+
|
| 115 |
+
Cron expressions use `croner`. If a timezone is omitted, the Gateway host’s
|
| 116 |
+
local timezone is used.
|
| 117 |
+
|
| 118 |
+
### Main vs isolated execution
|
| 119 |
+
|
| 120 |
+
#### Main session jobs (system events)
|
| 121 |
+
|
| 122 |
+
Main jobs enqueue a system event and optionally wake the heartbeat runner.
|
| 123 |
+
They must use `payload.kind = "systemEvent"`.
|
| 124 |
+
|
| 125 |
+
- `wakeMode: "next-heartbeat"` (default): event waits for the next scheduled heartbeat.
|
| 126 |
+
- `wakeMode: "now"`: event triggers an immediate heartbeat run.
|
| 127 |
+
|
| 128 |
+
This is the best fit when you want the normal heartbeat prompt + main-session context.
|
| 129 |
+
See [Heartbeat](/gateway/heartbeat).
|
| 130 |
+
|
| 131 |
+
#### Isolated jobs (dedicated cron sessions)
|
| 132 |
+
|
| 133 |
+
Isolated jobs run a dedicated agent turn in session `cron:<jobId>`.
|
| 134 |
+
|
| 135 |
+
Key behaviors:
|
| 136 |
+
|
| 137 |
+
- Prompt is prefixed with `[cron:<jobId> <job name>]` for traceability.
|
| 138 |
+
- Each run starts a **fresh session id** (no prior conversation carry-over).
|
| 139 |
+
- A summary is posted to the main session (prefix `Cron`, configurable).
|
| 140 |
+
- `wakeMode: "now"` triggers an immediate heartbeat after posting the summary.
|
| 141 |
+
- If `payload.deliver: true`, output is delivered to a channel; otherwise it stays internal.
|
| 142 |
+
|
| 143 |
+
Use isolated jobs for noisy, frequent, or "background chores" that shouldn't spam
|
| 144 |
+
your main chat history.
|
| 145 |
+
|
| 146 |
+
### Payload shapes (what runs)
|
| 147 |
+
|
| 148 |
+
Two payload kinds are supported:
|
| 149 |
+
|
| 150 |
+
- `systemEvent`: main-session only, routed through the heartbeat prompt.
|
| 151 |
+
- `agentTurn`: isolated-session only, runs a dedicated agent turn.
|
| 152 |
+
|
| 153 |
+
Common `agentTurn` fields:
|
| 154 |
+
|
| 155 |
+
- `message`: required text prompt.
|
| 156 |
+
- `model` / `thinking`: optional overrides (see below).
|
| 157 |
+
- `timeoutSeconds`: optional timeout override.
|
| 158 |
+
- `deliver`: `true` to send output to a channel target.
|
| 159 |
+
- `channel`: `last` or a specific channel.
|
| 160 |
+
- `to`: channel-specific target (phone/chat/channel id).
|
| 161 |
+
- `bestEffortDeliver`: avoid failing the job if delivery fails.
|
| 162 |
+
|
| 163 |
+
Isolation options (only for `session=isolated`):
|
| 164 |
+
|
| 165 |
+
- `postToMainPrefix` (CLI: `--post-prefix`): prefix for the system event in main.
|
| 166 |
+
- `postToMainMode`: `summary` (default) or `full`.
|
| 167 |
+
- `postToMainMaxChars`: max chars when `postToMainMode=full` (default 8000).
|
| 168 |
+
|
| 169 |
+
### Model and thinking overrides
|
| 170 |
+
|
| 171 |
+
Isolated jobs (`agentTurn`) can override the model and thinking level:
|
| 172 |
+
|
| 173 |
+
- `model`: Provider/model string (e.g., `anthropic/claude-sonnet-4-20250514`) or alias (e.g., `opus`)
|
| 174 |
+
- `thinking`: Thinking level (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`; GPT-5.2 + Codex models only)
|
| 175 |
+
|
| 176 |
+
Note: You can set `model` on main-session jobs too, but it changes the shared main
|
| 177 |
+
session model. We recommend model overrides only for isolated jobs to avoid
|
| 178 |
+
unexpected context shifts.
|
| 179 |
+
|
| 180 |
+
Resolution priority:
|
| 181 |
+
|
| 182 |
+
1. Job payload override (highest)
|
| 183 |
+
2. Hook-specific defaults (e.g., `hooks.gmail.model`)
|
| 184 |
+
3. Agent config default
|
| 185 |
+
|
| 186 |
+
### Delivery (channel + target)
|
| 187 |
+
|
| 188 |
+
Isolated jobs can deliver output to a channel. The job payload can specify:
|
| 189 |
+
|
| 190 |
+
- `channel`: `whatsapp` / `telegram` / `discord` / `slack` / `mattermost` (plugin) / `signal` / `imessage` / `last`
|
| 191 |
+
- `to`: channel-specific recipient target
|
| 192 |
+
|
| 193 |
+
If `channel` or `to` is omitted, cron can fall back to the main session’s “last route”
|
| 194 |
+
(the last place the agent replied).
|
| 195 |
+
|
| 196 |
+
Delivery notes:
|
| 197 |
+
|
| 198 |
+
- If `to` is set, cron auto-delivers the agent’s final output even if `deliver` is omitted.
|
| 199 |
+
- Use `deliver: true` when you want last-route delivery without an explicit `to`.
|
| 200 |
+
- Use `deliver: false` to keep output internal even if a `to` is present.
|
| 201 |
+
|
| 202 |
+
Target format reminders:
|
| 203 |
+
|
| 204 |
+
- Slack/Discord/Mattermost (plugin) targets should use explicit prefixes (e.g. `channel:<id>`, `user:<id>`) to avoid ambiguity.
|
| 205 |
+
- Telegram topics should use the `:topic:` form (see below).
|
| 206 |
+
|
| 207 |
+
#### Telegram delivery targets (topics / forum threads)
|
| 208 |
+
|
| 209 |
+
Telegram supports forum topics via `message_thread_id`. For cron delivery, you can encode
|
| 210 |
+
the topic/thread into the `to` field:
|
| 211 |
+
|
| 212 |
+
- `-1001234567890` (chat id only)
|
| 213 |
+
- `-1001234567890:topic:123` (preferred: explicit topic marker)
|
| 214 |
+
- `-1001234567890:123` (shorthand: numeric suffix)
|
| 215 |
+
|
| 216 |
+
Prefixed targets like `telegram:...` / `telegram:group:...` are also accepted:
|
| 217 |
+
|
| 218 |
+
- `telegram:group:-1001234567890:topic:123`
|
| 219 |
+
|
| 220 |
+
## JSON schema for tool calls
|
| 221 |
+
|
| 222 |
+
Use these shapes when calling Gateway `cron.*` tools directly (agent tool calls or RPC).
|
| 223 |
+
CLI flags accept human durations like `20m`, but tool calls use epoch milliseconds for
|
| 224 |
+
`atMs` and `everyMs` (ISO timestamps are accepted for `at` times).
|
| 225 |
+
|
| 226 |
+
### cron.add params
|
| 227 |
+
|
| 228 |
+
One-shot, main session job (system event):
|
| 229 |
+
|
| 230 |
+
```json
|
| 231 |
+
{
|
| 232 |
+
"name": "Reminder",
|
| 233 |
+
"schedule": { "kind": "at", "atMs": 1738262400000 },
|
| 234 |
+
"sessionTarget": "main",
|
| 235 |
+
"wakeMode": "now",
|
| 236 |
+
"payload": { "kind": "systemEvent", "text": "Reminder text" },
|
| 237 |
+
"deleteAfterRun": true
|
| 238 |
+
}
|
| 239 |
+
```
|
| 240 |
+
|
| 241 |
+
Recurring, isolated job with delivery:
|
| 242 |
+
|
| 243 |
+
```json
|
| 244 |
+
{
|
| 245 |
+
"name": "Morning brief",
|
| 246 |
+
"schedule": { "kind": "cron", "expr": "0 7 * * *", "tz": "America/Los_Angeles" },
|
| 247 |
+
"sessionTarget": "isolated",
|
| 248 |
+
"wakeMode": "next-heartbeat",
|
| 249 |
+
"payload": {
|
| 250 |
+
"kind": "agentTurn",
|
| 251 |
+
"message": "Summarize overnight updates.",
|
| 252 |
+
"deliver": true,
|
| 253 |
+
"channel": "slack",
|
| 254 |
+
"to": "channel:C1234567890",
|
| 255 |
+
"bestEffortDeliver": true
|
| 256 |
+
},
|
| 257 |
+
"isolation": { "postToMainPrefix": "Cron", "postToMainMode": "summary" }
|
| 258 |
+
}
|
| 259 |
+
```
|
| 260 |
+
|
| 261 |
+
Notes:
|
| 262 |
+
|
| 263 |
+
- `schedule.kind`: `at` (`atMs`), `every` (`everyMs`), or `cron` (`expr`, optional `tz`).
|
| 264 |
+
- `atMs` and `everyMs` are epoch milliseconds.
|
| 265 |
+
- `sessionTarget` must be `"main"` or `"isolated"` and must match `payload.kind`.
|
| 266 |
+
- Optional fields: `agentId`, `description`, `enabled`, `deleteAfterRun`, `isolation`.
|
| 267 |
+
- `wakeMode` defaults to `"next-heartbeat"` when omitted.
|
| 268 |
+
|
| 269 |
+
### cron.update params
|
| 270 |
+
|
| 271 |
+
```json
|
| 272 |
+
{
|
| 273 |
+
"jobId": "job-123",
|
| 274 |
+
"patch": {
|
| 275 |
+
"enabled": false,
|
| 276 |
+
"schedule": { "kind": "every", "everyMs": 3600000 }
|
| 277 |
+
}
|
| 278 |
+
}
|
| 279 |
+
```
|
| 280 |
+
|
| 281 |
+
Notes:
|
| 282 |
+
|
| 283 |
+
- `jobId` is canonical; `id` is accepted for compatibility.
|
| 284 |
+
- Use `agentId: null` in the patch to clear an agent binding.
|
| 285 |
+
|
| 286 |
+
### cron.run and cron.remove params
|
| 287 |
+
|
| 288 |
+
```json
|
| 289 |
+
{ "jobId": "job-123", "mode": "force" }
|
| 290 |
+
```
|
| 291 |
+
|
| 292 |
+
```json
|
| 293 |
+
{ "jobId": "job-123" }
|
| 294 |
+
```
|
| 295 |
+
|
| 296 |
+
## Storage & history
|
| 297 |
+
|
| 298 |
+
- Job store: `~/.openclaw/cron/jobs.json` (Gateway-managed JSON).
|
| 299 |
+
- Run history: `~/.openclaw/cron/runs/<jobId>.jsonl` (JSONL, auto-pruned).
|
| 300 |
+
- Override store path: `cron.store` in config.
|
| 301 |
+
|
| 302 |
+
## Configuration
|
| 303 |
+
|
| 304 |
+
```json5
|
| 305 |
+
{
|
| 306 |
+
cron: {
|
| 307 |
+
enabled: true, // default true
|
| 308 |
+
store: "~/.openclaw/cron/jobs.json",
|
| 309 |
+
maxConcurrentRuns: 1, // default 1
|
| 310 |
+
},
|
| 311 |
+
}
|
| 312 |
+
```
|
| 313 |
+
|
| 314 |
+
Disable cron entirely:
|
| 315 |
+
|
| 316 |
+
- `cron.enabled: false` (config)
|
| 317 |
+
- `OPENCLAW_SKIP_CRON=1` (env)
|
| 318 |
+
|
| 319 |
+
## CLI quickstart
|
| 320 |
+
|
| 321 |
+
One-shot reminder (UTC ISO, auto-delete after success):
|
| 322 |
+
|
| 323 |
+
```bash
|
| 324 |
+
openclaw cron add \
|
| 325 |
+
--name "Send reminder" \
|
| 326 |
+
--at "2026-01-12T18:00:00Z" \
|
| 327 |
+
--session main \
|
| 328 |
+
--system-event "Reminder: submit expense report." \
|
| 329 |
+
--wake now \
|
| 330 |
+
--delete-after-run
|
| 331 |
+
```
|
| 332 |
+
|
| 333 |
+
One-shot reminder (main session, wake immediately):
|
| 334 |
+
|
| 335 |
+
```bash
|
| 336 |
+
openclaw cron add \
|
| 337 |
+
--name "Calendar check" \
|
| 338 |
+
--at "20m" \
|
| 339 |
+
--session main \
|
| 340 |
+
--system-event "Next heartbeat: check calendar." \
|
| 341 |
+
--wake now
|
| 342 |
+
```
|
| 343 |
+
|
| 344 |
+
Recurring isolated job (deliver to WhatsApp):
|
| 345 |
+
|
| 346 |
+
```bash
|
| 347 |
+
openclaw cron add \
|
| 348 |
+
--name "Morning status" \
|
| 349 |
+
--cron "0 7 * * *" \
|
| 350 |
+
--tz "America/Los_Angeles" \
|
| 351 |
+
--session isolated \
|
| 352 |
+
--message "Summarize inbox + calendar for today." \
|
| 353 |
+
--deliver \
|
| 354 |
+
--channel whatsapp \
|
| 355 |
+
--to "+15551234567"
|
| 356 |
+
```
|
| 357 |
+
|
| 358 |
+
Recurring isolated job (deliver to a Telegram topic):
|
| 359 |
+
|
| 360 |
+
```bash
|
| 361 |
+
openclaw cron add \
|
| 362 |
+
--name "Nightly summary (topic)" \
|
| 363 |
+
--cron "0 22 * * *" \
|
| 364 |
+
--tz "America/Los_Angeles" \
|
| 365 |
+
--session isolated \
|
| 366 |
+
--message "Summarize today; send to the nightly topic." \
|
| 367 |
+
--deliver \
|
| 368 |
+
--channel telegram \
|
| 369 |
+
--to "-1001234567890:topic:123"
|
| 370 |
+
```
|
| 371 |
+
|
| 372 |
+
Isolated job with model and thinking override:
|
| 373 |
+
|
| 374 |
+
```bash
|
| 375 |
+
openclaw cron add \
|
| 376 |
+
--name "Deep analysis" \
|
| 377 |
+
--cron "0 6 * * 1" \
|
| 378 |
+
--tz "America/Los_Angeles" \
|
| 379 |
+
--session isolated \
|
| 380 |
+
--message "Weekly deep analysis of project progress." \
|
| 381 |
+
--model "opus" \
|
| 382 |
+
--thinking high \
|
| 383 |
+
--deliver \
|
| 384 |
+
--channel whatsapp \
|
| 385 |
+
--to "+15551234567"
|
| 386 |
+
```
|
| 387 |
+
|
| 388 |
+
Agent selection (multi-agent setups):
|
| 389 |
+
|
| 390 |
+
```bash
|
| 391 |
+
# Pin a job to agent "ops" (falls back to default if that agent is missing)
|
| 392 |
+
openclaw cron add --name "Ops sweep" --cron "0 6 * * *" --session isolated --message "Check ops queue" --agent ops
|
| 393 |
+
|
| 394 |
+
# Switch or clear the agent on an existing job
|
| 395 |
+
openclaw cron edit <jobId> --agent ops
|
| 396 |
+
openclaw cron edit <jobId> --clear-agent
|
| 397 |
+
```
|
| 398 |
+
|
| 399 |
+
Manual run (debug):
|
| 400 |
+
|
| 401 |
+
```bash
|
| 402 |
+
openclaw cron run <jobId> --force
|
| 403 |
+
```
|
| 404 |
+
|
| 405 |
+
Edit an existing job (patch fields):
|
| 406 |
+
|
| 407 |
+
```bash
|
| 408 |
+
openclaw cron edit <jobId> \
|
| 409 |
+
--message "Updated prompt" \
|
| 410 |
+
--model "opus" \
|
| 411 |
+
--thinking low
|
| 412 |
+
```
|
| 413 |
+
|
| 414 |
+
Run history:
|
| 415 |
+
|
| 416 |
+
```bash
|
| 417 |
+
openclaw cron runs --id <jobId> --limit 50
|
| 418 |
+
```
|
| 419 |
+
|
| 420 |
+
Immediate system event without creating a job:
|
| 421 |
+
|
| 422 |
+
```bash
|
| 423 |
+
openclaw system event --mode now --text "Next heartbeat: check battery."
|
| 424 |
+
```
|
| 425 |
+
|
| 426 |
+
## Gateway API surface
|
| 427 |
+
|
| 428 |
+
- `cron.list`, `cron.status`, `cron.add`, `cron.update`, `cron.remove`
|
| 429 |
+
- `cron.run` (force or due), `cron.runs`
|
| 430 |
+
For immediate system events without a job, use [`openclaw system event`](/cli/system).
|
| 431 |
+
|
| 432 |
+
## Troubleshooting
|
| 433 |
+
|
| 434 |
+
### “Nothing runs”
|
| 435 |
+
|
| 436 |
+
- Check cron is enabled: `cron.enabled` and `OPENCLAW_SKIP_CRON`.
|
| 437 |
+
- Check the Gateway is running continuously (cron runs inside the Gateway process).
|
| 438 |
+
- For `cron` schedules: confirm timezone (`--tz`) vs the host timezone.
|
| 439 |
+
|
| 440 |
+
### Telegram delivers to the wrong place
|
| 441 |
+
|
| 442 |
+
- For forum topics, use `-100…:topic:<id>` so it’s explicit and unambiguous.
|
| 443 |
+
- If you see `telegram:...` prefixes in logs or stored “last route” targets, that’s normal;
|
| 444 |
+
cron delivery accepts them and still parses topic IDs correctly.
|
docs/automation/cron-vs-heartbeat.md
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
summary: "Guidance for choosing between heartbeat and cron jobs for automation"
|
| 3 |
+
read_when:
|
| 4 |
+
- Deciding how to schedule recurring tasks
|
| 5 |
+
- Setting up background monitoring or notifications
|
| 6 |
+
- Optimizing token usage for periodic checks
|
| 7 |
+
title: "Cron vs Heartbeat"
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# Cron vs Heartbeat: When to Use Each
|
| 11 |
+
|
| 12 |
+
Both heartbeats and cron jobs let you run tasks on a schedule. This guide helps you choose the right mechanism for your use case.
|
| 13 |
+
|
| 14 |
+
## Quick Decision Guide
|
| 15 |
+
|
| 16 |
+
| Use Case | Recommended | Why |
|
| 17 |
+
| ------------------------------------ | ------------------- | ---------------------------------------- |
|
| 18 |
+
| Check inbox every 30 min | Heartbeat | Batches with other checks, context-aware |
|
| 19 |
+
| Send daily report at 9am sharp | Cron (isolated) | Exact timing needed |
|
| 20 |
+
| Monitor calendar for upcoming events | Heartbeat | Natural fit for periodic awareness |
|
| 21 |
+
| Run weekly deep analysis | Cron (isolated) | Standalone task, can use different model |
|
| 22 |
+
| Remind me in 20 minutes | Cron (main, `--at`) | One-shot with precise timing |
|
| 23 |
+
| Background project health check | Heartbeat | Piggybacks on existing cycle |
|
| 24 |
+
|
| 25 |
+
## Heartbeat: Periodic Awareness
|
| 26 |
+
|
| 27 |
+
Heartbeats run in the **main session** at a regular interval (default: 30 min). They're designed for the agent to check on things and surface anything important.
|
| 28 |
+
|
| 29 |
+
### When to use heartbeat
|
| 30 |
+
|
| 31 |
+
- **Multiple periodic checks**: Instead of 5 separate cron jobs checking inbox, calendar, weather, notifications, and project status, a single heartbeat can batch all of these.
|
| 32 |
+
- **Context-aware decisions**: The agent has full main-session context, so it can make smart decisions about what's urgent vs. what can wait.
|
| 33 |
+
- **Conversational continuity**: Heartbeat runs share the same session, so the agent remembers recent conversations and can follow up naturally.
|
| 34 |
+
- **Low-overhead monitoring**: One heartbeat replaces many small polling tasks.
|
| 35 |
+
|
| 36 |
+
### Heartbeat advantages
|
| 37 |
+
|
| 38 |
+
- **Batches multiple checks**: One agent turn can review inbox, calendar, and notifications together.
|
| 39 |
+
- **Reduces API calls**: A single heartbeat is cheaper than 5 isolated cron jobs.
|
| 40 |
+
- **Context-aware**: The agent knows what you've been working on and can prioritize accordingly.
|
| 41 |
+
- **Smart suppression**: If nothing needs attention, the agent replies `HEARTBEAT_OK` and no message is delivered.
|
| 42 |
+
- **Natural timing**: Drifts slightly based on queue load, which is fine for most monitoring.
|
| 43 |
+
|
| 44 |
+
### Heartbeat example: HEARTBEAT.md checklist
|
| 45 |
+
|
| 46 |
+
```md
|
| 47 |
+
# Heartbeat checklist
|
| 48 |
+
|
| 49 |
+
- Check email for urgent messages
|
| 50 |
+
- Review calendar for events in next 2 hours
|
| 51 |
+
- If a background task finished, summarize results
|
| 52 |
+
- If idle for 8+ hours, send a brief check-in
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
The agent reads this on each heartbeat and handles all items in one turn.
|
| 56 |
+
|
| 57 |
+
### Configuring heartbeat
|
| 58 |
+
|
| 59 |
+
```json5
|
| 60 |
+
{
|
| 61 |
+
agents: {
|
| 62 |
+
defaults: {
|
| 63 |
+
heartbeat: {
|
| 64 |
+
every: "30m", // interval
|
| 65 |
+
target: "last", // where to deliver alerts
|
| 66 |
+
activeHours: { start: "08:00", end: "22:00" }, // optional
|
| 67 |
+
},
|
| 68 |
+
},
|
| 69 |
+
},
|
| 70 |
+
}
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
See [Heartbeat](/gateway/heartbeat) for full configuration.
|
| 74 |
+
|
| 75 |
+
## Cron: Precise Scheduling
|
| 76 |
+
|
| 77 |
+
Cron jobs run at **exact times** and can run in isolated sessions without affecting main context.
|
| 78 |
+
|
| 79 |
+
### When to use cron
|
| 80 |
+
|
| 81 |
+
- **Exact timing required**: "Send this at 9:00 AM every Monday" (not "sometime around 9").
|
| 82 |
+
- **Standalone tasks**: Tasks that don't need conversational context.
|
| 83 |
+
- **Different model/thinking**: Heavy analysis that warrants a more powerful model.
|
| 84 |
+
- **One-shot reminders**: "Remind me in 20 minutes" with `--at`.
|
| 85 |
+
- **Noisy/frequent tasks**: Tasks that would clutter main session history.
|
| 86 |
+
- **External triggers**: Tasks that should run independently of whether the agent is otherwise active.
|
| 87 |
+
|
| 88 |
+
### Cron advantages
|
| 89 |
+
|
| 90 |
+
- **Exact timing**: 5-field cron expressions with timezone support.
|
| 91 |
+
- **Session isolation**: Runs in `cron:<jobId>` without polluting main history.
|
| 92 |
+
- **Model overrides**: Use a cheaper or more powerful model per job.
|
| 93 |
+
- **Delivery control**: Can deliver directly to a channel; still posts a summary to main by default (configurable).
|
| 94 |
+
- **No agent context needed**: Runs even if main session is idle or compacted.
|
| 95 |
+
- **One-shot support**: `--at` for precise future timestamps.
|
| 96 |
+
|
| 97 |
+
### Cron example: Daily morning briefing
|
| 98 |
+
|
| 99 |
+
```bash
|
| 100 |
+
openclaw cron add \
|
| 101 |
+
--name "Morning briefing" \
|
| 102 |
+
--cron "0 7 * * *" \
|
| 103 |
+
--tz "America/New_York" \
|
| 104 |
+
--session isolated \
|
| 105 |
+
--message "Generate today's briefing: weather, calendar, top emails, news summary." \
|
| 106 |
+
--model opus \
|
| 107 |
+
--deliver \
|
| 108 |
+
--channel whatsapp \
|
| 109 |
+
--to "+15551234567"
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
This runs at exactly 7:00 AM New York time, uses Opus for quality, and delivers directly to WhatsApp.
|
| 113 |
+
|
| 114 |
+
### Cron example: One-shot reminder
|
| 115 |
+
|
| 116 |
+
```bash
|
| 117 |
+
openclaw cron add \
|
| 118 |
+
--name "Meeting reminder" \
|
| 119 |
+
--at "20m" \
|
| 120 |
+
--session main \
|
| 121 |
+
--system-event "Reminder: standup meeting starts in 10 minutes." \
|
| 122 |
+
--wake now \
|
| 123 |
+
--delete-after-run
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
See [Cron jobs](/automation/cron-jobs) for full CLI reference.
|
| 127 |
+
|
| 128 |
+
## Decision Flowchart
|
| 129 |
+
|
| 130 |
+
```
|
| 131 |
+
Does the task need to run at an EXACT time?
|
| 132 |
+
YES -> Use cron
|
| 133 |
+
NO -> Continue...
|
| 134 |
+
|
| 135 |
+
Does the task need isolation from main session?
|
| 136 |
+
YES -> Use cron (isolated)
|
| 137 |
+
NO -> Continue...
|
| 138 |
+
|
| 139 |
+
Can this task be batched with other periodic checks?
|
| 140 |
+
YES -> Use heartbeat (add to HEARTBEAT.md)
|
| 141 |
+
NO -> Use cron
|
| 142 |
+
|
| 143 |
+
Is this a one-shot reminder?
|
| 144 |
+
YES -> Use cron with --at
|
| 145 |
+
NO -> Continue...
|
| 146 |
+
|
| 147 |
+
Does it need a different model or thinking level?
|
| 148 |
+
YES -> Use cron (isolated) with --model/--thinking
|
| 149 |
+
NO -> Use heartbeat
|
| 150 |
+
```
|
| 151 |
+
|
| 152 |
+
## Combining Both
|
| 153 |
+
|
| 154 |
+
The most efficient setup uses **both**:
|
| 155 |
+
|
| 156 |
+
1. **Heartbeat** handles routine monitoring (inbox, calendar, notifications) in one batched turn every 30 minutes.
|
| 157 |
+
2. **Cron** handles precise schedules (daily reports, weekly reviews) and one-shot reminders.
|
| 158 |
+
|
| 159 |
+
### Example: Efficient automation setup
|
| 160 |
+
|
| 161 |
+
**HEARTBEAT.md** (checked every 30 min):
|
| 162 |
+
|
| 163 |
+
```md
|
| 164 |
+
# Heartbeat checklist
|
| 165 |
+
|
| 166 |
+
- Scan inbox for urgent emails
|
| 167 |
+
- Check calendar for events in next 2h
|
| 168 |
+
- Review any pending tasks
|
| 169 |
+
- Light check-in if quiet for 8+ hours
|
| 170 |
+
```
|
| 171 |
+
|
| 172 |
+
**Cron jobs** (precise timing):
|
| 173 |
+
|
| 174 |
+
```bash
|
| 175 |
+
# Daily morning briefing at 7am
|
| 176 |
+
openclaw cron add --name "Morning brief" --cron "0 7 * * *" --session isolated --message "..." --deliver
|
| 177 |
+
|
| 178 |
+
# Weekly project review on Mondays at 9am
|
| 179 |
+
openclaw cron add --name "Weekly review" --cron "0 9 * * 1" --session isolated --message "..." --model opus
|
| 180 |
+
|
| 181 |
+
# One-shot reminder
|
| 182 |
+
openclaw cron add --name "Call back" --at "2h" --session main --system-event "Call back the client" --wake now
|
| 183 |
+
```
|
| 184 |
+
|
| 185 |
+
## Lobster: Deterministic workflows with approvals
|
| 186 |
+
|
| 187 |
+
Lobster is the workflow runtime for **multi-step tool pipelines** that need deterministic execution and explicit approvals.
|
| 188 |
+
Use it when the task is more than a single agent turn, and you want a resumable workflow with human checkpoints.
|
| 189 |
+
|
| 190 |
+
### When Lobster fits
|
| 191 |
+
|
| 192 |
+
- **Multi-step automation**: You need a fixed pipeline of tool calls, not a one-off prompt.
|
| 193 |
+
- **Approval gates**: Side effects should pause until you approve, then resume.
|
| 194 |
+
- **Resumable runs**: Continue a paused workflow without re-running earlier steps.
|
| 195 |
+
|
| 196 |
+
### How it pairs with heartbeat and cron
|
| 197 |
+
|
| 198 |
+
- **Heartbeat/cron** decide _when_ a run happens.
|
| 199 |
+
- **Lobster** defines _what steps_ happen once the run starts.
|
| 200 |
+
|
| 201 |
+
For scheduled workflows, use cron or heartbeat to trigger an agent turn that calls Lobster.
|
| 202 |
+
For ad-hoc workflows, call Lobster directly.
|
| 203 |
+
|
| 204 |
+
### Operational notes (from the code)
|
| 205 |
+
|
| 206 |
+
- Lobster runs as a **local subprocess** (`lobster` CLI) in tool mode and returns a **JSON envelope**.
|
| 207 |
+
- If the tool returns `needs_approval`, you resume with a `resumeToken` and `approve` flag.
|
| 208 |
+
- The tool is an **optional plugin**; enable it additively via `tools.alsoAllow: ["lobster"]` (recommended).
|
| 209 |
+
- If you pass `lobsterPath`, it must be an **absolute path**.
|
| 210 |
+
|
| 211 |
+
See [Lobster](/tools/lobster) for full usage and examples.
|
| 212 |
+
|
| 213 |
+
## Main Session vs Isolated Session
|
| 214 |
+
|
| 215 |
+
Both heartbeat and cron can interact with the main session, but differently:
|
| 216 |
+
|
| 217 |
+
| | Heartbeat | Cron (main) | Cron (isolated) |
|
| 218 |
+
| ------- | ------------------------------- | ------------------------ | ---------------------- |
|
| 219 |
+
| Session | Main | Main (via system event) | `cron:<jobId>` |
|
| 220 |
+
| History | Shared | Shared | Fresh each run |
|
| 221 |
+
| Context | Full | Full | None (starts clean) |
|
| 222 |
+
| Model | Main session model | Main session model | Can override |
|
| 223 |
+
| Output | Delivered if not `HEARTBEAT_OK` | Heartbeat prompt + event | Summary posted to main |
|
| 224 |
+
|
| 225 |
+
### When to use main session cron
|
| 226 |
+
|
| 227 |
+
Use `--session main` with `--system-event` when you want:
|
| 228 |
+
|
| 229 |
+
- The reminder/event to appear in main session context
|
| 230 |
+
- The agent to handle it during the next heartbeat with full context
|
| 231 |
+
- No separate isolated run
|
| 232 |
+
|
| 233 |
+
```bash
|
| 234 |
+
openclaw cron add \
|
| 235 |
+
--name "Check project" \
|
| 236 |
+
--every "4h" \
|
| 237 |
+
--session main \
|
| 238 |
+
--system-event "Time for a project health check" \
|
| 239 |
+
--wake now
|
| 240 |
+
```
|
| 241 |
+
|
| 242 |
+
### When to use isolated cron
|
| 243 |
+
|
| 244 |
+
Use `--session isolated` when you want:
|
| 245 |
+
|
| 246 |
+
- A clean slate without prior context
|
| 247 |
+
- Different model or thinking settings
|
| 248 |
+
- Output delivered directly to a channel (summary still posts to main by default)
|
| 249 |
+
- History that doesn't clutter main session
|
| 250 |
+
|
| 251 |
+
```bash
|
| 252 |
+
openclaw cron add \
|
| 253 |
+
--name "Deep analysis" \
|
| 254 |
+
--cron "0 6 * * 0" \
|
| 255 |
+
--session isolated \
|
| 256 |
+
--message "Weekly codebase analysis..." \
|
| 257 |
+
--model opus \
|
| 258 |
+
--thinking high \
|
| 259 |
+
--deliver
|
| 260 |
+
```
|
| 261 |
+
|
| 262 |
+
## Cost Considerations
|
| 263 |
+
|
| 264 |
+
| Mechanism | Cost Profile |
|
| 265 |
+
| --------------- | ------------------------------------------------------- |
|
| 266 |
+
| Heartbeat | One turn every N minutes; scales with HEARTBEAT.md size |
|
| 267 |
+
| Cron (main) | Adds event to next heartbeat (no isolated turn) |
|
| 268 |
+
| Cron (isolated) | Full agent turn per job; can use cheaper model |
|
| 269 |
+
|
| 270 |
+
**Tips**:
|
| 271 |
+
|
| 272 |
+
- Keep `HEARTBEAT.md` small to minimize token overhead.
|
| 273 |
+
- Batch similar checks into heartbeat instead of multiple cron jobs.
|
| 274 |
+
- Use `target: "none"` on heartbeat if you only want internal processing.
|
| 275 |
+
- Use isolated cron with a cheaper model for routine tasks.
|
| 276 |
+
|
| 277 |
+
## Related
|
| 278 |
+
|
| 279 |
+
- [Heartbeat](/gateway/heartbeat) - full heartbeat configuration
|
| 280 |
+
- [Cron jobs](/automation/cron-jobs) - full cron CLI and API reference
|
| 281 |
+
- [System](/cli/system) - system events + heartbeat controls
|
docs/automation/gmail-pubsub.md
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
summary: "Gmail Pub/Sub push wired into OpenClaw webhooks via gogcli"
|
| 3 |
+
read_when:
|
| 4 |
+
- Wiring Gmail inbox triggers to OpenClaw
|
| 5 |
+
- Setting up Pub/Sub push for agent wake
|
| 6 |
+
title: "Gmail PubSub"
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
# Gmail Pub/Sub -> OpenClaw
|
| 10 |
+
|
| 11 |
+
Goal: Gmail watch -> Pub/Sub push -> `gog gmail watch serve` -> OpenClaw webhook.
|
| 12 |
+
|
| 13 |
+
## Prereqs
|
| 14 |
+
|
| 15 |
+
- `gcloud` installed and logged in ([install guide](https://docs.cloud.google.com/sdk/docs/install-sdk)).
|
| 16 |
+
- `gog` (gogcli) installed and authorized for the Gmail account ([gogcli.sh](https://gogcli.sh/)).
|
| 17 |
+
- OpenClaw hooks enabled (see [Webhooks](/automation/webhook)).
|
| 18 |
+
- `tailscale` logged in ([tailscale.com](https://tailscale.com/)). Supported setup uses Tailscale Funnel for the public HTTPS endpoint.
|
| 19 |
+
Other tunnel services can work, but are DIY/unsupported and require manual wiring.
|
| 20 |
+
Right now, Tailscale is what we support.
|
| 21 |
+
|
| 22 |
+
Example hook config (enable Gmail preset mapping):
|
| 23 |
+
|
| 24 |
+
```json5
|
| 25 |
+
{
|
| 26 |
+
hooks: {
|
| 27 |
+
enabled: true,
|
| 28 |
+
token: "OPENCLAW_HOOK_TOKEN",
|
| 29 |
+
path: "/hooks",
|
| 30 |
+
presets: ["gmail"],
|
| 31 |
+
},
|
| 32 |
+
}
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
To deliver the Gmail summary to a chat surface, override the preset with a mapping
|
| 36 |
+
that sets `deliver` + optional `channel`/`to`:
|
| 37 |
+
|
| 38 |
+
```json5
|
| 39 |
+
{
|
| 40 |
+
hooks: {
|
| 41 |
+
enabled: true,
|
| 42 |
+
token: "OPENCLAW_HOOK_TOKEN",
|
| 43 |
+
presets: ["gmail"],
|
| 44 |
+
mappings: [
|
| 45 |
+
{
|
| 46 |
+
match: { path: "gmail" },
|
| 47 |
+
action: "agent",
|
| 48 |
+
wakeMode: "now",
|
| 49 |
+
name: "Gmail",
|
| 50 |
+
sessionKey: "hook:gmail:{{messages[0].id}}",
|
| 51 |
+
messageTemplate: "New email from {{messages[0].from}}\nSubject: {{messages[0].subject}}\n{{messages[0].snippet}}\n{{messages[0].body}}",
|
| 52 |
+
model: "openai/gpt-5.2-mini",
|
| 53 |
+
deliver: true,
|
| 54 |
+
channel: "last",
|
| 55 |
+
// to: "+15551234567"
|
| 56 |
+
},
|
| 57 |
+
],
|
| 58 |
+
},
|
| 59 |
+
}
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
If you want a fixed channel, set `channel` + `to`. Otherwise `channel: "last"`
|
| 63 |
+
uses the last delivery route (falls back to WhatsApp).
|
| 64 |
+
|
| 65 |
+
To force a cheaper model for Gmail runs, set `model` in the mapping
|
| 66 |
+
(`provider/model` or alias). If you enforce `agents.defaults.models`, include it there.
|
| 67 |
+
|
| 68 |
+
To set a default model and thinking level specifically for Gmail hooks, add
|
| 69 |
+
`hooks.gmail.model` / `hooks.gmail.thinking` in your config:
|
| 70 |
+
|
| 71 |
+
```json5
|
| 72 |
+
{
|
| 73 |
+
hooks: {
|
| 74 |
+
gmail: {
|
| 75 |
+
model: "openrouter/meta-llama/llama-3.3-70b-instruct:free",
|
| 76 |
+
thinking: "off",
|
| 77 |
+
},
|
| 78 |
+
},
|
| 79 |
+
}
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
Notes:
|
| 83 |
+
|
| 84 |
+
- Per-hook `model`/`thinking` in the mapping still overrides these defaults.
|
| 85 |
+
- Fallback order: `hooks.gmail.model` → `agents.defaults.model.fallbacks` → primary (auth/rate-limit/timeouts).
|
| 86 |
+
- If `agents.defaults.models` is set, the Gmail model must be in the allowlist.
|
| 87 |
+
- Gmail hook content is wrapped with external-content safety boundaries by default.
|
| 88 |
+
To disable (dangerous), set `hooks.gmail.allowUnsafeExternalContent: true`.
|
| 89 |
+
|
| 90 |
+
To customize payload handling further, add `hooks.mappings` or a JS/TS transform module
|
| 91 |
+
under `hooks.transformsDir` (see [Webhooks](/automation/webhook)).
|
| 92 |
+
|
| 93 |
+
## Wizard (recommended)
|
| 94 |
+
|
| 95 |
+
Use the OpenClaw helper to wire everything together (installs deps on macOS via brew):
|
| 96 |
+
|
| 97 |
+
```bash
|
| 98 |
+
openclaw webhooks gmail setup \
|
| 99 |
+
--account openclaw@gmail.com
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
Defaults:
|
| 103 |
+
|
| 104 |
+
- Uses Tailscale Funnel for the public push endpoint.
|
| 105 |
+
- Writes `hooks.gmail` config for `openclaw webhooks gmail run`.
|
| 106 |
+
- Enables the Gmail hook preset (`hooks.presets: ["gmail"]`).
|
| 107 |
+
|
| 108 |
+
Path note: when `tailscale.mode` is enabled, OpenClaw automatically sets
|
| 109 |
+
`hooks.gmail.serve.path` to `/` and keeps the public path at
|
| 110 |
+
`hooks.gmail.tailscale.path` (default `/gmail-pubsub`) because Tailscale
|
| 111 |
+
strips the set-path prefix before proxying.
|
| 112 |
+
If you need the backend to receive the prefixed path, set
|
| 113 |
+
`hooks.gmail.tailscale.target` (or `--tailscale-target`) to a full URL like
|
| 114 |
+
`http://127.0.0.1:8788/gmail-pubsub` and match `hooks.gmail.serve.path`.
|
| 115 |
+
|
| 116 |
+
Want a custom endpoint? Use `--push-endpoint <url>` or `--tailscale off`.
|
| 117 |
+
|
| 118 |
+
Platform note: on macOS the wizard installs `gcloud`, `gogcli`, and `tailscale`
|
| 119 |
+
via Homebrew; on Linux install them manually first.
|
| 120 |
+
|
| 121 |
+
Gateway auto-start (recommended):
|
| 122 |
+
|
| 123 |
+
- When `hooks.enabled=true` and `hooks.gmail.account` is set, the Gateway starts
|
| 124 |
+
`gog gmail watch serve` on boot and auto-renews the watch.
|
| 125 |
+
- Set `OPENCLAW_SKIP_GMAIL_WATCHER=1` to opt out (useful if you run the daemon yourself).
|
| 126 |
+
- Do not run the manual daemon at the same time, or you will hit
|
| 127 |
+
`listen tcp 127.0.0.1:8788: bind: address already in use`.
|
| 128 |
+
|
| 129 |
+
Manual daemon (starts `gog gmail watch serve` + auto-renew):
|
| 130 |
+
|
| 131 |
+
```bash
|
| 132 |
+
openclaw webhooks gmail run
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
## One-time setup
|
| 136 |
+
|
| 137 |
+
1. Select the GCP project **that owns the OAuth client** used by `gog`.
|
| 138 |
+
|
| 139 |
+
```bash
|
| 140 |
+
gcloud auth login
|
| 141 |
+
gcloud config set project <project-id>
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
Note: Gmail watch requires the Pub/Sub topic to live in the same project as the OAuth client.
|
| 145 |
+
|
| 146 |
+
2. Enable APIs:
|
| 147 |
+
|
| 148 |
+
```bash
|
| 149 |
+
gcloud services enable gmail.googleapis.com pubsub.googleapis.com
|
| 150 |
+
```
|
| 151 |
+
|
| 152 |
+
3. Create a topic:
|
| 153 |
+
|
| 154 |
+
```bash
|
| 155 |
+
gcloud pubsub topics create gog-gmail-watch
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
4. Allow Gmail push to publish:
|
| 159 |
+
|
| 160 |
+
```bash
|
| 161 |
+
gcloud pubsub topics add-iam-policy-binding gog-gmail-watch \
|
| 162 |
+
--member=serviceAccount:gmail-api-push@system.gserviceaccount.com \
|
| 163 |
+
--role=roles/pubsub.publisher
|
| 164 |
+
```
|
| 165 |
+
|
| 166 |
+
## Start the watch
|
| 167 |
+
|
| 168 |
+
```bash
|
| 169 |
+
gog gmail watch start \
|
| 170 |
+
--account openclaw@gmail.com \
|
| 171 |
+
--label INBOX \
|
| 172 |
+
--topic projects/<project-id>/topics/gog-gmail-watch
|
| 173 |
+
```
|
| 174 |
+
|
| 175 |
+
Save the `history_id` from the output (for debugging).
|
| 176 |
+
|
| 177 |
+
## Run the push handler
|
| 178 |
+
|
| 179 |
+
Local example (shared token auth):
|
| 180 |
+
|
| 181 |
+
```bash
|
| 182 |
+
gog gmail watch serve \
|
| 183 |
+
--account openclaw@gmail.com \
|
| 184 |
+
--bind 127.0.0.1 \
|
| 185 |
+
--port 8788 \
|
| 186 |
+
--path /gmail-pubsub \
|
| 187 |
+
--token <shared> \
|
| 188 |
+
--hook-url http://127.0.0.1:18789/hooks/gmail \
|
| 189 |
+
--hook-token OPENCLAW_HOOK_TOKEN \
|
| 190 |
+
--include-body \
|
| 191 |
+
--max-bytes 20000
|
| 192 |
+
```
|
| 193 |
+
|
| 194 |
+
Notes:
|
| 195 |
+
|
| 196 |
+
- `--token` protects the push endpoint (`x-gog-token` or `?token=`).
|
| 197 |
+
- `--hook-url` points to OpenClaw `/hooks/gmail` (mapped; isolated run + summary to main).
|
| 198 |
+
- `--include-body` and `--max-bytes` control the body snippet sent to OpenClaw.
|
| 199 |
+
|
| 200 |
+
Recommended: `openclaw webhooks gmail run` wraps the same flow and auto-renews the watch.
|
| 201 |
+
|
| 202 |
+
## Expose the handler (advanced, unsupported)
|
| 203 |
+
|
| 204 |
+
If you need a non-Tailscale tunnel, wire it manually and use the public URL in the push
|
| 205 |
+
subscription (unsupported, no guardrails):
|
| 206 |
+
|
| 207 |
+
```bash
|
| 208 |
+
cloudflared tunnel --url http://127.0.0.1:8788 --no-autoupdate
|
| 209 |
+
```
|
| 210 |
+
|
| 211 |
+
Use the generated URL as the push endpoint:
|
| 212 |
+
|
| 213 |
+
```bash
|
| 214 |
+
gcloud pubsub subscriptions create gog-gmail-watch-push \
|
| 215 |
+
--topic gog-gmail-watch \
|
| 216 |
+
--push-endpoint "https://<public-url>/gmail-pubsub?token=<shared>"
|
| 217 |
+
```
|
| 218 |
+
|
| 219 |
+
Production: use a stable HTTPS endpoint and configure Pub/Sub OIDC JWT, then run:
|
| 220 |
+
|
| 221 |
+
```bash
|
| 222 |
+
gog gmail watch serve --verify-oidc --oidc-email <svc@...>
|
| 223 |
+
```
|
| 224 |
+
|
| 225 |
+
## Test
|
| 226 |
+
|
| 227 |
+
Send a message to the watched inbox:
|
| 228 |
+
|
| 229 |
+
```bash
|
| 230 |
+
gog gmail send \
|
| 231 |
+
--account openclaw@gmail.com \
|
| 232 |
+
--to openclaw@gmail.com \
|
| 233 |
+
--subject "watch test" \
|
| 234 |
+
--body "ping"
|
| 235 |
+
```
|
| 236 |
+
|
| 237 |
+
Check watch state and history:
|
| 238 |
+
|
| 239 |
+
```bash
|
| 240 |
+
gog gmail watch status --account openclaw@gmail.com
|
| 241 |
+
gog gmail history --account openclaw@gmail.com --since <historyId>
|
| 242 |
+
```
|
| 243 |
+
|
| 244 |
+
## Troubleshooting
|
| 245 |
+
|
| 246 |
+
- `Invalid topicName`: project mismatch (topic not in the OAuth client project).
|
| 247 |
+
- `User not authorized`: missing `roles/pubsub.publisher` on the topic.
|
| 248 |
+
- Empty messages: Gmail push only provides `historyId`; fetch via `gog gmail history`.
|
| 249 |
+
|
| 250 |
+
## Cleanup
|
| 251 |
+
|
| 252 |
+
```bash
|
| 253 |
+
gog gmail watch stop --account openclaw@gmail.com
|
| 254 |
+
gcloud pubsub subscriptions delete gog-gmail-watch-push
|
| 255 |
+
gcloud pubsub topics delete gog-gmail-watch
|
| 256 |
+
```
|
docs/automation/poll.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
summary: "Poll sending via gateway + CLI"
|
| 3 |
+
read_when:
|
| 4 |
+
- Adding or modifying poll support
|
| 5 |
+
- Debugging poll sends from the CLI or gateway
|
| 6 |
+
title: "Polls"
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
# Polls
|
| 10 |
+
|
| 11 |
+
## Supported channels
|
| 12 |
+
|
| 13 |
+
- WhatsApp (web channel)
|
| 14 |
+
- Discord
|
| 15 |
+
- MS Teams (Adaptive Cards)
|
| 16 |
+
|
| 17 |
+
## CLI
|
| 18 |
+
|
| 19 |
+
```bash
|
| 20 |
+
# WhatsApp
|
| 21 |
+
openclaw message poll --target +15555550123 \
|
| 22 |
+
--poll-question "Lunch today?" --poll-option "Yes" --poll-option "No" --poll-option "Maybe"
|
| 23 |
+
openclaw message poll --target 123456789@g.us \
|
| 24 |
+
--poll-question "Meeting time?" --poll-option "10am" --poll-option "2pm" --poll-option "4pm" --poll-multi
|
| 25 |
+
|
| 26 |
+
# Discord
|
| 27 |
+
openclaw message poll --channel discord --target channel:123456789 \
|
| 28 |
+
--poll-question "Snack?" --poll-option "Pizza" --poll-option "Sushi"
|
| 29 |
+
openclaw message poll --channel discord --target channel:123456789 \
|
| 30 |
+
--poll-question "Plan?" --poll-option "A" --poll-option "B" --poll-duration-hours 48
|
| 31 |
+
|
| 32 |
+
# MS Teams
|
| 33 |
+
openclaw message poll --channel msteams --target conversation:19:abc@thread.tacv2 \
|
| 34 |
+
--poll-question "Lunch?" --poll-option "Pizza" --poll-option "Sushi"
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
Options:
|
| 38 |
+
|
| 39 |
+
- `--channel`: `whatsapp` (default), `discord`, or `msteams`
|
| 40 |
+
- `--poll-multi`: allow selecting multiple options
|
| 41 |
+
- `--poll-duration-hours`: Discord-only (defaults to 24 when omitted)
|
| 42 |
+
|
| 43 |
+
## Gateway RPC
|
| 44 |
+
|
| 45 |
+
Method: `poll`
|
| 46 |
+
|
| 47 |
+
Params:
|
| 48 |
+
|
| 49 |
+
- `to` (string, required)
|
| 50 |
+
- `question` (string, required)
|
| 51 |
+
- `options` (string[], required)
|
| 52 |
+
- `maxSelections` (number, optional)
|
| 53 |
+
- `durationHours` (number, optional)
|
| 54 |
+
- `channel` (string, optional, default: `whatsapp`)
|
| 55 |
+
- `idempotencyKey` (string, required)
|
| 56 |
+
|
| 57 |
+
## Channel differences
|
| 58 |
+
|
| 59 |
+
- WhatsApp: 2-12 options, `maxSelections` must be within option count, ignores `durationHours`.
|
| 60 |
+
- Discord: 2-10 options, `durationHours` clamped to 1-768 hours (default 24). `maxSelections > 1` enables multi-select; Discord does not support a strict selection count.
|
| 61 |
+
- MS Teams: Adaptive Card polls (OpenClaw-managed). No native poll API; `durationHours` is ignored.
|
| 62 |
+
|
| 63 |
+
## Agent tool (Message)
|
| 64 |
+
|
| 65 |
+
Use the `message` tool with `poll` action (`to`, `pollQuestion`, `pollOption`, optional `pollMulti`, `pollDurationHours`, `channel`).
|
| 66 |
+
|
| 67 |
+
Note: Discord has no “pick exactly N” mode; `pollMulti` maps to multi-select.
|
| 68 |
+
Teams polls are rendered as Adaptive Cards and require the gateway to stay online
|
| 69 |
+
to record votes in `~/.openclaw/msteams-polls.json`.
|
docs/automation/webhook.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
summary: "Webhook ingress for wake and isolated agent runs"
|
| 3 |
+
read_when:
|
| 4 |
+
- Adding or changing webhook endpoints
|
| 5 |
+
- Wiring external systems into OpenClaw
|
| 6 |
+
title: "Webhooks"
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
# Webhooks
|
| 10 |
+
|
| 11 |
+
Gateway can expose a small HTTP webhook endpoint for external triggers.
|
| 12 |
+
|
| 13 |
+
## Enable
|
| 14 |
+
|
| 15 |
+
```json5
|
| 16 |
+
{
|
| 17 |
+
hooks: {
|
| 18 |
+
enabled: true,
|
| 19 |
+
token: "shared-secret",
|
| 20 |
+
path: "/hooks",
|
| 21 |
+
},
|
| 22 |
+
}
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
Notes:
|
| 26 |
+
|
| 27 |
+
- `hooks.token` is required when `hooks.enabled=true`.
|
| 28 |
+
- `hooks.path` defaults to `/hooks`.
|
| 29 |
+
|
| 30 |
+
## Auth
|
| 31 |
+
|
| 32 |
+
Every request must include the hook token. Prefer headers:
|
| 33 |
+
|
| 34 |
+
- `Authorization: Bearer <token>` (recommended)
|
| 35 |
+
- `x-openclaw-token: <token>`
|
| 36 |
+
- `?token=<token>` (deprecated; logs a warning and will be removed in a future major release)
|
| 37 |
+
|
| 38 |
+
## Endpoints
|
| 39 |
+
|
| 40 |
+
### `POST /hooks/wake`
|
| 41 |
+
|
| 42 |
+
Payload:
|
| 43 |
+
|
| 44 |
+
```json
|
| 45 |
+
{ "text": "System line", "mode": "now" }
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
- `text` **required** (string): The description of the event (e.g., "New email received").
|
| 49 |
+
- `mode` optional (`now` | `next-heartbeat`): Whether to trigger an immediate heartbeat (default `now`) or wait for the next periodic check.
|
| 50 |
+
|
| 51 |
+
Effect:
|
| 52 |
+
|
| 53 |
+
- Enqueues a system event for the **main** session
|
| 54 |
+
- If `mode=now`, triggers an immediate heartbeat
|
| 55 |
+
|
| 56 |
+
### `POST /hooks/agent`
|
| 57 |
+
|
| 58 |
+
Payload:
|
| 59 |
+
|
| 60 |
+
```json
|
| 61 |
+
{
|
| 62 |
+
"message": "Run this",
|
| 63 |
+
"name": "Email",
|
| 64 |
+
"sessionKey": "hook:email:msg-123",
|
| 65 |
+
"wakeMode": "now",
|
| 66 |
+
"deliver": true,
|
| 67 |
+
"channel": "last",
|
| 68 |
+
"to": "+15551234567",
|
| 69 |
+
"model": "openai/gpt-5.2-mini",
|
| 70 |
+
"thinking": "low",
|
| 71 |
+
"timeoutSeconds": 120
|
| 72 |
+
}
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
- `message` **required** (string): The prompt or message for the agent to process.
|
| 76 |
+
- `name` optional (string): Human-readable name for the hook (e.g., "GitHub"), used as a prefix in session summaries.
|
| 77 |
+
- `sessionKey` optional (string): The key used to identify the agent's session. Defaults to a random `hook:<uuid>`. Using a consistent key allows for a multi-turn conversation within the hook context.
|
| 78 |
+
- `wakeMode` optional (`now` | `next-heartbeat`): Whether to trigger an immediate heartbeat (default `now`) or wait for the next periodic check.
|
| 79 |
+
- `deliver` optional (boolean): If `true`, the agent's response will be sent to the messaging channel. Defaults to `true`. Responses that are only heartbeat acknowledgments are automatically skipped.
|
| 80 |
+
- `channel` optional (string): The messaging channel for delivery. One of: `last`, `whatsapp`, `telegram`, `discord`, `slack`, `mattermost` (plugin), `signal`, `imessage`, `msteams`. Defaults to `last`.
|
| 81 |
+
- `to` optional (string): The recipient identifier for the channel (e.g., phone number for WhatsApp/Signal, chat ID for Telegram, channel ID for Discord/Slack/Mattermost (plugin), conversation ID for MS Teams). Defaults to the last recipient in the main session.
|
| 82 |
+
- `model` optional (string): Model override (e.g., `anthropic/claude-3-5-sonnet` or an alias). Must be in the allowed model list if restricted.
|
| 83 |
+
- `thinking` optional (string): Thinking level override (e.g., `low`, `medium`, `high`).
|
| 84 |
+
- `timeoutSeconds` optional (number): Maximum duration for the agent run in seconds.
|
| 85 |
+
|
| 86 |
+
Effect:
|
| 87 |
+
|
| 88 |
+
- Runs an **isolated** agent turn (own session key)
|
| 89 |
+
- Always posts a summary into the **main** session
|
| 90 |
+
- If `wakeMode=now`, triggers an immediate heartbeat
|
| 91 |
+
|
| 92 |
+
### `POST /hooks/<name>` (mapped)
|
| 93 |
+
|
| 94 |
+
Custom hook names are resolved via `hooks.mappings` (see configuration). A mapping can
|
| 95 |
+
turn arbitrary payloads into `wake` or `agent` actions, with optional templates or
|
| 96 |
+
code transforms.
|
| 97 |
+
|
| 98 |
+
Mapping options (summary):
|
| 99 |
+
|
| 100 |
+
- `hooks.presets: ["gmail"]` enables the built-in Gmail mapping.
|
| 101 |
+
- `hooks.mappings` lets you define `match`, `action`, and templates in config.
|
| 102 |
+
- `hooks.transformsDir` + `transform.module` loads a JS/TS module for custom logic.
|
| 103 |
+
- Use `match.source` to keep a generic ingest endpoint (payload-driven routing).
|
| 104 |
+
- TS transforms require a TS loader (e.g. `bun` or `tsx`) or precompiled `.js` at runtime.
|
| 105 |
+
- Set `deliver: true` + `channel`/`to` on mappings to route replies to a chat surface
|
| 106 |
+
(`channel` defaults to `last` and falls back to WhatsApp).
|
| 107 |
+
- `allowUnsafeExternalContent: true` disables the external content safety wrapper for that hook
|
| 108 |
+
(dangerous; only for trusted internal sources).
|
| 109 |
+
- `openclaw webhooks gmail setup` writes `hooks.gmail` config for `openclaw webhooks gmail run`.
|
| 110 |
+
See [Gmail Pub/Sub](/automation/gmail-pubsub) for the full Gmail watch flow.
|
| 111 |
+
|
| 112 |
+
## Responses
|
| 113 |
+
|
| 114 |
+
- `200` for `/hooks/wake`
|
| 115 |
+
- `202` for `/hooks/agent` (async run started)
|
| 116 |
+
- `401` on auth failure
|
| 117 |
+
- `400` on invalid payload
|
| 118 |
+
- `413` on oversized payloads
|
| 119 |
+
|
| 120 |
+
## Examples
|
| 121 |
+
|
| 122 |
+
```bash
|
| 123 |
+
curl -X POST http://127.0.0.1:18789/hooks/wake \
|
| 124 |
+
-H 'Authorization: Bearer SECRET' \
|
| 125 |
+
-H 'Content-Type: application/json' \
|
| 126 |
+
-d '{"text":"New email received","mode":"now"}'
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
```bash
|
| 130 |
+
curl -X POST http://127.0.0.1:18789/hooks/agent \
|
| 131 |
+
-H 'x-openclaw-token: SECRET' \
|
| 132 |
+
-H 'Content-Type: application/json' \
|
| 133 |
+
-d '{"message":"Summarize inbox","name":"Email","wakeMode":"next-heartbeat"}'
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
### Use a different model
|
| 137 |
+
|
| 138 |
+
Add `model` to the agent payload (or mapping) to override the model for that run:
|
| 139 |
+
|
| 140 |
+
```bash
|
| 141 |
+
curl -X POST http://127.0.0.1:18789/hooks/agent \
|
| 142 |
+
-H 'x-openclaw-token: SECRET' \
|
| 143 |
+
-H 'Content-Type: application/json' \
|
| 144 |
+
-d '{"message":"Summarize inbox","name":"Email","model":"openai/gpt-5.2-mini"}'
|
| 145 |
+
```
|
| 146 |
+
|
| 147 |
+
If you enforce `agents.defaults.models`, make sure the override model is included there.
|
| 148 |
+
|
| 149 |
+
```bash
|
| 150 |
+
curl -X POST http://127.0.0.1:18789/hooks/gmail \
|
| 151 |
+
-H 'Authorization: Bearer SECRET' \
|
| 152 |
+
-H 'Content-Type: application/json' \
|
| 153 |
+
-d '{"source":"gmail","messages":[{"from":"Ada","subject":"Hello","snippet":"Hi"}]}'
|
| 154 |
+
```
|
| 155 |
+
|
| 156 |
+
## Security
|
| 157 |
+
|
| 158 |
+
- Keep hook endpoints behind loopback, tailnet, or trusted reverse proxy.
|
| 159 |
+
- Use a dedicated hook token; do not reuse gateway auth tokens.
|
| 160 |
+
- Avoid including sensitive raw payloads in webhook logs.
|
| 161 |
+
- Hook payloads are treated as untrusted and wrapped with safety boundaries by default.
|
| 162 |
+
If you must disable this for a specific hook, set `allowUnsafeExternalContent: true`
|
| 163 |
+
in that hook's mapping (dangerous).
|