@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 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.1.2",
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
@@ -0,0 +1,2 @@
1
+ export {registerIsUserOnlineTool} from './tools/clawly-is-user-online'
2
+ export {registerSendAppPushTool} from './tools/clawly-send-app-push'