@2en/clawly-plugins 1.30.0-beta.0 → 1.30.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/auto-pair.ts +1 -1
- package/clawly-config-defaults.json5 +1 -0
- package/config-setup.ts +10 -4
- package/gateway/cron-delivery.test.ts +25 -5
- package/gateway/cron-delivery.ts +15 -2
- package/gateway/node-dangerous-allowlist.ts +3 -0
- package/gateway/offline-push.test.ts +57 -11
- package/gateway/offline-push.ts +22 -5
- package/gateway/presence.test.ts +2 -2
- package/gateway/presence.ts +11 -16
- package/package.json +1 -1
- package/tools/clawly-is-user-online.ts +5 -10
package/auto-pair.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type {PluginApi} from './index'
|
|
|
4
4
|
// Security note: clientId is self-reported by the connecting client. This is safe
|
|
5
5
|
// because the gateway enforces Ed25519 signature verification before a pairing
|
|
6
6
|
// request is created — only clients with valid device identity reach this stage.
|
|
7
|
-
const AUTO_APPROVE_CLIENT_IDS = new Set(['openclaw-ios', 'node-host'])
|
|
7
|
+
const AUTO_APPROVE_CLIENT_IDS = new Set(['openclaw-ios', 'openclaw-macos', 'node-host'])
|
|
8
8
|
const POLL_INTERVAL_MS = 3_000
|
|
9
9
|
|
|
10
10
|
type PendingRequest = {
|
package/config-setup.ts
CHANGED
|
@@ -250,12 +250,18 @@ export function patchBrowser(config: Record<string, unknown>): boolean {
|
|
|
250
250
|
dirty = true
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
// owner: grant mobile app owner permissions (required for cron tool etc.)
|
|
253
|
+
// owner: grant mobile/mac app owner permissions (required for cron tool etc.)
|
|
254
254
|
const ownerAllowFrom = Array.isArray(commands.ownerAllowFrom)
|
|
255
255
|
? (commands.ownerAllowFrom as string[])
|
|
256
256
|
: []
|
|
257
|
-
|
|
258
|
-
|
|
257
|
+
let ownerDirty = false
|
|
258
|
+
for (const clientId of ['openclaw-ios', 'openclaw-macos']) {
|
|
259
|
+
if (!ownerAllowFrom.includes(clientId)) {
|
|
260
|
+
ownerAllowFrom.push(clientId)
|
|
261
|
+
ownerDirty = true
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (ownerDirty) {
|
|
259
265
|
commands.ownerAllowFrom = ownerAllowFrom
|
|
260
266
|
config.commands = commands
|
|
261
267
|
dirty = true
|
|
@@ -490,7 +496,7 @@ export function patchContextPruning(config: Record<string, unknown>): boolean {
|
|
|
490
496
|
return false
|
|
491
497
|
}
|
|
492
498
|
|
|
493
|
-
const DEFAULT_HEARTBEAT_MODEL = `${PROVIDER_NAME}/
|
|
499
|
+
const DEFAULT_HEARTBEAT_MODEL = `${PROVIDER_NAME}/moonshotai/kimi-k2.5`
|
|
494
500
|
|
|
495
501
|
function resolveDefaultHeartbeatModel(pc: ConfigPluginConfig): string {
|
|
496
502
|
return toProviderModelId(pc.defaultHeartbeatModel) || DEFAULT_HEARTBEAT_MODEL
|
|
@@ -279,17 +279,35 @@ describe('cron-delivery', () => {
|
|
|
279
279
|
})
|
|
280
280
|
})
|
|
281
281
|
|
|
282
|
-
test('does not skip meaningful content ending with HEARTBEAT_OK', async () => {
|
|
282
|
+
test('does not skip meaningful content ending with HEARTBEAT_OK when long enough', async () => {
|
|
283
283
|
const {api, handlers} = createMockApi()
|
|
284
284
|
registerCronDelivery(api)
|
|
285
285
|
|
|
286
|
+
// Content after stripping HEARTBEAT_OK must exceed 300 chars to pass through
|
|
287
|
+
const longContent = 'A'.repeat(301) + ' HEARTBEAT_OK'
|
|
286
288
|
await handlers.get('agent_end')!(
|
|
287
|
-
{messages: makeMessages(
|
|
289
|
+
{messages: makeMessages(longContent)},
|
|
288
290
|
{sessionKey: 'agent:clawly:cron:weather-check', agentId: 'clawly'},
|
|
289
291
|
)
|
|
290
292
|
|
|
291
293
|
expect(injectCalls).toHaveLength(1)
|
|
292
294
|
})
|
|
295
|
+
|
|
296
|
+
test('skips short content ending with HEARTBEAT_OK', async () => {
|
|
297
|
+
const {api, logs, handlers} = createMockApi()
|
|
298
|
+
registerCronDelivery(api)
|
|
299
|
+
|
|
300
|
+
await handlers.get('agent_end')!(
|
|
301
|
+
{messages: makeMessages('Weather is good today. HEARTBEAT_OK')},
|
|
302
|
+
{sessionKey: 'agent:clawly:cron:weather-check', agentId: 'clawly'},
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
expect(injectCalls).toHaveLength(0)
|
|
306
|
+
expect(logs).toContainEqual({
|
|
307
|
+
level: 'info',
|
|
308
|
+
msg: expect.stringContaining('skipped (filtered: heartbeat ack)'),
|
|
309
|
+
})
|
|
310
|
+
})
|
|
293
311
|
})
|
|
294
312
|
|
|
295
313
|
describe('agentId validation', () => {
|
|
@@ -327,7 +345,9 @@ describe('cron-delivery', () => {
|
|
|
327
345
|
])
|
|
328
346
|
expect(logs).toContainEqual({
|
|
329
347
|
level: 'info',
|
|
330
|
-
msg:
|
|
348
|
+
msg: expect.stringContaining(
|
|
349
|
+
'cron-delivery: injected into agent:clawly:main (messageId=msg-001)',
|
|
350
|
+
),
|
|
331
351
|
})
|
|
332
352
|
})
|
|
333
353
|
|
|
@@ -360,7 +380,7 @@ describe('cron-delivery', () => {
|
|
|
360
380
|
expect(injectCalls).toHaveLength(0)
|
|
361
381
|
expect(logs).toContainEqual({
|
|
362
382
|
level: 'error',
|
|
363
|
-
msg: '
|
|
383
|
+
msg: expect.stringContaining('sessions.resolve failed: JSON parse error'),
|
|
364
384
|
})
|
|
365
385
|
})
|
|
366
386
|
|
|
@@ -378,7 +398,7 @@ describe('cron-delivery', () => {
|
|
|
378
398
|
expect(injectCalls).toHaveLength(1)
|
|
379
399
|
expect(logs).toContainEqual({
|
|
380
400
|
level: 'error',
|
|
381
|
-
msg: '
|
|
401
|
+
msg: expect.stringContaining('chat.inject failed: timeout'),
|
|
382
402
|
})
|
|
383
403
|
})
|
|
384
404
|
})
|
package/gateway/cron-delivery.ts
CHANGED
|
@@ -59,9 +59,13 @@ export function registerCronDelivery(api: PluginApi) {
|
|
|
59
59
|
`cron-delivery[debug]: agent_end fired sessionKey=${sessionKey} agentId=${agentId ?? 'undefined'} trigger=${String(ctx?.trigger ?? 'undefined')}`,
|
|
60
60
|
)
|
|
61
61
|
|
|
62
|
+
const t0 = Date.now()
|
|
62
63
|
try {
|
|
63
64
|
// Extract raw assistant text (preserving formatting)
|
|
64
65
|
const text = getRawLastAssistantText(event.messages)
|
|
66
|
+
api.logger.info(
|
|
67
|
+
`cron-delivery[debug]: extracted text len=${text?.length ?? 'null'} elapsed=${Date.now() - t0}ms`,
|
|
68
|
+
)
|
|
65
69
|
if (text == null) {
|
|
66
70
|
api.logger.info('cron-delivery: skipped (no assistant message)')
|
|
67
71
|
if (sessionKey) markCronDeliverySkipped(sessionKey, 'no assistant message')
|
|
@@ -97,9 +101,18 @@ export function registerCronDelivery(api: PluginApi) {
|
|
|
97
101
|
return
|
|
98
102
|
}
|
|
99
103
|
|
|
104
|
+
api.logger.info(
|
|
105
|
+
`cron-delivery[debug]: resolving session for agentId=${agentId} elapsed=${Date.now() - t0}ms`,
|
|
106
|
+
)
|
|
100
107
|
const mainSessionKey = await resolveSessionKey(agentId, api)
|
|
108
|
+
api.logger.info(
|
|
109
|
+
`cron-delivery[debug]: resolved mainSessionKey=${mainSessionKey} elapsed=${Date.now() - t0}ms`,
|
|
110
|
+
)
|
|
101
111
|
|
|
102
112
|
// Inject the cron result into the main session
|
|
113
|
+
api.logger.info(
|
|
114
|
+
`cron-delivery[debug]: injecting message len=${text.length} into ${mainSessionKey} elapsed=${Date.now() - t0}ms`,
|
|
115
|
+
)
|
|
103
116
|
const result = await injectAssistantMessage(
|
|
104
117
|
{
|
|
105
118
|
sessionKey: mainSessionKey,
|
|
@@ -110,12 +123,12 @@ export function registerCronDelivery(api: PluginApi) {
|
|
|
110
123
|
|
|
111
124
|
if (sessionKey) markCronDelivered(sessionKey)
|
|
112
125
|
api.logger.info(
|
|
113
|
-
`cron-delivery: injected into ${mainSessionKey} (messageId=${result.messageId})`,
|
|
126
|
+
`cron-delivery: injected into ${mainSessionKey} (messageId=${result.messageId}) elapsed=${Date.now() - t0}ms`,
|
|
114
127
|
)
|
|
115
128
|
} catch (err) {
|
|
116
129
|
const msg = err instanceof Error ? err.message : String(err)
|
|
117
130
|
if (sessionKey) markCronDeliverySkipped(sessionKey, msg)
|
|
118
|
-
api.logger.error(`cron-delivery: ${msg}`)
|
|
131
|
+
api.logger.error(`cron-delivery: error after ${Date.now() - t0}ms — ${msg}`)
|
|
119
132
|
}
|
|
120
133
|
})
|
|
121
134
|
|
|
@@ -29,6 +29,9 @@ const DANGEROUS_COMMANDS = [
|
|
|
29
29
|
// Reminders + calendar (iOS nodes)
|
|
30
30
|
'reminders.add',
|
|
31
31
|
'calendar.add',
|
|
32
|
+
// Device permissions (iOS nodes) — not in OpenClaw's iOS defaults
|
|
33
|
+
'device.permissions',
|
|
34
|
+
'device.requestPermission',
|
|
32
35
|
]
|
|
33
36
|
|
|
34
37
|
export function registerNodeDangerousAllowlist(api: PluginApi) {
|
|
@@ -391,21 +391,40 @@ describe('shouldSkipPushForMessage', () => {
|
|
|
391
391
|
expect(shouldSkipPushForMessage('HEARTBEAT_OK\n')).toBe('heartbeat ack')
|
|
392
392
|
})
|
|
393
393
|
|
|
394
|
-
test('
|
|
395
|
-
|
|
396
|
-
expect(shouldSkipPushForMessage('
|
|
394
|
+
test('skips short content ending with HEARTBEAT_OK (under ackMaxChars threshold)', () => {
|
|
395
|
+
// "All good." is 9 chars — well under 300 threshold, matches OpenClaw heartbeat runner behavior
|
|
396
|
+
expect(shouldSkipPushForMessage('All good. HEARTBEAT_OK')).toBe('heartbeat ack')
|
|
397
|
+
expect(shouldSkipPushForMessage('Nothing to report. HEARTBEAT_OK。')).toBe('heartbeat ack')
|
|
397
398
|
})
|
|
398
399
|
|
|
399
|
-
test('
|
|
400
|
+
test('skips verbose heartbeat response under ackMaxChars threshold', () => {
|
|
401
|
+
// 89 chars remaining — under 300, matches OpenClaw behavior
|
|
400
402
|
const verbose =
|
|
401
403
|
'The user said hello recently. Looking at HEARTBEAT.md checklist: nothing needs attention. HEARTBEAT_OK'
|
|
402
|
-
expect(shouldSkipPushForMessage(verbose)).
|
|
404
|
+
expect(shouldSkipPushForMessage(verbose)).toBe('heartbeat ack')
|
|
403
405
|
})
|
|
404
406
|
|
|
405
|
-
test('
|
|
407
|
+
test('skips HEARTBEAT_OK at start with short status note', () => {
|
|
408
|
+
expect(shouldSkipPushForMessage('HEARTBEAT_OK — all systems nominal.')).toBe('heartbeat ack')
|
|
409
|
+
expect(shouldSkipPushForMessage('HEARTBEAT_OK. Nothing to report.')).toBe('heartbeat ack')
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
test('does NOT skip HEARTBEAT_OK with >300 chars of real content', () => {
|
|
413
|
+
const longContent = 'A'.repeat(301)
|
|
414
|
+
expect(shouldSkipPushForMessage(`HEARTBEAT_OK ${longContent}`)).toBeNull()
|
|
415
|
+
expect(shouldSkipPushForMessage(`${longContent} HEARTBEAT_OK`)).toBeNull()
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
test('skips message mentioning HEARTBEAT_OK at start with short remaining text', () => {
|
|
419
|
+
// "is a status code I output after each check." is 45 chars — under 300, matches OpenClaw's stripTokenAtEdges
|
|
406
420
|
expect(
|
|
407
421
|
shouldSkipPushForMessage('HEARTBEAT_OK is a status code I output after each check.'),
|
|
408
|
-
).
|
|
422
|
+
).toBe('heartbeat ack')
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
test('does NOT skip HEARTBEAT_OK appearing only in middle of text', () => {
|
|
426
|
+
expect(shouldSkipPushForMessage('You asked about HEARTBEAT_OK earlier.')).toBeNull()
|
|
427
|
+
expect(shouldSkipPushForMessage('The token HEARTBEAT_OK is used for health checks.')).toBeNull()
|
|
409
428
|
})
|
|
410
429
|
|
|
411
430
|
test('skips system prompt leak', () => {
|
|
@@ -497,7 +516,7 @@ describe('offline-push with filtered messages', () => {
|
|
|
497
516
|
})
|
|
498
517
|
})
|
|
499
518
|
|
|
500
|
-
test('
|
|
519
|
+
test('skips push for short heartbeat response with HEARTBEAT_OK (under ackMaxChars)', async () => {
|
|
501
520
|
const {api, logs, handlers} = createMockApi()
|
|
502
521
|
registerOfflinePush(api)
|
|
503
522
|
|
|
@@ -513,11 +532,37 @@ describe('offline-push with filtered messages', () => {
|
|
|
513
532
|
{sessionKey: 'agent:clawly:main'},
|
|
514
533
|
)
|
|
515
534
|
|
|
535
|
+
expect(logs).toContainEqual({
|
|
536
|
+
level: 'info',
|
|
537
|
+
msg: expect.stringContaining('skipped (filtered: heartbeat ack)'),
|
|
538
|
+
})
|
|
539
|
+
expect(logs.filter((l) => l.msg.includes('notified'))).toHaveLength(0)
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
test('sends push for long heartbeat response over ackMaxChars and strips HEARTBEAT_OK from body', async () => {
|
|
543
|
+
const {api, logs, handlers} = createMockApi()
|
|
544
|
+
registerOfflinePush(api)
|
|
545
|
+
|
|
546
|
+
const longContent = 'A'.repeat(301)
|
|
547
|
+
await handlers.get('agent_end')!(
|
|
548
|
+
{
|
|
549
|
+
messages: [
|
|
550
|
+
{
|
|
551
|
+
role: 'assistant',
|
|
552
|
+
content: `${longContent}\n\nHEARTBEAT_OK`,
|
|
553
|
+
},
|
|
554
|
+
],
|
|
555
|
+
},
|
|
556
|
+
{sessionKey: 'agent:clawly:main'},
|
|
557
|
+
)
|
|
558
|
+
|
|
516
559
|
expect(logs).toContainEqual({
|
|
517
560
|
level: 'info',
|
|
518
561
|
msg: expect.stringContaining('notified (session=agent:clawly:main)'),
|
|
519
562
|
})
|
|
520
|
-
|
|
563
|
+
// HEARTBEAT_OK should be stripped from body, content truncated to 140
|
|
564
|
+
expect(lastPushOpts?.body?.length).toBe(141)
|
|
565
|
+
expect(lastPushOpts?.body?.endsWith('…')).toBe(true)
|
|
521
566
|
})
|
|
522
567
|
|
|
523
568
|
test('sends push for normal message text', async () => {
|
|
@@ -715,7 +760,7 @@ describe('isInternalRuntimeContextTriggered', () => {
|
|
|
715
760
|
// ── Heartbeat-triggered integration tests ────────────────────────
|
|
716
761
|
|
|
717
762
|
describe('offline-push with heartbeat-triggered turns', () => {
|
|
718
|
-
test('skips push for verbose heartbeat response', async () => {
|
|
763
|
+
test('skips push for verbose heartbeat response (caught by content filter before trigger check)', async () => {
|
|
719
764
|
const {api, logs, handlers} = createMockApi()
|
|
720
765
|
registerOfflinePush(api)
|
|
721
766
|
|
|
@@ -732,9 +777,10 @@ describe('offline-push with heartbeat-triggered turns', () => {
|
|
|
732
777
|
{sessionKey: 'agent:clawly:main'},
|
|
733
778
|
)
|
|
734
779
|
|
|
780
|
+
// Now caught by shouldSkipPushForMessage (content under 300 chars) before trigger check
|
|
735
781
|
expect(logs).toContainEqual({
|
|
736
782
|
level: 'info',
|
|
737
|
-
msg: '
|
|
783
|
+
msg: expect.stringContaining('skipped (filtered: heartbeat ack)'),
|
|
738
784
|
})
|
|
739
785
|
expect(logs.filter((l) => l.msg.includes('notified'))).toHaveLength(0)
|
|
740
786
|
})
|
package/gateway/offline-push.ts
CHANGED
|
@@ -157,10 +157,17 @@ export function shouldSkipPushForMessage(text: string): string | null {
|
|
|
157
157
|
// — the scenario is extremely unlikely and the cost is a missed push, not hidden UI.
|
|
158
158
|
if (/NO_REPLY[\p{P}\s]*$/u.test(text)) return 'silent reply'
|
|
159
159
|
|
|
160
|
-
// Heartbeat acknowledgment
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
// Heartbeat acknowledgment — strip HEARTBEAT_OK from both edges (mirrors
|
|
161
|
+
// OpenClaw's stripTokenAtEdges). Skip if remaining text ≤ ackMaxChars (300).
|
|
162
|
+
const HEARTBEAT_ACK_MAX_CHARS = 300
|
|
163
|
+
const hasAtEnd = /HEARTBEAT_OK[\p{P}\s]*$/u.test(text)
|
|
164
|
+
const hasAtStart = /^[\p{P}\s]*HEARTBEAT_OK/u.test(text)
|
|
165
|
+
if (hasAtEnd || hasAtStart) {
|
|
166
|
+
let stripped = text
|
|
167
|
+
if (hasAtEnd) stripped = stripped.replace(/HEARTBEAT_OK[\p{P}\s]*$/u, '')
|
|
168
|
+
if (hasAtStart) stripped = stripped.replace(/^[\p{P}\s]*HEARTBEAT_OK[\p{P}\s]*/u, '')
|
|
169
|
+
stripped = stripped.trim()
|
|
170
|
+
if (stripped.length <= HEARTBEAT_ACK_MAX_CHARS) return 'heartbeat ack'
|
|
164
171
|
}
|
|
165
172
|
|
|
166
173
|
// Agent echoed system prompt metadata — mobile hides as "systemPromptLeak"
|
|
@@ -270,7 +277,17 @@ export function registerOfflinePush(api: PluginApi) {
|
|
|
270
277
|
return
|
|
271
278
|
}
|
|
272
279
|
|
|
273
|
-
const noHeartbeat =
|
|
280
|
+
const noHeartbeat =
|
|
281
|
+
(() => {
|
|
282
|
+
if (!fullText) return null
|
|
283
|
+
const atEnd = /HEARTBEAT_OK[\p{P}\s]*$/u.test(fullText)
|
|
284
|
+
const atStart = /^[\p{P}\s]*HEARTBEAT_OK/u.test(fullText)
|
|
285
|
+
if (!atEnd && !atStart) return fullText.trim()
|
|
286
|
+
let s = fullText
|
|
287
|
+
if (atEnd) s = s.replace(/HEARTBEAT_OK[\p{P}\s]*$/u, '')
|
|
288
|
+
if (atStart) s = s.replace(/^[\p{P}\s]*HEARTBEAT_OK[\p{P}\s]*/u, '')
|
|
289
|
+
return s.trim()
|
|
290
|
+
})() ?? null
|
|
274
291
|
const cleaned = noHeartbeat ? stripPlaceholders(noHeartbeat) : null
|
|
275
292
|
const preview = cleaned && cleaned.length > 140 ? `${cleaned.slice(0, 140)}…` : cleaned
|
|
276
293
|
const body = preview || 'Your response is ready'
|
package/gateway/presence.test.ts
CHANGED
|
@@ -6,8 +6,8 @@ describe('isOnlineEntry', () => {
|
|
|
6
6
|
expect(isOnlineEntry({host: 'openclaw-ios', reason: 'foreground'})).toBe(true)
|
|
7
7
|
})
|
|
8
8
|
|
|
9
|
-
test('returns
|
|
10
|
-
expect(isOnlineEntry({host: 'openclaw-ios', reason: 'connect'})).toBe(
|
|
9
|
+
test('returns false for reason "connect" (WebSocket alive but not foreground)', () => {
|
|
10
|
+
expect(isOnlineEntry({host: 'openclaw-ios', reason: 'connect'})).toBe(false)
|
|
11
11
|
})
|
|
12
12
|
|
|
13
13
|
test('returns false for reason "background"', () => {
|
package/gateway/presence.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Online presence check — queries `system-presence` via the gateway CLI
|
|
3
|
-
* and checks if
|
|
3
|
+
* and checks if ANY device is in the foreground.
|
|
4
4
|
*
|
|
5
|
-
* Method: clawly.isOnline(
|
|
5
|
+
* Method: clawly.isOnline() → { isOnline: boolean }
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {$} from 'zx'
|
|
@@ -12,43 +12,38 @@ import {stripCliLogs} from '../lib/stripCliLogs'
|
|
|
12
12
|
|
|
13
13
|
$.verbose = false
|
|
14
14
|
|
|
15
|
-
const DEFAULT_HOST = 'openclaw-ios'
|
|
16
|
-
|
|
17
15
|
interface PresenceEntry {
|
|
18
16
|
host?: string
|
|
19
17
|
reason?: string
|
|
20
18
|
}
|
|
21
19
|
|
|
22
|
-
/** Returns true if the presence entry indicates the
|
|
20
|
+
/** Returns true if the presence entry indicates the user is actively viewing the app.
|
|
23
21
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* 恢复 'connect' 判断以修复在线检测。 */
|
|
22
|
+
* Only `foreground` counts — `connect` merely means the WebSocket is alive,
|
|
23
|
+
* which is true for backgrounded apps and should not suppress push notifications. */
|
|
27
24
|
export function isOnlineEntry(entry: PresenceEntry | undefined): boolean {
|
|
28
25
|
if (!entry) return false
|
|
29
|
-
return entry.reason === 'foreground'
|
|
26
|
+
return entry.reason === 'foreground'
|
|
30
27
|
}
|
|
31
28
|
|
|
32
29
|
/**
|
|
33
30
|
* Shells out to `openclaw gateway call system-presence` and checks
|
|
34
|
-
* whether
|
|
31
|
+
* whether ANY device has a foreground presence entry.
|
|
35
32
|
*/
|
|
36
|
-
export async function isClientOnline(
|
|
33
|
+
export async function isClientOnline(): Promise<boolean> {
|
|
37
34
|
try {
|
|
38
35
|
const result = await $`openclaw gateway call system-presence --json`
|
|
39
36
|
const jsonStr = stripCliLogs(result.stdout)
|
|
40
37
|
const entries: PresenceEntry[] = JSON.parse(jsonStr)
|
|
41
|
-
|
|
42
|
-
return isOnlineEntry(entry)
|
|
38
|
+
return entries.some(isOnlineEntry)
|
|
43
39
|
} catch {
|
|
44
40
|
return false
|
|
45
41
|
}
|
|
46
42
|
}
|
|
47
43
|
|
|
48
44
|
export function registerPresence(api: PluginApi) {
|
|
49
|
-
api.registerGatewayMethod('clawly.isOnline', async ({
|
|
50
|
-
const
|
|
51
|
-
const isOnline = await isClientOnline(host)
|
|
45
|
+
api.registerGatewayMethod('clawly.isOnline', async ({respond}) => {
|
|
46
|
+
const isOnline = await isClientOnline()
|
|
52
47
|
respond(true, {isOnline})
|
|
53
48
|
})
|
|
54
49
|
|
package/package.json
CHANGED
|
@@ -11,22 +11,17 @@ const TOOL_NAME = 'clawly_is_user_online'
|
|
|
11
11
|
|
|
12
12
|
const parameters: Record<string, unknown> = {
|
|
13
13
|
type: 'object',
|
|
14
|
-
properties: {
|
|
15
|
-
host: {
|
|
16
|
-
type: 'string',
|
|
17
|
-
description: 'Presence host identifier (default: "openclaw-ios")',
|
|
18
|
-
},
|
|
19
|
-
},
|
|
14
|
+
properties: {},
|
|
20
15
|
}
|
|
21
16
|
|
|
22
17
|
export function registerIsUserOnlineTool(api: PluginApi) {
|
|
23
18
|
api.registerTool({
|
|
24
19
|
name: TOOL_NAME,
|
|
25
|
-
description:
|
|
20
|
+
description:
|
|
21
|
+
"Check if the user's mobile device is currently online (any device in foreground).",
|
|
26
22
|
parameters,
|
|
27
|
-
async execute(
|
|
28
|
-
const
|
|
29
|
-
const isOnline = await isClientOnline(host)
|
|
23
|
+
async execute() {
|
|
24
|
+
const isOnline = await isClientOnline()
|
|
30
25
|
return {content: [{type: 'text', text: JSON.stringify({isOnline})}]}
|
|
31
26
|
},
|
|
32
27
|
})
|