@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.
Files changed (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +389 -0
  3. package/dist/api-client.d.ts +81 -0
  4. package/dist/api-client.d.ts.map +1 -0
  5. package/dist/api-client.js +216 -0
  6. package/dist/api-client.js.map +1 -0
  7. package/dist/cli.d.ts +112 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +233 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/config.d.ts +324 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +287 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/context.d.ts +87 -0
  16. package/dist/context.d.ts.map +1 -0
  17. package/dist/context.js +144 -0
  18. package/dist/context.js.map +1 -0
  19. package/dist/gateway/rpc-methods.d.ts +93 -0
  20. package/dist/gateway/rpc-methods.d.ts.map +1 -0
  21. package/dist/gateway/rpc-methods.js +145 -0
  22. package/dist/gateway/rpc-methods.js.map +1 -0
  23. package/dist/hooks.d.ts +86 -0
  24. package/dist/hooks.d.ts.map +1 -0
  25. package/dist/hooks.js +314 -0
  26. package/dist/hooks.js.map +1 -0
  27. package/dist/index.d.ts +106 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +221 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/logger.d.ts +22 -0
  32. package/dist/logger.d.ts.map +1 -0
  33. package/dist/logger.js +78 -0
  34. package/dist/logger.js.map +1 -0
  35. package/dist/register-openclaw.d.ts +43 -0
  36. package/dist/register-openclaw.d.ts.map +1 -0
  37. package/dist/register-openclaw.js +1838 -0
  38. package/dist/register-openclaw.js.map +1 -0
  39. package/dist/secrets.d.ts +56 -0
  40. package/dist/secrets.d.ts.map +1 -0
  41. package/dist/secrets.js +161 -0
  42. package/dist/secrets.js.map +1 -0
  43. package/dist/services/notification-service.d.ts +60 -0
  44. package/dist/services/notification-service.d.ts.map +1 -0
  45. package/dist/services/notification-service.js +145 -0
  46. package/dist/services/notification-service.js.map +1 -0
  47. package/dist/tools/contacts.d.ts +139 -0
  48. package/dist/tools/contacts.d.ts.map +1 -0
  49. package/dist/tools/contacts.js +333 -0
  50. package/dist/tools/contacts.js.map +1 -0
  51. package/dist/tools/email-send.d.ts +71 -0
  52. package/dist/tools/email-send.d.ts.map +1 -0
  53. package/dist/tools/email-send.js +132 -0
  54. package/dist/tools/email-send.js.map +1 -0
  55. package/dist/tools/file-share.d.ts +64 -0
  56. package/dist/tools/file-share.d.ts.map +1 -0
  57. package/dist/tools/file-share.js +133 -0
  58. package/dist/tools/file-share.js.map +1 -0
  59. package/dist/tools/index.d.ts +22 -0
  60. package/dist/tools/index.d.ts.map +1 -0
  61. package/dist/tools/index.js +33 -0
  62. package/dist/tools/index.js.map +1 -0
  63. package/dist/tools/memory-forget.d.ts +69 -0
  64. package/dist/tools/memory-forget.d.ts.map +1 -0
  65. package/dist/tools/memory-forget.js +224 -0
  66. package/dist/tools/memory-forget.js.map +1 -0
  67. package/dist/tools/memory-recall.d.ts +82 -0
  68. package/dist/tools/memory-recall.d.ts.map +1 -0
  69. package/dist/tools/memory-recall.js +161 -0
  70. package/dist/tools/memory-recall.js.map +1 -0
  71. package/dist/tools/memory-store.d.ts +80 -0
  72. package/dist/tools/memory-store.d.ts.map +1 -0
  73. package/dist/tools/memory-store.js +172 -0
  74. package/dist/tools/memory-store.js.map +1 -0
  75. package/dist/tools/message-search.d.ts +85 -0
  76. package/dist/tools/message-search.d.ts.map +1 -0
  77. package/dist/tools/message-search.js +137 -0
  78. package/dist/tools/message-search.js.map +1 -0
  79. package/dist/tools/notebooks.d.ts +155 -0
  80. package/dist/tools/notebooks.d.ts.map +1 -0
  81. package/dist/tools/notebooks.js +287 -0
  82. package/dist/tools/notebooks.js.map +1 -0
  83. package/dist/tools/notes.d.ts +272 -0
  84. package/dist/tools/notes.d.ts.map +1 -0
  85. package/dist/tools/notes.js +530 -0
  86. package/dist/tools/notes.js.map +1 -0
  87. package/dist/tools/projects.d.ts +139 -0
  88. package/dist/tools/projects.d.ts.map +1 -0
  89. package/dist/tools/projects.js +280 -0
  90. package/dist/tools/projects.js.map +1 -0
  91. package/dist/tools/relationships.d.ts +133 -0
  92. package/dist/tools/relationships.d.ts.map +1 -0
  93. package/dist/tools/relationships.js +281 -0
  94. package/dist/tools/relationships.js.map +1 -0
  95. package/dist/tools/sms-send.d.ts +62 -0
  96. package/dist/tools/sms-send.d.ts.map +1 -0
  97. package/dist/tools/sms-send.js +121 -0
  98. package/dist/tools/sms-send.js.map +1 -0
  99. package/dist/tools/threads.d.ts +127 -0
  100. package/dist/tools/threads.d.ts.map +1 -0
  101. package/dist/tools/threads.js +202 -0
  102. package/dist/tools/threads.js.map +1 -0
  103. package/dist/tools/todos.d.ts +142 -0
  104. package/dist/tools/todos.d.ts.map +1 -0
  105. package/dist/tools/todos.js +308 -0
  106. package/dist/tools/todos.js.map +1 -0
  107. package/dist/types/openclaw-api.d.ts +215 -0
  108. package/dist/types/openclaw-api.d.ts.map +1 -0
  109. package/dist/types/openclaw-api.js +10 -0
  110. package/dist/types/openclaw-api.js.map +1 -0
  111. package/dist/utils/zod-to-json-schema.d.ts +19 -0
  112. package/dist/utils/zod-to-json-schema.d.ts.map +1 -0
  113. package/dist/utils/zod-to-json-schema.js +132 -0
  114. package/dist/utils/zod-to-json-schema.js.map +1 -0
  115. package/openclaw.plugin.json +229 -0
  116. package/package.json +69 -0
  117. package/skills/contact-lookup/SKILL.md +30 -0
  118. package/skills/daily-summary/SKILL.md +23 -0
  119. package/skills/project-status/SKILL.md +33 -0
  120. 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