@2en/clawly-plugins 1.1.2 → 1.3.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/channel.ts +94 -0
- package/cron-hook.ts +66 -0
- package/index.ts +25 -0
- package/notification.ts +2 -0
- package/package.json +4 -1
- package/tools.ts +2 -0
package/channel.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clawly cron delivery channel — registers a minimal channel plugin so that
|
|
3
|
+
* cron jobs using `delivery: { channel: "clawly-cron" }` have a valid target.
|
|
4
|
+
*
|
|
5
|
+
* When a cron job delivers text, the channel injects it into the main session
|
|
6
|
+
* transcript via chat.inject (no LLM round-trip) and sends a push notification
|
|
7
|
+
* if the mobile client is offline.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {$} from 'zx'
|
|
11
|
+
import type {PluginApi} from './index'
|
|
12
|
+
import {sendPushNotification} from './notification'
|
|
13
|
+
import {isClientOnline} from './presence'
|
|
14
|
+
|
|
15
|
+
$.verbose = false
|
|
16
|
+
|
|
17
|
+
async function injectAndNotify(text: string, api: PluginApi): Promise<void> {
|
|
18
|
+
try {
|
|
19
|
+
// Resolve main session key from config
|
|
20
|
+
const config = api.pluginConfig as Record<string, unknown> | undefined
|
|
21
|
+
const agentId = typeof config?.agentId === 'string' ? config.agentId : 'clawly'
|
|
22
|
+
const mainKey = typeof config?.mainKey === 'string' ? config.mainKey : 'main'
|
|
23
|
+
const sessionKey = `agent:${agentId}:${mainKey}`
|
|
24
|
+
|
|
25
|
+
const params = JSON.stringify({sessionKey, message: text})
|
|
26
|
+
await $`openclaw gateway call chat.inject --json --params ${params}`
|
|
27
|
+
api.logger.info(`clawly-cron: injected message (${text.length} chars) into ${sessionKey}`)
|
|
28
|
+
|
|
29
|
+
// Push notification if client is offline
|
|
30
|
+
const online = await isClientOnline()
|
|
31
|
+
if (!online) {
|
|
32
|
+
const pushSent = await sendPushNotification({body: text}, api)
|
|
33
|
+
api.logger.info(`clawly-cron: push notification sent=${pushSent}`)
|
|
34
|
+
}
|
|
35
|
+
} catch (err) {
|
|
36
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
37
|
+
api.logger.error(`clawly-cron: failed to inject message — ${msg}`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function registerClawlyCronChannel(api: PluginApi) {
|
|
42
|
+
const channelRegistration = {
|
|
43
|
+
plugin: {
|
|
44
|
+
id: 'clawly-cron',
|
|
45
|
+
meta: {
|
|
46
|
+
id: 'clawly-cron',
|
|
47
|
+
label: 'Clawly Cron',
|
|
48
|
+
selectionLabel: 'Clawly Cron (webchat)',
|
|
49
|
+
docsPath: '',
|
|
50
|
+
blurb: 'Webchat-only cron delivery channel',
|
|
51
|
+
},
|
|
52
|
+
capabilities: {chatTypes: ['dm'] as const},
|
|
53
|
+
config: {
|
|
54
|
+
listAccountIds: () => ['default'],
|
|
55
|
+
resolveAccount: () => ({id: 'default'}),
|
|
56
|
+
defaultAccountId: () => 'default',
|
|
57
|
+
isEnabled: () => true,
|
|
58
|
+
isConfigured: () => true,
|
|
59
|
+
},
|
|
60
|
+
status: {
|
|
61
|
+
buildAccountSnapshot: async () => ({
|
|
62
|
+
accountId: 'default',
|
|
63
|
+
enabled: true,
|
|
64
|
+
configured: true,
|
|
65
|
+
running: true,
|
|
66
|
+
}),
|
|
67
|
+
buildChannelSummary: async () => ({
|
|
68
|
+
configured: true,
|
|
69
|
+
running: true,
|
|
70
|
+
}),
|
|
71
|
+
},
|
|
72
|
+
outbound: {
|
|
73
|
+
deliveryMode: 'direct' as const,
|
|
74
|
+
sendText: async (...args: unknown[]) => {
|
|
75
|
+
const firstArg = args[0] as Record<string, unknown> | undefined
|
|
76
|
+
const text = typeof firstArg?.text === 'string' ? firstArg.text : ''
|
|
77
|
+
api.logger.info(`clawly-cron sendText: text=${text.length} chars`)
|
|
78
|
+
if (text) {
|
|
79
|
+
// Fire-and-forget — delivery system may not await sendText
|
|
80
|
+
injectAndNotify(text, api).catch(() => {})
|
|
81
|
+
}
|
|
82
|
+
return {channel: 'clawly-cron', messageId: crypto.randomUUID()}
|
|
83
|
+
},
|
|
84
|
+
sendMedia: async (...args: unknown[]) => {
|
|
85
|
+
api.logger.info(`clawly-cron sendMedia: ${JSON.stringify(args)}`)
|
|
86
|
+
return {channel: 'clawly-cron', messageId: crypto.randomUUID()}
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
api.registerChannel(channelRegistration)
|
|
93
|
+
api.logger.info('channel: registered clawly-cron delivery channel')
|
|
94
|
+
}
|
package/cron-hook.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* before_tool_call hook for cron (action=add) — ensures delivery fields are
|
|
3
|
+
* always set correctly, even when the LLM omits them.
|
|
4
|
+
*
|
|
5
|
+
* Forces: delivery.channel = "clawly-cron", delivery.to = "self", delivery.mode = "announce"
|
|
6
|
+
* Patches: payload.kind "systemEvent" → "agentTurn"
|
|
7
|
+
*
|
|
8
|
+
* The cron tool name is "cron" (not "cron.create"). The LLM passes
|
|
9
|
+
* { action: "add", job: { delivery, payload, ... } } — delivery and payload
|
|
10
|
+
* live inside params.job (or at the top level for flat-params recovery).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {PluginApi} from './index'
|
|
14
|
+
|
|
15
|
+
type UnknownRecord = Record<string, unknown>
|
|
16
|
+
|
|
17
|
+
function isRecord(v: unknown): v is UnknownRecord {
|
|
18
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function patchJob(job: UnknownRecord): UnknownRecord {
|
|
22
|
+
const patched: UnknownRecord = {...job}
|
|
23
|
+
|
|
24
|
+
// Force delivery fields
|
|
25
|
+
const delivery = isRecord(job.delivery) ? job.delivery : {}
|
|
26
|
+
patched.delivery = {
|
|
27
|
+
...delivery,
|
|
28
|
+
mode: 'announce',
|
|
29
|
+
channel: 'clawly-cron',
|
|
30
|
+
to: 'self',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Patch payload.kind: systemEvent → agentTurn
|
|
34
|
+
if (isRecord(job.payload) && job.payload.kind === 'systemEvent') {
|
|
35
|
+
patched.payload = {...job.payload, kind: 'agentTurn'}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return patched
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function registerCronHook(api: PluginApi) {
|
|
42
|
+
api.on('before_tool_call', (event: {toolName: string; params: UnknownRecord}) => {
|
|
43
|
+
if (event.toolName !== 'cron') return
|
|
44
|
+
if (event.params.action !== 'add') return
|
|
45
|
+
|
|
46
|
+
const params: UnknownRecord = {}
|
|
47
|
+
|
|
48
|
+
// Patch job object if present (normal path)
|
|
49
|
+
if (isRecord(event.params.job)) {
|
|
50
|
+
params.job = patchJob(event.params.job)
|
|
51
|
+
} else {
|
|
52
|
+
// Flat-params recovery: delivery/payload may be at the top level
|
|
53
|
+
if ('delivery' in event.params || 'payload' in event.params) {
|
|
54
|
+
const synthetic = {...event.params}
|
|
55
|
+
delete synthetic.action
|
|
56
|
+
Object.assign(params, patchJob(synthetic))
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (Object.keys(params).length === 0) return
|
|
61
|
+
|
|
62
|
+
return {params}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
api.logger.info('hook: registered before_tool_call for cron add delivery enforcement')
|
|
66
|
+
}
|
package/index.ts
CHANGED
|
@@ -9,14 +9,22 @@
|
|
|
9
9
|
* - clawly.agent.send — send a message to the agent (+ optional push)
|
|
10
10
|
* - clawly.agent.echo — echo-wrapped agent message (bypasses LLM)
|
|
11
11
|
*
|
|
12
|
+
* Agent tools:
|
|
13
|
+
* - clawly_is_user_online — check if user's device is connected
|
|
14
|
+
* - clawly_send_app_push — send a push notification to user's device
|
|
15
|
+
*
|
|
12
16
|
* Commands:
|
|
13
17
|
* - /clawly_echo — echo text back without LLM
|
|
14
18
|
*
|
|
15
19
|
* Hooks:
|
|
16
20
|
* - tool_result_persist — copies TTS audio to persistent outbound directory
|
|
21
|
+
* - before_tool_call — enforces delivery fields on cron.create
|
|
17
22
|
*/
|
|
18
23
|
|
|
19
24
|
import {registerAgentSend} from './agent-send'
|
|
25
|
+
import {registerIsUserOnlineTool, registerSendAppPushTool} from './tools'
|
|
26
|
+
import {registerClawlyCronChannel} from './channel'
|
|
27
|
+
import {registerCronHook} from './cron-hook'
|
|
20
28
|
import {registerEchoCommand} from './echo'
|
|
21
29
|
import {registerNotification} from './notification'
|
|
22
30
|
import {registerOutboundHook, registerOutboundMethods} from './outbound'
|
|
@@ -53,6 +61,19 @@ export type PluginApi = {
|
|
|
53
61
|
requireAuth?: boolean
|
|
54
62
|
handler: (ctx: {args?: string}) => Promise<{text: string}> | {text: string}
|
|
55
63
|
}) => void
|
|
64
|
+
registerTool: (
|
|
65
|
+
tool: {
|
|
66
|
+
name: string
|
|
67
|
+
description: string
|
|
68
|
+
parameters: Record<string, unknown>
|
|
69
|
+
execute: (
|
|
70
|
+
toolCallId: string,
|
|
71
|
+
params: Record<string, unknown>,
|
|
72
|
+
) => Promise<{content: Array<{type: string; text: string}>; details?: unknown}>
|
|
73
|
+
},
|
|
74
|
+
opts?: {optional?: boolean},
|
|
75
|
+
) => void
|
|
76
|
+
registerChannel: (registration: {plugin: any}) => void
|
|
56
77
|
}
|
|
57
78
|
|
|
58
79
|
export default {
|
|
@@ -66,6 +87,10 @@ export default {
|
|
|
66
87
|
registerPresence(api)
|
|
67
88
|
registerNotification(api)
|
|
68
89
|
registerAgentSend(api)
|
|
90
|
+
registerIsUserOnlineTool(api)
|
|
91
|
+
registerSendAppPushTool(api)
|
|
92
|
+
registerClawlyCronChannel(api)
|
|
93
|
+
registerCronHook(api)
|
|
69
94
|
api.logger.info(`Loaded ${api.id} plugin.`)
|
|
70
95
|
},
|
|
71
96
|
}
|
package/notification.ts
CHANGED
|
@@ -45,6 +45,7 @@ export function getPushToken(): string | null {
|
|
|
45
45
|
export async function sendPushNotification(
|
|
46
46
|
opts: {body: string; title?: string; data?: Record<string, unknown>},
|
|
47
47
|
api: PluginApi,
|
|
48
|
+
extras?: Record<string, unknown>,
|
|
48
49
|
): Promise<boolean> {
|
|
49
50
|
const token = getPushToken()
|
|
50
51
|
if (!token) {
|
|
@@ -62,6 +63,7 @@ export async function sendPushNotification(
|
|
|
62
63
|
title: opts.title ?? 'Clawly',
|
|
63
64
|
body: opts.body,
|
|
64
65
|
data: opts.data,
|
|
66
|
+
...extras,
|
|
65
67
|
}),
|
|
66
68
|
})
|
|
67
69
|
const json = await res.json()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@2en/clawly-plugins",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"module": "index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -13,11 +13,14 @@
|
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"index.ts",
|
|
16
|
+
"channel.ts",
|
|
17
|
+
"cron-hook.ts",
|
|
16
18
|
"outbound.ts",
|
|
17
19
|
"echo.ts",
|
|
18
20
|
"presence.ts",
|
|
19
21
|
"notification.ts",
|
|
20
22
|
"agent-send.ts",
|
|
23
|
+
"tools.ts",
|
|
21
24
|
"openclaw.plugin.json"
|
|
22
25
|
],
|
|
23
26
|
"publishConfig": {
|
package/tools.ts
ADDED