darkfire514 commited on
Commit
caea1dc
·
verified ·
1 Parent(s): 4fc4790

Upload 351 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +15 -0
  2. assets/avatar-placeholder.svg +19 -0
  3. assets/chrome-extension/README.md +22 -0
  4. assets/chrome-extension/background.js +438 -0
  5. assets/chrome-extension/icons/icon128.png +0 -0
  6. assets/chrome-extension/icons/icon16.png +0 -0
  7. assets/chrome-extension/icons/icon32.png +0 -0
  8. assets/chrome-extension/icons/icon48.png +0 -0
  9. assets/chrome-extension/manifest.json +25 -0
  10. assets/chrome-extension/options.html +196 -0
  11. assets/chrome-extension/options.js +59 -0
  12. assets/dmg-background-small.png +3 -0
  13. assets/dmg-background.png +3 -0
  14. docs/.i18n/README.md +31 -0
  15. docs/.i18n/glossary.zh-CN.json +82 -0
  16. docs/.i18n/zh-CN.tm.jsonl +0 -0
  17. docs/CNAME +1 -0
  18. docs/_config.yml +53 -0
  19. docs/_layouts/default.html +145 -0
  20. docs/assets/markdown.css +179 -0
  21. docs/assets/openclaw-logo-text-dark.png +3 -0
  22. docs/assets/openclaw-logo-text.png +0 -0
  23. docs/assets/pixel-lobster.svg +60 -0
  24. docs/assets/showcase/agents-ui.jpg +3 -0
  25. docs/assets/showcase/bambu-cli.png +3 -0
  26. docs/assets/showcase/codexmonitor.png +3 -0
  27. docs/assets/showcase/gohome-grafana.png +3 -0
  28. docs/assets/showcase/ios-testflight.jpg +3 -0
  29. docs/assets/showcase/oura-health.png +3 -0
  30. docs/assets/showcase/padel-cli.svg +11 -0
  31. docs/assets/showcase/padel-screenshot.jpg +0 -0
  32. docs/assets/showcase/papla-tts.jpg +0 -0
  33. docs/assets/showcase/pr-review-telegram.jpg +3 -0
  34. docs/assets/showcase/roborock-screenshot.jpg +0 -0
  35. docs/assets/showcase/roborock-status.svg +13 -0
  36. docs/assets/showcase/roof-camera-sky.jpg +3 -0
  37. docs/assets/showcase/snag.png +3 -0
  38. docs/assets/showcase/tesco-shop.jpg +0 -0
  39. docs/assets/showcase/wienerlinien.png +3 -0
  40. docs/assets/showcase/wine-cellar-skill.jpg +0 -0
  41. docs/assets/showcase/winix-air-purifier.jpg +3 -0
  42. docs/assets/showcase/xuezh-pronunciation.jpeg +0 -0
  43. docs/assets/terminal.css +473 -0
  44. docs/assets/theme.js +55 -0
  45. docs/automation/auth-monitoring.md +44 -0
  46. docs/automation/cron-jobs.md +444 -0
  47. docs/automation/cron-vs-heartbeat.md +281 -0
  48. docs/automation/gmail-pubsub.md +256 -0
  49. docs/automation/poll.md +69 -0
  50. 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:&lt;port&gt;/</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

  • SHA256: e7f1d169b295579cd2e0d3aecddec9fa8403ced7bb11ef0f38d75196eb0fc53f
  • Pointer size: 131 Bytes
  • Size of remote file: 225 kB
assets/dmg-background.png ADDED

Git LFS Details

  • SHA256: 781f65f78828c978168d82ec2d0d116b9d49a20a63c723b46efa8d0ccf0d57a8
  • Pointer size: 132 Bytes
  • Size of remote file: 1.01 MB
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

  • SHA256: 13ccf8f9f40a72761aafee4306f5244d136137d5aea4e3e4993f8166c6899183
  • Pointer size: 131 Bytes
  • Size of remote file: 133 kB
docs/assets/openclaw-logo-text.png ADDED
docs/assets/pixel-lobster.svg ADDED
docs/assets/showcase/agents-ui.jpg ADDED

Git LFS Details

  • SHA256: 6437b3d5cfec32d6435f7c34dd2f0bf154d3e8e818d97aaffad118dd38029eaf
  • Pointer size: 131 Bytes
  • Size of remote file: 134 kB
docs/assets/showcase/bambu-cli.png ADDED

Git LFS Details

  • SHA256: 1407340aacfb846dea3121ab4824fe7a19401c7552de1aa563762ffc183419d2
  • Pointer size: 131 Bytes
  • Size of remote file: 149 kB
docs/assets/showcase/codexmonitor.png ADDED

Git LFS Details

  • SHA256: bff3e4a10d0702c5c530e66125754eb9876ac8b157f3702bac13423c08a82680
  • Pointer size: 131 Bytes
  • Size of remote file: 138 kB
docs/assets/showcase/gohome-grafana.png ADDED

Git LFS Details

  • SHA256: c5ed27283ba07ccf01e9a771369200b44821768e03cd15a1938e2c0b777f7f5a
  • Pointer size: 131 Bytes
  • Size of remote file: 388 kB
docs/assets/showcase/ios-testflight.jpg ADDED

Git LFS Details

  • SHA256: 0661a0624e6b8aa003dd377beb3da2020868bc6afc62548e2722b1c92fa607b9
  • Pointer size: 131 Bytes
  • Size of remote file: 170 kB
docs/assets/showcase/oura-health.png ADDED

Git LFS Details

  • SHA256: 701bbebf718cd6f3172c05bd36ff5c1279f54d1bb9014719d467dada46eb2bbf
  • Pointer size: 132 Bytes
  • Size of remote file: 1.25 MB
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

  • SHA256: 0af6093c601749cfa74646d687f7677c431747522f2056bf9c17c1c35f27494e
  • Pointer size: 131 Bytes
  • Size of remote file: 260 kB
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

  • SHA256: 13c04d0fa3dcb88b550348ac6b76eb69770dfc15b7c2467e9ad9f8189b243d65
  • Pointer size: 131 Bytes
  • Size of remote file: 126 kB
docs/assets/showcase/snag.png ADDED

Git LFS Details

  • SHA256: 4bf4b4c17338465e9c2b9d98d1c18af47cd248aa5a8837221f8c3eae24b1c65e
  • Pointer size: 131 Bytes
  • Size of remote file: 817 kB
docs/assets/showcase/tesco-shop.jpg ADDED
docs/assets/showcase/wienerlinien.png ADDED

Git LFS Details

  • SHA256: d29025b9096209bc2cd9ee6b38d6b81393cbfb6a5555341f64510c91eb07aee9
  • Pointer size: 131 Bytes
  • Size of remote file: 137 kB
docs/assets/showcase/wine-cellar-skill.jpg ADDED
docs/assets/showcase/winix-air-purifier.jpg ADDED

Git LFS Details

  • SHA256: 021c8734ef12a61a7884287d906e58155ad207ca9432568c149f0b327d1d57d1
  • Pointer size: 131 Bytes
  • Size of remote file: 202 kB
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).