@troykelly/openclaw-projects 0.0.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/LICENSE +21 -0
- package/README.md +389 -0
- package/dist/api-client.d.ts +81 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +216 -0
- package/dist/api-client.js.map +1 -0
- package/dist/cli.d.ts +112 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +233 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +324 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +287 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +87 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +144 -0
- package/dist/context.js.map +1 -0
- package/dist/gateway/rpc-methods.d.ts +93 -0
- package/dist/gateway/rpc-methods.d.ts.map +1 -0
- package/dist/gateway/rpc-methods.js +145 -0
- package/dist/gateway/rpc-methods.js.map +1 -0
- package/dist/hooks.d.ts +86 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +314 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +106 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +221 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +22 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +78 -0
- package/dist/logger.js.map +1 -0
- package/dist/register-openclaw.d.ts +43 -0
- package/dist/register-openclaw.d.ts.map +1 -0
- package/dist/register-openclaw.js +1838 -0
- package/dist/register-openclaw.js.map +1 -0
- package/dist/secrets.d.ts +56 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +161 -0
- package/dist/secrets.js.map +1 -0
- package/dist/services/notification-service.d.ts +60 -0
- package/dist/services/notification-service.d.ts.map +1 -0
- package/dist/services/notification-service.js +145 -0
- package/dist/services/notification-service.js.map +1 -0
- package/dist/tools/contacts.d.ts +139 -0
- package/dist/tools/contacts.d.ts.map +1 -0
- package/dist/tools/contacts.js +333 -0
- package/dist/tools/contacts.js.map +1 -0
- package/dist/tools/email-send.d.ts +71 -0
- package/dist/tools/email-send.d.ts.map +1 -0
- package/dist/tools/email-send.js +132 -0
- package/dist/tools/email-send.js.map +1 -0
- package/dist/tools/file-share.d.ts +64 -0
- package/dist/tools/file-share.d.ts.map +1 -0
- package/dist/tools/file-share.js +133 -0
- package/dist/tools/file-share.js.map +1 -0
- package/dist/tools/index.d.ts +22 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +33 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/memory-forget.d.ts +69 -0
- package/dist/tools/memory-forget.d.ts.map +1 -0
- package/dist/tools/memory-forget.js +224 -0
- package/dist/tools/memory-forget.js.map +1 -0
- package/dist/tools/memory-recall.d.ts +82 -0
- package/dist/tools/memory-recall.d.ts.map +1 -0
- package/dist/tools/memory-recall.js +161 -0
- package/dist/tools/memory-recall.js.map +1 -0
- package/dist/tools/memory-store.d.ts +80 -0
- package/dist/tools/memory-store.d.ts.map +1 -0
- package/dist/tools/memory-store.js +172 -0
- package/dist/tools/memory-store.js.map +1 -0
- package/dist/tools/message-search.d.ts +85 -0
- package/dist/tools/message-search.d.ts.map +1 -0
- package/dist/tools/message-search.js +137 -0
- package/dist/tools/message-search.js.map +1 -0
- package/dist/tools/notebooks.d.ts +155 -0
- package/dist/tools/notebooks.d.ts.map +1 -0
- package/dist/tools/notebooks.js +287 -0
- package/dist/tools/notebooks.js.map +1 -0
- package/dist/tools/notes.d.ts +272 -0
- package/dist/tools/notes.d.ts.map +1 -0
- package/dist/tools/notes.js +530 -0
- package/dist/tools/notes.js.map +1 -0
- package/dist/tools/projects.d.ts +139 -0
- package/dist/tools/projects.d.ts.map +1 -0
- package/dist/tools/projects.js +280 -0
- package/dist/tools/projects.js.map +1 -0
- package/dist/tools/relationships.d.ts +133 -0
- package/dist/tools/relationships.d.ts.map +1 -0
- package/dist/tools/relationships.js +281 -0
- package/dist/tools/relationships.js.map +1 -0
- package/dist/tools/sms-send.d.ts +62 -0
- package/dist/tools/sms-send.d.ts.map +1 -0
- package/dist/tools/sms-send.js +121 -0
- package/dist/tools/sms-send.js.map +1 -0
- package/dist/tools/threads.d.ts +127 -0
- package/dist/tools/threads.d.ts.map +1 -0
- package/dist/tools/threads.js +202 -0
- package/dist/tools/threads.js.map +1 -0
- package/dist/tools/todos.d.ts +142 -0
- package/dist/tools/todos.d.ts.map +1 -0
- package/dist/tools/todos.js +308 -0
- package/dist/tools/todos.js.map +1 -0
- package/dist/types/openclaw-api.d.ts +215 -0
- package/dist/types/openclaw-api.d.ts.map +1 -0
- package/dist/types/openclaw-api.js +10 -0
- package/dist/types/openclaw-api.js.map +1 -0
- package/dist/utils/zod-to-json-schema.d.ts +19 -0
- package/dist/utils/zod-to-json-schema.d.ts.map +1 -0
- package/dist/utils/zod-to-json-schema.js +132 -0
- package/dist/utils/zod-to-json-schema.js.map +1 -0
- package/openclaw.plugin.json +229 -0
- package/package.json +69 -0
- package/skills/contact-lookup/SKILL.md +30 -0
- package/skills/daily-summary/SKILL.md +23 -0
- package/skills/project-status/SKILL.md +33 -0
- package/skills/send-reminder/SKILL.md +42 -0
|
@@ -0,0 +1,1838 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw 2026 Plugin Registration
|
|
3
|
+
*
|
|
4
|
+
* This module implements the OpenClaw Gateway plugin API pattern:
|
|
5
|
+
* - Default export function taking `api` object
|
|
6
|
+
* - Tools registered via `api.registerTool()`
|
|
7
|
+
* - Hooks registered via `api.on()` (modern) or `api.registerHook()` (legacy fallback)
|
|
8
|
+
* - CLI registered via `api.registerCli()`
|
|
9
|
+
*/
|
|
10
|
+
import { validateRawConfig, resolveConfigSecrets, redactConfig } from './config.js';
|
|
11
|
+
import { createLogger } from './logger.js';
|
|
12
|
+
import { createApiClient } from './api-client.js';
|
|
13
|
+
import { extractContext, getUserScopeKey } from './context.js';
|
|
14
|
+
import { createGatewayMethods, registerGatewayRpcMethods } from './gateway/rpc-methods.js';
|
|
15
|
+
import { createNotificationService } from './services/notification-service.js';
|
|
16
|
+
import { createAutoCaptureHook, createGraphAwareRecallHook, } from './hooks.js';
|
|
17
|
+
/**
|
|
18
|
+
* Convert internal ToolResult format to AgentToolResult format expected by OpenClaw Gateway.
|
|
19
|
+
*
|
|
20
|
+
* The Gateway expects: { content: [{ type: "text", text: "..." }] }
|
|
21
|
+
* Our handlers return: { success: boolean, data?: { content: string, ... }, error?: string }
|
|
22
|
+
*/
|
|
23
|
+
function toAgentToolResult(result) {
|
|
24
|
+
if (result.success && result.data) {
|
|
25
|
+
return {
|
|
26
|
+
content: [{ type: 'text', text: result.data.content }],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// For errors, format the error message
|
|
30
|
+
const errorText = result.error ?? 'An unexpected error occurred';
|
|
31
|
+
return {
|
|
32
|
+
content: [{ type: 'text', text: `Error: ${errorText}` }],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Memory recall tool JSON Schema
|
|
37
|
+
*/
|
|
38
|
+
const memoryRecallSchema = {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
query: {
|
|
42
|
+
type: 'string',
|
|
43
|
+
description: 'Search query for semantic memory search',
|
|
44
|
+
minLength: 1,
|
|
45
|
+
maxLength: 1000,
|
|
46
|
+
},
|
|
47
|
+
limit: {
|
|
48
|
+
type: 'integer',
|
|
49
|
+
description: 'Maximum number of memories to return',
|
|
50
|
+
minimum: 1,
|
|
51
|
+
maximum: 20,
|
|
52
|
+
default: 5,
|
|
53
|
+
},
|
|
54
|
+
category: {
|
|
55
|
+
type: 'string',
|
|
56
|
+
description: 'Filter by memory category',
|
|
57
|
+
enum: ['preference', 'fact', 'decision', 'context', 'other'],
|
|
58
|
+
},
|
|
59
|
+
tags: {
|
|
60
|
+
type: 'array',
|
|
61
|
+
description: 'Filter by tags for categorical queries (e.g., ["music", "food"])',
|
|
62
|
+
items: {
|
|
63
|
+
type: 'string',
|
|
64
|
+
minLength: 1,
|
|
65
|
+
maxLength: 100,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
relationship_id: {
|
|
69
|
+
type: 'string',
|
|
70
|
+
description: 'Scope search to a specific relationship between contacts',
|
|
71
|
+
format: 'uuid',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
required: ['query'],
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Memory store tool JSON Schema
|
|
78
|
+
*/
|
|
79
|
+
const memoryStoreSchema = {
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: {
|
|
82
|
+
content: {
|
|
83
|
+
type: 'string',
|
|
84
|
+
description: 'Memory content to store',
|
|
85
|
+
minLength: 1,
|
|
86
|
+
maxLength: 10000,
|
|
87
|
+
},
|
|
88
|
+
category: {
|
|
89
|
+
type: 'string',
|
|
90
|
+
description: 'Memory category',
|
|
91
|
+
enum: ['preference', 'fact', 'decision', 'context', 'other'],
|
|
92
|
+
default: 'fact',
|
|
93
|
+
},
|
|
94
|
+
importance: {
|
|
95
|
+
type: 'number',
|
|
96
|
+
description: 'Importance score (0-1)',
|
|
97
|
+
minimum: 0,
|
|
98
|
+
maximum: 1,
|
|
99
|
+
default: 0.5,
|
|
100
|
+
},
|
|
101
|
+
tags: {
|
|
102
|
+
type: 'array',
|
|
103
|
+
description: 'Tags for structured retrieval (e.g., ["music", "work", "food"])',
|
|
104
|
+
items: {
|
|
105
|
+
type: 'string',
|
|
106
|
+
minLength: 1,
|
|
107
|
+
maxLength: 100,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
relationship_id: {
|
|
111
|
+
type: 'string',
|
|
112
|
+
description: 'Scope memory to a specific relationship between contacts',
|
|
113
|
+
format: 'uuid',
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
required: ['content'],
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Memory forget tool JSON Schema
|
|
120
|
+
*/
|
|
121
|
+
const memoryForgetSchema = {
|
|
122
|
+
type: 'object',
|
|
123
|
+
properties: {
|
|
124
|
+
memoryId: {
|
|
125
|
+
type: 'string',
|
|
126
|
+
description: 'ID of the memory to forget',
|
|
127
|
+
format: 'uuid',
|
|
128
|
+
},
|
|
129
|
+
query: {
|
|
130
|
+
type: 'string',
|
|
131
|
+
description: 'Search query to find memories to forget',
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Project list tool JSON Schema
|
|
137
|
+
*/
|
|
138
|
+
const projectListSchema = {
|
|
139
|
+
type: 'object',
|
|
140
|
+
properties: {
|
|
141
|
+
status: {
|
|
142
|
+
type: 'string',
|
|
143
|
+
description: 'Filter by project status',
|
|
144
|
+
enum: ['active', 'completed', 'archived', 'all'],
|
|
145
|
+
default: 'active',
|
|
146
|
+
},
|
|
147
|
+
limit: {
|
|
148
|
+
type: 'integer',
|
|
149
|
+
description: 'Maximum number of projects to return',
|
|
150
|
+
minimum: 1,
|
|
151
|
+
maximum: 50,
|
|
152
|
+
default: 10,
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* Project get tool JSON Schema
|
|
158
|
+
*/
|
|
159
|
+
const projectGetSchema = {
|
|
160
|
+
type: 'object',
|
|
161
|
+
properties: {
|
|
162
|
+
projectId: {
|
|
163
|
+
type: 'string',
|
|
164
|
+
description: 'Project ID to retrieve',
|
|
165
|
+
format: 'uuid',
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
required: ['projectId'],
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* Project create tool JSON Schema
|
|
172
|
+
*/
|
|
173
|
+
const projectCreateSchema = {
|
|
174
|
+
type: 'object',
|
|
175
|
+
properties: {
|
|
176
|
+
name: {
|
|
177
|
+
type: 'string',
|
|
178
|
+
description: 'Project name',
|
|
179
|
+
minLength: 1,
|
|
180
|
+
maxLength: 200,
|
|
181
|
+
},
|
|
182
|
+
description: {
|
|
183
|
+
type: 'string',
|
|
184
|
+
description: 'Project description',
|
|
185
|
+
maxLength: 5000,
|
|
186
|
+
},
|
|
187
|
+
status: {
|
|
188
|
+
type: 'string',
|
|
189
|
+
description: 'Initial project status',
|
|
190
|
+
enum: ['active', 'completed', 'archived'],
|
|
191
|
+
default: 'active',
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
required: ['name'],
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* Todo list tool JSON Schema
|
|
198
|
+
*/
|
|
199
|
+
const todoListSchema = {
|
|
200
|
+
type: 'object',
|
|
201
|
+
properties: {
|
|
202
|
+
projectId: {
|
|
203
|
+
type: 'string',
|
|
204
|
+
description: 'Filter by project ID',
|
|
205
|
+
format: 'uuid',
|
|
206
|
+
},
|
|
207
|
+
status: {
|
|
208
|
+
type: 'string',
|
|
209
|
+
description: 'Filter by todo status',
|
|
210
|
+
enum: ['pending', 'in_progress', 'completed', 'all'],
|
|
211
|
+
default: 'pending',
|
|
212
|
+
},
|
|
213
|
+
limit: {
|
|
214
|
+
type: 'integer',
|
|
215
|
+
description: 'Maximum number of todos to return',
|
|
216
|
+
minimum: 1,
|
|
217
|
+
maximum: 100,
|
|
218
|
+
default: 20,
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* Todo create tool JSON Schema
|
|
224
|
+
*/
|
|
225
|
+
const todoCreateSchema = {
|
|
226
|
+
type: 'object',
|
|
227
|
+
properties: {
|
|
228
|
+
title: {
|
|
229
|
+
type: 'string',
|
|
230
|
+
description: 'Todo title',
|
|
231
|
+
minLength: 1,
|
|
232
|
+
maxLength: 500,
|
|
233
|
+
},
|
|
234
|
+
description: {
|
|
235
|
+
type: 'string',
|
|
236
|
+
description: 'Todo description',
|
|
237
|
+
maxLength: 5000,
|
|
238
|
+
},
|
|
239
|
+
projectId: {
|
|
240
|
+
type: 'string',
|
|
241
|
+
description: 'Project to add the todo to',
|
|
242
|
+
format: 'uuid',
|
|
243
|
+
},
|
|
244
|
+
priority: {
|
|
245
|
+
type: 'string',
|
|
246
|
+
description: 'Todo priority',
|
|
247
|
+
enum: ['low', 'medium', 'high', 'urgent'],
|
|
248
|
+
default: 'medium',
|
|
249
|
+
},
|
|
250
|
+
dueDate: {
|
|
251
|
+
type: 'string',
|
|
252
|
+
description: 'Due date in ISO 8601 format',
|
|
253
|
+
format: 'date-time',
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
required: ['title'],
|
|
257
|
+
};
|
|
258
|
+
/**
|
|
259
|
+
* Todo complete tool JSON Schema
|
|
260
|
+
*/
|
|
261
|
+
const todoCompleteSchema = {
|
|
262
|
+
type: 'object',
|
|
263
|
+
properties: {
|
|
264
|
+
todoId: {
|
|
265
|
+
type: 'string',
|
|
266
|
+
description: 'Todo ID to mark as complete',
|
|
267
|
+
format: 'uuid',
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
required: ['todoId'],
|
|
271
|
+
};
|
|
272
|
+
/**
|
|
273
|
+
* Contact search tool JSON Schema
|
|
274
|
+
*/
|
|
275
|
+
const contactSearchSchema = {
|
|
276
|
+
type: 'object',
|
|
277
|
+
properties: {
|
|
278
|
+
query: {
|
|
279
|
+
type: 'string',
|
|
280
|
+
description: 'Search query for contacts',
|
|
281
|
+
minLength: 1,
|
|
282
|
+
maxLength: 500,
|
|
283
|
+
},
|
|
284
|
+
limit: {
|
|
285
|
+
type: 'integer',
|
|
286
|
+
description: 'Maximum number of contacts to return',
|
|
287
|
+
minimum: 1,
|
|
288
|
+
maximum: 50,
|
|
289
|
+
default: 10,
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
required: ['query'],
|
|
293
|
+
};
|
|
294
|
+
/**
|
|
295
|
+
* Contact get tool JSON Schema
|
|
296
|
+
*/
|
|
297
|
+
const contactGetSchema = {
|
|
298
|
+
type: 'object',
|
|
299
|
+
properties: {
|
|
300
|
+
contactId: {
|
|
301
|
+
type: 'string',
|
|
302
|
+
description: 'Contact ID to retrieve',
|
|
303
|
+
format: 'uuid',
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
required: ['contactId'],
|
|
307
|
+
};
|
|
308
|
+
/**
|
|
309
|
+
* Contact create tool JSON Schema
|
|
310
|
+
*/
|
|
311
|
+
const contactCreateSchema = {
|
|
312
|
+
type: 'object',
|
|
313
|
+
properties: {
|
|
314
|
+
name: {
|
|
315
|
+
type: 'string',
|
|
316
|
+
description: 'Contact name',
|
|
317
|
+
minLength: 1,
|
|
318
|
+
maxLength: 200,
|
|
319
|
+
},
|
|
320
|
+
email: {
|
|
321
|
+
type: 'string',
|
|
322
|
+
description: 'Contact email address',
|
|
323
|
+
format: 'email',
|
|
324
|
+
},
|
|
325
|
+
phone: {
|
|
326
|
+
type: 'string',
|
|
327
|
+
description: 'Contact phone number',
|
|
328
|
+
},
|
|
329
|
+
notes: {
|
|
330
|
+
type: 'string',
|
|
331
|
+
description: 'Notes about the contact',
|
|
332
|
+
maxLength: 5000,
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
required: ['name'],
|
|
336
|
+
};
|
|
337
|
+
/**
|
|
338
|
+
* SMS send tool JSON Schema
|
|
339
|
+
*/
|
|
340
|
+
const smsSendSchema = {
|
|
341
|
+
type: 'object',
|
|
342
|
+
properties: {
|
|
343
|
+
to: {
|
|
344
|
+
type: 'string',
|
|
345
|
+
description: 'Recipient phone number in E.164 format (e.g., +15551234567)',
|
|
346
|
+
pattern: '^\\+[1-9]\\d{1,14}$',
|
|
347
|
+
},
|
|
348
|
+
body: {
|
|
349
|
+
type: 'string',
|
|
350
|
+
description: 'SMS message body',
|
|
351
|
+
minLength: 1,
|
|
352
|
+
maxLength: 1600,
|
|
353
|
+
},
|
|
354
|
+
idempotencyKey: {
|
|
355
|
+
type: 'string',
|
|
356
|
+
description: 'Optional key to prevent duplicate sends',
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
required: ['to', 'body'],
|
|
360
|
+
};
|
|
361
|
+
/**
|
|
362
|
+
* Email send tool JSON Schema
|
|
363
|
+
*/
|
|
364
|
+
const emailSendSchema = {
|
|
365
|
+
type: 'object',
|
|
366
|
+
properties: {
|
|
367
|
+
to: {
|
|
368
|
+
type: 'string',
|
|
369
|
+
description: 'Recipient email address',
|
|
370
|
+
format: 'email',
|
|
371
|
+
},
|
|
372
|
+
subject: {
|
|
373
|
+
type: 'string',
|
|
374
|
+
description: 'Email subject line',
|
|
375
|
+
minLength: 1,
|
|
376
|
+
maxLength: 998,
|
|
377
|
+
},
|
|
378
|
+
body: {
|
|
379
|
+
type: 'string',
|
|
380
|
+
description: 'Plain text email body',
|
|
381
|
+
minLength: 1,
|
|
382
|
+
},
|
|
383
|
+
htmlBody: {
|
|
384
|
+
type: 'string',
|
|
385
|
+
description: 'Optional HTML email body',
|
|
386
|
+
},
|
|
387
|
+
threadId: {
|
|
388
|
+
type: 'string',
|
|
389
|
+
description: 'Optional thread ID for replies',
|
|
390
|
+
},
|
|
391
|
+
idempotencyKey: {
|
|
392
|
+
type: 'string',
|
|
393
|
+
description: 'Optional unique key to prevent duplicate sends',
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
required: ['to', 'subject', 'body'],
|
|
397
|
+
};
|
|
398
|
+
/**
|
|
399
|
+
* Message search tool JSON Schema
|
|
400
|
+
*/
|
|
401
|
+
const messageSearchSchema = {
|
|
402
|
+
type: 'object',
|
|
403
|
+
properties: {
|
|
404
|
+
query: {
|
|
405
|
+
type: 'string',
|
|
406
|
+
description: 'Search query (semantic matching)',
|
|
407
|
+
minLength: 1,
|
|
408
|
+
},
|
|
409
|
+
channel: {
|
|
410
|
+
type: 'string',
|
|
411
|
+
description: 'Filter by channel type',
|
|
412
|
+
enum: ['sms', 'email', 'all'],
|
|
413
|
+
default: 'all',
|
|
414
|
+
},
|
|
415
|
+
contactId: {
|
|
416
|
+
type: 'string',
|
|
417
|
+
description: 'Filter by contact ID',
|
|
418
|
+
format: 'uuid',
|
|
419
|
+
},
|
|
420
|
+
limit: {
|
|
421
|
+
type: 'integer',
|
|
422
|
+
description: 'Maximum results to return',
|
|
423
|
+
minimum: 1,
|
|
424
|
+
maximum: 100,
|
|
425
|
+
default: 10,
|
|
426
|
+
},
|
|
427
|
+
includeThread: {
|
|
428
|
+
type: 'boolean',
|
|
429
|
+
description: 'Include full thread context',
|
|
430
|
+
default: false,
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
required: ['query'],
|
|
434
|
+
};
|
|
435
|
+
/**
|
|
436
|
+
* Thread list tool JSON Schema
|
|
437
|
+
*/
|
|
438
|
+
const threadListSchema = {
|
|
439
|
+
type: 'object',
|
|
440
|
+
properties: {
|
|
441
|
+
channel: {
|
|
442
|
+
type: 'string',
|
|
443
|
+
description: 'Filter by channel type',
|
|
444
|
+
enum: ['sms', 'email'],
|
|
445
|
+
},
|
|
446
|
+
contactId: {
|
|
447
|
+
type: 'string',
|
|
448
|
+
description: 'Filter by contact ID',
|
|
449
|
+
format: 'uuid',
|
|
450
|
+
},
|
|
451
|
+
limit: {
|
|
452
|
+
type: 'integer',
|
|
453
|
+
description: 'Maximum threads to return',
|
|
454
|
+
minimum: 1,
|
|
455
|
+
maximum: 100,
|
|
456
|
+
default: 20,
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
/**
|
|
461
|
+
* Thread get tool JSON Schema
|
|
462
|
+
*/
|
|
463
|
+
const threadGetSchema = {
|
|
464
|
+
type: 'object',
|
|
465
|
+
properties: {
|
|
466
|
+
threadId: {
|
|
467
|
+
type: 'string',
|
|
468
|
+
description: 'Thread ID to retrieve',
|
|
469
|
+
},
|
|
470
|
+
messageLimit: {
|
|
471
|
+
type: 'integer',
|
|
472
|
+
description: 'Maximum messages to return',
|
|
473
|
+
minimum: 1,
|
|
474
|
+
maximum: 200,
|
|
475
|
+
default: 50,
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
required: ['threadId'],
|
|
479
|
+
};
|
|
480
|
+
/**
|
|
481
|
+
* Relationship set tool JSON Schema
|
|
482
|
+
*/
|
|
483
|
+
const relationshipSetSchema = {
|
|
484
|
+
type: 'object',
|
|
485
|
+
properties: {
|
|
486
|
+
contact_a: {
|
|
487
|
+
type: 'string',
|
|
488
|
+
description: 'Name or ID of the first contact',
|
|
489
|
+
minLength: 1,
|
|
490
|
+
maxLength: 200,
|
|
491
|
+
},
|
|
492
|
+
contact_b: {
|
|
493
|
+
type: 'string',
|
|
494
|
+
description: 'Name or ID of the second contact',
|
|
495
|
+
minLength: 1,
|
|
496
|
+
maxLength: 200,
|
|
497
|
+
},
|
|
498
|
+
relationship: {
|
|
499
|
+
type: 'string',
|
|
500
|
+
description: "Description of the relationship, e.g. 'partner', 'parent of', 'member of', 'works for'",
|
|
501
|
+
minLength: 1,
|
|
502
|
+
maxLength: 200,
|
|
503
|
+
},
|
|
504
|
+
notes: {
|
|
505
|
+
type: 'string',
|
|
506
|
+
description: 'Optional context about this relationship',
|
|
507
|
+
maxLength: 2000,
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
required: ['contact_a', 'contact_b', 'relationship'],
|
|
511
|
+
};
|
|
512
|
+
/**
|
|
513
|
+
* Relationship query tool JSON Schema
|
|
514
|
+
*/
|
|
515
|
+
const relationshipQuerySchema = {
|
|
516
|
+
type: 'object',
|
|
517
|
+
properties: {
|
|
518
|
+
contact: {
|
|
519
|
+
type: 'string',
|
|
520
|
+
description: 'Name or ID of the contact to query',
|
|
521
|
+
minLength: 1,
|
|
522
|
+
maxLength: 200,
|
|
523
|
+
},
|
|
524
|
+
type_filter: {
|
|
525
|
+
type: 'string',
|
|
526
|
+
description: 'Optional: filter by relationship type',
|
|
527
|
+
maxLength: 200,
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
required: ['contact'],
|
|
531
|
+
};
|
|
532
|
+
/**
|
|
533
|
+
* File share tool JSON Schema
|
|
534
|
+
*/
|
|
535
|
+
const fileShareSchema = {
|
|
536
|
+
type: 'object',
|
|
537
|
+
properties: {
|
|
538
|
+
fileId: {
|
|
539
|
+
type: 'string',
|
|
540
|
+
description: 'The file ID to create a share link for',
|
|
541
|
+
format: 'uuid',
|
|
542
|
+
},
|
|
543
|
+
expiresIn: {
|
|
544
|
+
type: 'integer',
|
|
545
|
+
description: 'Link expiry time in seconds (default: 3600, max: 604800)',
|
|
546
|
+
minimum: 60,
|
|
547
|
+
maximum: 604800,
|
|
548
|
+
default: 3600,
|
|
549
|
+
},
|
|
550
|
+
maxDownloads: {
|
|
551
|
+
type: 'integer',
|
|
552
|
+
description: 'Optional maximum number of downloads',
|
|
553
|
+
minimum: 1,
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
required: ['fileId'],
|
|
557
|
+
};
|
|
558
|
+
/**
|
|
559
|
+
* Create tool execution handlers
|
|
560
|
+
*/
|
|
561
|
+
function createToolHandlers(state) {
|
|
562
|
+
const { config, logger, apiClient, userId } = state;
|
|
563
|
+
return {
|
|
564
|
+
async memory_recall(params) {
|
|
565
|
+
const { query, limit = config.maxRecallMemories, category, tags, relationship_id } = params;
|
|
566
|
+
try {
|
|
567
|
+
const queryParams = new URLSearchParams({ q: query, limit: String(limit) });
|
|
568
|
+
if (category)
|
|
569
|
+
queryParams.set('category', category);
|
|
570
|
+
if (tags && tags.length > 0)
|
|
571
|
+
queryParams.set('tags', tags.join(','));
|
|
572
|
+
if (relationship_id)
|
|
573
|
+
queryParams.set('relationship_id', relationship_id);
|
|
574
|
+
const response = await apiClient.get(`/api/memories/search?${queryParams}`, { userId });
|
|
575
|
+
if (!response.success) {
|
|
576
|
+
return { success: false, error: response.error.message };
|
|
577
|
+
}
|
|
578
|
+
const memories = response.data.memories ?? [];
|
|
579
|
+
const content = memories.length > 0
|
|
580
|
+
? memories.map((m) => `- [${m.category}] ${m.content}`).join('\n')
|
|
581
|
+
: 'No relevant memories found.';
|
|
582
|
+
return {
|
|
583
|
+
success: true,
|
|
584
|
+
data: {
|
|
585
|
+
content,
|
|
586
|
+
details: { count: memories.length, memories, userId },
|
|
587
|
+
},
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
catch (error) {
|
|
591
|
+
logger.error('memory_recall failed', { error });
|
|
592
|
+
return { success: false, error: 'Failed to search memories' };
|
|
593
|
+
}
|
|
594
|
+
},
|
|
595
|
+
async memory_store(params) {
|
|
596
|
+
const { content, category = 'fact', importance = 0.5, tags, relationship_id } = params;
|
|
597
|
+
try {
|
|
598
|
+
const payload = { content, category, importance };
|
|
599
|
+
if (tags && tags.length > 0)
|
|
600
|
+
payload.tags = tags;
|
|
601
|
+
if (relationship_id)
|
|
602
|
+
payload.relationship_id = relationship_id;
|
|
603
|
+
const response = await apiClient.post('/api/memories', payload, { userId });
|
|
604
|
+
if (!response.success) {
|
|
605
|
+
return { success: false, error: response.error.message };
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
success: true,
|
|
609
|
+
data: {
|
|
610
|
+
content: `Memory stored successfully (ID: ${response.data.id})`,
|
|
611
|
+
details: { id: response.data.id, userId },
|
|
612
|
+
},
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
catch (error) {
|
|
616
|
+
logger.error('memory_store failed', { error });
|
|
617
|
+
return { success: false, error: 'Failed to store memory' };
|
|
618
|
+
}
|
|
619
|
+
},
|
|
620
|
+
async memory_forget(params) {
|
|
621
|
+
const { memoryId, query } = params;
|
|
622
|
+
try {
|
|
623
|
+
if (memoryId) {
|
|
624
|
+
const response = await apiClient.delete(`/api/memories/${memoryId}`, { userId });
|
|
625
|
+
if (!response.success) {
|
|
626
|
+
return { success: false, error: response.error.message };
|
|
627
|
+
}
|
|
628
|
+
return {
|
|
629
|
+
success: true,
|
|
630
|
+
data: { content: `Memory ${memoryId} forgotten successfully` },
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
if (query) {
|
|
634
|
+
const response = await apiClient.post('/api/memories/forget', { query }, { userId });
|
|
635
|
+
if (!response.success) {
|
|
636
|
+
return { success: false, error: response.error.message };
|
|
637
|
+
}
|
|
638
|
+
return {
|
|
639
|
+
success: true,
|
|
640
|
+
data: {
|
|
641
|
+
content: `Forgotten ${response.data.deleted} matching memories`,
|
|
642
|
+
details: { deletedCount: response.data.deleted },
|
|
643
|
+
},
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
return { success: false, error: 'Either memoryId or query is required' };
|
|
647
|
+
}
|
|
648
|
+
catch (error) {
|
|
649
|
+
logger.error('memory_forget failed', { error });
|
|
650
|
+
return { success: false, error: 'Failed to forget memory' };
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
async project_list(params) {
|
|
654
|
+
const { status = 'active', limit = 10 } = params;
|
|
655
|
+
try {
|
|
656
|
+
const queryParams = new URLSearchParams({ limit: String(limit) });
|
|
657
|
+
if (status !== 'all')
|
|
658
|
+
queryParams.set('status', status);
|
|
659
|
+
const response = await apiClient.get(`/api/projects?${queryParams}`, { userId });
|
|
660
|
+
if (!response.success) {
|
|
661
|
+
return { success: false, error: response.error.message };
|
|
662
|
+
}
|
|
663
|
+
const projects = response.data.projects ?? [];
|
|
664
|
+
const content = projects.length > 0
|
|
665
|
+
? projects.map((p) => `- ${p.name} (${p.status})`).join('\n')
|
|
666
|
+
: 'No projects found.';
|
|
667
|
+
return {
|
|
668
|
+
success: true,
|
|
669
|
+
data: { content, details: { count: projects.length, projects } },
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
catch (error) {
|
|
673
|
+
logger.error('project_list failed', { error });
|
|
674
|
+
return { success: false, error: 'Failed to list projects' };
|
|
675
|
+
}
|
|
676
|
+
},
|
|
677
|
+
async project_get(params) {
|
|
678
|
+
const { projectId } = params;
|
|
679
|
+
try {
|
|
680
|
+
const response = await apiClient.get(`/api/projects/${projectId}`, { userId });
|
|
681
|
+
if (!response.success) {
|
|
682
|
+
return { success: false, error: response.error.message };
|
|
683
|
+
}
|
|
684
|
+
const project = response.data;
|
|
685
|
+
return {
|
|
686
|
+
success: true,
|
|
687
|
+
data: {
|
|
688
|
+
content: `Project: ${project.name}\nStatus: ${project.status}\n${project.description || ''}`,
|
|
689
|
+
details: { project },
|
|
690
|
+
},
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
catch (error) {
|
|
694
|
+
logger.error('project_get failed', { error });
|
|
695
|
+
return { success: false, error: 'Failed to get project' };
|
|
696
|
+
}
|
|
697
|
+
},
|
|
698
|
+
async project_create(params) {
|
|
699
|
+
const { name, description, status = 'active' } = params;
|
|
700
|
+
try {
|
|
701
|
+
const response = await apiClient.post('/api/projects', { name, description, status }, { userId });
|
|
702
|
+
if (!response.success) {
|
|
703
|
+
return { success: false, error: response.error.message };
|
|
704
|
+
}
|
|
705
|
+
return {
|
|
706
|
+
success: true,
|
|
707
|
+
data: {
|
|
708
|
+
content: `Project "${name}" created successfully (ID: ${response.data.id})`,
|
|
709
|
+
details: { id: response.data.id },
|
|
710
|
+
},
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
catch (error) {
|
|
714
|
+
logger.error('project_create failed', { error });
|
|
715
|
+
return { success: false, error: 'Failed to create project' };
|
|
716
|
+
}
|
|
717
|
+
},
|
|
718
|
+
async todo_list(params) {
|
|
719
|
+
const { projectId, status = 'pending', limit = 20 } = params;
|
|
720
|
+
try {
|
|
721
|
+
const queryParams = new URLSearchParams({ limit: String(limit) });
|
|
722
|
+
if (status !== 'all')
|
|
723
|
+
queryParams.set('status', status);
|
|
724
|
+
if (projectId)
|
|
725
|
+
queryParams.set('projectId', projectId);
|
|
726
|
+
const response = await apiClient.get(`/api/todos?${queryParams}`, { userId });
|
|
727
|
+
if (!response.success) {
|
|
728
|
+
return { success: false, error: response.error.message };
|
|
729
|
+
}
|
|
730
|
+
const todos = response.data.todos ?? [];
|
|
731
|
+
const content = todos.length > 0
|
|
732
|
+
? todos.map((t) => `- [${t.status}] ${t.title}`).join('\n')
|
|
733
|
+
: 'No todos found.';
|
|
734
|
+
return {
|
|
735
|
+
success: true,
|
|
736
|
+
data: { content, details: { count: todos.length, todos } },
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
catch (error) {
|
|
740
|
+
logger.error('todo_list failed', { error });
|
|
741
|
+
return { success: false, error: 'Failed to list todos' };
|
|
742
|
+
}
|
|
743
|
+
},
|
|
744
|
+
async todo_create(params) {
|
|
745
|
+
const { title, description, projectId, priority = 'medium', dueDate } = params;
|
|
746
|
+
try {
|
|
747
|
+
const response = await apiClient.post('/api/todos', { title, description, projectId, priority, dueDate }, { userId });
|
|
748
|
+
if (!response.success) {
|
|
749
|
+
return { success: false, error: response.error.message };
|
|
750
|
+
}
|
|
751
|
+
return {
|
|
752
|
+
success: true,
|
|
753
|
+
data: {
|
|
754
|
+
content: `Todo "${title}" created successfully (ID: ${response.data.id})`,
|
|
755
|
+
details: { id: response.data.id },
|
|
756
|
+
},
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
catch (error) {
|
|
760
|
+
logger.error('todo_create failed', { error });
|
|
761
|
+
return { success: false, error: 'Failed to create todo' };
|
|
762
|
+
}
|
|
763
|
+
},
|
|
764
|
+
async todo_complete(params) {
|
|
765
|
+
const { todoId } = params;
|
|
766
|
+
try {
|
|
767
|
+
const response = await apiClient.patch(`/api/todos/${todoId}`, { status: 'completed' }, { userId });
|
|
768
|
+
if (!response.success) {
|
|
769
|
+
return { success: false, error: response.error.message };
|
|
770
|
+
}
|
|
771
|
+
return {
|
|
772
|
+
success: true,
|
|
773
|
+
data: { content: `Todo ${todoId} marked as complete` },
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
catch (error) {
|
|
777
|
+
logger.error('todo_complete failed', { error });
|
|
778
|
+
return { success: false, error: 'Failed to complete todo' };
|
|
779
|
+
}
|
|
780
|
+
},
|
|
781
|
+
async contact_search(params) {
|
|
782
|
+
const { query, limit = 10 } = params;
|
|
783
|
+
try {
|
|
784
|
+
const queryParams = new URLSearchParams({ q: query, limit: String(limit) });
|
|
785
|
+
const response = await apiClient.get(`/api/contacts/search?${queryParams}`, { userId });
|
|
786
|
+
if (!response.success) {
|
|
787
|
+
return { success: false, error: response.error.message };
|
|
788
|
+
}
|
|
789
|
+
const contacts = response.data.contacts ?? [];
|
|
790
|
+
const content = contacts.length > 0
|
|
791
|
+
? contacts.map((c) => `- ${c.name}${c.email ? ` (${c.email})` : ''}`).join('\n')
|
|
792
|
+
: 'No contacts found.';
|
|
793
|
+
return {
|
|
794
|
+
success: true,
|
|
795
|
+
data: { content, details: { count: contacts.length, contacts } },
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
catch (error) {
|
|
799
|
+
logger.error('contact_search failed', { error });
|
|
800
|
+
return { success: false, error: 'Failed to search contacts' };
|
|
801
|
+
}
|
|
802
|
+
},
|
|
803
|
+
async contact_get(params) {
|
|
804
|
+
const { contactId } = params;
|
|
805
|
+
try {
|
|
806
|
+
const response = await apiClient.get(`/api/contacts/${contactId}`, { userId });
|
|
807
|
+
if (!response.success) {
|
|
808
|
+
return { success: false, error: response.error.message };
|
|
809
|
+
}
|
|
810
|
+
const contact = response.data;
|
|
811
|
+
const lines = [`Contact: ${contact.name}`];
|
|
812
|
+
if (contact.email)
|
|
813
|
+
lines.push(`Email: ${contact.email}`);
|
|
814
|
+
if (contact.phone)
|
|
815
|
+
lines.push(`Phone: ${contact.phone}`);
|
|
816
|
+
if (contact.notes)
|
|
817
|
+
lines.push(`Notes: ${contact.notes}`);
|
|
818
|
+
return {
|
|
819
|
+
success: true,
|
|
820
|
+
data: { content: lines.join('\n'), details: { contact } },
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
catch (error) {
|
|
824
|
+
logger.error('contact_get failed', { error });
|
|
825
|
+
return { success: false, error: 'Failed to get contact' };
|
|
826
|
+
}
|
|
827
|
+
},
|
|
828
|
+
async contact_create(params) {
|
|
829
|
+
const { name, email, phone, notes } = params;
|
|
830
|
+
try {
|
|
831
|
+
const response = await apiClient.post('/api/contacts', { name, email, phone, notes }, { userId });
|
|
832
|
+
if (!response.success) {
|
|
833
|
+
return { success: false, error: response.error.message };
|
|
834
|
+
}
|
|
835
|
+
return {
|
|
836
|
+
success: true,
|
|
837
|
+
data: {
|
|
838
|
+
content: `Contact "${name}" created successfully (ID: ${response.data.id})`,
|
|
839
|
+
details: { id: response.data.id },
|
|
840
|
+
},
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
catch (error) {
|
|
844
|
+
logger.error('contact_create failed', { error });
|
|
845
|
+
return { success: false, error: 'Failed to create contact' };
|
|
846
|
+
}
|
|
847
|
+
},
|
|
848
|
+
async sms_send(params) {
|
|
849
|
+
const { to, body, idempotencyKey } = params;
|
|
850
|
+
// Check Twilio configuration
|
|
851
|
+
if (!config.twilioAccountSid || !config.twilioAuthToken || !config.twilioPhoneNumber) {
|
|
852
|
+
return {
|
|
853
|
+
success: false,
|
|
854
|
+
error: 'Twilio is not configured. Please configure Twilio credentials.',
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
// Validate E.164 format
|
|
858
|
+
const e164Regex = /^\+[1-9]\d{1,14}$/;
|
|
859
|
+
if (!e164Regex.test(to)) {
|
|
860
|
+
return {
|
|
861
|
+
success: false,
|
|
862
|
+
error: 'to: Phone number must be in E.164 format (e.g., +15551234567)',
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
// Validate body length
|
|
866
|
+
if (!body || body.length === 0) {
|
|
867
|
+
return {
|
|
868
|
+
success: false,
|
|
869
|
+
error: 'body: Message body cannot be empty',
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
if (body.length > 1600) {
|
|
873
|
+
return {
|
|
874
|
+
success: false,
|
|
875
|
+
error: 'body: Message body must be 1600 characters or less',
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
logger.info('sms_send invoked', {
|
|
879
|
+
userId,
|
|
880
|
+
bodyLength: body.length,
|
|
881
|
+
hasIdempotencyKey: !!idempotencyKey,
|
|
882
|
+
});
|
|
883
|
+
try {
|
|
884
|
+
const response = await apiClient.post('/api/twilio/sms/send', { to, body, idempotencyKey }, { userId });
|
|
885
|
+
if (!response.success) {
|
|
886
|
+
logger.error('sms_send API error', {
|
|
887
|
+
userId,
|
|
888
|
+
status: response.error.status,
|
|
889
|
+
code: response.error.code,
|
|
890
|
+
});
|
|
891
|
+
return {
|
|
892
|
+
success: false,
|
|
893
|
+
error: response.error.message || 'Failed to send SMS',
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
const { messageId, threadId, status } = response.data;
|
|
897
|
+
logger.debug('sms_send completed', {
|
|
898
|
+
userId,
|
|
899
|
+
messageId,
|
|
900
|
+
status,
|
|
901
|
+
});
|
|
902
|
+
return {
|
|
903
|
+
success: true,
|
|
904
|
+
data: {
|
|
905
|
+
content: `SMS sent successfully (ID: ${messageId}, Status: ${status})`,
|
|
906
|
+
details: { messageId, threadId, status, userId },
|
|
907
|
+
},
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
catch (error) {
|
|
911
|
+
logger.error('sms_send failed', {
|
|
912
|
+
userId,
|
|
913
|
+
error: error instanceof Error ? error.message : String(error),
|
|
914
|
+
});
|
|
915
|
+
// Sanitize error message (remove phone numbers for privacy)
|
|
916
|
+
let errorMessage = 'An unexpected error occurred while sending SMS.';
|
|
917
|
+
if (error instanceof Error) {
|
|
918
|
+
errorMessage = error.message.replace(/\+\d{1,15}/g, '[phone]');
|
|
919
|
+
}
|
|
920
|
+
return {
|
|
921
|
+
success: false,
|
|
922
|
+
error: errorMessage,
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
},
|
|
926
|
+
async email_send(params) {
|
|
927
|
+
const { to, subject, body, htmlBody, threadId, idempotencyKey } = params;
|
|
928
|
+
// Check Postmark configuration
|
|
929
|
+
if (!config.postmarkToken || !config.postmarkFromEmail) {
|
|
930
|
+
return {
|
|
931
|
+
success: false,
|
|
932
|
+
error: 'Postmark is not configured. Please configure Postmark credentials.',
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
// Validate email format
|
|
936
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
937
|
+
if (!emailRegex.test(to)) {
|
|
938
|
+
return {
|
|
939
|
+
success: false,
|
|
940
|
+
error: 'to: Invalid email address format',
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
// Validate subject
|
|
944
|
+
if (!subject || subject.length === 0) {
|
|
945
|
+
return {
|
|
946
|
+
success: false,
|
|
947
|
+
error: 'subject: Subject cannot be empty',
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
if (subject.length > 998) {
|
|
951
|
+
return {
|
|
952
|
+
success: false,
|
|
953
|
+
error: 'subject: Subject must be 998 characters or less',
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
// Validate body
|
|
957
|
+
if (!body || body.length === 0) {
|
|
958
|
+
return {
|
|
959
|
+
success: false,
|
|
960
|
+
error: 'body: Email body cannot be empty',
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
logger.info('email_send invoked', {
|
|
964
|
+
userId,
|
|
965
|
+
subjectLength: subject.length,
|
|
966
|
+
bodyLength: body.length,
|
|
967
|
+
hasHtmlBody: !!htmlBody,
|
|
968
|
+
hasThreadId: !!threadId,
|
|
969
|
+
hasIdempotencyKey: !!idempotencyKey,
|
|
970
|
+
});
|
|
971
|
+
try {
|
|
972
|
+
const response = await apiClient.post('/api/postmark/email/send', { to, subject, body, htmlBody, threadId, idempotencyKey }, { userId });
|
|
973
|
+
if (!response.success) {
|
|
974
|
+
logger.error('email_send API error', {
|
|
975
|
+
userId,
|
|
976
|
+
status: response.error.status,
|
|
977
|
+
code: response.error.code,
|
|
978
|
+
});
|
|
979
|
+
return {
|
|
980
|
+
success: false,
|
|
981
|
+
error: response.error.message || 'Failed to send email',
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
const { messageId, threadId: responseThreadId, status } = response.data;
|
|
985
|
+
logger.debug('email_send completed', {
|
|
986
|
+
userId,
|
|
987
|
+
messageId,
|
|
988
|
+
status,
|
|
989
|
+
});
|
|
990
|
+
return {
|
|
991
|
+
success: true,
|
|
992
|
+
data: {
|
|
993
|
+
content: `Email sent successfully (ID: ${messageId}, Status: ${status})`,
|
|
994
|
+
details: { messageId, threadId: responseThreadId, status, userId },
|
|
995
|
+
},
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
catch (error) {
|
|
999
|
+
logger.error('email_send failed', {
|
|
1000
|
+
userId,
|
|
1001
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1002
|
+
});
|
|
1003
|
+
// Sanitize error message (remove email addresses for privacy)
|
|
1004
|
+
let errorMessage = 'An unexpected error occurred while sending email.';
|
|
1005
|
+
if (error instanceof Error) {
|
|
1006
|
+
errorMessage = error.message.replace(/[^\s@]+@[^\s@]+\.[^\s@]+/g, '[email]');
|
|
1007
|
+
}
|
|
1008
|
+
return {
|
|
1009
|
+
success: false,
|
|
1010
|
+
error: errorMessage,
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
},
|
|
1014
|
+
async message_search(params) {
|
|
1015
|
+
const { query, channel = 'all', contactId, limit = 10, includeThread = false } = params;
|
|
1016
|
+
// Validate query
|
|
1017
|
+
if (!query || query.length === 0) {
|
|
1018
|
+
return {
|
|
1019
|
+
success: false,
|
|
1020
|
+
error: 'query: Search query cannot be empty',
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
logger.info('message_search invoked', {
|
|
1024
|
+
userId,
|
|
1025
|
+
queryLength: query.length,
|
|
1026
|
+
channel,
|
|
1027
|
+
hasContactId: !!contactId,
|
|
1028
|
+
limit,
|
|
1029
|
+
includeThread,
|
|
1030
|
+
});
|
|
1031
|
+
try {
|
|
1032
|
+
// Build query parameters
|
|
1033
|
+
const queryParams = new URLSearchParams();
|
|
1034
|
+
queryParams.set('q', query);
|
|
1035
|
+
queryParams.set('types', 'message');
|
|
1036
|
+
queryParams.set('limit', String(limit));
|
|
1037
|
+
if (channel !== 'all') {
|
|
1038
|
+
queryParams.set('channel', channel);
|
|
1039
|
+
}
|
|
1040
|
+
if (contactId) {
|
|
1041
|
+
queryParams.set('contactId', contactId);
|
|
1042
|
+
}
|
|
1043
|
+
if (includeThread) {
|
|
1044
|
+
queryParams.set('includeThread', 'true');
|
|
1045
|
+
}
|
|
1046
|
+
const response = await apiClient.get(`/api/search?${queryParams}`, { userId });
|
|
1047
|
+
if (!response.success) {
|
|
1048
|
+
logger.error('message_search API error', {
|
|
1049
|
+
userId,
|
|
1050
|
+
status: response.error.status,
|
|
1051
|
+
code: response.error.code,
|
|
1052
|
+
});
|
|
1053
|
+
return {
|
|
1054
|
+
success: false,
|
|
1055
|
+
error: response.error.message || 'Failed to search messages',
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
const { results, total } = response.data;
|
|
1059
|
+
// Transform results
|
|
1060
|
+
const messages = results.map((r) => ({
|
|
1061
|
+
id: r.id,
|
|
1062
|
+
body: r.body,
|
|
1063
|
+
direction: r.direction,
|
|
1064
|
+
channel: r.channel,
|
|
1065
|
+
contactName: r.contactName,
|
|
1066
|
+
timestamp: r.timestamp,
|
|
1067
|
+
similarity: r.score,
|
|
1068
|
+
}));
|
|
1069
|
+
logger.debug('message_search completed', {
|
|
1070
|
+
userId,
|
|
1071
|
+
resultCount: messages.length,
|
|
1072
|
+
total,
|
|
1073
|
+
});
|
|
1074
|
+
// Format content for display
|
|
1075
|
+
const content = messages.length > 0
|
|
1076
|
+
? messages.map((m) => {
|
|
1077
|
+
const prefix = m.direction === 'inbound' ? '←' : '→';
|
|
1078
|
+
const contact = m.contactName || 'Unknown';
|
|
1079
|
+
const similarity = `(${Math.round(m.similarity * 100)}%)`;
|
|
1080
|
+
return `${prefix} [${m.channel}] ${contact} ${similarity}: ${m.body.substring(0, 100)}${m.body.length > 100 ? '...' : ''}`;
|
|
1081
|
+
}).join('\n')
|
|
1082
|
+
: 'No messages found matching your query.';
|
|
1083
|
+
return {
|
|
1084
|
+
success: true,
|
|
1085
|
+
data: {
|
|
1086
|
+
content,
|
|
1087
|
+
details: { messages, total, userId },
|
|
1088
|
+
},
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
catch (error) {
|
|
1092
|
+
logger.error('message_search failed', {
|
|
1093
|
+
userId,
|
|
1094
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1095
|
+
});
|
|
1096
|
+
return {
|
|
1097
|
+
success: false,
|
|
1098
|
+
error: error instanceof Error ? error.message : 'An unexpected error occurred while searching messages.',
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
},
|
|
1102
|
+
async thread_list(params) {
|
|
1103
|
+
const { channel, contactId, limit = 20 } = params;
|
|
1104
|
+
logger.info('thread_list invoked', {
|
|
1105
|
+
userId,
|
|
1106
|
+
channel,
|
|
1107
|
+
hasContactId: !!contactId,
|
|
1108
|
+
limit,
|
|
1109
|
+
});
|
|
1110
|
+
try {
|
|
1111
|
+
const queryParams = new URLSearchParams();
|
|
1112
|
+
queryParams.set('limit', String(limit));
|
|
1113
|
+
if (channel) {
|
|
1114
|
+
queryParams.set('channel', channel);
|
|
1115
|
+
}
|
|
1116
|
+
if (contactId) {
|
|
1117
|
+
queryParams.set('contactId', contactId);
|
|
1118
|
+
}
|
|
1119
|
+
const response = await apiClient.get(`/api/threads?${queryParams}`, { userId });
|
|
1120
|
+
if (!response.success) {
|
|
1121
|
+
logger.error('thread_list API error', {
|
|
1122
|
+
userId,
|
|
1123
|
+
status: response.error.status,
|
|
1124
|
+
code: response.error.code,
|
|
1125
|
+
});
|
|
1126
|
+
return {
|
|
1127
|
+
success: false,
|
|
1128
|
+
error: response.error.message || 'Failed to list threads',
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
const { threads, total } = response.data;
|
|
1132
|
+
logger.debug('thread_list completed', {
|
|
1133
|
+
userId,
|
|
1134
|
+
threadCount: threads.length,
|
|
1135
|
+
total,
|
|
1136
|
+
});
|
|
1137
|
+
const content = threads.length > 0
|
|
1138
|
+
? threads.map((t) => {
|
|
1139
|
+
const contact = t.contactName || t.endpointValue;
|
|
1140
|
+
const msgCount = `${t.messageCount} message${t.messageCount !== 1 ? 's' : ''}`;
|
|
1141
|
+
return `[${t.channel}] ${contact} - ${msgCount}`;
|
|
1142
|
+
}).join('\n')
|
|
1143
|
+
: 'No threads found.';
|
|
1144
|
+
return {
|
|
1145
|
+
success: true,
|
|
1146
|
+
data: {
|
|
1147
|
+
content,
|
|
1148
|
+
details: { threads, total, userId },
|
|
1149
|
+
},
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
catch (error) {
|
|
1153
|
+
logger.error('thread_list failed', {
|
|
1154
|
+
userId,
|
|
1155
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1156
|
+
});
|
|
1157
|
+
return {
|
|
1158
|
+
success: false,
|
|
1159
|
+
error: error instanceof Error ? error.message : 'An unexpected error occurred while listing threads.',
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
},
|
|
1163
|
+
async thread_get(params) {
|
|
1164
|
+
const { threadId, messageLimit = 50 } = params;
|
|
1165
|
+
// Validate threadId
|
|
1166
|
+
if (!threadId || threadId.length === 0) {
|
|
1167
|
+
return {
|
|
1168
|
+
success: false,
|
|
1169
|
+
error: 'threadId: Thread ID is required',
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
logger.info('thread_get invoked', {
|
|
1173
|
+
userId,
|
|
1174
|
+
threadId,
|
|
1175
|
+
messageLimit,
|
|
1176
|
+
});
|
|
1177
|
+
try {
|
|
1178
|
+
const queryParams = new URLSearchParams();
|
|
1179
|
+
queryParams.set('messageLimit', String(messageLimit));
|
|
1180
|
+
const response = await apiClient.get(`/api/threads/${threadId}?${queryParams}`, { userId });
|
|
1181
|
+
if (!response.success) {
|
|
1182
|
+
logger.error('thread_get API error', {
|
|
1183
|
+
userId,
|
|
1184
|
+
threadId,
|
|
1185
|
+
status: response.error.status,
|
|
1186
|
+
code: response.error.code,
|
|
1187
|
+
});
|
|
1188
|
+
return {
|
|
1189
|
+
success: false,
|
|
1190
|
+
error: response.error.message || 'Failed to get thread',
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
const { thread, messages } = response.data;
|
|
1194
|
+
logger.debug('thread_get completed', {
|
|
1195
|
+
userId,
|
|
1196
|
+
threadId,
|
|
1197
|
+
messageCount: messages.length,
|
|
1198
|
+
});
|
|
1199
|
+
const contact = thread.contactName || thread.endpointValue || 'Unknown';
|
|
1200
|
+
const header = `Thread with ${contact} [${thread.channel}]`;
|
|
1201
|
+
const messageContent = messages.length > 0
|
|
1202
|
+
? messages.map((m) => {
|
|
1203
|
+
const prefix = m.direction === 'inbound' ? '←' : '→';
|
|
1204
|
+
const timestamp = new Date(m.createdAt).toLocaleString();
|
|
1205
|
+
return `${prefix} [${timestamp}] ${m.body}`;
|
|
1206
|
+
}).join('\n')
|
|
1207
|
+
: 'No messages in this thread.';
|
|
1208
|
+
const content = `${header}\n\n${messageContent}`;
|
|
1209
|
+
return {
|
|
1210
|
+
success: true,
|
|
1211
|
+
data: {
|
|
1212
|
+
content,
|
|
1213
|
+
details: { thread, messages, userId },
|
|
1214
|
+
},
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
catch (error) {
|
|
1218
|
+
logger.error('thread_get failed', {
|
|
1219
|
+
userId,
|
|
1220
|
+
threadId,
|
|
1221
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1222
|
+
});
|
|
1223
|
+
return {
|
|
1224
|
+
success: false,
|
|
1225
|
+
error: error instanceof Error ? error.message : 'An unexpected error occurred while getting thread.',
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
},
|
|
1229
|
+
async relationship_set(params) {
|
|
1230
|
+
const { contact_a, contact_b, relationship, notes } = params;
|
|
1231
|
+
if (!contact_a || !contact_b || !relationship) {
|
|
1232
|
+
return {
|
|
1233
|
+
success: false,
|
|
1234
|
+
error: 'contact_a, contact_b, and relationship are required',
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
logger.info('relationship_set invoked', {
|
|
1238
|
+
userId,
|
|
1239
|
+
contactALength: contact_a.length,
|
|
1240
|
+
contactBLength: contact_b.length,
|
|
1241
|
+
relationshipLength: relationship.length,
|
|
1242
|
+
hasNotes: !!notes,
|
|
1243
|
+
});
|
|
1244
|
+
try {
|
|
1245
|
+
const body = {
|
|
1246
|
+
contactA: contact_a,
|
|
1247
|
+
contactB: contact_b,
|
|
1248
|
+
relationshipType: relationship,
|
|
1249
|
+
};
|
|
1250
|
+
if (notes) {
|
|
1251
|
+
body.notes = notes;
|
|
1252
|
+
}
|
|
1253
|
+
const response = await apiClient.post('/api/relationships/set', body, { userId });
|
|
1254
|
+
if (!response.success) {
|
|
1255
|
+
return { success: false, error: response.error.message };
|
|
1256
|
+
}
|
|
1257
|
+
const { relationship: rel, contactA, contactB, relationshipType, created } = response.data;
|
|
1258
|
+
const content = created
|
|
1259
|
+
? `Recorded: ${contactA.displayName} [${relationshipType.label}] ${contactB.displayName}`
|
|
1260
|
+
: `Relationship already exists: ${contactA.displayName} [${relationshipType.label}] ${contactB.displayName}`;
|
|
1261
|
+
return {
|
|
1262
|
+
success: true,
|
|
1263
|
+
data: {
|
|
1264
|
+
content,
|
|
1265
|
+
details: {
|
|
1266
|
+
relationshipId: rel.id,
|
|
1267
|
+
created,
|
|
1268
|
+
contactA,
|
|
1269
|
+
contactB,
|
|
1270
|
+
relationshipType,
|
|
1271
|
+
userId,
|
|
1272
|
+
},
|
|
1273
|
+
},
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
catch (error) {
|
|
1277
|
+
logger.error('relationship_set failed', { error });
|
|
1278
|
+
return { success: false, error: 'Failed to set relationship' };
|
|
1279
|
+
}
|
|
1280
|
+
},
|
|
1281
|
+
async relationship_query(params) {
|
|
1282
|
+
const { contact, type_filter } = params;
|
|
1283
|
+
if (!contact) {
|
|
1284
|
+
return {
|
|
1285
|
+
success: false,
|
|
1286
|
+
error: 'contact is required',
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
logger.info('relationship_query invoked', {
|
|
1290
|
+
userId,
|
|
1291
|
+
contactLength: contact.length,
|
|
1292
|
+
hasTypeFilter: !!type_filter,
|
|
1293
|
+
});
|
|
1294
|
+
try {
|
|
1295
|
+
const queryParams = new URLSearchParams({ contact });
|
|
1296
|
+
if (type_filter) {
|
|
1297
|
+
queryParams.set('type_filter', type_filter);
|
|
1298
|
+
}
|
|
1299
|
+
const response = await apiClient.get(`/api/relationships/query?${queryParams}`, { userId });
|
|
1300
|
+
if (!response.success) {
|
|
1301
|
+
return { success: false, error: response.error.message };
|
|
1302
|
+
}
|
|
1303
|
+
const { contactId, contactName, relatedContacts } = response.data;
|
|
1304
|
+
if (relatedContacts.length === 0) {
|
|
1305
|
+
return {
|
|
1306
|
+
success: true,
|
|
1307
|
+
data: {
|
|
1308
|
+
content: `No relationships found for ${contactName}.`,
|
|
1309
|
+
details: { contactId, contactName, relatedContacts: [], userId },
|
|
1310
|
+
},
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
const lines = [`Relationships for ${contactName}:`];
|
|
1314
|
+
for (const rel of relatedContacts) {
|
|
1315
|
+
const kindTag = rel.contactKind !== 'person' ? ` [${rel.contactKind}]` : '';
|
|
1316
|
+
const notesTag = rel.notes ? ` -- ${rel.notes}` : '';
|
|
1317
|
+
lines.push(`- ${rel.relationshipTypeLabel}: ${rel.contactName}${kindTag}${notesTag}`);
|
|
1318
|
+
}
|
|
1319
|
+
return {
|
|
1320
|
+
success: true,
|
|
1321
|
+
data: {
|
|
1322
|
+
content: lines.join('\n'),
|
|
1323
|
+
details: { contactId, contactName, relatedContacts, userId },
|
|
1324
|
+
},
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
catch (error) {
|
|
1328
|
+
logger.error('relationship_query failed', { error });
|
|
1329
|
+
return { success: false, error: 'Failed to query relationships' };
|
|
1330
|
+
}
|
|
1331
|
+
},
|
|
1332
|
+
async file_share(params) {
|
|
1333
|
+
const { fileId, expiresIn = 3600, maxDownloads } = params;
|
|
1334
|
+
if (!fileId) {
|
|
1335
|
+
return {
|
|
1336
|
+
success: false,
|
|
1337
|
+
error: 'fileId is required',
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
// Validate expiresIn range
|
|
1341
|
+
if (expiresIn < 60 || expiresIn > 604800) {
|
|
1342
|
+
return {
|
|
1343
|
+
success: false,
|
|
1344
|
+
error: 'expiresIn must be between 60 and 604800 seconds (1 minute to 7 days)',
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
logger.info('file_share invoked', {
|
|
1348
|
+
userId,
|
|
1349
|
+
fileId,
|
|
1350
|
+
expiresIn,
|
|
1351
|
+
maxDownloads,
|
|
1352
|
+
});
|
|
1353
|
+
try {
|
|
1354
|
+
const body = { expiresIn };
|
|
1355
|
+
if (maxDownloads !== undefined) {
|
|
1356
|
+
body.maxDownloads = maxDownloads;
|
|
1357
|
+
}
|
|
1358
|
+
const response = await apiClient.post(`/api/files/${fileId}/share`, body, { userId });
|
|
1359
|
+
if (!response.success) {
|
|
1360
|
+
logger.error('file_share API error', {
|
|
1361
|
+
userId,
|
|
1362
|
+
fileId,
|
|
1363
|
+
status: response.error.status,
|
|
1364
|
+
code: response.error.code,
|
|
1365
|
+
});
|
|
1366
|
+
return {
|
|
1367
|
+
success: false,
|
|
1368
|
+
error: response.error.message || 'Failed to create share link',
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
const { url, shareToken, expiresAt, filename, contentType, sizeBytes } = response.data;
|
|
1372
|
+
logger.debug('file_share completed', {
|
|
1373
|
+
userId,
|
|
1374
|
+
fileId,
|
|
1375
|
+
shareToken,
|
|
1376
|
+
expiresAt,
|
|
1377
|
+
});
|
|
1378
|
+
// Format file size
|
|
1379
|
+
const formatSize = (bytes) => {
|
|
1380
|
+
if (bytes < 1024)
|
|
1381
|
+
return `${bytes} B`;
|
|
1382
|
+
if (bytes < 1024 * 1024)
|
|
1383
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1384
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
1385
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1386
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
1387
|
+
};
|
|
1388
|
+
// Format duration
|
|
1389
|
+
const formatDuration = (seconds) => {
|
|
1390
|
+
if (seconds < 60)
|
|
1391
|
+
return `${seconds} seconds`;
|
|
1392
|
+
if (seconds < 3600)
|
|
1393
|
+
return `${Math.floor(seconds / 60)} minutes`;
|
|
1394
|
+
if (seconds < 86400)
|
|
1395
|
+
return `${Math.floor(seconds / 3600)} hours`;
|
|
1396
|
+
return `${Math.floor(seconds / 86400)} days`;
|
|
1397
|
+
};
|
|
1398
|
+
const expiryText = formatDuration(expiresIn);
|
|
1399
|
+
const sizeText = formatSize(sizeBytes);
|
|
1400
|
+
const downloadLimit = maxDownloads ? ` (max ${maxDownloads} downloads)` : '';
|
|
1401
|
+
return {
|
|
1402
|
+
success: true,
|
|
1403
|
+
data: {
|
|
1404
|
+
content: `Share link created for "${filename}" (${sizeText}). ` +
|
|
1405
|
+
`Valid for ${expiryText}${downloadLimit}.\n\nURL: ${url}`,
|
|
1406
|
+
details: {
|
|
1407
|
+
url,
|
|
1408
|
+
shareToken,
|
|
1409
|
+
expiresAt,
|
|
1410
|
+
expiresIn,
|
|
1411
|
+
filename,
|
|
1412
|
+
contentType,
|
|
1413
|
+
sizeBytes,
|
|
1414
|
+
userId,
|
|
1415
|
+
},
|
|
1416
|
+
},
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
catch (error) {
|
|
1420
|
+
logger.error('file_share failed', {
|
|
1421
|
+
userId,
|
|
1422
|
+
fileId,
|
|
1423
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1424
|
+
});
|
|
1425
|
+
return {
|
|
1426
|
+
success: false,
|
|
1427
|
+
error: error instanceof Error ? error.message : 'An unexpected error occurred while creating share link.',
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
},
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* OpenClaw 2026 Plugin Registration Function
|
|
1435
|
+
*
|
|
1436
|
+
* This is the main entry point for the plugin using the OpenClaw API pattern.
|
|
1437
|
+
* Registers all tools, hooks, and CLI commands via the provided API object.
|
|
1438
|
+
*/
|
|
1439
|
+
export const registerOpenClaw = async (api) => {
|
|
1440
|
+
// Validate and resolve configuration
|
|
1441
|
+
const rawConfig = validateRawConfig(api.config);
|
|
1442
|
+
const config = await resolveConfigSecrets(rawConfig);
|
|
1443
|
+
// Create logger and API client
|
|
1444
|
+
const logger = api.logger ?? createLogger('openclaw-projects');
|
|
1445
|
+
const apiClient = createApiClient({ config, logger });
|
|
1446
|
+
// Extract context and user ID
|
|
1447
|
+
const context = extractContext(api.runtime);
|
|
1448
|
+
const userId = getUserScopeKey({
|
|
1449
|
+
agentId: context.agent.agentId,
|
|
1450
|
+
sessionKey: context.session.sessionId,
|
|
1451
|
+
}, config.userScoping);
|
|
1452
|
+
// Store plugin state
|
|
1453
|
+
const state = { config, logger, apiClient, userId };
|
|
1454
|
+
// Create tool handlers
|
|
1455
|
+
const handlers = createToolHandlers(state);
|
|
1456
|
+
// Register all 19 tools with correct OpenClaw Gateway execute signature
|
|
1457
|
+
// Signature: (toolCallId: string, params: T, signal?: AbortSignal, onUpdate?: (partial: any) => void) => AgentToolResult
|
|
1458
|
+
const tools = [
|
|
1459
|
+
{
|
|
1460
|
+
name: 'memory_recall',
|
|
1461
|
+
description: 'Search through long-term memories. Use when you need context about user preferences, past decisions, or previously discussed topics.',
|
|
1462
|
+
parameters: memoryRecallSchema,
|
|
1463
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
1464
|
+
const result = await handlers.memory_recall(params);
|
|
1465
|
+
return toAgentToolResult(result);
|
|
1466
|
+
},
|
|
1467
|
+
},
|
|
1468
|
+
{
|
|
1469
|
+
name: 'memory_store',
|
|
1470
|
+
description: 'Store a new memory for future reference. Use when the user shares important preferences, facts, or decisions.',
|
|
1471
|
+
parameters: memoryStoreSchema,
|
|
1472
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
1473
|
+
const result = await handlers.memory_store(params);
|
|
1474
|
+
return toAgentToolResult(result);
|
|
1475
|
+
},
|
|
1476
|
+
},
|
|
1477
|
+
{
|
|
1478
|
+
name: 'memory_forget',
|
|
1479
|
+
description: 'Remove a memory by ID or search query. Use when information is outdated or the user requests deletion.',
|
|
1480
|
+
parameters: memoryForgetSchema,
|
|
1481
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
1482
|
+
const result = await handlers.memory_forget(params);
|
|
1483
|
+
return toAgentToolResult(result);
|
|
1484
|
+
},
|
|
1485
|
+
},
|
|
1486
|
+
{
|
|
1487
|
+
name: 'project_list',
|
|
1488
|
+
description: 'List projects for the user. Use to see what projects exist or filter by status.',
|
|
1489
|
+
parameters: projectListSchema,
|
|
1490
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
1491
|
+
const result = await handlers.project_list(params);
|
|
1492
|
+
return toAgentToolResult(result);
|
|
1493
|
+
},
|
|
1494
|
+
},
|
|
1495
|
+
{
|
|
1496
|
+
name: 'project_get',
|
|
1497
|
+
description: 'Get details about a specific project. Use when you need full project information.',
|
|
1498
|
+
parameters: projectGetSchema,
|
|
1499
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
1500
|
+
const result = await handlers.project_get(params);
|
|
1501
|
+
return toAgentToolResult(result);
|
|
1502
|
+
},
|
|
1503
|
+
},
|
|
1504
|
+
{
|
|
1505
|
+
name: 'project_create',
|
|
1506
|
+
description: 'Create a new project. Use when the user wants to start tracking a new initiative.',
|
|
1507
|
+
parameters: projectCreateSchema,
|
|
1508
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
1509
|
+
const result = await handlers.project_create(params);
|
|
1510
|
+
return toAgentToolResult(result);
|
|
1511
|
+
},
|
|
1512
|
+
},
|
|
1513
|
+
{
|
|
1514
|
+
name: 'todo_list',
|
|
1515
|
+
description: 'List todos, optionally filtered by project or status. Use to see pending tasks.',
|
|
1516
|
+
parameters: todoListSchema,
|
|
1517
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
1518
|
+
const result = await handlers.todo_list(params);
|
|
1519
|
+
return toAgentToolResult(result);
|
|
1520
|
+
},
|
|
1521
|
+
},
|
|
1522
|
+
{
|
|
1523
|
+
name: 'todo_create',
|
|
1524
|
+
description: 'Create a new todo item. Use when the user wants to track a task.',
|
|
1525
|
+
parameters: todoCreateSchema,
|
|
1526
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
1527
|
+
const result = await handlers.todo_create(params);
|
|
1528
|
+
return toAgentToolResult(result);
|
|
1529
|
+
},
|
|
1530
|
+
},
|
|
1531
|
+
{
|
|
1532
|
+
name: 'todo_complete',
|
|
1533
|
+
description: 'Mark a todo as complete. Use when a task is done.',
|
|
1534
|
+
parameters: todoCompleteSchema,
|
|
1535
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
1536
|
+
const result = await handlers.todo_complete(params);
|
|
1537
|
+
return toAgentToolResult(result);
|
|
1538
|
+
},
|
|
1539
|
+
},
|
|
1540
|
+
{
|
|
1541
|
+
name: 'contact_search',
|
|
1542
|
+
description: 'Search contacts by name, email, or other fields. Use to find people.',
|
|
1543
|
+
parameters: contactSearchSchema,
|
|
1544
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
1545
|
+
const result = await handlers.contact_search(params);
|
|
1546
|
+
return toAgentToolResult(result);
|
|
1547
|
+
},
|
|
1548
|
+
},
|
|
1549
|
+
{
|
|
1550
|
+
name: 'contact_get',
|
|
1551
|
+
description: 'Get details about a specific contact. Use when you need full contact information.',
|
|
1552
|
+
parameters: contactGetSchema,
|
|
1553
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
1554
|
+
const result = await handlers.contact_get(params);
|
|
1555
|
+
return toAgentToolResult(result);
|
|
1556
|
+
},
|
|
1557
|
+
},
|
|
1558
|
+
{
|
|
1559
|
+
name: 'contact_create',
|
|
1560
|
+
description: 'Create a new contact. Use when the user mentions someone new to track.',
|
|
1561
|
+
parameters: contactCreateSchema,
|
|
1562
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
1563
|
+
const result = await handlers.contact_create(params);
|
|
1564
|
+
return toAgentToolResult(result);
|
|
1565
|
+
},
|
|
1566
|
+
},
|
|
1567
|
+
{
|
|
1568
|
+
name: 'sms_send',
|
|
1569
|
+
description: 'Send an SMS message to a phone number. Use when you need to notify someone via text message. Requires the recipient phone number in E.164 format (e.g., +15551234567).',
|
|
1570
|
+
parameters: smsSendSchema,
|
|
1571
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
1572
|
+
const result = await handlers.sms_send(params);
|
|
1573
|
+
return toAgentToolResult(result);
|
|
1574
|
+
},
|
|
1575
|
+
},
|
|
1576
|
+
{
|
|
1577
|
+
name: 'email_send',
|
|
1578
|
+
description: 'Send an email message. Use when you need to communicate via email. Requires the recipient email address, subject, and body.',
|
|
1579
|
+
parameters: emailSendSchema,
|
|
1580
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
1581
|
+
const result = await handlers.email_send(params);
|
|
1582
|
+
return toAgentToolResult(result);
|
|
1583
|
+
},
|
|
1584
|
+
},
|
|
1585
|
+
{
|
|
1586
|
+
name: 'message_search',
|
|
1587
|
+
description: 'Search message history semantically. Use when you need to find past conversations, messages about specific topics, or communications with contacts. Supports filtering by channel (SMS/email) and contact.',
|
|
1588
|
+
parameters: messageSearchSchema,
|
|
1589
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
1590
|
+
const result = await handlers.message_search(params);
|
|
1591
|
+
return toAgentToolResult(result);
|
|
1592
|
+
},
|
|
1593
|
+
},
|
|
1594
|
+
{
|
|
1595
|
+
name: 'thread_list',
|
|
1596
|
+
description: 'List message threads (conversations). Use to see recent conversations with contacts. Can filter by channel (SMS/email) or contact.',
|
|
1597
|
+
parameters: threadListSchema,
|
|
1598
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
1599
|
+
const result = await handlers.thread_list(params);
|
|
1600
|
+
return toAgentToolResult(result);
|
|
1601
|
+
},
|
|
1602
|
+
},
|
|
1603
|
+
{
|
|
1604
|
+
name: 'thread_get',
|
|
1605
|
+
description: 'Get a thread with its message history. Use to view the full conversation in a thread.',
|
|
1606
|
+
parameters: threadGetSchema,
|
|
1607
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
1608
|
+
const result = await handlers.thread_get(params);
|
|
1609
|
+
return toAgentToolResult(result);
|
|
1610
|
+
},
|
|
1611
|
+
},
|
|
1612
|
+
{
|
|
1613
|
+
name: 'relationship_set',
|
|
1614
|
+
description: "Record a relationship between two people, groups, or organisations. Examples: 'Troy is Alex\\'s partner', 'Sam is a member of The Kelly Household', 'Troy works for Acme Corp'. The system handles directionality and type matching automatically.",
|
|
1615
|
+
parameters: relationshipSetSchema,
|
|
1616
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
1617
|
+
const result = await handlers.relationship_set(params);
|
|
1618
|
+
return toAgentToolResult(result);
|
|
1619
|
+
},
|
|
1620
|
+
},
|
|
1621
|
+
{
|
|
1622
|
+
name: 'relationship_query',
|
|
1623
|
+
description: "Query a contact's relationships. Returns all relationships including family, partners, group memberships, professional connections, etc. Handles directional relationships automatically.",
|
|
1624
|
+
parameters: relationshipQuerySchema,
|
|
1625
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
1626
|
+
const result = await handlers.relationship_query(params);
|
|
1627
|
+
return toAgentToolResult(result);
|
|
1628
|
+
},
|
|
1629
|
+
},
|
|
1630
|
+
{
|
|
1631
|
+
name: 'file_share',
|
|
1632
|
+
description: 'Generate a shareable download link for a file. Use when you need to share a file with someone outside the system. The link is time-limited and can be configured with an expiry time and optional download limit.',
|
|
1633
|
+
parameters: fileShareSchema,
|
|
1634
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
1635
|
+
const result = await handlers.file_share(params);
|
|
1636
|
+
return toAgentToolResult(result);
|
|
1637
|
+
},
|
|
1638
|
+
},
|
|
1639
|
+
];
|
|
1640
|
+
for (const tool of tools) {
|
|
1641
|
+
api.registerTool(tool);
|
|
1642
|
+
}
|
|
1643
|
+
// Register hooks using api.on() (modern) with fallback to registerHook (legacy)
|
|
1644
|
+
// The auto-recall and auto-capture hooks are consolidated from hooks.ts
|
|
1645
|
+
// into this registration path using the correct OpenClaw hook contract.
|
|
1646
|
+
/** Default timeout for hook execution (5 seconds) */
|
|
1647
|
+
const HOOK_TIMEOUT_MS = 5000;
|
|
1648
|
+
if (config.autoRecall) {
|
|
1649
|
+
// Create the graph-aware auto-recall hook which traverses the user's
|
|
1650
|
+
// relationship graph for multi-scope context retrieval.
|
|
1651
|
+
// Falls back to basic memory search if the graph-aware endpoint is unavailable.
|
|
1652
|
+
const autoRecallHook = createGraphAwareRecallHook({
|
|
1653
|
+
client: apiClient,
|
|
1654
|
+
logger,
|
|
1655
|
+
config,
|
|
1656
|
+
userId,
|
|
1657
|
+
timeoutMs: HOOK_TIMEOUT_MS,
|
|
1658
|
+
});
|
|
1659
|
+
/**
|
|
1660
|
+
* before_agent_start handler: Extracts the user's prompt from the event,
|
|
1661
|
+
* performs semantic memory search, and returns { prependContext } to inject
|
|
1662
|
+
* relevant memories into the conversation.
|
|
1663
|
+
*/
|
|
1664
|
+
const beforeAgentStartHandler = async (event, _ctx) => {
|
|
1665
|
+
logger.debug('Auto-recall hook triggered', {
|
|
1666
|
+
promptLength: event.prompt?.length ?? 0,
|
|
1667
|
+
});
|
|
1668
|
+
try {
|
|
1669
|
+
// Use the consolidated hook which has timeout protection and
|
|
1670
|
+
// uses the user's actual prompt for semantic search
|
|
1671
|
+
const result = await autoRecallHook({ prompt: event.prompt });
|
|
1672
|
+
if (result && result.prependContext) {
|
|
1673
|
+
return { prependContext: result.prependContext };
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
catch (error) {
|
|
1677
|
+
// Hook errors should never crash the agent
|
|
1678
|
+
logger.error('Auto-recall hook failed', {
|
|
1679
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
// Return undefined (void) when no context is available
|
|
1683
|
+
};
|
|
1684
|
+
if (typeof api.on === 'function') {
|
|
1685
|
+
// Modern registration: api.on('before_agent_start', handler)
|
|
1686
|
+
// Cast needed: our typed handler satisfies the runtime contract but
|
|
1687
|
+
// the generic api.on() signature uses (...args: unknown[]) => unknown
|
|
1688
|
+
api.on('before_agent_start', beforeAgentStartHandler);
|
|
1689
|
+
}
|
|
1690
|
+
else {
|
|
1691
|
+
// Legacy fallback: api.registerHook('beforeAgentStart', handler)
|
|
1692
|
+
api.registerHook('beforeAgentStart', beforeAgentStartHandler);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
if (config.autoCapture) {
|
|
1696
|
+
// Create the auto-capture hook using the consolidated hooks.ts implementation
|
|
1697
|
+
const autoCaptureHook = createAutoCaptureHook({
|
|
1698
|
+
client: apiClient,
|
|
1699
|
+
logger,
|
|
1700
|
+
config,
|
|
1701
|
+
userId,
|
|
1702
|
+
timeoutMs: HOOK_TIMEOUT_MS * 2, // Allow more time for capture (10s)
|
|
1703
|
+
});
|
|
1704
|
+
/**
|
|
1705
|
+
* agent_end handler: Extracts messages from the completed conversation,
|
|
1706
|
+
* filters sensitive content, and posts to the capture API for memory storage.
|
|
1707
|
+
*/
|
|
1708
|
+
const agentEndHandler = async (event, _ctx) => {
|
|
1709
|
+
logger.debug('Auto-capture hook triggered', {
|
|
1710
|
+
messageCount: event.messages?.length ?? 0,
|
|
1711
|
+
success: event.success,
|
|
1712
|
+
});
|
|
1713
|
+
try {
|
|
1714
|
+
// Convert the event messages to the format expected by the capture hook
|
|
1715
|
+
const messages = (event.messages ?? []).map((msg) => {
|
|
1716
|
+
if (typeof msg === 'object' && msg !== null) {
|
|
1717
|
+
const msgObj = msg;
|
|
1718
|
+
return {
|
|
1719
|
+
role: String(msgObj.role ?? 'unknown'),
|
|
1720
|
+
content: String(msgObj.content ?? ''),
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
return { role: 'unknown', content: String(msg) };
|
|
1724
|
+
});
|
|
1725
|
+
await autoCaptureHook({ messages });
|
|
1726
|
+
}
|
|
1727
|
+
catch (error) {
|
|
1728
|
+
// Hook errors should never crash the agent
|
|
1729
|
+
logger.error('Auto-capture hook failed', {
|
|
1730
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
};
|
|
1734
|
+
if (typeof api.on === 'function') {
|
|
1735
|
+
// Modern registration: api.on('agent_end', handler)
|
|
1736
|
+
// Cast needed: our typed handler satisfies the runtime contract but
|
|
1737
|
+
// the generic api.on() signature uses (...args: unknown[]) => unknown
|
|
1738
|
+
api.on('agent_end', agentEndHandler);
|
|
1739
|
+
}
|
|
1740
|
+
else {
|
|
1741
|
+
// Legacy fallback: api.registerHook('agentEnd', handler)
|
|
1742
|
+
api.registerHook('agentEnd', agentEndHandler);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
// Register Gateway RPC methods (Issue #324)
|
|
1746
|
+
const gatewayMethods = createGatewayMethods({
|
|
1747
|
+
logger,
|
|
1748
|
+
apiClient,
|
|
1749
|
+
userId,
|
|
1750
|
+
});
|
|
1751
|
+
registerGatewayRpcMethods(api, gatewayMethods);
|
|
1752
|
+
// Register background notification service (Issue #325)
|
|
1753
|
+
// Create a simple event emitter for notifications
|
|
1754
|
+
// In production, this would be provided by the OpenClaw runtime
|
|
1755
|
+
const eventEmitter = {
|
|
1756
|
+
emit: (event, payload) => {
|
|
1757
|
+
logger.debug('Notification event emitted', { event, payload });
|
|
1758
|
+
},
|
|
1759
|
+
on: (_event, _handler) => { },
|
|
1760
|
+
off: (_event, _handler) => { },
|
|
1761
|
+
};
|
|
1762
|
+
const notificationService = createNotificationService({
|
|
1763
|
+
logger,
|
|
1764
|
+
apiClient,
|
|
1765
|
+
userId,
|
|
1766
|
+
events: eventEmitter,
|
|
1767
|
+
config: {
|
|
1768
|
+
enabled: config.autoRecall, // Only enable if auto-recall is enabled
|
|
1769
|
+
pollIntervalMs: 30000,
|
|
1770
|
+
},
|
|
1771
|
+
});
|
|
1772
|
+
api.registerService(notificationService);
|
|
1773
|
+
// Register CLI commands
|
|
1774
|
+
api.registerCli(({ program }) => {
|
|
1775
|
+
program
|
|
1776
|
+
.command('status')
|
|
1777
|
+
.description('Show plugin status and statistics')
|
|
1778
|
+
.action(async () => {
|
|
1779
|
+
try {
|
|
1780
|
+
const response = await apiClient.get('/api/health', { userId });
|
|
1781
|
+
console.log('Plugin Status:', response.success ? 'Connected' : 'Error');
|
|
1782
|
+
}
|
|
1783
|
+
catch {
|
|
1784
|
+
console.log('Plugin Status: Error - Unable to connect');
|
|
1785
|
+
}
|
|
1786
|
+
});
|
|
1787
|
+
program
|
|
1788
|
+
.command('recall')
|
|
1789
|
+
.description('Recall memories matching a query')
|
|
1790
|
+
.action(async (...args) => {
|
|
1791
|
+
const query = typeof args[0] === 'string' ? args[0] : '';
|
|
1792
|
+
const options = (args[1] ?? {});
|
|
1793
|
+
const result = await handlers.memory_recall({
|
|
1794
|
+
query,
|
|
1795
|
+
limit: options.limit ? parseInt(options.limit, 10) : 5,
|
|
1796
|
+
});
|
|
1797
|
+
if (result.success && result.data) {
|
|
1798
|
+
console.log(result.data.content);
|
|
1799
|
+
}
|
|
1800
|
+
else {
|
|
1801
|
+
console.error('Error:', result.error);
|
|
1802
|
+
}
|
|
1803
|
+
});
|
|
1804
|
+
});
|
|
1805
|
+
logger.info('OpenClaw Projects plugin registered', {
|
|
1806
|
+
agentId: context.agent.agentId,
|
|
1807
|
+
sessionId: context.session.sessionId,
|
|
1808
|
+
userId,
|
|
1809
|
+
toolCount: tools.length,
|
|
1810
|
+
config: redactConfig(config),
|
|
1811
|
+
});
|
|
1812
|
+
};
|
|
1813
|
+
/** Default export for OpenClaw 2026 API compatibility */
|
|
1814
|
+
export default registerOpenClaw;
|
|
1815
|
+
/** Export JSON Schemas for external use */
|
|
1816
|
+
export const schemas = {
|
|
1817
|
+
memoryRecall: memoryRecallSchema,
|
|
1818
|
+
memoryStore: memoryStoreSchema,
|
|
1819
|
+
memoryForget: memoryForgetSchema,
|
|
1820
|
+
projectList: projectListSchema,
|
|
1821
|
+
projectGet: projectGetSchema,
|
|
1822
|
+
projectCreate: projectCreateSchema,
|
|
1823
|
+
todoList: todoListSchema,
|
|
1824
|
+
todoCreate: todoCreateSchema,
|
|
1825
|
+
todoComplete: todoCompleteSchema,
|
|
1826
|
+
contactSearch: contactSearchSchema,
|
|
1827
|
+
contactGet: contactGetSchema,
|
|
1828
|
+
contactCreate: contactCreateSchema,
|
|
1829
|
+
smsSend: smsSendSchema,
|
|
1830
|
+
emailSend: emailSendSchema,
|
|
1831
|
+
messageSearch: messageSearchSchema,
|
|
1832
|
+
threadList: threadListSchema,
|
|
1833
|
+
threadGet: threadGetSchema,
|
|
1834
|
+
relationshipSet: relationshipSetSchema,
|
|
1835
|
+
relationshipQuery: relationshipQuerySchema,
|
|
1836
|
+
fileShare: fileShareSchema,
|
|
1837
|
+
};
|
|
1838
|
+
//# sourceMappingURL=register-openclaw.js.map
|