@troykelly/openclaw-projects 0.0.10 → 0.0.12
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/dist/config.d.ts +30 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +28 -1
- package/dist/config.js.map +1 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +17 -3
- package/dist/hooks.js.map +1 -1
- package/dist/register-openclaw.d.ts +3 -1
- package/dist/register-openclaw.d.ts.map +1 -1
- package/dist/register-openclaw.js +611 -55
- package/dist/register-openclaw.js.map +1 -1
- package/dist/tools/contacts.d.ts +6 -6
- package/dist/tools/contacts.d.ts.map +1 -1
- package/dist/tools/contacts.js +7 -7
- package/dist/tools/contacts.js.map +1 -1
- package/dist/tools/context-search.d.ts +79 -0
- package/dist/tools/context-search.d.ts.map +1 -0
- package/dist/tools/context-search.js +265 -0
- package/dist/tools/context-search.js.map +1 -0
- package/dist/tools/email-send.d.ts.map +1 -1
- package/dist/tools/email-send.js +1 -14
- package/dist/tools/email-send.js.map +1 -1
- package/dist/tools/entity-links.d.ts +117 -0
- package/dist/tools/entity-links.d.ts.map +1 -0
- package/dist/tools/entity-links.js +446 -0
- package/dist/tools/entity-links.js.map +1 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +8 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/memory-forget.js +5 -5
- package/dist/tools/memory-forget.js.map +1 -1
- package/dist/tools/memory-recall.d.ts +28 -0
- package/dist/tools/memory-recall.d.ts.map +1 -1
- package/dist/tools/memory-recall.js +44 -4
- package/dist/tools/memory-recall.js.map +1 -1
- package/dist/tools/memory-store.d.ts +57 -0
- package/dist/tools/memory-store.d.ts.map +1 -1
- package/dist/tools/memory-store.js +29 -2
- package/dist/tools/memory-store.js.map +1 -1
- package/dist/tools/message-search.d.ts +1 -1
- package/dist/tools/message-search.d.ts.map +1 -1
- package/dist/tools/message-search.js +20 -2
- package/dist/tools/message-search.js.map +1 -1
- package/dist/tools/notes.d.ts +2 -2
- package/dist/tools/project-search.d.ts +92 -0
- package/dist/tools/project-search.d.ts.map +1 -0
- package/dist/tools/project-search.js +160 -0
- package/dist/tools/project-search.js.map +1 -0
- package/dist/tools/relationships.js +1 -1
- package/dist/tools/relationships.js.map +1 -1
- package/dist/tools/skill-store.d.ts +12 -12
- package/dist/tools/threads.d.ts +2 -2
- package/dist/tools/threads.d.ts.map +1 -1
- package/dist/tools/threads.js +30 -6
- package/dist/tools/threads.js.map +1 -1
- package/dist/tools/todo-search.d.ts +95 -0
- package/dist/tools/todo-search.d.ts.map +1 -0
- package/dist/tools/todo-search.js +164 -0
- package/dist/tools/todo-search.js.map +1 -0
- package/dist/types/openclaw-api.d.ts +15 -0
- package/dist/types/openclaw-api.d.ts.map +1 -1
- package/dist/utils/auto-linker.d.ts +66 -0
- package/dist/utils/auto-linker.d.ts.map +1 -0
- package/dist/utils/auto-linker.js +354 -0
- package/dist/utils/auto-linker.js.map +1 -0
- package/dist/utils/geo.d.ts +24 -0
- package/dist/utils/geo.d.ts.map +1 -0
- package/dist/utils/geo.js +38 -0
- package/dist/utils/geo.js.map +1 -0
- package/dist/utils/inbound-gate.d.ts +85 -0
- package/dist/utils/inbound-gate.d.ts.map +1 -0
- package/dist/utils/inbound-gate.js +133 -0
- package/dist/utils/inbound-gate.js.map +1 -0
- package/dist/utils/injection-protection.d.ts +81 -0
- package/dist/utils/injection-protection.d.ts.map +1 -0
- package/dist/utils/injection-protection.js +179 -0
- package/dist/utils/injection-protection.js.map +1 -0
- package/dist/utils/nominatim.d.ts +18 -0
- package/dist/utils/nominatim.d.ts.map +1 -0
- package/dist/utils/nominatim.js +56 -0
- package/dist/utils/nominatim.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +81 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +188 -0
- package/dist/utils/rate-limiter.js.map +1 -0
- package/dist/utils/spam-filter.d.ts +79 -0
- package/dist/utils/spam-filter.d.ts.map +1 -0
- package/dist/utils/spam-filter.js +237 -0
- package/dist/utils/spam-filter.js.map +1 -0
- package/dist/utils/token-budget.d.ts +68 -0
- package/dist/utils/token-budget.d.ts.map +1 -0
- package/dist/utils/token-budget.js +142 -0
- package/dist/utils/token-budget.js.map +1 -0
- package/openclaw.plugin.json +3 -3
- package/package.json +1 -1
|
@@ -8,15 +8,19 @@
|
|
|
8
8
|
* - CLI registered via `api.registerCli()`
|
|
9
9
|
*/
|
|
10
10
|
import { ZodError } from 'zod';
|
|
11
|
-
import { validateRawConfig, resolveConfigSecretsSync, redactConfig } from './config.js';
|
|
12
|
-
import { createLogger } from './logger.js';
|
|
13
11
|
import { createApiClient } from './api-client.js';
|
|
12
|
+
import { redactConfig, resolveConfigSecretsSync, validateRawConfig } from './config.js';
|
|
14
13
|
import { extractContext, getUserScopeKey } from './context.js';
|
|
15
|
-
import { createSkillStorePutTool, createSkillStoreGetTool, createSkillStoreListTool, createSkillStoreDeleteTool, createSkillStoreSearchTool, createSkillStoreCollectionsTool, createSkillStoreAggregateTool, } from './tools/index.js';
|
|
16
|
-
import { createGatewayMethods, registerGatewayRpcMethods } from './gateway/rpc-methods.js';
|
|
17
14
|
import { createOAuthGatewayMethods, registerOAuthGatewayRpcMethods } from './gateway/oauth-rpc-methods.js';
|
|
18
|
-
import {
|
|
15
|
+
import { createGatewayMethods, registerGatewayRpcMethods } from './gateway/rpc-methods.js';
|
|
19
16
|
import { createAutoCaptureHook, createGraphAwareRecallHook } from './hooks.js';
|
|
17
|
+
import { createLogger } from './logger.js';
|
|
18
|
+
import { detectInjectionPatterns, sanitizeMetadataField, sanitizeMessageForContext, wrapExternalMessage } from './utils/injection-protection.js';
|
|
19
|
+
import { haversineDistanceKm, computeGeoScore, blendScores } from './utils/geo.js';
|
|
20
|
+
import { reverseGeocode } from './utils/nominatim.js';
|
|
21
|
+
import { createNotificationService } from './services/notification-service.js';
|
|
22
|
+
import { autoLinkInboundMessage } from './utils/auto-linker.js';
|
|
23
|
+
import { createProjectSearchTool, createContextSearchTool, createSkillStoreAggregateTool, createSkillStoreCollectionsTool, createSkillStoreDeleteTool, createSkillStoreGetTool, createSkillStoreListTool, createSkillStorePutTool, createSkillStoreSearchTool, createLinksSetTool, createLinksQueryTool, createLinksRemoveTool, } from './tools/index.js';
|
|
20
24
|
/**
|
|
21
25
|
* Convert internal ToolResult format to AgentToolResult format expected by OpenClaw Gateway.
|
|
22
26
|
*
|
|
@@ -73,6 +77,28 @@ const memoryRecallSchema = {
|
|
|
73
77
|
description: 'Scope search to a specific relationship between contacts',
|
|
74
78
|
format: 'uuid',
|
|
75
79
|
},
|
|
80
|
+
location: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
description: 'Current location for geo-aware recall ranking',
|
|
83
|
+
properties: {
|
|
84
|
+
lat: { type: 'number', minimum: -90, maximum: 90 },
|
|
85
|
+
lng: { type: 'number', minimum: -180, maximum: 180 },
|
|
86
|
+
},
|
|
87
|
+
required: ['lat', 'lng'],
|
|
88
|
+
},
|
|
89
|
+
location_radius_km: {
|
|
90
|
+
type: 'number',
|
|
91
|
+
description: 'Filter memories within this radius (km) of the given location',
|
|
92
|
+
minimum: 0.1,
|
|
93
|
+
maximum: 100,
|
|
94
|
+
},
|
|
95
|
+
location_weight: {
|
|
96
|
+
type: 'number',
|
|
97
|
+
description: 'Weight for geo scoring (0 = content only, 1 = geo only)',
|
|
98
|
+
minimum: 0,
|
|
99
|
+
maximum: 1,
|
|
100
|
+
default: 0.3,
|
|
101
|
+
},
|
|
76
102
|
},
|
|
77
103
|
required: ['query'],
|
|
78
104
|
};
|
|
@@ -121,6 +147,35 @@ const memoryStoreSchema = {
|
|
|
121
147
|
description: 'Scope memory to a specific relationship between contacts',
|
|
122
148
|
format: 'uuid',
|
|
123
149
|
},
|
|
150
|
+
location: {
|
|
151
|
+
type: 'object',
|
|
152
|
+
description: 'Geographic location to associate with this memory',
|
|
153
|
+
properties: {
|
|
154
|
+
lat: {
|
|
155
|
+
type: 'number',
|
|
156
|
+
description: 'Latitude (-90 to 90)',
|
|
157
|
+
minimum: -90,
|
|
158
|
+
maximum: 90,
|
|
159
|
+
},
|
|
160
|
+
lng: {
|
|
161
|
+
type: 'number',
|
|
162
|
+
description: 'Longitude (-180 to 180)',
|
|
163
|
+
minimum: -180,
|
|
164
|
+
maximum: 180,
|
|
165
|
+
},
|
|
166
|
+
address: {
|
|
167
|
+
type: 'string',
|
|
168
|
+
description: 'Street address (max 500 chars)',
|
|
169
|
+
maxLength: 500,
|
|
170
|
+
},
|
|
171
|
+
place_label: {
|
|
172
|
+
type: 'string',
|
|
173
|
+
description: 'Short place name (max 200 chars)',
|
|
174
|
+
maxLength: 200,
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
required: ['lat', 'lng'],
|
|
178
|
+
},
|
|
124
179
|
},
|
|
125
180
|
required: ['text'],
|
|
126
181
|
};
|
|
@@ -213,18 +268,22 @@ const todoListSchema = {
|
|
|
213
268
|
description: 'Filter by project ID',
|
|
214
269
|
format: 'uuid',
|
|
215
270
|
},
|
|
216
|
-
|
|
217
|
-
type: '
|
|
218
|
-
description: 'Filter by
|
|
219
|
-
enum: ['pending', 'in_progress', 'completed', 'all'],
|
|
220
|
-
default: 'pending',
|
|
271
|
+
completed: {
|
|
272
|
+
type: 'boolean',
|
|
273
|
+
description: 'Filter by completion status. true = completed only, false = active only, omit = all.',
|
|
221
274
|
},
|
|
222
275
|
limit: {
|
|
223
276
|
type: 'integer',
|
|
224
277
|
description: 'Maximum number of todos to return',
|
|
225
278
|
minimum: 1,
|
|
226
|
-
maximum:
|
|
227
|
-
default:
|
|
279
|
+
maximum: 200,
|
|
280
|
+
default: 50,
|
|
281
|
+
},
|
|
282
|
+
offset: {
|
|
283
|
+
type: 'integer',
|
|
284
|
+
description: 'Offset for pagination',
|
|
285
|
+
minimum: 0,
|
|
286
|
+
default: 0,
|
|
228
287
|
},
|
|
229
288
|
},
|
|
230
289
|
};
|
|
@@ -278,6 +337,95 @@ const todoCompleteSchema = {
|
|
|
278
337
|
},
|
|
279
338
|
required: ['todoId'],
|
|
280
339
|
};
|
|
340
|
+
/**
|
|
341
|
+
* Todo search tool JSON Schema (Issue #1216)
|
|
342
|
+
*/
|
|
343
|
+
const todoSearchSchema = {
|
|
344
|
+
type: 'object',
|
|
345
|
+
properties: {
|
|
346
|
+
query: {
|
|
347
|
+
type: 'string',
|
|
348
|
+
description: 'Natural language search query for finding work items',
|
|
349
|
+
minLength: 1,
|
|
350
|
+
maxLength: 1000,
|
|
351
|
+
},
|
|
352
|
+
limit: {
|
|
353
|
+
type: 'integer',
|
|
354
|
+
description: 'Maximum number of results to return',
|
|
355
|
+
minimum: 1,
|
|
356
|
+
maximum: 50,
|
|
357
|
+
default: 10,
|
|
358
|
+
},
|
|
359
|
+
kind: {
|
|
360
|
+
type: 'string',
|
|
361
|
+
description: 'Filter by work item kind',
|
|
362
|
+
enum: ['task', 'project', 'initiative', 'epic', 'issue'],
|
|
363
|
+
},
|
|
364
|
+
status: {
|
|
365
|
+
type: 'string',
|
|
366
|
+
description: 'Filter by status (e.g., open, completed, in_progress)',
|
|
367
|
+
maxLength: 50,
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
required: ['query'],
|
|
371
|
+
};
|
|
372
|
+
/**
|
|
373
|
+
* Project search tool JSON Schema (Issue #1217)
|
|
374
|
+
*/
|
|
375
|
+
const projectSearchSchema = {
|
|
376
|
+
type: 'object',
|
|
377
|
+
properties: {
|
|
378
|
+
query: {
|
|
379
|
+
type: 'string',
|
|
380
|
+
description: 'Natural language search query for finding projects',
|
|
381
|
+
minLength: 1,
|
|
382
|
+
maxLength: 1000,
|
|
383
|
+
},
|
|
384
|
+
limit: {
|
|
385
|
+
type: 'integer',
|
|
386
|
+
description: 'Maximum number of results to return',
|
|
387
|
+
minimum: 1,
|
|
388
|
+
maximum: 50,
|
|
389
|
+
default: 10,
|
|
390
|
+
},
|
|
391
|
+
status: {
|
|
392
|
+
type: 'string',
|
|
393
|
+
description: 'Filter by project status',
|
|
394
|
+
enum: ['active', 'completed', 'archived'],
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
required: ['query'],
|
|
398
|
+
};
|
|
399
|
+
/**
|
|
400
|
+
* Context search tool JSON Schema (Issue #1219)
|
|
401
|
+
*/
|
|
402
|
+
const contextSearchSchema = {
|
|
403
|
+
type: 'object',
|
|
404
|
+
properties: {
|
|
405
|
+
query: {
|
|
406
|
+
type: 'string',
|
|
407
|
+
description: 'Natural language search query across memories, todos, projects, and messages',
|
|
408
|
+
minLength: 1,
|
|
409
|
+
maxLength: 1000,
|
|
410
|
+
},
|
|
411
|
+
entity_types: {
|
|
412
|
+
type: 'array',
|
|
413
|
+
description: 'Filter to specific entity types. Defaults to all (memory, todo, project, message).',
|
|
414
|
+
items: {
|
|
415
|
+
type: 'string',
|
|
416
|
+
enum: ['memory', 'todo', 'project', 'message'],
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
limit: {
|
|
420
|
+
type: 'integer',
|
|
421
|
+
description: 'Maximum number of results to return',
|
|
422
|
+
minimum: 1,
|
|
423
|
+
maximum: 50,
|
|
424
|
+
default: 10,
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
required: ['query'],
|
|
428
|
+
};
|
|
281
429
|
/**
|
|
282
430
|
* Contact search tool JSON Schema
|
|
283
431
|
*/
|
|
@@ -877,6 +1025,90 @@ const skillStoreAggregateSchema = {
|
|
|
877
1025
|
},
|
|
878
1026
|
required: ['skill_id', 'operation'],
|
|
879
1027
|
};
|
|
1028
|
+
/**
|
|
1029
|
+
* Entity linking tool JSON Schemas (Issue #1220)
|
|
1030
|
+
*/
|
|
1031
|
+
const linksSetSchema = {
|
|
1032
|
+
type: 'object',
|
|
1033
|
+
properties: {
|
|
1034
|
+
source_type: {
|
|
1035
|
+
type: 'string',
|
|
1036
|
+
description: 'Type of the source entity',
|
|
1037
|
+
enum: ['memory', 'todo', 'project', 'contact'],
|
|
1038
|
+
},
|
|
1039
|
+
source_id: {
|
|
1040
|
+
type: 'string',
|
|
1041
|
+
description: 'UUID of the source entity',
|
|
1042
|
+
format: 'uuid',
|
|
1043
|
+
},
|
|
1044
|
+
target_type: {
|
|
1045
|
+
type: 'string',
|
|
1046
|
+
description: 'Type of the target entity or external reference',
|
|
1047
|
+
enum: ['memory', 'todo', 'project', 'contact', 'github_issue', 'url'],
|
|
1048
|
+
},
|
|
1049
|
+
target_ref: {
|
|
1050
|
+
type: 'string',
|
|
1051
|
+
description: 'Reference to the target: UUID for internal entities, "owner/repo#N" for GitHub issues, URL for urls',
|
|
1052
|
+
minLength: 1,
|
|
1053
|
+
},
|
|
1054
|
+
label: {
|
|
1055
|
+
type: 'string',
|
|
1056
|
+
description: 'Optional label describing the link (e.g., "spawned from", "tracks", "related to")',
|
|
1057
|
+
maxLength: 100,
|
|
1058
|
+
},
|
|
1059
|
+
},
|
|
1060
|
+
required: ['source_type', 'source_id', 'target_type', 'target_ref'],
|
|
1061
|
+
};
|
|
1062
|
+
const linksQuerySchema = {
|
|
1063
|
+
type: 'object',
|
|
1064
|
+
properties: {
|
|
1065
|
+
entity_type: {
|
|
1066
|
+
type: 'string',
|
|
1067
|
+
description: 'Type of the entity to query links for',
|
|
1068
|
+
enum: ['memory', 'todo', 'project', 'contact'],
|
|
1069
|
+
},
|
|
1070
|
+
entity_id: {
|
|
1071
|
+
type: 'string',
|
|
1072
|
+
description: 'UUID of the entity to query links for',
|
|
1073
|
+
format: 'uuid',
|
|
1074
|
+
},
|
|
1075
|
+
link_types: {
|
|
1076
|
+
type: 'array',
|
|
1077
|
+
description: 'Optional filter to only return links to specific entity types',
|
|
1078
|
+
items: {
|
|
1079
|
+
type: 'string',
|
|
1080
|
+
enum: ['memory', 'todo', 'project', 'contact', 'github_issue', 'url'],
|
|
1081
|
+
},
|
|
1082
|
+
},
|
|
1083
|
+
},
|
|
1084
|
+
required: ['entity_type', 'entity_id'],
|
|
1085
|
+
};
|
|
1086
|
+
const linksRemoveSchema = {
|
|
1087
|
+
type: 'object',
|
|
1088
|
+
properties: {
|
|
1089
|
+
source_type: {
|
|
1090
|
+
type: 'string',
|
|
1091
|
+
description: 'Type of the source entity',
|
|
1092
|
+
enum: ['memory', 'todo', 'project', 'contact'],
|
|
1093
|
+
},
|
|
1094
|
+
source_id: {
|
|
1095
|
+
type: 'string',
|
|
1096
|
+
description: 'UUID of the source entity',
|
|
1097
|
+
format: 'uuid',
|
|
1098
|
+
},
|
|
1099
|
+
target_type: {
|
|
1100
|
+
type: 'string',
|
|
1101
|
+
description: 'Type of the target entity or external reference',
|
|
1102
|
+
enum: ['memory', 'todo', 'project', 'contact', 'github_issue', 'url'],
|
|
1103
|
+
},
|
|
1104
|
+
target_ref: {
|
|
1105
|
+
type: 'string',
|
|
1106
|
+
description: 'Reference to the target',
|
|
1107
|
+
minLength: 1,
|
|
1108
|
+
},
|
|
1109
|
+
},
|
|
1110
|
+
required: ['source_type', 'source_id', 'target_type', 'target_ref'],
|
|
1111
|
+
};
|
|
880
1112
|
/**
|
|
881
1113
|
* Create tool execution handlers
|
|
882
1114
|
*/
|
|
@@ -884,9 +1116,11 @@ function createToolHandlers(state) {
|
|
|
884
1116
|
const { config, logger, apiClient, userId } = state;
|
|
885
1117
|
return {
|
|
886
1118
|
async memory_recall(params) {
|
|
887
|
-
const { query, limit = config.maxRecallMemories, category, tags, relationship_id, } = params;
|
|
1119
|
+
const { query, limit = config.maxRecallMemories, category, tags, relationship_id, location, location_radius_km, location_weight, } = params;
|
|
888
1120
|
try {
|
|
889
|
-
|
|
1121
|
+
// Over-fetch when location is provided to allow geo re-ranking
|
|
1122
|
+
const apiLimit = location ? Math.min(limit * 3, 60) : limit;
|
|
1123
|
+
const queryParams = new URLSearchParams({ q: query, limit: String(apiLimit) });
|
|
890
1124
|
if (category)
|
|
891
1125
|
queryParams.set('memory_type', category);
|
|
892
1126
|
if (tags && tags.length > 0)
|
|
@@ -897,7 +1131,36 @@ function createToolHandlers(state) {
|
|
|
897
1131
|
if (!response.success) {
|
|
898
1132
|
return { success: false, error: response.error.message };
|
|
899
1133
|
}
|
|
900
|
-
|
|
1134
|
+
let memories = (response.data.results ?? []).map((m) => ({
|
|
1135
|
+
...m,
|
|
1136
|
+
category: m.type === 'note' ? 'other' : m.type,
|
|
1137
|
+
score: m.similarity,
|
|
1138
|
+
}));
|
|
1139
|
+
// Apply geo re-ranking if location is provided
|
|
1140
|
+
if (location) {
|
|
1141
|
+
const { lat: qLat, lng: qLng } = location;
|
|
1142
|
+
const weight = location_weight ?? 0.3;
|
|
1143
|
+
// Filter by radius if specified
|
|
1144
|
+
if (location_radius_km !== undefined) {
|
|
1145
|
+
memories = memories.filter((m) => {
|
|
1146
|
+
if (m.lat == null || m.lng == null)
|
|
1147
|
+
return false;
|
|
1148
|
+
return haversineDistanceKm(qLat, qLng, m.lat, m.lng) <= location_radius_km;
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
// Compute blended scores and re-sort
|
|
1152
|
+
memories = memories
|
|
1153
|
+
.map((m) => {
|
|
1154
|
+
const contentScore = m.score ?? 0;
|
|
1155
|
+
let geoScore = 0.5;
|
|
1156
|
+
if (m.lat != null && m.lng != null) {
|
|
1157
|
+
geoScore = computeGeoScore(haversineDistanceKm(qLat, qLng, m.lat, m.lng));
|
|
1158
|
+
}
|
|
1159
|
+
return { ...m, score: blendScores(contentScore, geoScore, weight) };
|
|
1160
|
+
})
|
|
1161
|
+
.sort((a, b) => (b.score ?? 0) - (a.score ?? 0))
|
|
1162
|
+
.slice(0, limit);
|
|
1163
|
+
}
|
|
901
1164
|
const content = memories.length > 0 ? memories.map((m) => `- [${m.type}] ${m.content}`).join('\n') : 'No relevant memories found.';
|
|
902
1165
|
return {
|
|
903
1166
|
success: true,
|
|
@@ -914,7 +1177,7 @@ function createToolHandlers(state) {
|
|
|
914
1177
|
},
|
|
915
1178
|
async memory_store(params) {
|
|
916
1179
|
// Accept 'text' (OpenClaw native) or 'content' (backwards compat)
|
|
917
|
-
const { text, content: contentAlias, category = 'other', importance = 0.7, tags, relationship_id, } = params;
|
|
1180
|
+
const { text, content: contentAlias, category = 'other', importance = 0.7, tags, relationship_id, location, } = params;
|
|
918
1181
|
const memoryText = text || contentAlias;
|
|
919
1182
|
if (!memoryText) {
|
|
920
1183
|
return { success: false, error: 'text is required' };
|
|
@@ -931,6 +1194,24 @@ function createToolHandlers(state) {
|
|
|
931
1194
|
payload.tags = tags;
|
|
932
1195
|
if (relationship_id)
|
|
933
1196
|
payload.relationship_id = relationship_id;
|
|
1197
|
+
if (location) {
|
|
1198
|
+
payload.lat = location.lat;
|
|
1199
|
+
payload.lng = location.lng;
|
|
1200
|
+
// Reverse geocode if address is missing and Nominatim is configured
|
|
1201
|
+
if (!location.address && config.nominatimUrl) {
|
|
1202
|
+
const geocoded = await reverseGeocode(location.lat, location.lng, config.nominatimUrl);
|
|
1203
|
+
if (geocoded) {
|
|
1204
|
+
payload.address = geocoded.address;
|
|
1205
|
+
if (!location.place_label && geocoded.placeLabel) {
|
|
1206
|
+
payload.place_label = geocoded.placeLabel;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
if (location.address)
|
|
1211
|
+
payload.address = location.address;
|
|
1212
|
+
if (location.place_label)
|
|
1213
|
+
payload.place_label = location.place_label;
|
|
1214
|
+
}
|
|
934
1215
|
const response = await apiClient.post('/api/memories/unified', payload, { userId });
|
|
935
1216
|
if (!response.success) {
|
|
936
1217
|
return { success: false, error: response.error.message };
|
|
@@ -1012,6 +1293,7 @@ function createToolHandlers(state) {
|
|
|
1012
1293
|
const queryParams = new URLSearchParams({ item_type: 'project', limit: String(limit) });
|
|
1013
1294
|
if (status !== 'all')
|
|
1014
1295
|
queryParams.set('status', status);
|
|
1296
|
+
queryParams.set('user_email', userId); // Issue #1172: scope by user
|
|
1015
1297
|
const response = await apiClient.get(`/api/work-items?${queryParams}`, { userId });
|
|
1016
1298
|
if (!response.success) {
|
|
1017
1299
|
return { success: false, error: response.error.message };
|
|
@@ -1031,7 +1313,7 @@ function createToolHandlers(state) {
|
|
|
1031
1313
|
async project_get(params) {
|
|
1032
1314
|
const { projectId } = params;
|
|
1033
1315
|
try {
|
|
1034
|
-
const response = await apiClient.get(`/api/work-items/${projectId}`, { userId });
|
|
1316
|
+
const response = await apiClient.get(`/api/work-items/${projectId}?user_email=${encodeURIComponent(userId)}`, { userId });
|
|
1035
1317
|
if (!response.success) {
|
|
1036
1318
|
return { success: false, error: response.error.message };
|
|
1037
1319
|
}
|
|
@@ -1052,7 +1334,7 @@ function createToolHandlers(state) {
|
|
|
1052
1334
|
async project_create(params) {
|
|
1053
1335
|
const { name, description, status = 'active', } = params;
|
|
1054
1336
|
try {
|
|
1055
|
-
const response = await apiClient.post('/api/work-items', { title: name, description, item_type: 'project', status }, { userId });
|
|
1337
|
+
const response = await apiClient.post('/api/work-items', { title: name, description, item_type: 'project', status, user_email: userId }, { userId });
|
|
1056
1338
|
if (!response.success) {
|
|
1057
1339
|
return { success: false, error: response.error.message };
|
|
1058
1340
|
}
|
|
@@ -1070,22 +1352,41 @@ function createToolHandlers(state) {
|
|
|
1070
1352
|
}
|
|
1071
1353
|
},
|
|
1072
1354
|
async todo_list(params) {
|
|
1073
|
-
const { projectId,
|
|
1355
|
+
const { projectId, completed, limit = 50, offset = 0, } = params;
|
|
1074
1356
|
try {
|
|
1075
|
-
const queryParams = new URLSearchParams({
|
|
1076
|
-
|
|
1077
|
-
|
|
1357
|
+
const queryParams = new URLSearchParams({
|
|
1358
|
+
item_type: 'task',
|
|
1359
|
+
limit: String(limit),
|
|
1360
|
+
offset: String(offset),
|
|
1361
|
+
user_email: userId, // Issue #1172: scope by user
|
|
1362
|
+
});
|
|
1078
1363
|
if (projectId)
|
|
1079
1364
|
queryParams.set('parent_work_item_id', projectId);
|
|
1365
|
+
if (completed !== undefined) {
|
|
1366
|
+
queryParams.set('status', completed ? 'completed' : 'active');
|
|
1367
|
+
}
|
|
1080
1368
|
const response = await apiClient.get(`/api/work-items?${queryParams}`, { userId });
|
|
1081
1369
|
if (!response.success) {
|
|
1082
1370
|
return { success: false, error: response.error.message };
|
|
1083
1371
|
}
|
|
1084
1372
|
const todos = response.data.items ?? [];
|
|
1085
|
-
const
|
|
1373
|
+
const total = response.data.total ?? todos.length;
|
|
1374
|
+
if (todos.length === 0) {
|
|
1375
|
+
return {
|
|
1376
|
+
success: true,
|
|
1377
|
+
data: { content: 'No todos found.', details: { count: 0, total: 0, todos: [] } },
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
const content = todos
|
|
1381
|
+
.map((t) => {
|
|
1382
|
+
const checkbox = t.completed ? '[x]' : '[ ]';
|
|
1383
|
+
const dueStr = t.dueDate ? ` (due: ${t.dueDate})` : '';
|
|
1384
|
+
return `- ${checkbox} ${t.title}${dueStr}`;
|
|
1385
|
+
})
|
|
1386
|
+
.join('\n');
|
|
1086
1387
|
return {
|
|
1087
1388
|
success: true,
|
|
1088
|
-
data: { content, details: { count: todos.length, todos } },
|
|
1389
|
+
data: { content, details: { count: todos.length, total, todos } },
|
|
1089
1390
|
};
|
|
1090
1391
|
}
|
|
1091
1392
|
catch (error) {
|
|
@@ -1096,7 +1397,7 @@ function createToolHandlers(state) {
|
|
|
1096
1397
|
async todo_create(params) {
|
|
1097
1398
|
const { title, description, projectId, priority = 'medium', dueDate, } = params;
|
|
1098
1399
|
try {
|
|
1099
|
-
const body = { title, description, item_type: 'task', priority };
|
|
1400
|
+
const body = { title, description, item_type: 'task', priority, user_email: userId };
|
|
1100
1401
|
if (projectId)
|
|
1101
1402
|
body.parent_work_item_id = projectId;
|
|
1102
1403
|
if (dueDate)
|
|
@@ -1121,7 +1422,7 @@ function createToolHandlers(state) {
|
|
|
1121
1422
|
async todo_complete(params) {
|
|
1122
1423
|
const { todoId } = params;
|
|
1123
1424
|
try {
|
|
1124
|
-
const response = await apiClient.patch(`/api/work-items/${todoId}/status`, { status: 'completed' }, { userId });
|
|
1425
|
+
const response = await apiClient.patch(`/api/work-items/${todoId}/status?user_email=${encodeURIComponent(userId)}`, { status: 'completed' }, { userId });
|
|
1125
1426
|
if (!response.success) {
|
|
1126
1427
|
return { success: false, error: response.error.message };
|
|
1127
1428
|
}
|
|
@@ -1135,10 +1436,87 @@ function createToolHandlers(state) {
|
|
|
1135
1436
|
return { success: false, error: 'Failed to complete todo' };
|
|
1136
1437
|
}
|
|
1137
1438
|
},
|
|
1439
|
+
async todo_search(params) {
|
|
1440
|
+
const { query, limit = 10, kind, status, } = params;
|
|
1441
|
+
if (!query || query.trim().length === 0) {
|
|
1442
|
+
return { success: false, error: 'query is required' };
|
|
1443
|
+
}
|
|
1444
|
+
try {
|
|
1445
|
+
// Over-fetch by 3x to compensate for client-side kind/status filtering (Issue #1216 review fix)
|
|
1446
|
+
const fetchLimit = (kind || status) ? Math.min(limit * 3, 50) : limit;
|
|
1447
|
+
const queryParams = new URLSearchParams({
|
|
1448
|
+
q: query.trim(),
|
|
1449
|
+
types: 'work_item',
|
|
1450
|
+
limit: String(fetchLimit),
|
|
1451
|
+
semantic: 'true',
|
|
1452
|
+
user_email: userId, // Issue #1216: scope results to current user
|
|
1453
|
+
});
|
|
1454
|
+
const response = await apiClient.get(`/api/search?${queryParams}`, { userId });
|
|
1455
|
+
if (!response.success) {
|
|
1456
|
+
return { success: false, error: response.error.message };
|
|
1457
|
+
}
|
|
1458
|
+
let results = response.data.results ?? [];
|
|
1459
|
+
// Client-side filtering by kind and status, then truncate to requested limit
|
|
1460
|
+
if (kind) {
|
|
1461
|
+
results = results.filter((r) => r.metadata?.kind === kind);
|
|
1462
|
+
}
|
|
1463
|
+
if (status) {
|
|
1464
|
+
results = results.filter((r) => r.metadata?.status === status);
|
|
1465
|
+
}
|
|
1466
|
+
results = results.slice(0, limit);
|
|
1467
|
+
if (results.length === 0) {
|
|
1468
|
+
return {
|
|
1469
|
+
success: true,
|
|
1470
|
+
data: {
|
|
1471
|
+
content: 'No matching work items found.',
|
|
1472
|
+
details: { count: 0, results: [], searchType: response.data.search_type },
|
|
1473
|
+
},
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
const content = results
|
|
1477
|
+
.map((r) => {
|
|
1478
|
+
const kindStr = r.metadata?.kind ? `[${r.metadata.kind}]` : '';
|
|
1479
|
+
const statusStr = r.metadata?.status ? ` (${r.metadata.status})` : '';
|
|
1480
|
+
const snippetStr = r.snippet ? ` - ${r.snippet}` : '';
|
|
1481
|
+
return `- ${kindStr} **${r.title}**${statusStr}${snippetStr}`;
|
|
1482
|
+
})
|
|
1483
|
+
.join('\n');
|
|
1484
|
+
return {
|
|
1485
|
+
success: true,
|
|
1486
|
+
data: {
|
|
1487
|
+
content,
|
|
1488
|
+
details: {
|
|
1489
|
+
count: results.length,
|
|
1490
|
+
results: results.map((r) => ({
|
|
1491
|
+
id: r.id,
|
|
1492
|
+
title: r.title,
|
|
1493
|
+
snippet: r.snippet,
|
|
1494
|
+
score: r.score,
|
|
1495
|
+
kind: r.metadata?.kind,
|
|
1496
|
+
status: r.metadata?.status,
|
|
1497
|
+
})),
|
|
1498
|
+
searchType: response.data.search_type,
|
|
1499
|
+
},
|
|
1500
|
+
},
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
catch (error) {
|
|
1504
|
+
logger.error('todo_search failed', { error });
|
|
1505
|
+
return { success: false, error: 'Failed to search work items' };
|
|
1506
|
+
}
|
|
1507
|
+
},
|
|
1508
|
+
async project_search(params) {
|
|
1509
|
+
const tool = createProjectSearchTool({ client: apiClient, logger, config, userId });
|
|
1510
|
+
return tool.execute(params);
|
|
1511
|
+
},
|
|
1512
|
+
async context_search(params) {
|
|
1513
|
+
const tool = createContextSearchTool({ client: apiClient, logger, config, userId });
|
|
1514
|
+
return tool.execute(params);
|
|
1515
|
+
},
|
|
1138
1516
|
async contact_search(params) {
|
|
1139
1517
|
const { query, limit = 10 } = params;
|
|
1140
1518
|
try {
|
|
1141
|
-
const queryParams = new URLSearchParams({ search: query, limit: String(limit) });
|
|
1519
|
+
const queryParams = new URLSearchParams({ search: query, limit: String(limit), user_email: userId });
|
|
1142
1520
|
const response = await apiClient.get(`/api/contacts?${queryParams}`, {
|
|
1143
1521
|
userId,
|
|
1144
1522
|
});
|
|
@@ -1160,9 +1538,7 @@ function createToolHandlers(state) {
|
|
|
1160
1538
|
async contact_get(params) {
|
|
1161
1539
|
const { contactId } = params;
|
|
1162
1540
|
try {
|
|
1163
|
-
const response = await apiClient.get(`/api/contacts/${contactId}`, {
|
|
1164
|
-
userId,
|
|
1165
|
-
});
|
|
1541
|
+
const response = await apiClient.get(`/api/contacts/${contactId}?user_email=${encodeURIComponent(userId)}`, { userId });
|
|
1166
1542
|
if (!response.success) {
|
|
1167
1543
|
return { success: false, error: response.error.message };
|
|
1168
1544
|
}
|
|
@@ -1188,7 +1564,7 @@ function createToolHandlers(state) {
|
|
|
1188
1564
|
const { name, notes } = params;
|
|
1189
1565
|
try {
|
|
1190
1566
|
// API requires displayName, not name. Email/phone are stored as separate contact_endpoint records.
|
|
1191
|
-
const response = await apiClient.post('/api/contacts', { displayName: name, notes }, { userId });
|
|
1567
|
+
const response = await apiClient.post('/api/contacts', { displayName: name, notes, user_email: userId }, { userId });
|
|
1192
1568
|
if (!response.success) {
|
|
1193
1569
|
return { success: false, error: response.error.message };
|
|
1194
1570
|
}
|
|
@@ -1285,13 +1661,6 @@ function createToolHandlers(state) {
|
|
|
1285
1661
|
},
|
|
1286
1662
|
async email_send(params) {
|
|
1287
1663
|
const { to, subject, body, htmlBody, threadId, idempotencyKey } = params;
|
|
1288
|
-
// Check Postmark configuration
|
|
1289
|
-
if (!config.postmarkToken || !config.postmarkFromEmail) {
|
|
1290
|
-
return {
|
|
1291
|
-
success: false,
|
|
1292
|
-
error: 'Postmark is not configured. Please configure Postmark credentials.',
|
|
1293
|
-
};
|
|
1294
|
-
}
|
|
1295
1664
|
// Validate email format
|
|
1296
1665
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1297
1666
|
if (!emailRegex.test(to)) {
|
|
@@ -1329,7 +1698,7 @@ function createToolHandlers(state) {
|
|
|
1329
1698
|
hasIdempotencyKey: !!idempotencyKey,
|
|
1330
1699
|
});
|
|
1331
1700
|
try {
|
|
1332
|
-
const response = await apiClient.post('/api/email/
|
|
1701
|
+
const response = await apiClient.post('/api/postmark/email/send', { to, subject, body, htmlBody, threadId, idempotencyKey }, { userId });
|
|
1333
1702
|
if (!response.success) {
|
|
1334
1703
|
logger.error('email_send API error', {
|
|
1335
1704
|
userId,
|
|
@@ -1432,15 +1801,35 @@ function createToolHandlers(state) {
|
|
|
1432
1801
|
resultCount: messages.length,
|
|
1433
1802
|
total,
|
|
1434
1803
|
});
|
|
1435
|
-
//
|
|
1804
|
+
// Detect and log potential injection patterns in inbound messages
|
|
1805
|
+
for (const m of messages) {
|
|
1806
|
+
if (m.direction === 'inbound' && m.body) {
|
|
1807
|
+
const detection = detectInjectionPatterns(m.body);
|
|
1808
|
+
if (detection.detected) {
|
|
1809
|
+
logger.warn('potential prompt injection detected in message_search result', {
|
|
1810
|
+
userId,
|
|
1811
|
+
messageId: m.id,
|
|
1812
|
+
patterns: detection.patterns,
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
// Format content for display with injection protection
|
|
1436
1818
|
const content = messages.length > 0
|
|
1437
1819
|
? messages
|
|
1438
1820
|
.map((m) => {
|
|
1439
1821
|
const prefix = m.direction === 'inbound' ? '←' : '→';
|
|
1440
|
-
const contact = m.contactName || 'Unknown';
|
|
1822
|
+
const contact = sanitizeMetadataField(m.contactName || 'Unknown');
|
|
1823
|
+
const safeChannel = sanitizeMetadataField(m.channel);
|
|
1441
1824
|
const similarity = `(${Math.round(m.similarity * 100)}%)`;
|
|
1442
|
-
const
|
|
1443
|
-
|
|
1825
|
+
const rawBody = m.body || '';
|
|
1826
|
+
const truncatedBody = rawBody.substring(0, 100) + (rawBody.length > 100 ? '...' : '');
|
|
1827
|
+
const bodyText = sanitizeMessageForContext(truncatedBody, {
|
|
1828
|
+
direction: m.direction,
|
|
1829
|
+
channel: m.channel,
|
|
1830
|
+
sender: m.contactName || 'Unknown',
|
|
1831
|
+
});
|
|
1832
|
+
return `${prefix} [${safeChannel}] ${contact} ${similarity}: ${bodyText}`;
|
|
1444
1833
|
})
|
|
1445
1834
|
.join('\n')
|
|
1446
1835
|
: 'No messages found matching your query.';
|
|
@@ -1502,17 +1891,22 @@ function createToolHandlers(state) {
|
|
|
1502
1891
|
threadCount: results.length,
|
|
1503
1892
|
total,
|
|
1504
1893
|
});
|
|
1894
|
+
// Format content with injection protection.
|
|
1895
|
+
// Sanitize all fields that may contain external message content.
|
|
1505
1896
|
const content = results.length > 0
|
|
1506
1897
|
? results
|
|
1507
1898
|
.map((r) => {
|
|
1508
1899
|
// Handle both thread and search result formats
|
|
1509
1900
|
if ('channel' in r) {
|
|
1510
1901
|
const t = r;
|
|
1511
|
-
const
|
|
1902
|
+
const safeContact = sanitizeMetadataField(t.contactName || t.endpointValue || 'Unknown');
|
|
1903
|
+
const safeChannel = sanitizeMetadataField(t.channel);
|
|
1512
1904
|
const msgCount = t.messageCount ? `${t.messageCount} message${t.messageCount !== 1 ? 's' : ''}` : '';
|
|
1513
|
-
return `[${
|
|
1905
|
+
return `[${safeChannel}] ${safeContact}${msgCount ? ` - ${msgCount}` : ''}`;
|
|
1514
1906
|
}
|
|
1515
|
-
|
|
1907
|
+
const safeTitle = r.title ? sanitizeMetadataField(r.title) : '';
|
|
1908
|
+
const wrappedSnippet = r.snippet ? wrapExternalMessage(r.snippet) : '';
|
|
1909
|
+
return `- ${safeTitle || wrappedSnippet || r.id}`;
|
|
1516
1910
|
})
|
|
1517
1911
|
.join('\n')
|
|
1518
1912
|
: 'No threads found.';
|
|
@@ -1571,14 +1965,34 @@ function createToolHandlers(state) {
|
|
|
1571
1965
|
threadId,
|
|
1572
1966
|
messageCount: messages.length,
|
|
1573
1967
|
});
|
|
1574
|
-
const contact = thread.contactName || thread.endpointValue || 'Unknown';
|
|
1575
|
-
const
|
|
1968
|
+
const contact = sanitizeMetadataField(thread.contactName || thread.endpointValue || 'Unknown');
|
|
1969
|
+
const safeChannel = sanitizeMetadataField(thread.channel);
|
|
1970
|
+
const header = `Thread with ${contact} [${safeChannel}]`;
|
|
1971
|
+
// Detect and log potential injection patterns in inbound messages
|
|
1972
|
+
for (const m of messages) {
|
|
1973
|
+
if (m.direction === 'inbound' && m.body) {
|
|
1974
|
+
const detection = detectInjectionPatterns(m.body);
|
|
1975
|
+
if (detection.detected) {
|
|
1976
|
+
logger.warn('potential prompt injection detected in thread_get result', {
|
|
1977
|
+
userId,
|
|
1978
|
+
threadId,
|
|
1979
|
+
messageId: m.id,
|
|
1980
|
+
patterns: detection.patterns,
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1576
1985
|
const messageContent = messages.length > 0
|
|
1577
1986
|
? messages
|
|
1578
1987
|
.map((m) => {
|
|
1579
1988
|
const prefix = m.direction === 'inbound' ? '←' : '→';
|
|
1580
1989
|
const timestamp = new Date(m.createdAt).toLocaleString();
|
|
1581
|
-
|
|
1990
|
+
const body = sanitizeMessageForContext(m.body || '', {
|
|
1991
|
+
direction: m.direction,
|
|
1992
|
+
channel: thread.channel,
|
|
1993
|
+
sender: contact,
|
|
1994
|
+
});
|
|
1995
|
+
return `${prefix} [${timestamp}] ${body}`;
|
|
1582
1996
|
})
|
|
1583
1997
|
.join('\n')
|
|
1584
1998
|
: 'No messages in this thread.';
|
|
@@ -1623,6 +2037,7 @@ function createToolHandlers(state) {
|
|
|
1623
2037
|
contact_a,
|
|
1624
2038
|
contact_b,
|
|
1625
2039
|
relationship_type: relationship,
|
|
2040
|
+
user_email: userId, // Issue #1172: scope by user
|
|
1626
2041
|
};
|
|
1627
2042
|
if (notes) {
|
|
1628
2043
|
body.notes = notes;
|
|
@@ -1669,15 +2084,40 @@ function createToolHandlers(state) {
|
|
|
1669
2084
|
hasTypeFilter: !!type_filter,
|
|
1670
2085
|
});
|
|
1671
2086
|
try {
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
2087
|
+
// Resolve contact to a UUID — accept UUID directly or search by name
|
|
2088
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2089
|
+
let contactId;
|
|
2090
|
+
if (uuidRegex.test(contact)) {
|
|
2091
|
+
contactId = contact;
|
|
1675
2092
|
}
|
|
1676
|
-
|
|
2093
|
+
else {
|
|
2094
|
+
// Search for contact by name (Issue #1172: scope by user_email)
|
|
2095
|
+
const searchParams = new URLSearchParams({ search: contact, limit: '1', user_email: userId });
|
|
2096
|
+
const searchResponse = await apiClient.get(`/api/contacts?${searchParams}`, { userId });
|
|
2097
|
+
if (!searchResponse.success) {
|
|
2098
|
+
return { success: false, error: searchResponse.error.message };
|
|
2099
|
+
}
|
|
2100
|
+
const contacts = searchResponse.data.contacts ?? [];
|
|
2101
|
+
if (contacts.length === 0) {
|
|
2102
|
+
return { success: false, error: 'Contact not found.' };
|
|
2103
|
+
}
|
|
2104
|
+
contactId = contacts[0].id;
|
|
2105
|
+
}
|
|
2106
|
+
// Use graph traversal endpoint which returns relatedContacts
|
|
2107
|
+
const response = await apiClient.get(`/api/contacts/${contactId}/relationships?user_email=${encodeURIComponent(userId)}`, { userId });
|
|
1677
2108
|
if (!response.success) {
|
|
2109
|
+
if (response.error.code === 'NOT_FOUND') {
|
|
2110
|
+
return { success: false, error: 'Contact not found.' };
|
|
2111
|
+
}
|
|
1678
2112
|
return { success: false, error: response.error.message };
|
|
1679
2113
|
}
|
|
1680
|
-
|
|
2114
|
+
let { relatedContacts } = response.data;
|
|
2115
|
+
const { contactName } = response.data;
|
|
2116
|
+
// Apply type_filter client-side if provided
|
|
2117
|
+
if (type_filter && relatedContacts.length > 0) {
|
|
2118
|
+
const filterLower = type_filter.toLowerCase();
|
|
2119
|
+
relatedContacts = relatedContacts.filter((rel) => rel.relationshipTypeName.toLowerCase().includes(filterLower) || rel.relationshipTypeLabel.toLowerCase().includes(filterLower));
|
|
2120
|
+
}
|
|
1681
2121
|
if (relatedContacts.length === 0) {
|
|
1682
2122
|
return {
|
|
1683
2123
|
success: true,
|
|
@@ -1825,6 +2265,18 @@ function createToolHandlers(state) {
|
|
|
1825
2265
|
skill_store_aggregate: (params) => aggregateTool.execute(params),
|
|
1826
2266
|
};
|
|
1827
2267
|
})(),
|
|
2268
|
+
// Entity link tools: delegate to tool modules (Issue #1220)
|
|
2269
|
+
...(() => {
|
|
2270
|
+
const toolOptions = { client: apiClient, logger, config, userId };
|
|
2271
|
+
const setTool = createLinksSetTool(toolOptions);
|
|
2272
|
+
const queryTool = createLinksQueryTool(toolOptions);
|
|
2273
|
+
const removeTool = createLinksRemoveTool(toolOptions);
|
|
2274
|
+
return {
|
|
2275
|
+
links_set: (params) => setTool.execute(params),
|
|
2276
|
+
links_query: (params) => queryTool.execute(params),
|
|
2277
|
+
links_remove: (params) => removeTool.execute(params),
|
|
2278
|
+
};
|
|
2279
|
+
})(),
|
|
1828
2280
|
};
|
|
1829
2281
|
}
|
|
1830
2282
|
/**
|
|
@@ -1876,7 +2328,7 @@ export const registerOpenClaw = (api) => {
|
|
|
1876
2328
|
const state = { config, logger, apiClient, userId };
|
|
1877
2329
|
// Create tool handlers
|
|
1878
2330
|
const handlers = createToolHandlers(state);
|
|
1879
|
-
// Register all
|
|
2331
|
+
// Register all 30 tools with correct OpenClaw Gateway execute signature
|
|
1880
2332
|
// Signature: (toolCallId: string, params: T, signal?: AbortSignal, onUpdate?: (partial: any) => void) => AgentToolResult
|
|
1881
2333
|
const tools = [
|
|
1882
2334
|
{
|
|
@@ -1960,6 +2412,33 @@ export const registerOpenClaw = (api) => {
|
|
|
1960
2412
|
return toAgentToolResult(result);
|
|
1961
2413
|
},
|
|
1962
2414
|
},
|
|
2415
|
+
{
|
|
2416
|
+
name: 'todo_search',
|
|
2417
|
+
description: 'Search todos and work items by natural language query. Uses semantic and text search to find relevant items. Optionally filter by kind or status.',
|
|
2418
|
+
parameters: todoSearchSchema,
|
|
2419
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
2420
|
+
const result = await handlers.todo_search(params);
|
|
2421
|
+
return toAgentToolResult(result);
|
|
2422
|
+
},
|
|
2423
|
+
},
|
|
2424
|
+
{
|
|
2425
|
+
name: 'project_search',
|
|
2426
|
+
description: 'Search projects by natural language query. Uses semantic and text search to find relevant projects. Optionally filter by status (active, completed, archived).',
|
|
2427
|
+
parameters: projectSearchSchema,
|
|
2428
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
2429
|
+
const result = await handlers.project_search(params);
|
|
2430
|
+
return toAgentToolResult(result);
|
|
2431
|
+
},
|
|
2432
|
+
},
|
|
2433
|
+
{
|
|
2434
|
+
name: 'context_search',
|
|
2435
|
+
description: 'Search across memories, todos, projects, and messages simultaneously. Use when you need broad context about a topic, person, or project. Returns a blended ranked list from all entity types. Optionally filter by entity_types to narrow the search.',
|
|
2436
|
+
parameters: contextSearchSchema,
|
|
2437
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
2438
|
+
const result = await handlers.context_search(params);
|
|
2439
|
+
return toAgentToolResult(result);
|
|
2440
|
+
},
|
|
2441
|
+
},
|
|
1963
2442
|
{
|
|
1964
2443
|
name: 'contact_search',
|
|
1965
2444
|
description: 'Search contacts by name, email, or other fields. Use to find people.',
|
|
@@ -2122,6 +2601,33 @@ export const registerOpenClaw = (api) => {
|
|
|
2122
2601
|
return toAgentToolResult(result);
|
|
2123
2602
|
},
|
|
2124
2603
|
},
|
|
2604
|
+
{
|
|
2605
|
+
name: 'links_set',
|
|
2606
|
+
description: 'Create a link between two entities (memory, todo, project, contact, GitHub issue, or URL). Links are bidirectional and can be traversed from either end. Use to connect related items for cross-reference and context discovery.',
|
|
2607
|
+
parameters: linksSetSchema,
|
|
2608
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
2609
|
+
const result = await handlers.links_set(params);
|
|
2610
|
+
return toAgentToolResult(result);
|
|
2611
|
+
},
|
|
2612
|
+
},
|
|
2613
|
+
{
|
|
2614
|
+
name: 'links_query',
|
|
2615
|
+
description: 'Query all links for an entity (memory, todo, project, or contact). Returns connected entities including other items, GitHub issues, and URLs. Optionally filter by link target types.',
|
|
2616
|
+
parameters: linksQuerySchema,
|
|
2617
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
2618
|
+
const result = await handlers.links_query(params);
|
|
2619
|
+
return toAgentToolResult(result);
|
|
2620
|
+
},
|
|
2621
|
+
},
|
|
2622
|
+
{
|
|
2623
|
+
name: 'links_remove',
|
|
2624
|
+
description: 'Remove a link between two entities. Deletes both directions of the link. Use when a connection is no longer relevant or was created in error.',
|
|
2625
|
+
parameters: linksRemoveSchema,
|
|
2626
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
2627
|
+
const result = await handlers.links_remove(params);
|
|
2628
|
+
return toAgentToolResult(result);
|
|
2629
|
+
},
|
|
2630
|
+
},
|
|
2125
2631
|
];
|
|
2126
2632
|
for (const tool of tools) {
|
|
2127
2633
|
api.registerTool(tool);
|
|
@@ -2228,6 +2734,54 @@ export const registerOpenClaw = (api) => {
|
|
|
2228
2734
|
api.registerHook('agentEnd', agentEndHandler);
|
|
2229
2735
|
}
|
|
2230
2736
|
}
|
|
2737
|
+
// Register auto-linking hook for inbound messages (Issue #1223)
|
|
2738
|
+
// When an inbound SMS/email arrives, automatically link the thread to
|
|
2739
|
+
// matching contacts (by sender email/phone) and related projects/todos
|
|
2740
|
+
// (by semantic content matching).
|
|
2741
|
+
{
|
|
2742
|
+
/**
|
|
2743
|
+
* message_received handler: Extracts sender and content info from the
|
|
2744
|
+
* inbound message event and runs auto-linking in the background.
|
|
2745
|
+
* Failures are logged but never crash message processing.
|
|
2746
|
+
*/
|
|
2747
|
+
const messageReceivedHandler = async (event, _ctx) => {
|
|
2748
|
+
// Skip if no thread ID (nothing to link to)
|
|
2749
|
+
if (!event.threadId) {
|
|
2750
|
+
logger.debug('Auto-link skipped: no threadId in message_received event');
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2753
|
+
// Skip if no content and no sender info (nothing to match on)
|
|
2754
|
+
if (!event.content && !event.senderEmail && !event.senderPhone && !event.sender) {
|
|
2755
|
+
logger.debug('Auto-link skipped: no content or sender info in event');
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2758
|
+
try {
|
|
2759
|
+
await autoLinkInboundMessage({
|
|
2760
|
+
client: apiClient,
|
|
2761
|
+
logger,
|
|
2762
|
+
userId,
|
|
2763
|
+
message: {
|
|
2764
|
+
threadId: event.threadId,
|
|
2765
|
+
senderEmail: event.senderEmail ?? (event.sender?.includes('@') ? event.sender : undefined),
|
|
2766
|
+
senderPhone: event.senderPhone ?? (event.sender && !event.sender.includes('@') ? event.sender : undefined),
|
|
2767
|
+
content: event.content ?? '',
|
|
2768
|
+
},
|
|
2769
|
+
});
|
|
2770
|
+
}
|
|
2771
|
+
catch (error) {
|
|
2772
|
+
// Hook errors should never crash inbound message processing
|
|
2773
|
+
logger.error('Auto-link hook failed', {
|
|
2774
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2775
|
+
});
|
|
2776
|
+
}
|
|
2777
|
+
};
|
|
2778
|
+
if (typeof api.on === 'function') {
|
|
2779
|
+
api.on('message_received', messageReceivedHandler);
|
|
2780
|
+
}
|
|
2781
|
+
else {
|
|
2782
|
+
api.registerHook('messageReceived', messageReceivedHandler);
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2231
2785
|
// Register Gateway RPC methods (Issue #324)
|
|
2232
2786
|
const gatewayMethods = createGatewayMethods({
|
|
2233
2787
|
logger,
|
|
@@ -2327,6 +2881,8 @@ export const schemas = {
|
|
|
2327
2881
|
todoList: todoListSchema,
|
|
2328
2882
|
todoCreate: todoCreateSchema,
|
|
2329
2883
|
todoComplete: todoCompleteSchema,
|
|
2884
|
+
todoSearch: todoSearchSchema,
|
|
2885
|
+
projectSearch: projectSearchSchema,
|
|
2330
2886
|
contactSearch: contactSearchSchema,
|
|
2331
2887
|
contactGet: contactGetSchema,
|
|
2332
2888
|
contactCreate: contactCreateSchema,
|