@2en/clawly-plugins 1.30.0-beta.4 → 1.30.0-beta.5
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/gateway/cron-delivery.test.ts +16 -0
- package/gateway/offline-push.test.ts +43 -0
- package/gateway/offline-push.ts +8 -8
- package/gateway/presence.ts +7 -4
- package/gateway-fetch.ts +12 -5
- package/index.ts +1 -1
- package/lib/calendar-cache.ts +4 -0
- package/openclaw.plugin.json +1 -0
- package/package.json +1 -1
- package/tools/clawly-calendar.ts +44 -0
|
@@ -263,6 +263,22 @@ describe('cron-delivery', () => {
|
|
|
263
263
|
})
|
|
264
264
|
})
|
|
265
265
|
|
|
266
|
+
test('skips HEARTBEAT OK with space (bare)', async () => {
|
|
267
|
+
const {api, logs, handlers} = createMockApi()
|
|
268
|
+
registerCronDelivery(api)
|
|
269
|
+
|
|
270
|
+
await handlers.get('agent_end')!(
|
|
271
|
+
{messages: makeMessages('HEARTBEAT OK')},
|
|
272
|
+
{sessionKey: 'agent:clawly:cron:weather-check', agentId: 'clawly'},
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
expect(injectCalls).toHaveLength(0)
|
|
276
|
+
expect(logs).toContainEqual({
|
|
277
|
+
level: 'info',
|
|
278
|
+
msg: expect.stringContaining('skipped (filtered: heartbeat ack)'),
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
|
|
266
282
|
test('skips empty text', async () => {
|
|
267
283
|
const {api, logs, handlers} = createMockApi()
|
|
268
284
|
registerCronDelivery(api)
|
|
@@ -446,6 +446,22 @@ describe('shouldSkipPushForMessage', () => {
|
|
|
446
446
|
expect(shouldSkipPushForMessage('The token HEARTBEAT_OK is used for health checks.')).toBeNull()
|
|
447
447
|
})
|
|
448
448
|
|
|
449
|
+
test('skips heartbeat ack with space variant (HEARTBEAT OK)', () => {
|
|
450
|
+
expect(shouldSkipPushForMessage('HEARTBEAT OK')).toBe('heartbeat ack')
|
|
451
|
+
expect(shouldSkipPushForMessage('HEARTBEAT OK.')).toBe('heartbeat ack')
|
|
452
|
+
expect(shouldSkipPushForMessage('HEARTBEAT OK\n')).toBe('heartbeat ack')
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
test('skips short content ending with HEARTBEAT OK (space variant)', () => {
|
|
456
|
+
expect(shouldSkipPushForMessage('All good. HEARTBEAT OK')).toBe('heartbeat ack')
|
|
457
|
+
expect(shouldSkipPushForMessage('Nothing to report. HEARTBEAT OK。')).toBe('heartbeat ack')
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
test('skips HEARTBEAT OK at start with short status note (space variant)', () => {
|
|
461
|
+
expect(shouldSkipPushForMessage('HEARTBEAT OK — all systems nominal.')).toBe('heartbeat ack')
|
|
462
|
+
expect(shouldSkipPushForMessage('HEARTBEAT OK. Nothing to report.')).toBe('heartbeat ack')
|
|
463
|
+
})
|
|
464
|
+
|
|
449
465
|
test('skips system prompt leak', () => {
|
|
450
466
|
expect(
|
|
451
467
|
shouldSkipPushForMessage('Here is some Conversation info (untrusted metadata) text'),
|
|
@@ -584,6 +600,33 @@ describe('offline-push with filtered messages', () => {
|
|
|
584
600
|
expect(lastPushOpts?.body?.endsWith('…')).toBe(true)
|
|
585
601
|
})
|
|
586
602
|
|
|
603
|
+
test('sends push for long content and strips HEARTBEAT OK (space variant) from body', async () => {
|
|
604
|
+
const {api, logs, handlers} = createMockApi()
|
|
605
|
+
registerOfflinePush(api)
|
|
606
|
+
|
|
607
|
+
const longContent = 'A'.repeat(301)
|
|
608
|
+
await handlers.get('agent_end')!(
|
|
609
|
+
{
|
|
610
|
+
messages: [
|
|
611
|
+
{
|
|
612
|
+
role: 'assistant',
|
|
613
|
+
content: `${longContent}\n\nHEARTBEAT OK`,
|
|
614
|
+
},
|
|
615
|
+
],
|
|
616
|
+
},
|
|
617
|
+
{sessionKey: 'agent:clawly:main'},
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
expect(logs).toContainEqual({
|
|
621
|
+
level: 'info',
|
|
622
|
+
msg: expect.stringContaining('notified (session=agent:clawly:main)'),
|
|
623
|
+
})
|
|
624
|
+
// HEARTBEAT OK should be stripped from body, content truncated to 140
|
|
625
|
+
expect(lastPushOpts?.body?.length).toBe(141)
|
|
626
|
+
expect(lastPushOpts?.body?.endsWith('…')).toBe(true)
|
|
627
|
+
expect(lastPushOpts?.body).not.toContain('HEARTBEAT')
|
|
628
|
+
})
|
|
629
|
+
|
|
587
630
|
test('sends push for normal message text', async () => {
|
|
588
631
|
const {api, logs, handlers} = createMockApi()
|
|
589
632
|
registerOfflinePush(api)
|
package/gateway/offline-push.ts
CHANGED
|
@@ -160,12 +160,12 @@ export function shouldSkipPushForMessage(text: string): string | null {
|
|
|
160
160
|
// Heartbeat acknowledgment — strip HEARTBEAT_OK from both edges (mirrors
|
|
161
161
|
// OpenClaw's stripTokenAtEdges). Skip if remaining text ≤ ackMaxChars (300).
|
|
162
162
|
const HEARTBEAT_ACK_MAX_CHARS = 300
|
|
163
|
-
const hasAtEnd = /
|
|
164
|
-
const hasAtStart = /^[\p{P}\s]*
|
|
163
|
+
const hasAtEnd = /HEARTBEAT[_ ]OK[\p{P}\s]*$/u.test(text)
|
|
164
|
+
const hasAtStart = /^[\p{P}\s]*HEARTBEAT[_ ]OK/u.test(text)
|
|
165
165
|
if (hasAtEnd || hasAtStart) {
|
|
166
166
|
let stripped = text
|
|
167
|
-
if (hasAtEnd) stripped = stripped.replace(/
|
|
168
|
-
if (hasAtStart) stripped = stripped.replace(/^[\p{P}\s]*
|
|
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
169
|
stripped = stripped.trim()
|
|
170
170
|
if (stripped.length <= HEARTBEAT_ACK_MAX_CHARS) return 'heartbeat ack'
|
|
171
171
|
}
|
|
@@ -308,12 +308,12 @@ export function registerOfflinePush(api: PluginApi) {
|
|
|
308
308
|
const noHeartbeat =
|
|
309
309
|
(() => {
|
|
310
310
|
if (!fullText) return null
|
|
311
|
-
const atEnd = /
|
|
312
|
-
const atStart = /^[\p{P}\s]*
|
|
311
|
+
const atEnd = /HEARTBEAT[_ ]OK[\p{P}\s]*$/u.test(fullText)
|
|
312
|
+
const atStart = /^[\p{P}\s]*HEARTBEAT[_ ]OK/u.test(fullText)
|
|
313
313
|
if (!atEnd && !atStart) return fullText.trim()
|
|
314
314
|
let s = fullText
|
|
315
|
-
if (atEnd) s = s.replace(/
|
|
316
|
-
if (atStart) s = s.replace(/^[\p{P}\s]*
|
|
315
|
+
if (atEnd) s = s.replace(/HEARTBEAT[_ ]OK[\p{P}\s]*$/u, '')
|
|
316
|
+
if (atStart) s = s.replace(/^[\p{P}\s]*HEARTBEAT[_ ]OK[\p{P}\s]*/u, '')
|
|
317
317
|
return s.trim()
|
|
318
318
|
})() ?? null
|
|
319
319
|
const cleaned = noHeartbeat ? stripPlaceholders(noHeartbeat) : null
|
package/gateway/presence.ts
CHANGED
|
@@ -27,14 +27,17 @@ export function isOnlineEntry(entry: PresenceEntry | undefined): boolean {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* Shells out to `openclaw gateway call system-presence` and
|
|
31
|
-
*
|
|
30
|
+
* Shells out to `openclaw gateway call system-presence` and returns true
|
|
31
|
+
* if any device has a `foreground` presence entry.
|
|
32
|
+
*
|
|
33
|
+
* False-negatives (beacon lapsed) are handled client-side — the mobile
|
|
34
|
+
* app suppresses `agent_end` notifications while in the foreground.
|
|
32
35
|
*/
|
|
33
36
|
export async function isClientOnline(): Promise<boolean> {
|
|
34
37
|
try {
|
|
35
38
|
const result = await $`openclaw gateway call system-presence --json`
|
|
36
|
-
const
|
|
37
|
-
const entries: PresenceEntry[] =
|
|
39
|
+
const parsed: unknown = JSON.parse(stripCliLogs(result.stdout))
|
|
40
|
+
const entries: PresenceEntry[] = Array.isArray(parsed) ? parsed : []
|
|
38
41
|
return entries.some(isOnlineEntry)
|
|
39
42
|
} catch {
|
|
40
43
|
return false
|
package/gateway-fetch.ts
CHANGED
|
@@ -11,11 +11,18 @@ export type HandlerResult = {ok: boolean; data?: unknown; error?: {code: string;
|
|
|
11
11
|
export function getGatewayConfig(api: PluginApi): GatewayCfg {
|
|
12
12
|
const cfg = api.pluginConfig && typeof api.pluginConfig === 'object' ? api.pluginConfig : {}
|
|
13
13
|
const c = cfg as Record<string, unknown>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
|
|
15
|
+
// URL: prefer clawlyApiBaseUrl, fall back to deprecated skillGatewayBaseUrl
|
|
16
|
+
const rawUrl =
|
|
17
|
+
(typeof c.clawlyApiBaseUrl === 'string' ? (c.clawlyApiBaseUrl as string) : '') ||
|
|
18
|
+
(typeof c.skillGatewayBaseUrl === 'string' ? (c.skillGatewayBaseUrl as string) : '')
|
|
19
|
+
const baseUrl = rawUrl.replace(/\/$/, '')
|
|
20
|
+
|
|
21
|
+
// Token: prefer modelGatewayToken, fall back to deprecated skillGatewayToken
|
|
22
|
+
const token =
|
|
23
|
+
(typeof c.modelGatewayToken === 'string' ? (c.modelGatewayToken as string) : '') ||
|
|
24
|
+
(typeof c.skillGatewayToken === 'string' ? (c.skillGatewayToken as string) : '')
|
|
25
|
+
|
|
19
26
|
return {baseUrl, token}
|
|
20
27
|
}
|
|
21
28
|
|
package/index.ts
CHANGED
|
@@ -80,7 +80,7 @@ export default {
|
|
|
80
80
|
registerAutoPair(api)
|
|
81
81
|
registerAutoUpdate(api)
|
|
82
82
|
|
|
83
|
-
// Email & calendar (optional — requires
|
|
83
|
+
// Email & calendar (optional — requires API base URL + token)
|
|
84
84
|
const gw = getGatewayConfig(api)
|
|
85
85
|
if (gw.baseUrl && gw.token) {
|
|
86
86
|
registerEmail(api, gw)
|
package/lib/calendar-cache.ts
CHANGED
|
@@ -23,6 +23,9 @@ export interface NativeCalendar {
|
|
|
23
23
|
type: string
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export type AlarmMethod = 'alarm' | 'alert' | 'email' | 'default' | 'sms'
|
|
27
|
+
export type AlarmInput = {relativeOffset?: number; method?: AlarmMethod}
|
|
28
|
+
|
|
26
29
|
export interface NativeCalendarEvent {
|
|
27
30
|
id: string
|
|
28
31
|
calendarId: string
|
|
@@ -35,6 +38,7 @@ export interface NativeCalendarEvent {
|
|
|
35
38
|
url: string
|
|
36
39
|
timeZone: string
|
|
37
40
|
organizer: string
|
|
41
|
+
alarms?: AlarmInput[]
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
export interface CalendarCache {
|
package/openclaw.plugin.json
CHANGED
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"defaultNoInput": { "type": "boolean" },
|
|
48
48
|
"defaultTimeoutMs": { "type": "number", "minimum": 1000 },
|
|
49
49
|
"configPath": { "type": "string" },
|
|
50
|
+
"clawlyApiBaseUrl": { "type": "string" },
|
|
50
51
|
"skillGatewayBaseUrl": { "type": "string" },
|
|
51
52
|
"skillGatewayToken": { "type": "string" },
|
|
52
53
|
"modelGatewayBaseUrl": { "type": "string" },
|
package/package.json
CHANGED
package/tools/clawly-calendar.ts
CHANGED
|
@@ -158,6 +158,26 @@ export function registerCalendarTools(api: PluginApi) {
|
|
|
158
158
|
location: {type: 'string', description: 'Event location'},
|
|
159
159
|
notes: {type: 'string', description: 'Event description/notes'},
|
|
160
160
|
timeZone: {type: 'string', description: 'IANA time zone (e.g. America/New_York)'},
|
|
161
|
+
alarms: {
|
|
162
|
+
type: 'array',
|
|
163
|
+
description:
|
|
164
|
+
'Array of alarm objects to set reminders. Each alarm has a relativeOffset in minutes (negative = before event). Example: [{relativeOffset: -15}] for 15 minutes before. Only alarms with valid relativeOffset (number) or method (string) will be applied. To verify reminders were set, use clawly_calendar_list_events after creation.',
|
|
165
|
+
items: {
|
|
166
|
+
type: 'object',
|
|
167
|
+
properties: {
|
|
168
|
+
relativeOffset: {
|
|
169
|
+
type: 'number',
|
|
170
|
+
description:
|
|
171
|
+
'Minutes relative to the event start time. Use negative values for reminders before the event (e.g. -15 = 15 min before, -60 = 1 hour before).',
|
|
172
|
+
},
|
|
173
|
+
method: {
|
|
174
|
+
type: 'string',
|
|
175
|
+
enum: ['alarm', 'alert', 'email', 'default', 'sms'],
|
|
176
|
+
description: 'Alarm method (Android only). iOS always uses notification.',
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
161
181
|
},
|
|
162
182
|
},
|
|
163
183
|
async execute(_toolCallId, params) {
|
|
@@ -211,6 +231,9 @@ export function registerCalendarTools(api: PluginApi) {
|
|
|
211
231
|
...(params.location ? {location: params.location} : {}),
|
|
212
232
|
...(params.notes ? {notes: params.notes} : {}),
|
|
213
233
|
...(params.timeZone ? {timeZone: params.timeZone} : {}),
|
|
234
|
+
...(Array.isArray(params.alarms) && params.alarms.length > 0
|
|
235
|
+
? {alarms: params.alarms}
|
|
236
|
+
: {}),
|
|
214
237
|
}
|
|
215
238
|
|
|
216
239
|
const resultPromise = waitForActionResult(actionId)
|
|
@@ -290,6 +313,26 @@ export function registerCalendarTools(api: PluginApi) {
|
|
|
290
313
|
location: {type: 'string', description: 'New event location'},
|
|
291
314
|
notes: {type: 'string', description: 'New event description/notes'},
|
|
292
315
|
timeZone: {type: 'string', description: 'IANA time zone (e.g. America/New_York)'},
|
|
316
|
+
alarms: {
|
|
317
|
+
type: 'array',
|
|
318
|
+
description:
|
|
319
|
+
'Array of alarm objects to set reminders. Each alarm has a relativeOffset in minutes (negative = before event). Example: [{relativeOffset: -15}] for 15 minutes before. Replaces all existing alarms. Pass an empty array [] to clear all existing reminders.',
|
|
320
|
+
items: {
|
|
321
|
+
type: 'object',
|
|
322
|
+
properties: {
|
|
323
|
+
relativeOffset: {
|
|
324
|
+
type: 'number',
|
|
325
|
+
description:
|
|
326
|
+
'Minutes relative to the event start time. Use negative values for reminders before the event (e.g. -15 = 15 min before, -60 = 1 hour before).',
|
|
327
|
+
},
|
|
328
|
+
method: {
|
|
329
|
+
type: 'string',
|
|
330
|
+
enum: ['alarm', 'alert', 'email', 'default', 'sms'],
|
|
331
|
+
description: 'Alarm method (Android only). iOS always uses notification.',
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
},
|
|
293
336
|
},
|
|
294
337
|
},
|
|
295
338
|
async execute(_toolCallId, params) {
|
|
@@ -314,6 +357,7 @@ export function registerCalendarTools(api: PluginApi) {
|
|
|
314
357
|
...(params.location !== undefined ? {location: params.location} : {}),
|
|
315
358
|
...(params.notes !== undefined ? {notes: params.notes} : {}),
|
|
316
359
|
...(params.timeZone !== undefined ? {timeZone: params.timeZone} : {}),
|
|
360
|
+
...(Array.isArray(params.alarms) ? {alarms: params.alarms} : {}),
|
|
317
361
|
}
|
|
318
362
|
|
|
319
363
|
const resultPromise = waitForActionResult(actionId)
|