@2en/clawly-plugins 1.4.0 → 1.5.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/calendar.ts +257 -0
- package/channel.ts +2 -2
- package/{echo.ts → command/clawly_echo.ts} +1 -1
- package/command/index.ts +6 -0
- package/email.ts +313 -0
- package/{agent-send.ts → gateway/agent.ts} +1 -1
- package/gateway/clawhub2gateway.md +68 -0
- package/{clawhub2gateway.ts → gateway/clawhub2gateway.ts} +1 -1
- package/gateway/index.ts +14 -0
- package/gateway/memory-browser.md +55 -0
- package/{memory.ts → gateway/memory.ts} +1 -1
- package/{notification.ts → gateway/notification.ts} +1 -1
- package/{presence.ts → gateway/presence.ts} +1 -1
- package/gateway-fetch.ts +41 -0
- package/index.ts +17 -15
- package/openclaw.plugin.json +3 -1
- package/package.json +6 -8
- package/tools/clawly-is-user-online.ts +1 -1
- package/tools/clawly-send-app-push.ts +1 -1
- package/tools/index.ts +8 -0
- package/tools.ts +0 -2
package/calendar.ts
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calendar integration via Clawly backend.
|
|
3
|
+
*
|
|
4
|
+
* Gateway methods (external RPC):
|
|
5
|
+
* - calendar.listCalendars — list available calendars
|
|
6
|
+
* - calendar.listEvents — list events in a time range
|
|
7
|
+
* - calendar.createEvent — create a new calendar event
|
|
8
|
+
*
|
|
9
|
+
* Agent tools (LLM-callable):
|
|
10
|
+
* - calendar_list_calendars — list available calendars
|
|
11
|
+
* - calendar_list_events — list events in a time range
|
|
12
|
+
* - calendar_create_event — create a new calendar event
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type {PluginApi} from './index'
|
|
16
|
+
import type {GatewayCfg, HandlerResult} from './gateway-fetch'
|
|
17
|
+
import {gatewayFetch, toToolResult} from './gateway-fetch'
|
|
18
|
+
|
|
19
|
+
// ── Handlers ──
|
|
20
|
+
|
|
21
|
+
async function handleListCalendars(
|
|
22
|
+
cfg: GatewayCfg,
|
|
23
|
+
params: Record<string, unknown>,
|
|
24
|
+
): Promise<HandlerResult> {
|
|
25
|
+
const qs = typeof params.provider === 'string' ? `?provider=${params.provider}` : ''
|
|
26
|
+
const res = await gatewayFetch(cfg, `/calendar/calendars${qs}`)
|
|
27
|
+
const data = await res.json()
|
|
28
|
+
if (!res.ok)
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
error: {code: data.error ?? 'error', message: data.message ?? res.statusText},
|
|
32
|
+
}
|
|
33
|
+
return {ok: true, data}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function handleListEvents(
|
|
37
|
+
cfg: GatewayCfg,
|
|
38
|
+
params: Record<string, unknown>,
|
|
39
|
+
): Promise<HandlerResult> {
|
|
40
|
+
const calendarId = typeof params.calendarId === 'string' ? params.calendarId : ''
|
|
41
|
+
const start =
|
|
42
|
+
typeof params.start === 'number'
|
|
43
|
+
? params.start
|
|
44
|
+
: typeof params.start === 'string'
|
|
45
|
+
? Number(params.start)
|
|
46
|
+
: 0
|
|
47
|
+
const end =
|
|
48
|
+
typeof params.end === 'number'
|
|
49
|
+
? params.end
|
|
50
|
+
: typeof params.end === 'string'
|
|
51
|
+
? Number(params.end)
|
|
52
|
+
: 0
|
|
53
|
+
|
|
54
|
+
if (!calendarId || !start || !end)
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
error: {code: 'invalid_params', message: 'calendarId, start, and end are required'},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const qs = new URLSearchParams({calendar_id: calendarId, start: String(start), end: String(end)})
|
|
61
|
+
if (typeof params.provider === 'string') qs.set('provider', params.provider)
|
|
62
|
+
|
|
63
|
+
const res = await gatewayFetch(cfg, `/calendar/events?${qs.toString()}`)
|
|
64
|
+
const data = await res.json()
|
|
65
|
+
if (!res.ok)
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
error: {code: data.error ?? 'error', message: data.message ?? res.statusText},
|
|
69
|
+
}
|
|
70
|
+
return {ok: true, data}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function handleCreateEvent(
|
|
74
|
+
cfg: GatewayCfg,
|
|
75
|
+
params: Record<string, unknown>,
|
|
76
|
+
): Promise<HandlerResult> {
|
|
77
|
+
const calendarId = typeof params.calendarId === 'string' ? params.calendarId : ''
|
|
78
|
+
const title = typeof params.title === 'string' ? params.title : ''
|
|
79
|
+
const startTime = typeof params.startTime === 'number' ? params.startTime : 0
|
|
80
|
+
const endTime = typeof params.endTime === 'number' ? params.endTime : 0
|
|
81
|
+
|
|
82
|
+
if (!calendarId || !title || !startTime || !endTime)
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
error: {
|
|
86
|
+
code: 'invalid_params',
|
|
87
|
+
message: 'calendarId, title, startTime, and endTime are required',
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const res = await gatewayFetch(cfg, '/calendar/events', {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
body: JSON.stringify({
|
|
94
|
+
calendarId,
|
|
95
|
+
title,
|
|
96
|
+
startTime,
|
|
97
|
+
endTime,
|
|
98
|
+
description: typeof params.description === 'string' ? params.description : undefined,
|
|
99
|
+
location: typeof params.location === 'string' ? params.location : undefined,
|
|
100
|
+
participants: Array.isArray(params.participants) ? params.participants : undefined,
|
|
101
|
+
provider: typeof params.provider === 'string' ? params.provider : undefined,
|
|
102
|
+
}),
|
|
103
|
+
})
|
|
104
|
+
const data = await res.json()
|
|
105
|
+
if (!res.ok)
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
error: {code: data.error ?? 'error', message: data.message ?? res.statusText},
|
|
109
|
+
}
|
|
110
|
+
return {ok: true, data}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Registration ──
|
|
114
|
+
|
|
115
|
+
export function registerCalendar(api: PluginApi, cfg: GatewayCfg) {
|
|
116
|
+
// Gateway methods (external RPC)
|
|
117
|
+
|
|
118
|
+
api.registerGatewayMethod('calendar.listCalendars', async ({params, respond}) => {
|
|
119
|
+
try {
|
|
120
|
+
const r = await handleListCalendars(cfg, params)
|
|
121
|
+
r.ok ? respond(true, r.data) : respond(false, undefined, r.error)
|
|
122
|
+
} catch (err) {
|
|
123
|
+
respond(false, undefined, {
|
|
124
|
+
code: 'error',
|
|
125
|
+
message: err instanceof Error ? err.message : String(err),
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
api.registerGatewayMethod('calendar.listEvents', async ({params, respond}) => {
|
|
131
|
+
try {
|
|
132
|
+
const r = await handleListEvents(cfg, params)
|
|
133
|
+
r.ok ? respond(true, r.data) : respond(false, undefined, r.error)
|
|
134
|
+
} catch (err) {
|
|
135
|
+
respond(false, undefined, {
|
|
136
|
+
code: 'error',
|
|
137
|
+
message: err instanceof Error ? err.message : String(err),
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
api.registerGatewayMethod('calendar.createEvent', async ({params, respond}) => {
|
|
143
|
+
try {
|
|
144
|
+
const r = await handleCreateEvent(cfg, params)
|
|
145
|
+
r.ok ? respond(true, r.data) : respond(false, undefined, r.error)
|
|
146
|
+
} catch (err) {
|
|
147
|
+
respond(false, undefined, {
|
|
148
|
+
code: 'error',
|
|
149
|
+
message: err instanceof Error ? err.message : String(err),
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// Agent tools (LLM-callable)
|
|
155
|
+
|
|
156
|
+
api.registerTool({
|
|
157
|
+
name: 'calendar_list_calendars',
|
|
158
|
+
description:
|
|
159
|
+
'List available calendars for the connected account. ' +
|
|
160
|
+
'If no calendar account is connected, the result will contain a CONNECTION_REQUIRED error.',
|
|
161
|
+
parameters: {
|
|
162
|
+
type: 'object',
|
|
163
|
+
properties: {
|
|
164
|
+
provider: {type: 'string', description: 'Calendar provider: google or microsoft'},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
async execute(_toolCallId, params) {
|
|
168
|
+
try {
|
|
169
|
+
return toToolResult(await handleListCalendars(cfg, params))
|
|
170
|
+
} catch (err) {
|
|
171
|
+
return {
|
|
172
|
+
content: [
|
|
173
|
+
{
|
|
174
|
+
type: 'text',
|
|
175
|
+
text: JSON.stringify({error: err instanceof Error ? err.message : String(err)}),
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
api.registerTool({
|
|
184
|
+
name: 'calendar_list_events',
|
|
185
|
+
description: 'List calendar events in a time range.',
|
|
186
|
+
parameters: {
|
|
187
|
+
type: 'object',
|
|
188
|
+
required: ['calendarId', 'start', 'end'],
|
|
189
|
+
properties: {
|
|
190
|
+
calendarId: {type: 'string', description: 'Calendar ID (from calendar_list_calendars)'},
|
|
191
|
+
start: {type: 'number', description: 'Start time as Unix timestamp (seconds)'},
|
|
192
|
+
end: {type: 'number', description: 'End time as Unix timestamp (seconds)'},
|
|
193
|
+
provider: {type: 'string', description: 'Calendar provider: google or microsoft'},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
async execute(_toolCallId, params) {
|
|
197
|
+
try {
|
|
198
|
+
return toToolResult(await handleListEvents(cfg, params))
|
|
199
|
+
} catch (err) {
|
|
200
|
+
return {
|
|
201
|
+
content: [
|
|
202
|
+
{
|
|
203
|
+
type: 'text',
|
|
204
|
+
text: JSON.stringify({error: err instanceof Error ? err.message : String(err)}),
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
api.registerTool({
|
|
213
|
+
name: 'calendar_create_event',
|
|
214
|
+
description: 'Create a new calendar event.',
|
|
215
|
+
parameters: {
|
|
216
|
+
type: 'object',
|
|
217
|
+
required: ['calendarId', 'title', 'startTime', 'endTime'],
|
|
218
|
+
properties: {
|
|
219
|
+
calendarId: {type: 'string', description: 'Calendar ID (from calendar_list_calendars)'},
|
|
220
|
+
title: {type: 'string', description: 'Event title'},
|
|
221
|
+
startTime: {type: 'number', description: 'Start time as Unix timestamp (seconds)'},
|
|
222
|
+
endTime: {type: 'number', description: 'End time as Unix timestamp (seconds)'},
|
|
223
|
+
description: {type: 'string', description: 'Event description'},
|
|
224
|
+
location: {type: 'string', description: 'Event location'},
|
|
225
|
+
participants: {
|
|
226
|
+
type: 'array',
|
|
227
|
+
items: {
|
|
228
|
+
type: 'object',
|
|
229
|
+
properties: {name: {type: 'string'}, email: {type: 'string'}},
|
|
230
|
+
required: ['email'],
|
|
231
|
+
},
|
|
232
|
+
description: 'Event participants',
|
|
233
|
+
},
|
|
234
|
+
provider: {type: 'string', description: 'Calendar provider: google or microsoft'},
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
async execute(_toolCallId, params) {
|
|
238
|
+
try {
|
|
239
|
+
return toToolResult(await handleCreateEvent(cfg, params))
|
|
240
|
+
} catch (err) {
|
|
241
|
+
return {
|
|
242
|
+
content: [
|
|
243
|
+
{
|
|
244
|
+
type: 'text',
|
|
245
|
+
text: JSON.stringify({error: err instanceof Error ? err.message : String(err)}),
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
api.logger.info(
|
|
254
|
+
'calendar: registered gateway methods (calendar.listCalendars, calendar.listEvents, calendar.createEvent) ' +
|
|
255
|
+
'and agent tools (calendar_list_calendars, calendar_list_events, calendar_create_event)',
|
|
256
|
+
)
|
|
257
|
+
}
|
package/channel.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
import {$} from 'zx'
|
|
11
11
|
import type {PluginApi} from './index'
|
|
12
|
-
import {sendPushNotification} from './notification'
|
|
13
|
-
import {isClientOnline} from './presence'
|
|
12
|
+
import {sendPushNotification} from './gateway/notification'
|
|
13
|
+
import {isClientOnline} from './gateway/presence'
|
|
14
14
|
|
|
15
15
|
$.verbose = false
|
|
16
16
|
|
package/command/index.ts
ADDED
package/email.ts
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email integration via Clawly backend.
|
|
3
|
+
*
|
|
4
|
+
* Gateway methods (external RPC):
|
|
5
|
+
* - email.search — search/list email messages
|
|
6
|
+
* - email.read — read a single email message
|
|
7
|
+
* - email.send — send an email
|
|
8
|
+
* - email.getConnectUrl — get OAuth URL to connect an email account
|
|
9
|
+
*
|
|
10
|
+
* Agent tools (LLM-callable):
|
|
11
|
+
* - email_search — search/list email messages
|
|
12
|
+
* - email_read — read a single email message by ID
|
|
13
|
+
* - email_send — send an email
|
|
14
|
+
* - email_connect — get OAuth URL to connect an email account
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type {PluginApi} from './index'
|
|
18
|
+
import type {GatewayCfg, HandlerResult} from './gateway-fetch'
|
|
19
|
+
import {gatewayFetch, toToolResult} from './gateway-fetch'
|
|
20
|
+
|
|
21
|
+
// ── Handlers ──
|
|
22
|
+
|
|
23
|
+
async function handleSearch(
|
|
24
|
+
cfg: GatewayCfg,
|
|
25
|
+
params: Record<string, unknown>,
|
|
26
|
+
): Promise<HandlerResult> {
|
|
27
|
+
const qs = new URLSearchParams()
|
|
28
|
+
if (typeof params.query === 'string') qs.set('query', params.query)
|
|
29
|
+
if (typeof params.from === 'string') qs.set('from', params.from)
|
|
30
|
+
if (typeof params.to === 'string') qs.set('to', params.to)
|
|
31
|
+
if (typeof params.folder === 'string') qs.set('in', params.folder)
|
|
32
|
+
if (typeof params.limit === 'number') qs.set('limit', String(params.limit))
|
|
33
|
+
if (typeof params.provider === 'string') qs.set('provider', params.provider)
|
|
34
|
+
|
|
35
|
+
const res = await gatewayFetch(cfg, `/email/messages?${qs.toString()}`)
|
|
36
|
+
const data = await res.json()
|
|
37
|
+
if (!res.ok)
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
error: {code: data.error ?? 'error', message: data.message ?? res.statusText},
|
|
41
|
+
}
|
|
42
|
+
return {ok: true, data}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function handleRead(
|
|
46
|
+
cfg: GatewayCfg,
|
|
47
|
+
params: Record<string, unknown>,
|
|
48
|
+
): Promise<HandlerResult> {
|
|
49
|
+
const id = typeof params.id === 'string' ? params.id : ''
|
|
50
|
+
if (!id) return {ok: false, error: {code: 'invalid_params', message: 'id is required'}}
|
|
51
|
+
|
|
52
|
+
const qs = typeof params.provider === 'string' ? `?provider=${params.provider}` : ''
|
|
53
|
+
const res = await gatewayFetch(cfg, `/email/messages/${encodeURIComponent(id)}${qs}`)
|
|
54
|
+
const data = await res.json()
|
|
55
|
+
if (!res.ok)
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
error: {code: data.error ?? 'error', message: data.message ?? res.statusText},
|
|
59
|
+
}
|
|
60
|
+
return {ok: true, data}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function handleSend(
|
|
64
|
+
cfg: GatewayCfg,
|
|
65
|
+
params: Record<string, unknown>,
|
|
66
|
+
): Promise<HandlerResult> {
|
|
67
|
+
const to = Array.isArray(params.to)
|
|
68
|
+
? params.to
|
|
69
|
+
: typeof params.to === 'string'
|
|
70
|
+
? [{email: params.to}]
|
|
71
|
+
: []
|
|
72
|
+
const subject = typeof params.subject === 'string' ? params.subject : ''
|
|
73
|
+
const body = typeof params.body === 'string' ? params.body : ''
|
|
74
|
+
|
|
75
|
+
if (!to.length || !subject || !body)
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
error: {code: 'invalid_params', message: 'to, subject, and body are required'},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const res = await gatewayFetch(cfg, '/email/messages/send', {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
to,
|
|
85
|
+
subject,
|
|
86
|
+
body,
|
|
87
|
+
replyToMessageId:
|
|
88
|
+
typeof params.replyToMessageId === 'string' ? params.replyToMessageId : undefined,
|
|
89
|
+
provider: typeof params.provider === 'string' ? params.provider : undefined,
|
|
90
|
+
}),
|
|
91
|
+
})
|
|
92
|
+
const data = await res.json()
|
|
93
|
+
if (!res.ok)
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
error: {code: data.error ?? 'error', message: data.message ?? res.statusText},
|
|
97
|
+
}
|
|
98
|
+
return {ok: true, data}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function handleGetConnectUrl(
|
|
102
|
+
cfg: GatewayCfg,
|
|
103
|
+
params: Record<string, unknown>,
|
|
104
|
+
): Promise<HandlerResult> {
|
|
105
|
+
const provider = typeof params.provider === 'string' ? params.provider : undefined
|
|
106
|
+
const res = await gatewayFetch(cfg, '/connections/start', {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
body: JSON.stringify({provider}),
|
|
109
|
+
})
|
|
110
|
+
const data = await res.json()
|
|
111
|
+
if (!res.ok)
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
error: {code: data.error ?? 'error', message: data.message ?? res.statusText},
|
|
115
|
+
}
|
|
116
|
+
return {ok: true, data}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Registration ──
|
|
120
|
+
|
|
121
|
+
export function registerEmail(api: PluginApi, cfg: GatewayCfg) {
|
|
122
|
+
// Gateway methods (external RPC)
|
|
123
|
+
|
|
124
|
+
api.registerGatewayMethod('email.search', async ({params, respond}) => {
|
|
125
|
+
try {
|
|
126
|
+
const r = await handleSearch(cfg, params)
|
|
127
|
+
r.ok ? respond(true, r.data) : respond(false, undefined, r.error)
|
|
128
|
+
} catch (err) {
|
|
129
|
+
respond(false, undefined, {
|
|
130
|
+
code: 'error',
|
|
131
|
+
message: err instanceof Error ? err.message : String(err),
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
api.registerGatewayMethod('email.read', async ({params, respond}) => {
|
|
137
|
+
try {
|
|
138
|
+
const r = await handleRead(cfg, params)
|
|
139
|
+
r.ok ? respond(true, r.data) : respond(false, undefined, r.error)
|
|
140
|
+
} catch (err) {
|
|
141
|
+
respond(false, undefined, {
|
|
142
|
+
code: 'error',
|
|
143
|
+
message: err instanceof Error ? err.message : String(err),
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
api.registerGatewayMethod('email.send', async ({params, respond}) => {
|
|
149
|
+
try {
|
|
150
|
+
const r = await handleSend(cfg, params)
|
|
151
|
+
r.ok ? respond(true, r.data) : respond(false, undefined, r.error)
|
|
152
|
+
} catch (err) {
|
|
153
|
+
respond(false, undefined, {
|
|
154
|
+
code: 'error',
|
|
155
|
+
message: err instanceof Error ? err.message : String(err),
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
api.registerGatewayMethod('email.getConnectUrl', async ({params, respond}) => {
|
|
161
|
+
try {
|
|
162
|
+
const r = await handleGetConnectUrl(cfg, params)
|
|
163
|
+
r.ok ? respond(true, r.data) : respond(false, undefined, r.error)
|
|
164
|
+
} catch (err) {
|
|
165
|
+
respond(false, undefined, {
|
|
166
|
+
code: 'error',
|
|
167
|
+
message: err instanceof Error ? err.message : String(err),
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
// Agent tools (LLM-callable)
|
|
173
|
+
|
|
174
|
+
api.registerTool({
|
|
175
|
+
name: 'email_search',
|
|
176
|
+
description:
|
|
177
|
+
'Search or list email messages. Returns a list of email messages matching the query. ' +
|
|
178
|
+
'If no email account is connected, the result will contain a CONNECTION_REQUIRED error ' +
|
|
179
|
+
'with available providers — use email_connect to get an OAuth URL.',
|
|
180
|
+
parameters: {
|
|
181
|
+
type: 'object',
|
|
182
|
+
properties: {
|
|
183
|
+
query: {type: 'string', description: 'Search query (e.g. "from:alice subject:meeting")'},
|
|
184
|
+
from: {type: 'string', description: 'Filter by sender email'},
|
|
185
|
+
to: {type: 'string', description: 'Filter by recipient email'},
|
|
186
|
+
folder: {type: 'string', description: 'Folder name (e.g. INBOX, SENT)'},
|
|
187
|
+
limit: {type: 'number', description: 'Max results (default 10)'},
|
|
188
|
+
provider: {type: 'string', description: 'Email provider: google or microsoft'},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
async execute(_toolCallId, params) {
|
|
192
|
+
try {
|
|
193
|
+
return toToolResult(await handleSearch(cfg, params))
|
|
194
|
+
} catch (err) {
|
|
195
|
+
return {
|
|
196
|
+
content: [
|
|
197
|
+
{
|
|
198
|
+
type: 'text',
|
|
199
|
+
text: JSON.stringify({error: err instanceof Error ? err.message : String(err)}),
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
api.registerTool({
|
|
208
|
+
name: 'email_read',
|
|
209
|
+
description: 'Read a single email message by ID. Returns full message content including body.',
|
|
210
|
+
parameters: {
|
|
211
|
+
type: 'object',
|
|
212
|
+
required: ['id'],
|
|
213
|
+
properties: {
|
|
214
|
+
id: {type: 'string', description: 'Message ID'},
|
|
215
|
+
provider: {type: 'string', description: 'Email provider: google or microsoft'},
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
async execute(_toolCallId, params) {
|
|
219
|
+
try {
|
|
220
|
+
return toToolResult(await handleRead(cfg, params))
|
|
221
|
+
} catch (err) {
|
|
222
|
+
return {
|
|
223
|
+
content: [
|
|
224
|
+
{
|
|
225
|
+
type: 'text',
|
|
226
|
+
text: JSON.stringify({error: err instanceof Error ? err.message : String(err)}),
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
api.registerTool({
|
|
235
|
+
name: 'email_send',
|
|
236
|
+
description: 'Send an email. Requires a connected email account.',
|
|
237
|
+
parameters: {
|
|
238
|
+
type: 'object',
|
|
239
|
+
required: ['to', 'subject', 'body'],
|
|
240
|
+
properties: {
|
|
241
|
+
to: {
|
|
242
|
+
oneOf: [
|
|
243
|
+
{type: 'string', description: 'Recipient email address'},
|
|
244
|
+
{
|
|
245
|
+
type: 'array',
|
|
246
|
+
items: {
|
|
247
|
+
type: 'object',
|
|
248
|
+
properties: {name: {type: 'string'}, email: {type: 'string'}},
|
|
249
|
+
required: ['email'],
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
description: 'Recipient(s) — single email string or array of {name?, email}',
|
|
254
|
+
},
|
|
255
|
+
subject: {type: 'string', description: 'Email subject'},
|
|
256
|
+
body: {type: 'string', description: 'Email body (plain text or HTML)'},
|
|
257
|
+
replyToMessageId: {type: 'string', description: 'Message ID to reply to'},
|
|
258
|
+
provider: {type: 'string', description: 'Email provider: google or microsoft'},
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
async execute(_toolCallId, params) {
|
|
262
|
+
try {
|
|
263
|
+
return toToolResult(await handleSend(cfg, params))
|
|
264
|
+
} catch (err) {
|
|
265
|
+
return {
|
|
266
|
+
content: [
|
|
267
|
+
{
|
|
268
|
+
type: 'text',
|
|
269
|
+
text: JSON.stringify({error: err instanceof Error ? err.message : String(err)}),
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
api.registerTool({
|
|
278
|
+
name: 'email_connect',
|
|
279
|
+
description:
|
|
280
|
+
'Get an OAuth URL to connect an email account (Gmail or Outlook). ' +
|
|
281
|
+
'Returns an authorizeUrl that the user should open in a browser to authorize access. ' +
|
|
282
|
+
'Use this when email operations return CONNECTION_REQUIRED.',
|
|
283
|
+
parameters: {
|
|
284
|
+
type: 'object',
|
|
285
|
+
properties: {
|
|
286
|
+
provider: {
|
|
287
|
+
type: 'string',
|
|
288
|
+
enum: ['google', 'microsoft'],
|
|
289
|
+
description: 'Email provider to connect (google for Gmail, microsoft for Outlook)',
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
async execute(_toolCallId, params) {
|
|
294
|
+
try {
|
|
295
|
+
return toToolResult(await handleGetConnectUrl(cfg, params))
|
|
296
|
+
} catch (err) {
|
|
297
|
+
return {
|
|
298
|
+
content: [
|
|
299
|
+
{
|
|
300
|
+
type: 'text',
|
|
301
|
+
text: JSON.stringify({error: err instanceof Error ? err.message : String(err)}),
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
api.logger.info(
|
|
310
|
+
'email: registered gateway methods (email.search, email.read, email.send, email.getConnectUrl) ' +
|
|
311
|
+
'and agent tools (email_search, email_read, email_send, email_connect)',
|
|
312
|
+
)
|
|
313
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# clawhub2gateway
|
|
2
|
+
|
|
3
|
+
OpenClaw plugin that wraps the **ClawHub CLI** as **Gateway WebSocket RPC methods**.
|
|
4
|
+
|
|
5
|
+
## What you get
|
|
6
|
+
|
|
7
|
+
Gateway methods (call via WS RPC):
|
|
8
|
+
|
|
9
|
+
- `clawhub2gateway.search`
|
|
10
|
+
- `clawhub2gateway.explore`
|
|
11
|
+
- `clawhub2gateway.install`
|
|
12
|
+
- `clawhub2gateway.update`
|
|
13
|
+
- `clawhub2gateway.list`
|
|
14
|
+
- `clawhub2gateway.inspect`
|
|
15
|
+
- `clawhub2gateway.star`
|
|
16
|
+
- `clawhub2gateway.unstar`
|
|
17
|
+
|
|
18
|
+
## Install on a gateway host
|
|
19
|
+
|
|
20
|
+
1) Install the ClawHub CLI (must be on PATH for the OpenClaw process):
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm i -g clawhub
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
2) Put this plugin on the gateway host and make it discoverable. Common options:
|
|
27
|
+
|
|
28
|
+
- **Workspace plugin**: copy this folder to `<workspace>/.openclaw/extensions/clawhub2gateway/`
|
|
29
|
+
with `index.ts` + `openclaw.plugin.json`
|
|
30
|
+
- **Config path**: set `plugins.load.paths` to the plugin folder path
|
|
31
|
+
|
|
32
|
+
3) Enable the plugin in your `openclaw.json`:
|
|
33
|
+
|
|
34
|
+
```json5
|
|
35
|
+
{
|
|
36
|
+
"plugins": {
|
|
37
|
+
"enabled": true,
|
|
38
|
+
"entries": {
|
|
39
|
+
"clawhub2gateway": { "enabled": true, "config": {} }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Plugin config
|
|
46
|
+
|
|
47
|
+
Configured under `plugins.entries.clawhub2gateway.config`:
|
|
48
|
+
|
|
49
|
+
- `bin` (string, default: `"clawhub"`): command to execute
|
|
50
|
+
- `defaultDir` (string, default: `"skills"`): default `--dir`
|
|
51
|
+
- `defaultNoInput` (boolean, default: `true`): default `--no-input`
|
|
52
|
+
- `defaultTimeoutMs` (number, default: `60000`)
|
|
53
|
+
- `configPath` (string, optional): sets `CLAWHUB_CONFIG_PATH`. Defaults to
|
|
54
|
+
`<OPENCLAW_STATE_DIR>/clawhub/config.json` when state dir is known.
|
|
55
|
+
|
|
56
|
+
## Call examples (RPC params)
|
|
57
|
+
|
|
58
|
+
Install:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{ "method": "clawhub2gateway.install", "params": { "slug": "my-skill-pack" } }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Explore:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{ "method": "clawhub2gateway.explore", "params": { "limit": 50, "sort": "trending", "json": true } }
|
|
68
|
+
```
|
package/gateway/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type {PluginApi} from '../index'
|
|
2
|
+
import {registerAgentSend} from './agent'
|
|
3
|
+
import {registerClawhub2gateway} from './clawhub2gateway'
|
|
4
|
+
import {registerMemoryBrowser} from './memory'
|
|
5
|
+
import {registerNotification} from './notification'
|
|
6
|
+
import {registerPresence} from './presence'
|
|
7
|
+
|
|
8
|
+
export function registerGateway(api: PluginApi) {
|
|
9
|
+
registerPresence(api)
|
|
10
|
+
registerNotification(api)
|
|
11
|
+
registerAgentSend(api)
|
|
12
|
+
registerMemoryBrowser(api)
|
|
13
|
+
registerClawhub2gateway(api)
|
|
14
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# memory-browser
|
|
2
|
+
|
|
3
|
+
OpenClaw plugin that exposes the **memory directory** (all `.md` files) as **Gateway WebSocket RPC methods**.
|
|
4
|
+
|
|
5
|
+
## What you get
|
|
6
|
+
|
|
7
|
+
Gateway methods (call via WS RPC):
|
|
8
|
+
|
|
9
|
+
- **`memory-browser.list`** — List all `.md` files in the memory directory (recursive). Returns `{ files: string[] }` (relative paths).
|
|
10
|
+
- **`memory-browser.get`** — Return the content of a single `.md` file. Params: `{ path: string }` (relative path, e.g. `"notes/foo.md"`). Returns `{ path: string, content: string }`.
|
|
11
|
+
|
|
12
|
+
## Memory directory
|
|
13
|
+
|
|
14
|
+
The memory directory is resolved in this order:
|
|
15
|
+
|
|
16
|
+
1. Plugin config `memoryDir` (absolute path)
|
|
17
|
+
2. `<OPENCLAW_STATE_DIR>/memory` (or `<CLAWDBOT_STATE_DIR>/memory`)
|
|
18
|
+
3. `<process.cwd()>/memory`
|
|
19
|
+
|
|
20
|
+
## Install on a gateway host
|
|
21
|
+
|
|
22
|
+
1. Put this plugin on the gateway host and make it discoverable, e.g.:
|
|
23
|
+
- Workspace: `<workspace>/.openclaw/extensions/memory-browser/` with `index.ts` + `openclaw.plugin.json`
|
|
24
|
+
- Or set `plugins.load.paths` to the plugin folder
|
|
25
|
+
|
|
26
|
+
2. Enable the plugin in `openclaw.json`:
|
|
27
|
+
|
|
28
|
+
```json5
|
|
29
|
+
{
|
|
30
|
+
"plugins": {
|
|
31
|
+
"enabled": true,
|
|
32
|
+
"entries": {
|
|
33
|
+
"memory-browser": { "enabled": true, "config": {} }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Optional config: `config.memoryDir` to override the memory directory path.
|
|
40
|
+
|
|
41
|
+
## Call examples (RPC params)
|
|
42
|
+
|
|
43
|
+
List all `.md` files:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{ "method": "memory-browser.list", "params": {} }
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Get one file:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{ "method": "memory-browser.get", "params": { "path": "notes/meeting-2024.md" } }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`path` must be a relative path to a `.md` file inside the memory directory; directory traversal (`..`) is rejected.
|
|
@@ -10,7 +10,7 @@ import fs from 'node:fs/promises'
|
|
|
10
10
|
import os from 'node:os'
|
|
11
11
|
import path from 'node:path'
|
|
12
12
|
|
|
13
|
-
import type {PluginApi} from '
|
|
13
|
+
import type {PluginApi} from '../index'
|
|
14
14
|
|
|
15
15
|
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
16
16
|
return Boolean(v && typeof v === 'object' && !Array.isArray(v))
|
|
@@ -13,7 +13,7 @@ import fs from 'node:fs'
|
|
|
13
13
|
import os from 'node:os'
|
|
14
14
|
import path from 'node:path'
|
|
15
15
|
|
|
16
|
-
import type {PluginApi} from '
|
|
16
|
+
import type {PluginApi} from '../index'
|
|
17
17
|
|
|
18
18
|
const TOKEN_DIR = path.join(os.homedir(), '.openclaw', 'clawly')
|
|
19
19
|
const TOKEN_FILE = path.join(TOKEN_DIR, 'expo-push-token.json')
|
package/gateway-fetch.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for plugins that proxy to the Clawly model-gateway backend.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {PluginApi} from './index'
|
|
6
|
+
|
|
7
|
+
export type GatewayCfg = {baseUrl: string; token: string}
|
|
8
|
+
|
|
9
|
+
export type HandlerResult = {ok: boolean; data?: unknown; error?: {code: string; message: string}}
|
|
10
|
+
|
|
11
|
+
export function getGatewayConfig(api: PluginApi): GatewayCfg {
|
|
12
|
+
const cfg = api.pluginConfig && typeof api.pluginConfig === 'object' ? api.pluginConfig : {}
|
|
13
|
+
const baseUrl =
|
|
14
|
+
typeof (cfg as Record<string, unknown>).gatewayBaseUrl === 'string'
|
|
15
|
+
? ((cfg as Record<string, unknown>).gatewayBaseUrl as string).replace(/\/$/, '')
|
|
16
|
+
: ''
|
|
17
|
+
const token =
|
|
18
|
+
typeof (cfg as Record<string, unknown>).gatewayToken === 'string'
|
|
19
|
+
? ((cfg as Record<string, unknown>).gatewayToken as string)
|
|
20
|
+
: ''
|
|
21
|
+
return {baseUrl, token}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function gatewayFetch(
|
|
25
|
+
cfg: GatewayCfg,
|
|
26
|
+
path: string,
|
|
27
|
+
init?: RequestInit,
|
|
28
|
+
): Promise<Response> {
|
|
29
|
+
return fetch(`${cfg.baseUrl}${path}`, {
|
|
30
|
+
...init,
|
|
31
|
+
headers: {
|
|
32
|
+
...(init?.headers ?? {}),
|
|
33
|
+
Authorization: `Bearer ${cfg.token}`,
|
|
34
|
+
'Content-Type': 'application/json',
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function toToolResult(result: HandlerResult) {
|
|
40
|
+
return {content: [{type: 'text', text: JSON.stringify(result.ok ? result.data : result.error)}]}
|
|
41
|
+
}
|
package/index.ts
CHANGED
|
@@ -24,16 +24,15 @@
|
|
|
24
24
|
* - before_tool_call — enforces delivery fields on cron.create
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import {
|
|
28
|
-
import {registerIsUserOnlineTool, registerSendAppPushTool} from './tools'
|
|
27
|
+
import {registerCalendar} from './calendar'
|
|
29
28
|
import {registerClawlyCronChannel} from './channel'
|
|
30
|
-
import {
|
|
29
|
+
import {registerCommands} from './command'
|
|
31
30
|
import {registerCronHook} from './cron-hook'
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
31
|
+
import {registerEmail} from './email'
|
|
32
|
+
import {registerGateway} from './gateway'
|
|
33
|
+
import {getGatewayConfig} from './gateway-fetch'
|
|
35
34
|
import {registerOutboundHook, registerOutboundMethods} from './outbound'
|
|
36
|
-
import {
|
|
35
|
+
import {registerTools} from './tools'
|
|
37
36
|
|
|
38
37
|
type PluginRuntime = {
|
|
39
38
|
system?: {
|
|
@@ -100,16 +99,19 @@ export default {
|
|
|
100
99
|
register(api: PluginApi) {
|
|
101
100
|
registerOutboundHook(api)
|
|
102
101
|
registerOutboundMethods(api)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
registerNotification(api)
|
|
106
|
-
registerAgentSend(api)
|
|
107
|
-
registerIsUserOnlineTool(api)
|
|
108
|
-
registerSendAppPushTool(api)
|
|
102
|
+
registerCommands(api)
|
|
103
|
+
registerTools(api)
|
|
109
104
|
registerClawlyCronChannel(api)
|
|
110
105
|
registerCronHook(api)
|
|
111
|
-
|
|
112
|
-
|
|
106
|
+
registerGateway(api)
|
|
107
|
+
|
|
108
|
+
// Email & calendar (optional — requires gatewayBaseUrl + gatewayToken in config)
|
|
109
|
+
const gw = getGatewayConfig(api)
|
|
110
|
+
if (gw.baseUrl && gw.token) {
|
|
111
|
+
registerEmail(api, gw)
|
|
112
|
+
registerCalendar(api, gw)
|
|
113
|
+
}
|
|
114
|
+
|
|
113
115
|
api.logger.info(`Loaded ${api.id} plugin.`)
|
|
114
116
|
},
|
|
115
117
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -45,7 +45,9 @@
|
|
|
45
45
|
"defaultDir": { "type": "string", "minLength": 1 },
|
|
46
46
|
"defaultNoInput": { "type": "boolean" },
|
|
47
47
|
"defaultTimeoutMs": { "type": "number", "minimum": 1000 },
|
|
48
|
-
"configPath": { "type": "string" }
|
|
48
|
+
"configPath": { "type": "string" },
|
|
49
|
+
"gatewayBaseUrl": { "type": "string" },
|
|
50
|
+
"gatewayToken": { "type": "string" }
|
|
49
51
|
},
|
|
50
52
|
"required": []
|
|
51
53
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@2en/clawly-plugins",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"module": "index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -12,18 +12,16 @@
|
|
|
12
12
|
"zx": "npm:zx@8.8.5-lite"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
|
+
"command",
|
|
16
|
+
"gateway",
|
|
15
17
|
"tools",
|
|
16
18
|
"index.ts",
|
|
19
|
+
"calendar.ts",
|
|
17
20
|
"channel.ts",
|
|
18
|
-
"clawhub2gateway.ts",
|
|
19
21
|
"cron-hook.ts",
|
|
20
|
-
"
|
|
22
|
+
"email.ts",
|
|
23
|
+
"gateway-fetch.ts",
|
|
21
24
|
"outbound.ts",
|
|
22
|
-
"echo.ts",
|
|
23
|
-
"presence.ts",
|
|
24
|
-
"notification.ts",
|
|
25
|
-
"agent-send.ts",
|
|
26
|
-
"tools.ts",
|
|
27
25
|
"openclaw.plugin.json"
|
|
28
26
|
],
|
|
29
27
|
"publishConfig": {
|
package/tools/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type {PluginApi} from '../index'
|
|
2
|
+
import {registerIsUserOnlineTool} from './clawly-is-user-online'
|
|
3
|
+
import {registerSendAppPushTool} from './clawly-send-app-push'
|
|
4
|
+
|
|
5
|
+
export function registerTools(api: PluginApi) {
|
|
6
|
+
registerIsUserOnlineTool(api)
|
|
7
|
+
registerSendAppPushTool(api)
|
|
8
|
+
}
|
package/tools.ts
DELETED