@2en/clawly-plugins 1.23.0 → 1.24.0
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/offline-push.test.ts +33 -8
- package/gateway/offline-push.ts +8 -4
- package/index.ts +3 -0
- package/internal/hooks/auto-update.ts +122 -0
- package/package.json +3 -2
|
@@ -274,15 +274,21 @@ describe('shouldSkipPushForMessage', () => {
|
|
|
274
274
|
expect(shouldSkipPushForMessage(' NO_REPLY ')).toBe('silent reply')
|
|
275
275
|
})
|
|
276
276
|
|
|
277
|
-
test('skips
|
|
277
|
+
test('skips heartbeat ack when HEARTBEAT_OK is the only content', () => {
|
|
278
278
|
expect(shouldSkipPushForMessage('HEARTBEAT_OK')).toBe('heartbeat ack')
|
|
279
|
-
expect(shouldSkipPushForMessage('
|
|
279
|
+
expect(shouldSkipPushForMessage('HEARTBEAT_OK.')).toBe('heartbeat ack')
|
|
280
|
+
expect(shouldSkipPushForMessage('HEARTBEAT_OK\n')).toBe('heartbeat ack')
|
|
280
281
|
})
|
|
281
282
|
|
|
282
|
-
test('
|
|
283
|
+
test('does NOT skip meaningful content ending with HEARTBEAT_OK', () => {
|
|
284
|
+
expect(shouldSkipPushForMessage('All good. HEARTBEAT_OK')).toBeNull()
|
|
285
|
+
expect(shouldSkipPushForMessage('Nothing to report. HEARTBEAT_OK。')).toBeNull()
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test('does NOT skip verbose heartbeat response with content before HEARTBEAT_OK', () => {
|
|
283
289
|
const verbose =
|
|
284
290
|
'The user said hello recently. Looking at HEARTBEAT.md checklist: nothing needs attention. HEARTBEAT_OK'
|
|
285
|
-
expect(shouldSkipPushForMessage(verbose)).
|
|
291
|
+
expect(shouldSkipPushForMessage(verbose)).toBeNull()
|
|
286
292
|
})
|
|
287
293
|
|
|
288
294
|
test('does not skip message mentioning HEARTBEAT_OK mid-text', () => {
|
|
@@ -291,10 +297,6 @@ describe('shouldSkipPushForMessage', () => {
|
|
|
291
297
|
).toBeNull()
|
|
292
298
|
})
|
|
293
299
|
|
|
294
|
-
test('skips heartbeat ack with non-ASCII trailing punctuation', () => {
|
|
295
|
-
expect(shouldSkipPushForMessage('Nothing to report. HEARTBEAT_OK。')).toBe('heartbeat ack')
|
|
296
|
-
})
|
|
297
|
-
|
|
298
300
|
test('skips system prompt leak', () => {
|
|
299
301
|
expect(
|
|
300
302
|
shouldSkipPushForMessage('Here is some Conversation info (untrusted metadata) text'),
|
|
@@ -356,6 +358,29 @@ describe('offline-push with filtered messages', () => {
|
|
|
356
358
|
})
|
|
357
359
|
})
|
|
358
360
|
|
|
361
|
+
test('sends push for meaningful heartbeat response and strips HEARTBEAT_OK from body', async () => {
|
|
362
|
+
const {api, logs, handlers} = createMockApi()
|
|
363
|
+
registerOfflinePush(api)
|
|
364
|
+
|
|
365
|
+
await handlers.get('agent_end')!(
|
|
366
|
+
{
|
|
367
|
+
messages: [
|
|
368
|
+
{
|
|
369
|
+
role: 'assistant',
|
|
370
|
+
content: 'Hey! Just checking in — saw some interesting news today.\n\nHEARTBEAT_OK',
|
|
371
|
+
},
|
|
372
|
+
],
|
|
373
|
+
},
|
|
374
|
+
{sessionKey: 'agent:clawly:main'},
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
expect(logs).toContainEqual({
|
|
378
|
+
level: 'info',
|
|
379
|
+
msg: expect.stringContaining('notified (session=agent:clawly:main)'),
|
|
380
|
+
})
|
|
381
|
+
expect(lastPushOpts?.body).toBe('Hey! Just checking in — saw some interesting news today.')
|
|
382
|
+
})
|
|
383
|
+
|
|
359
384
|
test('sends push for normal message text', async () => {
|
|
360
385
|
const {api, logs, handlers} = createMockApi()
|
|
361
386
|
registerOfflinePush(api)
|
package/gateway/offline-push.ts
CHANGED
|
@@ -79,8 +79,11 @@ export function shouldSkipPushForMessage(text: string): string | null {
|
|
|
79
79
|
// Agent sentinel "nothing to say" — mobile hides as "silentReply"
|
|
80
80
|
if (trimmed === 'NO_REPLY') return 'silent reply'
|
|
81
81
|
|
|
82
|
-
// Heartbeat acknowledgment (HEARTBEAT_OK as ending sentinel) —
|
|
83
|
-
if (/HEARTBEAT_OK[\p{P}\s]*$/u.test(text))
|
|
82
|
+
// Heartbeat acknowledgment (HEARTBEAT_OK as ending sentinel) — only skip if no substantial content before it
|
|
83
|
+
if (/HEARTBEAT_OK[\p{P}\s]*$/u.test(text)) {
|
|
84
|
+
const stripped = text.replace(/HEARTBEAT_OK[\p{P}\s]*$/u, '').trim()
|
|
85
|
+
if (stripped.length === 0) return 'heartbeat ack'
|
|
86
|
+
}
|
|
84
87
|
|
|
85
88
|
// Agent echoed system prompt metadata — mobile hides as "systemPromptLeak"
|
|
86
89
|
if (text.includes('Conversation info (untrusted metadata)')) return 'system prompt leak'
|
|
@@ -118,8 +121,9 @@ export function registerOfflinePush(api: PluginApi) {
|
|
|
118
121
|
return
|
|
119
122
|
}
|
|
120
123
|
|
|
121
|
-
const
|
|
122
|
-
const
|
|
124
|
+
const cleaned = fullText?.replace(/HEARTBEAT_OK[\p{P}\s]*$/u, '').trim() ?? null
|
|
125
|
+
const preview = cleaned && cleaned.length > 140 ? `${cleaned.slice(0, 140)}…` : cleaned
|
|
126
|
+
const body = preview || 'Your response is ready'
|
|
123
127
|
|
|
124
128
|
const sent = await sendPushNotification(
|
|
125
129
|
{
|
package/index.ts
CHANGED
|
@@ -27,9 +27,11 @@
|
|
|
27
27
|
* - before_tool_call — enforces delivery fields on cron.create
|
|
28
28
|
* - agent_end — sends push notification when client is offline
|
|
29
29
|
* - gateway_start — auto-approves device pairing for Clawly mobile clients (clientId: openclaw-ios)
|
|
30
|
+
* - gateway_start — registers auto-update cron job (0 3 * * *) for clawly-plugins
|
|
30
31
|
*/
|
|
31
32
|
|
|
32
33
|
import {registerAutoPair} from './auto-pair'
|
|
34
|
+
import {registerAutoUpdate} from './internal/hooks/auto-update'
|
|
33
35
|
import {registerCalendar} from './calendar'
|
|
34
36
|
import {registerCommands} from './command'
|
|
35
37
|
import {setupConfig} from './config-setup'
|
|
@@ -186,6 +188,7 @@ export default {
|
|
|
186
188
|
setupConfig(api)
|
|
187
189
|
registerGateway(api)
|
|
188
190
|
registerAutoPair(api)
|
|
191
|
+
registerAutoUpdate(api)
|
|
189
192
|
|
|
190
193
|
// Email & calendar (optional — requires skillGatewayBaseUrl + skillGatewayToken in config)
|
|
191
194
|
const gw = getGatewayConfig(api)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registers a cron job (0 3 * * *) on gateway_start that
|
|
3
|
+
* checks user presence and auto-updates clawly-plugins when offline.
|
|
4
|
+
*/
|
|
5
|
+
import {$} from 'zx'
|
|
6
|
+
|
|
7
|
+
import type {PluginApi} from '../../index'
|
|
8
|
+
|
|
9
|
+
$.verbose = false
|
|
10
|
+
|
|
11
|
+
// ── Types ──────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
interface CronJobEntry {
|
|
14
|
+
id: string
|
|
15
|
+
name: string
|
|
16
|
+
description: string
|
|
17
|
+
enabled: boolean
|
|
18
|
+
schedule: {kind: 'cron'; expr: string; tz?: string} | {kind: string}
|
|
19
|
+
sessionTarget: string
|
|
20
|
+
wakeMode: string
|
|
21
|
+
payload: {kind: string; message?: string}
|
|
22
|
+
delivery: {mode: string}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface JobConfig {
|
|
26
|
+
description: string
|
|
27
|
+
cron: string
|
|
28
|
+
session: 'main' | 'isolated'
|
|
29
|
+
wake: 'now' | 'next-heartbeat'
|
|
30
|
+
deliver: boolean
|
|
31
|
+
message: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Config ─────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
const JOB_NAME = 'Clawly Plugins Auto-Update'
|
|
37
|
+
|
|
38
|
+
const JOB_CONFIG: JobConfig = {
|
|
39
|
+
description: 'Auto-update clawly-plugins when user is offline',
|
|
40
|
+
cron: '0 3 * * *',
|
|
41
|
+
session: 'isolated',
|
|
42
|
+
wake: 'now',
|
|
43
|
+
deliver: false,
|
|
44
|
+
message: [
|
|
45
|
+
'Auto-update check for clawly-plugins.',
|
|
46
|
+
'',
|
|
47
|
+
'Steps:',
|
|
48
|
+
'1. Call the clawly_is_user_online tool to check if the user is online.',
|
|
49
|
+
'2. If the user IS online, stop — do nothing.',
|
|
50
|
+
'3. If the user is NOT online, run this exact command in terminal:',
|
|
51
|
+
` openclaw gateway call clawly.plugins.update --json --params '${JSON.stringify({pluginId: 'clawly-plugins', npmPkgName: '@2en/clawly-plugins', strategy: 'force', restart: true})}'`,
|
|
52
|
+
'',
|
|
53
|
+
'Do not send any messages to the user. Just execute the steps silently.',
|
|
54
|
+
].join('\n'),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Helpers ────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
function configToArgs(config: JobConfig): string[] {
|
|
60
|
+
const args = [
|
|
61
|
+
'--description',
|
|
62
|
+
config.description,
|
|
63
|
+
'--cron',
|
|
64
|
+
config.cron,
|
|
65
|
+
'--session',
|
|
66
|
+
config.session,
|
|
67
|
+
'--wake',
|
|
68
|
+
config.wake,
|
|
69
|
+
'--message',
|
|
70
|
+
config.message,
|
|
71
|
+
]
|
|
72
|
+
if (!config.deliver) args.push('--no-deliver')
|
|
73
|
+
return args
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function needsUpdate(job: CronJobEntry, config: JobConfig): boolean {
|
|
77
|
+
if (job.description !== config.description) return true
|
|
78
|
+
if (job.schedule.kind !== 'cron' || (job.schedule as {expr: string}).expr !== config.cron)
|
|
79
|
+
return true
|
|
80
|
+
if (job.sessionTarget !== config.session) return true
|
|
81
|
+
if (job.wakeMode !== config.wake) return true
|
|
82
|
+
if (job.payload.message !== config.message) return true
|
|
83
|
+
const wantMode = config.deliver ? 'announce' : 'none'
|
|
84
|
+
if (job.delivery.mode !== wantMode) return true
|
|
85
|
+
return false
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function findJob(): Promise<CronJobEntry | null> {
|
|
89
|
+
try {
|
|
90
|
+
const {stdout} = await $`openclaw cron list --json`
|
|
91
|
+
const parsed = JSON.parse(stdout)
|
|
92
|
+
const jobs: unknown[] = parsed?.jobs ?? parsed ?? []
|
|
93
|
+
if (!Array.isArray(jobs)) return null
|
|
94
|
+
return (jobs.find((j: any) => j.name === JOB_NAME) as CronJobEntry) ?? null
|
|
95
|
+
} catch {
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Registration ───────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
export function registerAutoUpdate(api: PluginApi) {
|
|
103
|
+
api.on('gateway_start', async () => {
|
|
104
|
+
try {
|
|
105
|
+
const existing = await findJob()
|
|
106
|
+
|
|
107
|
+
if (existing) {
|
|
108
|
+
if (!needsUpdate(existing, JOB_CONFIG)) {
|
|
109
|
+
api.logger.info('auto-update: cron job up to date')
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
await $`openclaw cron edit ${[existing.id, ...configToArgs(JOB_CONFIG)]}`
|
|
113
|
+
api.logger.info('auto-update: updated cron job')
|
|
114
|
+
} else {
|
|
115
|
+
await $`openclaw cron add ${['--name', JOB_NAME, ...configToArgs(JOB_CONFIG)]}`
|
|
116
|
+
api.logger.info('auto-update: registered cron job')
|
|
117
|
+
}
|
|
118
|
+
} catch (err) {
|
|
119
|
+
api.logger.warn(`auto-update: failed to register cron job: ${String(err)}`)
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@2en/clawly-plugins",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.0",
|
|
4
4
|
"module": "index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"outbound.ts",
|
|
27
27
|
"model-gateway-setup.ts",
|
|
28
28
|
"skill-command-restore.ts",
|
|
29
|
-
"openclaw.plugin.json"
|
|
29
|
+
"openclaw.plugin.json",
|
|
30
|
+
"internal"
|
|
30
31
|
],
|
|
31
32
|
"publishConfig": {
|
|
32
33
|
"access": "public"
|