@2en/clawly-plugins 1.14.0 → 1.16.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/auto-pair.ts ADDED
@@ -0,0 +1,72 @@
1
+ import type {PluginApi} from './index'
2
+
3
+ const AUTO_APPROVE_CLIENT_IDS = new Set(['openclaw-ios'])
4
+ const POLL_INTERVAL_MS = 3_000
5
+
6
+ type PendingRequest = {
7
+ requestId: string
8
+ deviceId: string
9
+ clientId?: string
10
+ displayName?: string
11
+ platform?: string
12
+ }
13
+
14
+ type PairedDevice = {
15
+ deviceId: string
16
+ displayName?: string
17
+ platform?: string
18
+ }
19
+
20
+ type DevicePairingList = {
21
+ pending: PendingRequest[]
22
+ paired: PairedDevice[]
23
+ }
24
+
25
+ export function registerAutoPair(api: PluginApi) {
26
+ let timer: ReturnType<typeof setInterval> | null = null
27
+ let sdk: {
28
+ listDevicePairing: () => Promise<DevicePairingList>
29
+ approveDevicePairing: (
30
+ requestId: string,
31
+ ) => Promise<{requestId: string; device: PairedDevice} | null>
32
+ } | null = null
33
+
34
+ api.on('gateway_start', async () => {
35
+ try {
36
+ sdk = await import('openclaw/plugin-sdk')
37
+ } catch {
38
+ api.logger.warn('auto-pair: openclaw/plugin-sdk not available, skipping')
39
+ return
40
+ }
41
+
42
+ api.logger.info('auto-pair: started polling for pending pairing requests')
43
+
44
+ timer = setInterval(async () => {
45
+ if (!sdk) return
46
+ try {
47
+ const {pending} = await sdk.listDevicePairing()
48
+ for (const req of pending) {
49
+ if (req.clientId && AUTO_APPROVE_CLIENT_IDS.has(req.clientId)) {
50
+ const result = await sdk.approveDevicePairing(req.requestId)
51
+ if (result) {
52
+ api.logger.info(
53
+ `auto-pair: approved device=${result.device.deviceId} ` +
54
+ `name=${result.device.displayName ?? 'unknown'} ` +
55
+ `platform=${result.device.platform ?? 'unknown'}`,
56
+ )
57
+ }
58
+ }
59
+ }
60
+ } catch (err) {
61
+ api.logger.warn(`auto-pair: poll error: ${String(err)}`)
62
+ }
63
+ }, POLL_INTERVAL_MS)
64
+ })
65
+
66
+ api.on('gateway_stop', () => {
67
+ if (timer) {
68
+ clearInterval(timer)
69
+ timer = null
70
+ }
71
+ })
72
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Channel configuration management via openclaw.json.
3
+ *
4
+ * Methods:
5
+ * - clawly.channels.configure — add/update a channel account config
6
+ * - clawly.channels.disconnect — remove a channel account config
7
+ */
8
+
9
+ import fs from 'node:fs'
10
+ import path from 'node:path'
11
+
12
+ import type {PluginApi} from '../index'
13
+
14
+ const TOKEN_CHANNELS = new Set(['telegram', 'discord', 'slack', 'irc', 'googlechat'])
15
+
16
+ function resolveStateDir(api: PluginApi): string {
17
+ return api.runtime.state?.resolveStateDir?.(process.env) ?? process.env.OPENCLAW_STATE_DIR ?? ''
18
+ }
19
+
20
+ function readOpenclawConfig(configPath: string): Record<string, unknown> {
21
+ try {
22
+ return JSON.parse(fs.readFileSync(configPath, 'utf-8'))
23
+ } catch {
24
+ return {}
25
+ }
26
+ }
27
+
28
+ function writeOpenclawConfig(configPath: string, config: Record<string, unknown>) {
29
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n')
30
+ }
31
+
32
+ export function registerChannelsConfigure(api: PluginApi) {
33
+ // ── clawly.channels.configure ──────────────────────────────────
34
+
35
+ api.registerGatewayMethod('clawly.channels.configure', async ({params, respond}) => {
36
+ const channel = typeof params.channel === 'string' ? params.channel.trim() : ''
37
+ const accountId = typeof params.accountId === 'string' ? params.accountId.trim() : 'default'
38
+ const config =
39
+ params.config && typeof params.config === 'object' && !Array.isArray(params.config)
40
+ ? (params.config as Record<string, unknown>)
41
+ : null
42
+
43
+ if (!channel) {
44
+ respond(false, undefined, {code: 'invalid_params', message: 'channel is required'})
45
+ return
46
+ }
47
+
48
+ if (!TOKEN_CHANNELS.has(channel)) {
49
+ respond(false, undefined, {
50
+ code: 'invalid_params',
51
+ message: `unsupported channel: ${channel}. Supported: ${[...TOKEN_CHANNELS].join(', ')}`,
52
+ })
53
+ return
54
+ }
55
+
56
+ if (!config) {
57
+ respond(false, undefined, {code: 'invalid_params', message: 'config object is required'})
58
+ return
59
+ }
60
+
61
+ const stateDir = resolveStateDir(api)
62
+ if (!stateDir) {
63
+ respond(false, undefined, {code: 'internal', message: 'cannot resolve openclaw state dir'})
64
+ return
65
+ }
66
+
67
+ const configPath = path.join(stateDir, 'openclaw.json')
68
+
69
+ try {
70
+ const ocConfig = readOpenclawConfig(configPath)
71
+
72
+ // Ensure channels.<channel>.accounts.<accountId> path exists
73
+ if (!ocConfig.channels || typeof ocConfig.channels !== 'object') {
74
+ ocConfig.channels = {}
75
+ }
76
+ const channels = ocConfig.channels as Record<string, unknown>
77
+
78
+ if (!channels[channel] || typeof channels[channel] !== 'object') {
79
+ channels[channel] = {}
80
+ }
81
+ const channelConfig = channels[channel] as Record<string, unknown>
82
+
83
+ if (!channelConfig.accounts || typeof channelConfig.accounts !== 'object') {
84
+ channelConfig.accounts = {}
85
+ }
86
+ const accounts = channelConfig.accounts as Record<string, unknown>
87
+
88
+ // Merge config with enabled: true
89
+ accounts[accountId] = {
90
+ ...((accounts[accountId] as Record<string, unknown>) ?? {}),
91
+ ...config,
92
+ enabled: true,
93
+ }
94
+
95
+ writeOpenclawConfig(configPath, ocConfig)
96
+ api.logger.info(`channels.configure: wrote ${channel}/${accountId} to openclaw.json`)
97
+
98
+ respond(true, {ok: true, channel, accountId})
99
+ } catch (err) {
100
+ const msg = err instanceof Error ? err.message : String(err)
101
+ api.logger.error(`channels.configure: failed — ${msg}`)
102
+ respond(false, undefined, {code: 'internal', message: msg})
103
+ }
104
+ })
105
+
106
+ // ── clawly.channels.disconnect ─────────────────────────────────
107
+
108
+ api.registerGatewayMethod('clawly.channels.disconnect', async ({params, respond}) => {
109
+ const channel = typeof params.channel === 'string' ? params.channel.trim() : ''
110
+ const accountId = typeof params.accountId === 'string' ? params.accountId.trim() : 'default'
111
+
112
+ if (!channel) {
113
+ respond(false, undefined, {code: 'invalid_params', message: 'channel is required'})
114
+ return
115
+ }
116
+
117
+ const stateDir = resolveStateDir(api)
118
+ if (!stateDir) {
119
+ respond(false, undefined, {code: 'internal', message: 'cannot resolve openclaw state dir'})
120
+ return
121
+ }
122
+
123
+ const configPath = path.join(stateDir, 'openclaw.json')
124
+
125
+ try {
126
+ const ocConfig = readOpenclawConfig(configPath)
127
+ const channels = ocConfig.channels as Record<string, unknown> | undefined
128
+
129
+ if (channels && typeof channels === 'object') {
130
+ const channelConfig = channels[channel] as Record<string, unknown> | undefined
131
+ if (channelConfig && typeof channelConfig === 'object') {
132
+ const accounts = channelConfig.accounts as Record<string, unknown> | undefined
133
+ if (accounts && typeof accounts === 'object' && accountId in accounts) {
134
+ delete accounts[accountId]
135
+ }
136
+ }
137
+ }
138
+
139
+ writeOpenclawConfig(configPath, ocConfig)
140
+ api.logger.info(`channels.disconnect: removed ${channel}/${accountId} from openclaw.json`)
141
+
142
+ respond(true, {ok: true, channel, accountId})
143
+ } catch (err) {
144
+ const msg = err instanceof Error ? err.message : String(err)
145
+ api.logger.error(`channels.disconnect: failed — ${msg}`)
146
+ respond(false, undefined, {code: 'internal', message: msg})
147
+ }
148
+ })
149
+
150
+ api.logger.info('channels: registered clawly.channels.configure + clawly.channels.disconnect')
151
+ }
package/gateway/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type {PluginApi} from '../index'
2
2
  import {registerAgentSend} from './agent'
3
+ import {registerChannelsConfigure} from './channels-configure'
3
4
  import {registerClawhub2gateway} from './clawhub2gateway'
4
5
  import {registerInject} from './inject'
5
6
  import {registerMemoryBrowser} from './memory'
@@ -15,4 +16,5 @@ export function registerGateway(api: PluginApi) {
15
16
  registerMemoryBrowser(api)
16
17
  registerClawhub2gateway(api)
17
18
  registerPlugins(api)
19
+ registerChannelsConfigure(api)
18
20
  }
package/index.ts CHANGED
@@ -24,8 +24,10 @@
24
24
  * Hooks:
25
25
  * - tool_result_persist — copies TTS audio to persistent outbound directory
26
26
  * - before_tool_call — enforces delivery fields on cron.create
27
+ * - gateway_start — auto-approves device pairing for Clawly mobile clients (clientId: openclaw-ios)
27
28
  */
28
29
 
30
+ import {registerAutoPair} from './auto-pair'
29
31
  import {registerCalendar} from './calendar'
30
32
  import {registerClawlyCronChannel} from './channel'
31
33
  import {registerCommands} from './command'
@@ -114,6 +116,7 @@ export default {
114
116
  registerClawlyCronChannel(api)
115
117
  registerCronHook(api)
116
118
  registerGateway(api)
119
+ registerAutoPair(api)
117
120
 
118
121
  // Email & calendar (optional — requires skillGatewayBaseUrl + skillGatewayToken in config)
119
122
  const gw = getGatewayConfig(api)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2en/clawly-plugins",
3
- "version": "1.14.0",
3
+ "version": "1.16.0",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "repository": {
@@ -17,6 +17,7 @@
17
17
  "lib",
18
18
  "tools",
19
19
  "index.ts",
20
+ "auto-pair.ts",
20
21
  "calendar.ts",
21
22
  "channel.ts",
22
23
  "cron-hook.ts",