@troykelly/openclaw-projects 0.0.11 → 0.0.13
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 +16 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +11 -0
- package/dist/config.js.map +1 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +22 -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 +640 -55
- package/dist/register-openclaw.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 +267 -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-recall.d.ts +28 -0
- package/dist/tools/memory-recall.d.ts.map +1 -1
- package/dist/tools/memory-recall.js +42 -3
- 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 +2 -2
- package/dist/tools/message-search.d.ts.map +1 -1
- package/dist/tools/message-search.js +36 -3
- 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/skill-store.d.ts +6 -6
- package/dist/tools/threads.d.ts +3 -3
- package/dist/tools/threads.d.ts.map +1 -1
- package/dist/tools/threads.js +45 -7
- 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-log-rate-limiter.d.ts +62 -0
- package/dist/utils/injection-log-rate-limiter.d.ts.map +1 -0
- package/dist/utils/injection-log-rate-limiter.js +106 -0
- package/dist/utils/injection-log-rate-limiter.js.map +1 -0
- package/dist/utils/injection-protection.d.ts +133 -0
- package/dist/utils/injection-protection.d.ts.map +1 -0
- package/dist/utils/injection-protection.js +252 -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/prompt-guard-client.d.ts +59 -0
- package/dist/utils/prompt-guard-client.d.ts.map +1 -0
- package/dist/utils/prompt-guard-client.js +99 -0
- package/dist/utils/prompt-guard-client.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 +25 -3
- package/package.json +12 -11
- package/LICENSE +0 -21
|
@@ -8,15 +8,20 @@
|
|
|
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 { createNotificationService } from './services/notification-service.js';
|
|
19
|
+
import { createContextSearchTool, createLinksQueryTool, createLinksRemoveTool, createLinksSetTool, createProjectSearchTool, createSkillStoreAggregateTool, createSkillStoreCollectionsTool, createSkillStoreDeleteTool, createSkillStoreGetTool, createSkillStoreListTool, createSkillStorePutTool, createSkillStoreSearchTool, } from './tools/index.js';
|
|
20
|
+
import { autoLinkInboundMessage } from './utils/auto-linker.js';
|
|
21
|
+
import { blendScores, computeGeoScore, haversineDistanceKm } from './utils/geo.js';
|
|
22
|
+
import { createBoundaryMarkers, detectInjectionPatternsAsync, sanitizeMessageForContext, sanitizeMetadataField, wrapExternalMessage, } from './utils/injection-protection.js';
|
|
23
|
+
import { injectionLogLimiter } from './utils/injection-log-rate-limiter.js';
|
|
24
|
+
import { reverseGeocode } from './utils/nominatim.js';
|
|
20
25
|
/**
|
|
21
26
|
* Convert internal ToolResult format to AgentToolResult format expected by OpenClaw Gateway.
|
|
22
27
|
*
|
|
@@ -73,6 +78,28 @@ const memoryRecallSchema = {
|
|
|
73
78
|
description: 'Scope search to a specific relationship between contacts',
|
|
74
79
|
format: 'uuid',
|
|
75
80
|
},
|
|
81
|
+
location: {
|
|
82
|
+
type: 'object',
|
|
83
|
+
description: 'Current location for geo-aware recall ranking',
|
|
84
|
+
properties: {
|
|
85
|
+
lat: { type: 'number', minimum: -90, maximum: 90 },
|
|
86
|
+
lng: { type: 'number', minimum: -180, maximum: 180 },
|
|
87
|
+
},
|
|
88
|
+
required: ['lat', 'lng'],
|
|
89
|
+
},
|
|
90
|
+
location_radius_km: {
|
|
91
|
+
type: 'number',
|
|
92
|
+
description: 'Filter memories within this radius (km) of the given location',
|
|
93
|
+
minimum: 0.1,
|
|
94
|
+
maximum: 100,
|
|
95
|
+
},
|
|
96
|
+
location_weight: {
|
|
97
|
+
type: 'number',
|
|
98
|
+
description: 'Weight for geo scoring (0 = content only, 1 = geo only)',
|
|
99
|
+
minimum: 0,
|
|
100
|
+
maximum: 1,
|
|
101
|
+
default: 0.3,
|
|
102
|
+
},
|
|
76
103
|
},
|
|
77
104
|
required: ['query'],
|
|
78
105
|
};
|
|
@@ -121,6 +148,35 @@ const memoryStoreSchema = {
|
|
|
121
148
|
description: 'Scope memory to a specific relationship between contacts',
|
|
122
149
|
format: 'uuid',
|
|
123
150
|
},
|
|
151
|
+
location: {
|
|
152
|
+
type: 'object',
|
|
153
|
+
description: 'Geographic location to associate with this memory',
|
|
154
|
+
properties: {
|
|
155
|
+
lat: {
|
|
156
|
+
type: 'number',
|
|
157
|
+
description: 'Latitude (-90 to 90)',
|
|
158
|
+
minimum: -90,
|
|
159
|
+
maximum: 90,
|
|
160
|
+
},
|
|
161
|
+
lng: {
|
|
162
|
+
type: 'number',
|
|
163
|
+
description: 'Longitude (-180 to 180)',
|
|
164
|
+
minimum: -180,
|
|
165
|
+
maximum: 180,
|
|
166
|
+
},
|
|
167
|
+
address: {
|
|
168
|
+
type: 'string',
|
|
169
|
+
description: 'Street address (max 500 chars)',
|
|
170
|
+
maxLength: 500,
|
|
171
|
+
},
|
|
172
|
+
place_label: {
|
|
173
|
+
type: 'string',
|
|
174
|
+
description: 'Short place name (max 200 chars)',
|
|
175
|
+
maxLength: 200,
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
required: ['lat', 'lng'],
|
|
179
|
+
},
|
|
124
180
|
},
|
|
125
181
|
required: ['text'],
|
|
126
182
|
};
|
|
@@ -213,18 +269,22 @@ const todoListSchema = {
|
|
|
213
269
|
description: 'Filter by project ID',
|
|
214
270
|
format: 'uuid',
|
|
215
271
|
},
|
|
216
|
-
|
|
217
|
-
type: '
|
|
218
|
-
description: 'Filter by
|
|
219
|
-
enum: ['pending', 'in_progress', 'completed', 'all'],
|
|
220
|
-
default: 'pending',
|
|
272
|
+
completed: {
|
|
273
|
+
type: 'boolean',
|
|
274
|
+
description: 'Filter by completion status. true = completed only, false = active only, omit = all.',
|
|
221
275
|
},
|
|
222
276
|
limit: {
|
|
223
277
|
type: 'integer',
|
|
224
278
|
description: 'Maximum number of todos to return',
|
|
225
279
|
minimum: 1,
|
|
226
|
-
maximum:
|
|
227
|
-
default:
|
|
280
|
+
maximum: 200,
|
|
281
|
+
default: 50,
|
|
282
|
+
},
|
|
283
|
+
offset: {
|
|
284
|
+
type: 'integer',
|
|
285
|
+
description: 'Offset for pagination',
|
|
286
|
+
minimum: 0,
|
|
287
|
+
default: 0,
|
|
228
288
|
},
|
|
229
289
|
},
|
|
230
290
|
};
|
|
@@ -278,6 +338,95 @@ const todoCompleteSchema = {
|
|
|
278
338
|
},
|
|
279
339
|
required: ['todoId'],
|
|
280
340
|
};
|
|
341
|
+
/**
|
|
342
|
+
* Todo search tool JSON Schema (Issue #1216)
|
|
343
|
+
*/
|
|
344
|
+
const todoSearchSchema = {
|
|
345
|
+
type: 'object',
|
|
346
|
+
properties: {
|
|
347
|
+
query: {
|
|
348
|
+
type: 'string',
|
|
349
|
+
description: 'Natural language search query for finding work items',
|
|
350
|
+
minLength: 1,
|
|
351
|
+
maxLength: 1000,
|
|
352
|
+
},
|
|
353
|
+
limit: {
|
|
354
|
+
type: 'integer',
|
|
355
|
+
description: 'Maximum number of results to return',
|
|
356
|
+
minimum: 1,
|
|
357
|
+
maximum: 50,
|
|
358
|
+
default: 10,
|
|
359
|
+
},
|
|
360
|
+
kind: {
|
|
361
|
+
type: 'string',
|
|
362
|
+
description: 'Filter by work item kind',
|
|
363
|
+
enum: ['task', 'project', 'initiative', 'epic', 'issue'],
|
|
364
|
+
},
|
|
365
|
+
status: {
|
|
366
|
+
type: 'string',
|
|
367
|
+
description: 'Filter by status (e.g., open, completed, in_progress)',
|
|
368
|
+
maxLength: 50,
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
required: ['query'],
|
|
372
|
+
};
|
|
373
|
+
/**
|
|
374
|
+
* Project search tool JSON Schema (Issue #1217)
|
|
375
|
+
*/
|
|
376
|
+
const projectSearchSchema = {
|
|
377
|
+
type: 'object',
|
|
378
|
+
properties: {
|
|
379
|
+
query: {
|
|
380
|
+
type: 'string',
|
|
381
|
+
description: 'Natural language search query for finding projects',
|
|
382
|
+
minLength: 1,
|
|
383
|
+
maxLength: 1000,
|
|
384
|
+
},
|
|
385
|
+
limit: {
|
|
386
|
+
type: 'integer',
|
|
387
|
+
description: 'Maximum number of results to return',
|
|
388
|
+
minimum: 1,
|
|
389
|
+
maximum: 50,
|
|
390
|
+
default: 10,
|
|
391
|
+
},
|
|
392
|
+
status: {
|
|
393
|
+
type: 'string',
|
|
394
|
+
description: 'Filter by project status',
|
|
395
|
+
enum: ['active', 'completed', 'archived'],
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
required: ['query'],
|
|
399
|
+
};
|
|
400
|
+
/**
|
|
401
|
+
* Context search tool JSON Schema (Issue #1219)
|
|
402
|
+
*/
|
|
403
|
+
const contextSearchSchema = {
|
|
404
|
+
type: 'object',
|
|
405
|
+
properties: {
|
|
406
|
+
query: {
|
|
407
|
+
type: 'string',
|
|
408
|
+
description: 'Natural language search query across memories, todos, projects, and messages',
|
|
409
|
+
minLength: 1,
|
|
410
|
+
maxLength: 1000,
|
|
411
|
+
},
|
|
412
|
+
entity_types: {
|
|
413
|
+
type: 'array',
|
|
414
|
+
description: 'Filter to specific entity types. Defaults to all (memory, todo, project, message).',
|
|
415
|
+
items: {
|
|
416
|
+
type: 'string',
|
|
417
|
+
enum: ['memory', 'todo', 'project', 'message'],
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
limit: {
|
|
421
|
+
type: 'integer',
|
|
422
|
+
description: 'Maximum number of results to return',
|
|
423
|
+
minimum: 1,
|
|
424
|
+
maximum: 50,
|
|
425
|
+
default: 10,
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
required: ['query'],
|
|
429
|
+
};
|
|
281
430
|
/**
|
|
282
431
|
* Contact search tool JSON Schema
|
|
283
432
|
*/
|
|
@@ -877,6 +1026,90 @@ const skillStoreAggregateSchema = {
|
|
|
877
1026
|
},
|
|
878
1027
|
required: ['skill_id', 'operation'],
|
|
879
1028
|
};
|
|
1029
|
+
/**
|
|
1030
|
+
* Entity linking tool JSON Schemas (Issue #1220)
|
|
1031
|
+
*/
|
|
1032
|
+
const linksSetSchema = {
|
|
1033
|
+
type: 'object',
|
|
1034
|
+
properties: {
|
|
1035
|
+
source_type: {
|
|
1036
|
+
type: 'string',
|
|
1037
|
+
description: 'Type of the source entity',
|
|
1038
|
+
enum: ['memory', 'todo', 'project', 'contact'],
|
|
1039
|
+
},
|
|
1040
|
+
source_id: {
|
|
1041
|
+
type: 'string',
|
|
1042
|
+
description: 'UUID of the source entity',
|
|
1043
|
+
format: 'uuid',
|
|
1044
|
+
},
|
|
1045
|
+
target_type: {
|
|
1046
|
+
type: 'string',
|
|
1047
|
+
description: 'Type of the target entity or external reference',
|
|
1048
|
+
enum: ['memory', 'todo', 'project', 'contact', 'github_issue', 'url'],
|
|
1049
|
+
},
|
|
1050
|
+
target_ref: {
|
|
1051
|
+
type: 'string',
|
|
1052
|
+
description: 'Reference to the target: UUID for internal entities, "owner/repo#N" for GitHub issues, URL for urls',
|
|
1053
|
+
minLength: 1,
|
|
1054
|
+
},
|
|
1055
|
+
label: {
|
|
1056
|
+
type: 'string',
|
|
1057
|
+
description: 'Optional label describing the link (e.g., "spawned from", "tracks", "related to")',
|
|
1058
|
+
maxLength: 100,
|
|
1059
|
+
},
|
|
1060
|
+
},
|
|
1061
|
+
required: ['source_type', 'source_id', 'target_type', 'target_ref'],
|
|
1062
|
+
};
|
|
1063
|
+
const linksQuerySchema = {
|
|
1064
|
+
type: 'object',
|
|
1065
|
+
properties: {
|
|
1066
|
+
entity_type: {
|
|
1067
|
+
type: 'string',
|
|
1068
|
+
description: 'Type of the entity to query links for',
|
|
1069
|
+
enum: ['memory', 'todo', 'project', 'contact'],
|
|
1070
|
+
},
|
|
1071
|
+
entity_id: {
|
|
1072
|
+
type: 'string',
|
|
1073
|
+
description: 'UUID of the entity to query links for',
|
|
1074
|
+
format: 'uuid',
|
|
1075
|
+
},
|
|
1076
|
+
link_types: {
|
|
1077
|
+
type: 'array',
|
|
1078
|
+
description: 'Optional filter to only return links to specific entity types',
|
|
1079
|
+
items: {
|
|
1080
|
+
type: 'string',
|
|
1081
|
+
enum: ['memory', 'todo', 'project', 'contact', 'github_issue', 'url'],
|
|
1082
|
+
},
|
|
1083
|
+
},
|
|
1084
|
+
},
|
|
1085
|
+
required: ['entity_type', 'entity_id'],
|
|
1086
|
+
};
|
|
1087
|
+
const linksRemoveSchema = {
|
|
1088
|
+
type: 'object',
|
|
1089
|
+
properties: {
|
|
1090
|
+
source_type: {
|
|
1091
|
+
type: 'string',
|
|
1092
|
+
description: 'Type of the source entity',
|
|
1093
|
+
enum: ['memory', 'todo', 'project', 'contact'],
|
|
1094
|
+
},
|
|
1095
|
+
source_id: {
|
|
1096
|
+
type: 'string',
|
|
1097
|
+
description: 'UUID of the source entity',
|
|
1098
|
+
format: 'uuid',
|
|
1099
|
+
},
|
|
1100
|
+
target_type: {
|
|
1101
|
+
type: 'string',
|
|
1102
|
+
description: 'Type of the target entity or external reference',
|
|
1103
|
+
enum: ['memory', 'todo', 'project', 'contact', 'github_issue', 'url'],
|
|
1104
|
+
},
|
|
1105
|
+
target_ref: {
|
|
1106
|
+
type: 'string',
|
|
1107
|
+
description: 'Reference to the target',
|
|
1108
|
+
minLength: 1,
|
|
1109
|
+
},
|
|
1110
|
+
},
|
|
1111
|
+
required: ['source_type', 'source_id', 'target_type', 'target_ref'],
|
|
1112
|
+
};
|
|
880
1113
|
/**
|
|
881
1114
|
* Create tool execution handlers
|
|
882
1115
|
*/
|
|
@@ -884,9 +1117,11 @@ function createToolHandlers(state) {
|
|
|
884
1117
|
const { config, logger, apiClient, userId } = state;
|
|
885
1118
|
return {
|
|
886
1119
|
async memory_recall(params) {
|
|
887
|
-
const { query, limit = config.maxRecallMemories, category, tags, relationship_id, } = params;
|
|
1120
|
+
const { query, limit = config.maxRecallMemories, category, tags, relationship_id, location, location_radius_km, location_weight, } = params;
|
|
888
1121
|
try {
|
|
889
|
-
|
|
1122
|
+
// Over-fetch when location is provided to allow geo re-ranking
|
|
1123
|
+
const apiLimit = location ? Math.min(limit * 3, 60) : limit;
|
|
1124
|
+
const queryParams = new URLSearchParams({ q: query, limit: String(apiLimit) });
|
|
890
1125
|
if (category)
|
|
891
1126
|
queryParams.set('memory_type', category);
|
|
892
1127
|
if (tags && tags.length > 0)
|
|
@@ -897,7 +1132,36 @@ function createToolHandlers(state) {
|
|
|
897
1132
|
if (!response.success) {
|
|
898
1133
|
return { success: false, error: response.error.message };
|
|
899
1134
|
}
|
|
900
|
-
|
|
1135
|
+
let memories = (response.data.results ?? []).map((m) => ({
|
|
1136
|
+
...m,
|
|
1137
|
+
category: m.type === 'note' ? 'other' : m.type,
|
|
1138
|
+
score: m.similarity,
|
|
1139
|
+
}));
|
|
1140
|
+
// Apply geo re-ranking if location is provided
|
|
1141
|
+
if (location) {
|
|
1142
|
+
const { lat: qLat, lng: qLng } = location;
|
|
1143
|
+
const weight = location_weight ?? 0.3;
|
|
1144
|
+
// Filter by radius if specified
|
|
1145
|
+
if (location_radius_km !== undefined) {
|
|
1146
|
+
memories = memories.filter((m) => {
|
|
1147
|
+
if (m.lat == null || m.lng == null)
|
|
1148
|
+
return false;
|
|
1149
|
+
return haversineDistanceKm(qLat, qLng, m.lat, m.lng) <= location_radius_km;
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
// Compute blended scores and re-sort
|
|
1153
|
+
memories = memories
|
|
1154
|
+
.map((m) => {
|
|
1155
|
+
const contentScore = m.score ?? 0;
|
|
1156
|
+
let geoScore = 0.5;
|
|
1157
|
+
if (m.lat != null && m.lng != null) {
|
|
1158
|
+
geoScore = computeGeoScore(haversineDistanceKm(qLat, qLng, m.lat, m.lng));
|
|
1159
|
+
}
|
|
1160
|
+
return { ...m, score: blendScores(contentScore, geoScore, weight) };
|
|
1161
|
+
})
|
|
1162
|
+
.sort((a, b) => (b.score ?? 0) - (a.score ?? 0))
|
|
1163
|
+
.slice(0, limit);
|
|
1164
|
+
}
|
|
901
1165
|
const content = memories.length > 0 ? memories.map((m) => `- [${m.type}] ${m.content}`).join('\n') : 'No relevant memories found.';
|
|
902
1166
|
return {
|
|
903
1167
|
success: true,
|
|
@@ -914,7 +1178,7 @@ function createToolHandlers(state) {
|
|
|
914
1178
|
},
|
|
915
1179
|
async memory_store(params) {
|
|
916
1180
|
// Accept 'text' (OpenClaw native) or 'content' (backwards compat)
|
|
917
|
-
const { text, content: contentAlias, category = 'other', importance = 0.7, tags, relationship_id, } = params;
|
|
1181
|
+
const { text, content: contentAlias, category = 'other', importance = 0.7, tags, relationship_id, location, } = params;
|
|
918
1182
|
const memoryText = text || contentAlias;
|
|
919
1183
|
if (!memoryText) {
|
|
920
1184
|
return { success: false, error: 'text is required' };
|
|
@@ -931,6 +1195,24 @@ function createToolHandlers(state) {
|
|
|
931
1195
|
payload.tags = tags;
|
|
932
1196
|
if (relationship_id)
|
|
933
1197
|
payload.relationship_id = relationship_id;
|
|
1198
|
+
if (location) {
|
|
1199
|
+
payload.lat = location.lat;
|
|
1200
|
+
payload.lng = location.lng;
|
|
1201
|
+
// Reverse geocode if address is missing and Nominatim is configured
|
|
1202
|
+
if (!location.address && config.nominatimUrl) {
|
|
1203
|
+
const geocoded = await reverseGeocode(location.lat, location.lng, config.nominatimUrl);
|
|
1204
|
+
if (geocoded) {
|
|
1205
|
+
payload.address = geocoded.address;
|
|
1206
|
+
if (!location.place_label && geocoded.placeLabel) {
|
|
1207
|
+
payload.place_label = geocoded.placeLabel;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
if (location.address)
|
|
1212
|
+
payload.address = location.address;
|
|
1213
|
+
if (location.place_label)
|
|
1214
|
+
payload.place_label = location.place_label;
|
|
1215
|
+
}
|
|
934
1216
|
const response = await apiClient.post('/api/memories/unified', payload, { userId });
|
|
935
1217
|
if (!response.success) {
|
|
936
1218
|
return { success: false, error: response.error.message };
|
|
@@ -1012,6 +1294,7 @@ function createToolHandlers(state) {
|
|
|
1012
1294
|
const queryParams = new URLSearchParams({ item_type: 'project', limit: String(limit) });
|
|
1013
1295
|
if (status !== 'all')
|
|
1014
1296
|
queryParams.set('status', status);
|
|
1297
|
+
queryParams.set('user_email', userId); // Issue #1172: scope by user
|
|
1015
1298
|
const response = await apiClient.get(`/api/work-items?${queryParams}`, { userId });
|
|
1016
1299
|
if (!response.success) {
|
|
1017
1300
|
return { success: false, error: response.error.message };
|
|
@@ -1031,7 +1314,7 @@ function createToolHandlers(state) {
|
|
|
1031
1314
|
async project_get(params) {
|
|
1032
1315
|
const { projectId } = params;
|
|
1033
1316
|
try {
|
|
1034
|
-
const response = await apiClient.get(`/api/work-items/${projectId}`, { userId });
|
|
1317
|
+
const response = await apiClient.get(`/api/work-items/${projectId}?user_email=${encodeURIComponent(userId)}`, { userId });
|
|
1035
1318
|
if (!response.success) {
|
|
1036
1319
|
return { success: false, error: response.error.message };
|
|
1037
1320
|
}
|
|
@@ -1052,7 +1335,7 @@ function createToolHandlers(state) {
|
|
|
1052
1335
|
async project_create(params) {
|
|
1053
1336
|
const { name, description, status = 'active', } = params;
|
|
1054
1337
|
try {
|
|
1055
|
-
const response = await apiClient.post('/api/work-items', { title: name, description, item_type: 'project', status }, { userId });
|
|
1338
|
+
const response = await apiClient.post('/api/work-items', { title: name, description, item_type: 'project', status, user_email: userId }, { userId });
|
|
1056
1339
|
if (!response.success) {
|
|
1057
1340
|
return { success: false, error: response.error.message };
|
|
1058
1341
|
}
|
|
@@ -1070,22 +1353,41 @@ function createToolHandlers(state) {
|
|
|
1070
1353
|
}
|
|
1071
1354
|
},
|
|
1072
1355
|
async todo_list(params) {
|
|
1073
|
-
const { projectId,
|
|
1356
|
+
const { projectId, completed, limit = 50, offset = 0, } = params;
|
|
1074
1357
|
try {
|
|
1075
|
-
const queryParams = new URLSearchParams({
|
|
1076
|
-
|
|
1077
|
-
|
|
1358
|
+
const queryParams = new URLSearchParams({
|
|
1359
|
+
item_type: 'task',
|
|
1360
|
+
limit: String(limit),
|
|
1361
|
+
offset: String(offset),
|
|
1362
|
+
user_email: userId, // Issue #1172: scope by user
|
|
1363
|
+
});
|
|
1078
1364
|
if (projectId)
|
|
1079
1365
|
queryParams.set('parent_work_item_id', projectId);
|
|
1366
|
+
if (completed !== undefined) {
|
|
1367
|
+
queryParams.set('status', completed ? 'completed' : 'active');
|
|
1368
|
+
}
|
|
1080
1369
|
const response = await apiClient.get(`/api/work-items?${queryParams}`, { userId });
|
|
1081
1370
|
if (!response.success) {
|
|
1082
1371
|
return { success: false, error: response.error.message };
|
|
1083
1372
|
}
|
|
1084
1373
|
const todos = response.data.items ?? [];
|
|
1085
|
-
const
|
|
1374
|
+
const total = response.data.total ?? todos.length;
|
|
1375
|
+
if (todos.length === 0) {
|
|
1376
|
+
return {
|
|
1377
|
+
success: true,
|
|
1378
|
+
data: { content: 'No todos found.', details: { count: 0, total: 0, todos: [] } },
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
const content = todos
|
|
1382
|
+
.map((t) => {
|
|
1383
|
+
const checkbox = t.completed ? '[x]' : '[ ]';
|
|
1384
|
+
const dueStr = t.dueDate ? ` (due: ${t.dueDate})` : '';
|
|
1385
|
+
return `- ${checkbox} ${t.title}${dueStr}`;
|
|
1386
|
+
})
|
|
1387
|
+
.join('\n');
|
|
1086
1388
|
return {
|
|
1087
1389
|
success: true,
|
|
1088
|
-
data: { content, details: { count: todos.length, todos } },
|
|
1390
|
+
data: { content, details: { count: todos.length, total, todos } },
|
|
1089
1391
|
};
|
|
1090
1392
|
}
|
|
1091
1393
|
catch (error) {
|
|
@@ -1096,7 +1398,7 @@ function createToolHandlers(state) {
|
|
|
1096
1398
|
async todo_create(params) {
|
|
1097
1399
|
const { title, description, projectId, priority = 'medium', dueDate, } = params;
|
|
1098
1400
|
try {
|
|
1099
|
-
const body = { title, description, item_type: 'task', priority };
|
|
1401
|
+
const body = { title, description, item_type: 'task', priority, user_email: userId };
|
|
1100
1402
|
if (projectId)
|
|
1101
1403
|
body.parent_work_item_id = projectId;
|
|
1102
1404
|
if (dueDate)
|
|
@@ -1121,7 +1423,7 @@ function createToolHandlers(state) {
|
|
|
1121
1423
|
async todo_complete(params) {
|
|
1122
1424
|
const { todoId } = params;
|
|
1123
1425
|
try {
|
|
1124
|
-
const response = await apiClient.patch(`/api/work-items/${todoId}/status`, { status: 'completed' }, { userId });
|
|
1426
|
+
const response = await apiClient.patch(`/api/work-items/${todoId}/status?user_email=${encodeURIComponent(userId)}`, { status: 'completed' }, { userId });
|
|
1125
1427
|
if (!response.success) {
|
|
1126
1428
|
return { success: false, error: response.error.message };
|
|
1127
1429
|
}
|
|
@@ -1135,10 +1437,87 @@ function createToolHandlers(state) {
|
|
|
1135
1437
|
return { success: false, error: 'Failed to complete todo' };
|
|
1136
1438
|
}
|
|
1137
1439
|
},
|
|
1440
|
+
async todo_search(params) {
|
|
1441
|
+
const { query, limit = 10, kind, status, } = params;
|
|
1442
|
+
if (!query || query.trim().length === 0) {
|
|
1443
|
+
return { success: false, error: 'query is required' };
|
|
1444
|
+
}
|
|
1445
|
+
try {
|
|
1446
|
+
// Over-fetch by 3x to compensate for client-side kind/status filtering (Issue #1216 review fix)
|
|
1447
|
+
const fetchLimit = kind || status ? Math.min(limit * 3, 50) : limit;
|
|
1448
|
+
const queryParams = new URLSearchParams({
|
|
1449
|
+
q: query.trim(),
|
|
1450
|
+
types: 'work_item',
|
|
1451
|
+
limit: String(fetchLimit),
|
|
1452
|
+
semantic: 'true',
|
|
1453
|
+
user_email: userId, // Issue #1216: scope results to current user
|
|
1454
|
+
});
|
|
1455
|
+
const response = await apiClient.get(`/api/search?${queryParams}`, { userId });
|
|
1456
|
+
if (!response.success) {
|
|
1457
|
+
return { success: false, error: response.error.message };
|
|
1458
|
+
}
|
|
1459
|
+
let results = response.data.results ?? [];
|
|
1460
|
+
// Client-side filtering by kind and status, then truncate to requested limit
|
|
1461
|
+
if (kind) {
|
|
1462
|
+
results = results.filter((r) => r.metadata?.kind === kind);
|
|
1463
|
+
}
|
|
1464
|
+
if (status) {
|
|
1465
|
+
results = results.filter((r) => r.metadata?.status === status);
|
|
1466
|
+
}
|
|
1467
|
+
results = results.slice(0, limit);
|
|
1468
|
+
if (results.length === 0) {
|
|
1469
|
+
return {
|
|
1470
|
+
success: true,
|
|
1471
|
+
data: {
|
|
1472
|
+
content: 'No matching work items found.',
|
|
1473
|
+
details: { count: 0, results: [], searchType: response.data.search_type },
|
|
1474
|
+
},
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
const content = results
|
|
1478
|
+
.map((r) => {
|
|
1479
|
+
const kindStr = r.metadata?.kind ? `[${r.metadata.kind}]` : '';
|
|
1480
|
+
const statusStr = r.metadata?.status ? ` (${r.metadata.status})` : '';
|
|
1481
|
+
const snippetStr = r.snippet ? ` - ${r.snippet}` : '';
|
|
1482
|
+
return `- ${kindStr} **${r.title}**${statusStr}${snippetStr}`;
|
|
1483
|
+
})
|
|
1484
|
+
.join('\n');
|
|
1485
|
+
return {
|
|
1486
|
+
success: true,
|
|
1487
|
+
data: {
|
|
1488
|
+
content,
|
|
1489
|
+
details: {
|
|
1490
|
+
count: results.length,
|
|
1491
|
+
results: results.map((r) => ({
|
|
1492
|
+
id: r.id,
|
|
1493
|
+
title: r.title,
|
|
1494
|
+
snippet: r.snippet,
|
|
1495
|
+
score: r.score,
|
|
1496
|
+
kind: r.metadata?.kind,
|
|
1497
|
+
status: r.metadata?.status,
|
|
1498
|
+
})),
|
|
1499
|
+
searchType: response.data.search_type,
|
|
1500
|
+
},
|
|
1501
|
+
},
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
catch (error) {
|
|
1505
|
+
logger.error('todo_search failed', { error });
|
|
1506
|
+
return { success: false, error: 'Failed to search work items' };
|
|
1507
|
+
}
|
|
1508
|
+
},
|
|
1509
|
+
async project_search(params) {
|
|
1510
|
+
const tool = createProjectSearchTool({ client: apiClient, logger, config, userId });
|
|
1511
|
+
return tool.execute(params);
|
|
1512
|
+
},
|
|
1513
|
+
async context_search(params) {
|
|
1514
|
+
const tool = createContextSearchTool({ client: apiClient, logger, config, userId });
|
|
1515
|
+
return tool.execute(params);
|
|
1516
|
+
},
|
|
1138
1517
|
async contact_search(params) {
|
|
1139
1518
|
const { query, limit = 10 } = params;
|
|
1140
1519
|
try {
|
|
1141
|
-
const queryParams = new URLSearchParams({ search: query, limit: String(limit) });
|
|
1520
|
+
const queryParams = new URLSearchParams({ search: query, limit: String(limit), user_email: userId });
|
|
1142
1521
|
const response = await apiClient.get(`/api/contacts?${queryParams}`, {
|
|
1143
1522
|
userId,
|
|
1144
1523
|
});
|
|
@@ -1160,9 +1539,7 @@ function createToolHandlers(state) {
|
|
|
1160
1539
|
async contact_get(params) {
|
|
1161
1540
|
const { contactId } = params;
|
|
1162
1541
|
try {
|
|
1163
|
-
const response = await apiClient.get(`/api/contacts/${contactId}`, {
|
|
1164
|
-
userId,
|
|
1165
|
-
});
|
|
1542
|
+
const response = await apiClient.get(`/api/contacts/${contactId}?user_email=${encodeURIComponent(userId)}`, { userId });
|
|
1166
1543
|
if (!response.success) {
|
|
1167
1544
|
return { success: false, error: response.error.message };
|
|
1168
1545
|
}
|
|
@@ -1188,7 +1565,7 @@ function createToolHandlers(state) {
|
|
|
1188
1565
|
const { name, notes } = params;
|
|
1189
1566
|
try {
|
|
1190
1567
|
// 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 });
|
|
1568
|
+
const response = await apiClient.post('/api/contacts', { displayName: name, notes, user_email: userId }, { userId });
|
|
1192
1569
|
if (!response.success) {
|
|
1193
1570
|
return { success: false, error: response.error.message };
|
|
1194
1571
|
}
|
|
@@ -1285,13 +1662,6 @@ function createToolHandlers(state) {
|
|
|
1285
1662
|
},
|
|
1286
1663
|
async email_send(params) {
|
|
1287
1664
|
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
1665
|
// Validate email format
|
|
1296
1666
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1297
1667
|
if (!emailRegex.test(to)) {
|
|
@@ -1329,7 +1699,7 @@ function createToolHandlers(state) {
|
|
|
1329
1699
|
hasIdempotencyKey: !!idempotencyKey,
|
|
1330
1700
|
});
|
|
1331
1701
|
try {
|
|
1332
|
-
const response = await apiClient.post('/api/email/
|
|
1702
|
+
const response = await apiClient.post('/api/postmark/email/send', { to, subject, body, htmlBody, threadId, idempotencyKey }, { userId });
|
|
1333
1703
|
if (!response.success) {
|
|
1334
1704
|
logger.error('email_send API error', {
|
|
1335
1705
|
userId,
|
|
@@ -1432,15 +1802,50 @@ function createToolHandlers(state) {
|
|
|
1432
1802
|
resultCount: messages.length,
|
|
1433
1803
|
total,
|
|
1434
1804
|
});
|
|
1435
|
-
//
|
|
1805
|
+
// SECURITY: Run injection detection on the FULL message body BEFORE any
|
|
1806
|
+
// truncation. Bodies are later truncated to 100 chars for display, but
|
|
1807
|
+
// detection must see the complete content to catch payloads that an
|
|
1808
|
+
// attacker could hide beyond the truncation boundary. (Issue #1258)
|
|
1809
|
+
// Rate-limited to prevent log flooding from volume attacks. (#1257)
|
|
1810
|
+
for (const m of messages) {
|
|
1811
|
+
if (m.direction === 'inbound' && m.body) {
|
|
1812
|
+
const detection = await detectInjectionPatternsAsync(m.body, {
|
|
1813
|
+
promptGuardUrl: config.promptGuardUrl,
|
|
1814
|
+
});
|
|
1815
|
+
if (detection.detected) {
|
|
1816
|
+
const logDecision = injectionLogLimiter.shouldLog(userId);
|
|
1817
|
+
if (logDecision.log) {
|
|
1818
|
+
logger.warn(logDecision.summary ? 'injection detection log summary for previous window' : 'potential prompt injection detected in message_search result', {
|
|
1819
|
+
userId,
|
|
1820
|
+
messageId: m.id,
|
|
1821
|
+
patterns: detection.patterns,
|
|
1822
|
+
source: detection.source,
|
|
1823
|
+
...(logDecision.suppressed > 0 && { suppressedCount: logDecision.suppressed }),
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
// Format content for display with injection protection.
|
|
1830
|
+
// NOTE: Truncation happens here AFTER detection above — do not reorder.
|
|
1831
|
+
// Generate a per-invocation nonce for boundary markers (#1255)
|
|
1832
|
+
const { nonce } = createBoundaryMarkers();
|
|
1436
1833
|
const content = messages.length > 0
|
|
1437
1834
|
? messages
|
|
1438
1835
|
.map((m) => {
|
|
1439
1836
|
const prefix = m.direction === 'inbound' ? '←' : '→';
|
|
1440
|
-
const contact = m.contactName || 'Unknown';
|
|
1837
|
+
const contact = sanitizeMetadataField(m.contactName || 'Unknown', nonce);
|
|
1838
|
+
const safeChannel = sanitizeMetadataField(m.channel, nonce);
|
|
1441
1839
|
const similarity = `(${Math.round(m.similarity * 100)}%)`;
|
|
1442
|
-
const
|
|
1443
|
-
|
|
1840
|
+
const rawBody = m.body || '';
|
|
1841
|
+
const truncatedBody = rawBody.substring(0, 100) + (rawBody.length > 100 ? '...' : '');
|
|
1842
|
+
const bodyText = sanitizeMessageForContext(truncatedBody, {
|
|
1843
|
+
direction: m.direction,
|
|
1844
|
+
channel: m.channel,
|
|
1845
|
+
sender: m.contactName || 'Unknown',
|
|
1846
|
+
nonce,
|
|
1847
|
+
});
|
|
1848
|
+
return `${prefix} [${safeChannel}] ${contact} ${similarity}: ${bodyText}`;
|
|
1444
1849
|
})
|
|
1445
1850
|
.join('\n')
|
|
1446
1851
|
: 'No messages found matching your query.';
|
|
@@ -1502,17 +1907,24 @@ function createToolHandlers(state) {
|
|
|
1502
1907
|
threadCount: results.length,
|
|
1503
1908
|
total,
|
|
1504
1909
|
});
|
|
1910
|
+
// Format content with injection protection.
|
|
1911
|
+
// Sanitize all fields that may contain external message content.
|
|
1912
|
+
// Generate a per-invocation nonce for boundary markers (#1255)
|
|
1913
|
+
const { nonce: threadListNonce } = createBoundaryMarkers();
|
|
1505
1914
|
const content = results.length > 0
|
|
1506
1915
|
? results
|
|
1507
1916
|
.map((r) => {
|
|
1508
1917
|
// Handle both thread and search result formats
|
|
1509
1918
|
if ('channel' in r) {
|
|
1510
1919
|
const t = r;
|
|
1511
|
-
const
|
|
1920
|
+
const safeContact = sanitizeMetadataField(t.contactName || t.endpointValue || 'Unknown', threadListNonce);
|
|
1921
|
+
const safeChannel = sanitizeMetadataField(t.channel, threadListNonce);
|
|
1512
1922
|
const msgCount = t.messageCount ? `${t.messageCount} message${t.messageCount !== 1 ? 's' : ''}` : '';
|
|
1513
|
-
return `[${
|
|
1923
|
+
return `[${safeChannel}] ${safeContact}${msgCount ? ` - ${msgCount}` : ''}`;
|
|
1514
1924
|
}
|
|
1515
|
-
|
|
1925
|
+
const safeTitle = r.title ? sanitizeMetadataField(r.title, threadListNonce) : '';
|
|
1926
|
+
const wrappedSnippet = r.snippet ? wrapExternalMessage(r.snippet, { nonce: threadListNonce }) : '';
|
|
1927
|
+
return `- ${safeTitle || wrappedSnippet || r.id}`;
|
|
1516
1928
|
})
|
|
1517
1929
|
.join('\n')
|
|
1518
1930
|
: 'No threads found.';
|
|
@@ -1571,14 +1983,45 @@ function createToolHandlers(state) {
|
|
|
1571
1983
|
threadId,
|
|
1572
1984
|
messageCount: messages.length,
|
|
1573
1985
|
});
|
|
1574
|
-
|
|
1575
|
-
const
|
|
1986
|
+
// Generate a per-invocation nonce for boundary markers (#1255)
|
|
1987
|
+
const { nonce: threadGetNonce } = createBoundaryMarkers();
|
|
1988
|
+
const contact = sanitizeMetadataField(thread.contactName || thread.endpointValue || 'Unknown', threadGetNonce);
|
|
1989
|
+
const safeChannel = sanitizeMetadataField(thread.channel, threadGetNonce);
|
|
1990
|
+
const header = `Thread with ${contact} [${safeChannel}]`;
|
|
1991
|
+
// Detect and log potential injection patterns in inbound messages
|
|
1992
|
+
// Rate-limited to prevent log flooding from volume attacks. (#1257)
|
|
1993
|
+
for (const m of messages) {
|
|
1994
|
+
if (m.direction === 'inbound' && m.body) {
|
|
1995
|
+
const detection = await detectInjectionPatternsAsync(m.body, {
|
|
1996
|
+
promptGuardUrl: config.promptGuardUrl,
|
|
1997
|
+
});
|
|
1998
|
+
if (detection.detected) {
|
|
1999
|
+
const logDecision = injectionLogLimiter.shouldLog(userId);
|
|
2000
|
+
if (logDecision.log) {
|
|
2001
|
+
logger.warn(logDecision.summary ? 'injection detection log summary for previous window' : 'potential prompt injection detected in thread_get result', {
|
|
2002
|
+
userId,
|
|
2003
|
+
threadId,
|
|
2004
|
+
messageId: m.id,
|
|
2005
|
+
patterns: detection.patterns,
|
|
2006
|
+
source: detection.source,
|
|
2007
|
+
...(logDecision.suppressed > 0 && { suppressedCount: logDecision.suppressed }),
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
1576
2013
|
const messageContent = messages.length > 0
|
|
1577
2014
|
? messages
|
|
1578
2015
|
.map((m) => {
|
|
1579
2016
|
const prefix = m.direction === 'inbound' ? '←' : '→';
|
|
1580
2017
|
const timestamp = new Date(m.createdAt).toLocaleString();
|
|
1581
|
-
|
|
2018
|
+
const body = sanitizeMessageForContext(m.body || '', {
|
|
2019
|
+
direction: m.direction,
|
|
2020
|
+
channel: thread.channel,
|
|
2021
|
+
sender: contact,
|
|
2022
|
+
nonce: threadGetNonce,
|
|
2023
|
+
});
|
|
2024
|
+
return `${prefix} [${timestamp}] ${body}`;
|
|
1582
2025
|
})
|
|
1583
2026
|
.join('\n')
|
|
1584
2027
|
: 'No messages in this thread.';
|
|
@@ -1623,6 +2066,7 @@ function createToolHandlers(state) {
|
|
|
1623
2066
|
contact_a,
|
|
1624
2067
|
contact_b,
|
|
1625
2068
|
relationship_type: relationship,
|
|
2069
|
+
user_email: userId, // Issue #1172: scope by user
|
|
1626
2070
|
};
|
|
1627
2071
|
if (notes) {
|
|
1628
2072
|
body.notes = notes;
|
|
@@ -1669,15 +2113,40 @@ function createToolHandlers(state) {
|
|
|
1669
2113
|
hasTypeFilter: !!type_filter,
|
|
1670
2114
|
});
|
|
1671
2115
|
try {
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
2116
|
+
// Resolve contact to a UUID — accept UUID directly or search by name
|
|
2117
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2118
|
+
let contactId;
|
|
2119
|
+
if (uuidRegex.test(contact)) {
|
|
2120
|
+
contactId = contact;
|
|
1675
2121
|
}
|
|
1676
|
-
|
|
2122
|
+
else {
|
|
2123
|
+
// Search for contact by name (Issue #1172: scope by user_email)
|
|
2124
|
+
const searchParams = new URLSearchParams({ search: contact, limit: '1', user_email: userId });
|
|
2125
|
+
const searchResponse = await apiClient.get(`/api/contacts?${searchParams}`, { userId });
|
|
2126
|
+
if (!searchResponse.success) {
|
|
2127
|
+
return { success: false, error: searchResponse.error.message };
|
|
2128
|
+
}
|
|
2129
|
+
const contacts = searchResponse.data.contacts ?? [];
|
|
2130
|
+
if (contacts.length === 0) {
|
|
2131
|
+
return { success: false, error: 'Contact not found.' };
|
|
2132
|
+
}
|
|
2133
|
+
contactId = contacts[0].id;
|
|
2134
|
+
}
|
|
2135
|
+
// Use graph traversal endpoint which returns relatedContacts
|
|
2136
|
+
const response = await apiClient.get(`/api/contacts/${contactId}/relationships?user_email=${encodeURIComponent(userId)}`, { userId });
|
|
1677
2137
|
if (!response.success) {
|
|
2138
|
+
if (response.error.code === 'NOT_FOUND') {
|
|
2139
|
+
return { success: false, error: 'Contact not found.' };
|
|
2140
|
+
}
|
|
1678
2141
|
return { success: false, error: response.error.message };
|
|
1679
2142
|
}
|
|
1680
|
-
|
|
2143
|
+
let { relatedContacts } = response.data;
|
|
2144
|
+
const { contactName } = response.data;
|
|
2145
|
+
// Apply type_filter client-side if provided
|
|
2146
|
+
if (type_filter && relatedContacts.length > 0) {
|
|
2147
|
+
const filterLower = type_filter.toLowerCase();
|
|
2148
|
+
relatedContacts = relatedContacts.filter((rel) => rel.relationshipTypeName.toLowerCase().includes(filterLower) || rel.relationshipTypeLabel.toLowerCase().includes(filterLower));
|
|
2149
|
+
}
|
|
1681
2150
|
if (relatedContacts.length === 0) {
|
|
1682
2151
|
return {
|
|
1683
2152
|
success: true,
|
|
@@ -1825,6 +2294,18 @@ function createToolHandlers(state) {
|
|
|
1825
2294
|
skill_store_aggregate: (params) => aggregateTool.execute(params),
|
|
1826
2295
|
};
|
|
1827
2296
|
})(),
|
|
2297
|
+
// Entity link tools: delegate to tool modules (Issue #1220)
|
|
2298
|
+
...(() => {
|
|
2299
|
+
const toolOptions = { client: apiClient, logger, config, userId };
|
|
2300
|
+
const setTool = createLinksSetTool(toolOptions);
|
|
2301
|
+
const queryTool = createLinksQueryTool(toolOptions);
|
|
2302
|
+
const removeTool = createLinksRemoveTool(toolOptions);
|
|
2303
|
+
return {
|
|
2304
|
+
links_set: (params) => setTool.execute(params),
|
|
2305
|
+
links_query: (params) => queryTool.execute(params),
|
|
2306
|
+
links_remove: (params) => removeTool.execute(params),
|
|
2307
|
+
};
|
|
2308
|
+
})(),
|
|
1828
2309
|
};
|
|
1829
2310
|
}
|
|
1830
2311
|
/**
|
|
@@ -1876,7 +2357,7 @@ export const registerOpenClaw = (api) => {
|
|
|
1876
2357
|
const state = { config, logger, apiClient, userId };
|
|
1877
2358
|
// Create tool handlers
|
|
1878
2359
|
const handlers = createToolHandlers(state);
|
|
1879
|
-
// Register all
|
|
2360
|
+
// Register all 30 tools with correct OpenClaw Gateway execute signature
|
|
1880
2361
|
// Signature: (toolCallId: string, params: T, signal?: AbortSignal, onUpdate?: (partial: any) => void) => AgentToolResult
|
|
1881
2362
|
const tools = [
|
|
1882
2363
|
{
|
|
@@ -1960,6 +2441,33 @@ export const registerOpenClaw = (api) => {
|
|
|
1960
2441
|
return toAgentToolResult(result);
|
|
1961
2442
|
},
|
|
1962
2443
|
},
|
|
2444
|
+
{
|
|
2445
|
+
name: 'todo_search',
|
|
2446
|
+
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.',
|
|
2447
|
+
parameters: todoSearchSchema,
|
|
2448
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
2449
|
+
const result = await handlers.todo_search(params);
|
|
2450
|
+
return toAgentToolResult(result);
|
|
2451
|
+
},
|
|
2452
|
+
},
|
|
2453
|
+
{
|
|
2454
|
+
name: 'project_search',
|
|
2455
|
+
description: 'Search projects by natural language query. Uses semantic and text search to find relevant projects. Optionally filter by status (active, completed, archived).',
|
|
2456
|
+
parameters: projectSearchSchema,
|
|
2457
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
2458
|
+
const result = await handlers.project_search(params);
|
|
2459
|
+
return toAgentToolResult(result);
|
|
2460
|
+
},
|
|
2461
|
+
},
|
|
2462
|
+
{
|
|
2463
|
+
name: 'context_search',
|
|
2464
|
+
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.',
|
|
2465
|
+
parameters: contextSearchSchema,
|
|
2466
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
2467
|
+
const result = await handlers.context_search(params);
|
|
2468
|
+
return toAgentToolResult(result);
|
|
2469
|
+
},
|
|
2470
|
+
},
|
|
1963
2471
|
{
|
|
1964
2472
|
name: 'contact_search',
|
|
1965
2473
|
description: 'Search contacts by name, email, or other fields. Use to find people.',
|
|
@@ -2122,6 +2630,33 @@ export const registerOpenClaw = (api) => {
|
|
|
2122
2630
|
return toAgentToolResult(result);
|
|
2123
2631
|
},
|
|
2124
2632
|
},
|
|
2633
|
+
{
|
|
2634
|
+
name: 'links_set',
|
|
2635
|
+
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.',
|
|
2636
|
+
parameters: linksSetSchema,
|
|
2637
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
2638
|
+
const result = await handlers.links_set(params);
|
|
2639
|
+
return toAgentToolResult(result);
|
|
2640
|
+
},
|
|
2641
|
+
},
|
|
2642
|
+
{
|
|
2643
|
+
name: 'links_query',
|
|
2644
|
+
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.',
|
|
2645
|
+
parameters: linksQuerySchema,
|
|
2646
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
2647
|
+
const result = await handlers.links_query(params);
|
|
2648
|
+
return toAgentToolResult(result);
|
|
2649
|
+
},
|
|
2650
|
+
},
|
|
2651
|
+
{
|
|
2652
|
+
name: 'links_remove',
|
|
2653
|
+
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.',
|
|
2654
|
+
parameters: linksRemoveSchema,
|
|
2655
|
+
execute: async (_toolCallId, params, _signal, _onUpdate) => {
|
|
2656
|
+
const result = await handlers.links_remove(params);
|
|
2657
|
+
return toAgentToolResult(result);
|
|
2658
|
+
},
|
|
2659
|
+
},
|
|
2125
2660
|
];
|
|
2126
2661
|
for (const tool of tools) {
|
|
2127
2662
|
api.registerTool(tool);
|
|
@@ -2228,6 +2763,54 @@ export const registerOpenClaw = (api) => {
|
|
|
2228
2763
|
api.registerHook('agentEnd', agentEndHandler);
|
|
2229
2764
|
}
|
|
2230
2765
|
}
|
|
2766
|
+
// Register auto-linking hook for inbound messages (Issue #1223)
|
|
2767
|
+
// When an inbound SMS/email arrives, automatically link the thread to
|
|
2768
|
+
// matching contacts (by sender email/phone) and related projects/todos
|
|
2769
|
+
// (by semantic content matching).
|
|
2770
|
+
{
|
|
2771
|
+
/**
|
|
2772
|
+
* message_received handler: Extracts sender and content info from the
|
|
2773
|
+
* inbound message event and runs auto-linking in the background.
|
|
2774
|
+
* Failures are logged but never crash message processing.
|
|
2775
|
+
*/
|
|
2776
|
+
const messageReceivedHandler = async (event, _ctx) => {
|
|
2777
|
+
// Skip if no thread ID (nothing to link to)
|
|
2778
|
+
if (!event.threadId) {
|
|
2779
|
+
logger.debug('Auto-link skipped: no threadId in message_received event');
|
|
2780
|
+
return;
|
|
2781
|
+
}
|
|
2782
|
+
// Skip if no content and no sender info (nothing to match on)
|
|
2783
|
+
if (!event.content && !event.senderEmail && !event.senderPhone && !event.sender) {
|
|
2784
|
+
logger.debug('Auto-link skipped: no content or sender info in event');
|
|
2785
|
+
return;
|
|
2786
|
+
}
|
|
2787
|
+
try {
|
|
2788
|
+
await autoLinkInboundMessage({
|
|
2789
|
+
client: apiClient,
|
|
2790
|
+
logger,
|
|
2791
|
+
userId,
|
|
2792
|
+
message: {
|
|
2793
|
+
threadId: event.threadId,
|
|
2794
|
+
senderEmail: event.senderEmail ?? (event.sender?.includes('@') ? event.sender : undefined),
|
|
2795
|
+
senderPhone: event.senderPhone ?? (event.sender && !event.sender.includes('@') ? event.sender : undefined),
|
|
2796
|
+
content: event.content ?? '',
|
|
2797
|
+
},
|
|
2798
|
+
});
|
|
2799
|
+
}
|
|
2800
|
+
catch (error) {
|
|
2801
|
+
// Hook errors should never crash inbound message processing
|
|
2802
|
+
logger.error('Auto-link hook failed', {
|
|
2803
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2804
|
+
});
|
|
2805
|
+
}
|
|
2806
|
+
};
|
|
2807
|
+
if (typeof api.on === 'function') {
|
|
2808
|
+
api.on('message_received', messageReceivedHandler);
|
|
2809
|
+
}
|
|
2810
|
+
else {
|
|
2811
|
+
api.registerHook('messageReceived', messageReceivedHandler);
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2231
2814
|
// Register Gateway RPC methods (Issue #324)
|
|
2232
2815
|
const gatewayMethods = createGatewayMethods({
|
|
2233
2816
|
logger,
|
|
@@ -2327,6 +2910,8 @@ export const schemas = {
|
|
|
2327
2910
|
todoList: todoListSchema,
|
|
2328
2911
|
todoCreate: todoCreateSchema,
|
|
2329
2912
|
todoComplete: todoCompleteSchema,
|
|
2913
|
+
todoSearch: todoSearchSchema,
|
|
2914
|
+
projectSearch: projectSearchSchema,
|
|
2330
2915
|
contactSearch: contactSearchSchema,
|
|
2331
2916
|
contactGet: contactGetSchema,
|
|
2332
2917
|
contactCreate: contactCreateSchema,
|