@2en/clawly-plugins 1.3.1 → 1.4.1
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/email.ts +313 -0
- package/gateway-fetch.ts +41 -0
- package/index.ts +11 -0
- package/openclaw.plugin.json +4 -1
- package/package.json +4 -1
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/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
|
+
}
|
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
|
@@ -22,10 +22,13 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import {registerAgentSend} from './agent-send'
|
|
25
|
+
import {registerCalendar} from './calendar'
|
|
25
26
|
import {registerIsUserOnlineTool, registerSendAppPushTool} from './tools'
|
|
26
27
|
import {registerClawlyCronChannel} from './channel'
|
|
27
28
|
import {registerCronHook} from './cron-hook'
|
|
28
29
|
import {registerEchoCommand} from './echo'
|
|
30
|
+
import {registerEmail} from './email'
|
|
31
|
+
import {getGatewayConfig} from './gateway-fetch'
|
|
29
32
|
import {registerNotification} from './notification'
|
|
30
33
|
import {registerOutboundHook, registerOutboundMethods} from './outbound'
|
|
31
34
|
import {registerPresence} from './presence'
|
|
@@ -91,6 +94,14 @@ export default {
|
|
|
91
94
|
registerSendAppPushTool(api)
|
|
92
95
|
registerClawlyCronChannel(api)
|
|
93
96
|
registerCronHook(api)
|
|
97
|
+
|
|
98
|
+
// Email & calendar (optional — requires gatewayBaseUrl + gatewayToken in config)
|
|
99
|
+
const gw = getGatewayConfig(api)
|
|
100
|
+
if (gw.baseUrl && gw.token) {
|
|
101
|
+
registerEmail(api, gw)
|
|
102
|
+
registerCalendar(api, gw)
|
|
103
|
+
}
|
|
104
|
+
|
|
94
105
|
api.logger.info(`Loaded ${api.id} plugin.`)
|
|
95
106
|
},
|
|
96
107
|
}
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@2en/clawly-plugins",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"module": "index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -14,8 +14,11 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
"tools",
|
|
16
16
|
"index.ts",
|
|
17
|
+
"calendar.ts",
|
|
17
18
|
"channel.ts",
|
|
18
19
|
"cron-hook.ts",
|
|
20
|
+
"email.ts",
|
|
21
|
+
"gateway-fetch.ts",
|
|
19
22
|
"outbound.ts",
|
|
20
23
|
"echo.ts",
|
|
21
24
|
"presence.ts",
|