@prmichaelsen/remember-mcp 0.1.0 → 0.2.0
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/agent/progress.yaml +96 -36
- package/dist/server-factory.js +1075 -1
- package/dist/server.js +1075 -1
- package/dist/tools/create-relationship.d.ts +79 -0
- package/dist/tools/delete-relationship.d.ts +41 -0
- package/dist/tools/find-similar.d.ts +77 -0
- package/dist/tools/query-memory.d.ts +115 -0
- package/dist/tools/search-relationship.d.ts +86 -0
- package/dist/tools/update-memory.d.ts +92 -0
- package/dist/tools/update-relationship.d.ts +74 -0
- package/package.json +1 -1
- package/src/server-factory.ts +46 -0
- package/src/server.ts +46 -0
- package/src/tools/create-relationship.ts +242 -0
- package/src/tools/delete-relationship.ts +160 -0
- package/src/tools/find-similar.ts +199 -0
- package/src/tools/query-memory.ts +284 -0
- package/src/tools/search-relationship.ts +228 -0
- package/src/tools/update-memory.ts +230 -0
- package/src/tools/update-relationship.ts +189 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* remember_query_memory tool
|
|
3
|
+
* RAG (Retrieval-Augmented Generation) queries with natural language
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Memory, SearchFilters } from '../types/memory.js';
|
|
7
|
+
import { getMemoryCollection } from '../weaviate/schema.js';
|
|
8
|
+
import { logger } from '../utils/logger.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Tool definition for remember_query_memory
|
|
12
|
+
*/
|
|
13
|
+
export const queryMemoryTool = {
|
|
14
|
+
name: 'remember_query_memory',
|
|
15
|
+
description: `Query memories using natural language for RAG (Retrieval-Augmented Generation).
|
|
16
|
+
|
|
17
|
+
This tool is optimized for LLM context retrieval. It returns relevant memories
|
|
18
|
+
with their full content and context, formatted for easy consumption by LLMs.
|
|
19
|
+
|
|
20
|
+
Use this when you need to:
|
|
21
|
+
- Answer questions based on stored memories
|
|
22
|
+
- Provide context for conversations
|
|
23
|
+
- Retrieve information for decision-making
|
|
24
|
+
- Build responses using past knowledge
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
- "What do I know about camping?"
|
|
28
|
+
- "Tell me about the recipes I've saved"
|
|
29
|
+
- "What meetings did I have last week?"
|
|
30
|
+
- "What are my project goals?"
|
|
31
|
+
`,
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
query: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
description: 'Natural language query',
|
|
38
|
+
},
|
|
39
|
+
limit: {
|
|
40
|
+
type: 'number',
|
|
41
|
+
description: 'Maximum number of memories to retrieve. Default: 5',
|
|
42
|
+
minimum: 1,
|
|
43
|
+
maximum: 50,
|
|
44
|
+
default: 5,
|
|
45
|
+
},
|
|
46
|
+
min_relevance: {
|
|
47
|
+
type: 'number',
|
|
48
|
+
description: 'Minimum relevance score (0-1). Default: 0.6',
|
|
49
|
+
minimum: 0,
|
|
50
|
+
maximum: 1,
|
|
51
|
+
default: 0.6,
|
|
52
|
+
},
|
|
53
|
+
filters: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
description: 'Optional filters to narrow results',
|
|
56
|
+
properties: {
|
|
57
|
+
types: {
|
|
58
|
+
type: 'array',
|
|
59
|
+
items: { type: 'string' },
|
|
60
|
+
description: 'Filter by content types',
|
|
61
|
+
},
|
|
62
|
+
tags: {
|
|
63
|
+
type: 'array',
|
|
64
|
+
items: { type: 'string' },
|
|
65
|
+
description: 'Filter by tags',
|
|
66
|
+
},
|
|
67
|
+
weight_min: {
|
|
68
|
+
type: 'number',
|
|
69
|
+
description: 'Minimum weight (0-1)',
|
|
70
|
+
},
|
|
71
|
+
trust_min: {
|
|
72
|
+
type: 'number',
|
|
73
|
+
description: 'Minimum trust level (0-1)',
|
|
74
|
+
},
|
|
75
|
+
date_from: {
|
|
76
|
+
type: 'string',
|
|
77
|
+
description: 'Start date (ISO 8601)',
|
|
78
|
+
},
|
|
79
|
+
date_to: {
|
|
80
|
+
type: 'string',
|
|
81
|
+
description: 'End date (ISO 8601)',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
include_context: {
|
|
86
|
+
type: 'boolean',
|
|
87
|
+
description: 'Include full context metadata. Default: true',
|
|
88
|
+
default: true,
|
|
89
|
+
},
|
|
90
|
+
format: {
|
|
91
|
+
type: 'string',
|
|
92
|
+
description: 'Output format: "detailed" (full objects) or "compact" (text summary). Default: detailed',
|
|
93
|
+
enum: ['detailed', 'compact'],
|
|
94
|
+
default: 'detailed',
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
required: ['query'],
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Query memory arguments
|
|
103
|
+
*/
|
|
104
|
+
export interface QueryMemoryArgs {
|
|
105
|
+
query: string;
|
|
106
|
+
limit?: number;
|
|
107
|
+
min_relevance?: number;
|
|
108
|
+
filters?: SearchFilters;
|
|
109
|
+
include_context?: boolean;
|
|
110
|
+
format?: 'detailed' | 'compact';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Memory with relevance score
|
|
115
|
+
*/
|
|
116
|
+
export interface RelevantMemory extends Partial<Memory> {
|
|
117
|
+
relevance: number; // 0-1 relevance score
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Query memory result
|
|
122
|
+
*/
|
|
123
|
+
export interface QueryMemoryResult {
|
|
124
|
+
query: string;
|
|
125
|
+
memories: RelevantMemory[] | string; // Array for detailed, string for compact
|
|
126
|
+
total: number;
|
|
127
|
+
min_relevance: number;
|
|
128
|
+
context_summary?: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Handle remember_query_memory tool
|
|
133
|
+
*/
|
|
134
|
+
export async function handleQueryMemory(
|
|
135
|
+
args: QueryMemoryArgs,
|
|
136
|
+
userId: string
|
|
137
|
+
): Promise<string> {
|
|
138
|
+
try {
|
|
139
|
+
logger.info('Querying memories', { userId, query: args.query });
|
|
140
|
+
|
|
141
|
+
const collection = getMemoryCollection(userId);
|
|
142
|
+
const limit = args.limit ?? 5;
|
|
143
|
+
const minRelevance = args.min_relevance ?? 0.6;
|
|
144
|
+
const includeContext = args.include_context ?? true;
|
|
145
|
+
const format = args.format ?? 'detailed';
|
|
146
|
+
|
|
147
|
+
// Build where filter for memories only
|
|
148
|
+
const whereFilters: any[] = [
|
|
149
|
+
{
|
|
150
|
+
path: 'doc_type',
|
|
151
|
+
operator: 'Equal',
|
|
152
|
+
valueText: 'memory',
|
|
153
|
+
},
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
// Add type filter
|
|
157
|
+
if (args.filters?.types && args.filters.types.length > 0) {
|
|
158
|
+
whereFilters.push({
|
|
159
|
+
path: 'type',
|
|
160
|
+
operator: 'ContainsAny',
|
|
161
|
+
valueTextArray: args.filters.types,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Add weight filter
|
|
166
|
+
if (args.filters?.weight_min !== undefined) {
|
|
167
|
+
whereFilters.push({
|
|
168
|
+
path: 'weight',
|
|
169
|
+
operator: 'GreaterThanEqual',
|
|
170
|
+
valueNumber: args.filters.weight_min,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Add trust filter
|
|
175
|
+
if (args.filters?.trust_min !== undefined) {
|
|
176
|
+
whereFilters.push({
|
|
177
|
+
path: 'trust',
|
|
178
|
+
operator: 'GreaterThanEqual',
|
|
179
|
+
valueNumber: args.filters.trust_min,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Add date range filters
|
|
184
|
+
if (args.filters?.date_from) {
|
|
185
|
+
whereFilters.push({
|
|
186
|
+
path: 'created_at',
|
|
187
|
+
operator: 'GreaterThanEqual',
|
|
188
|
+
valueDate: new Date(args.filters.date_from),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (args.filters?.date_to) {
|
|
193
|
+
whereFilters.push({
|
|
194
|
+
path: 'created_at',
|
|
195
|
+
operator: 'LessThanEqual',
|
|
196
|
+
valueDate: new Date(args.filters.date_to),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Build search options
|
|
201
|
+
const searchOptions: any = {
|
|
202
|
+
limit: limit,
|
|
203
|
+
distance: 1 - minRelevance, // Convert relevance to distance
|
|
204
|
+
returnMetadata: ['distance'],
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Add filters if present
|
|
208
|
+
if (whereFilters.length > 0) {
|
|
209
|
+
searchOptions.filters = whereFilters.length > 1 ? {
|
|
210
|
+
operator: 'And' as const,
|
|
211
|
+
operands: whereFilters,
|
|
212
|
+
} : whereFilters[0];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Perform semantic search using nearText
|
|
216
|
+
const results = await collection.query.nearText(args.query, searchOptions);
|
|
217
|
+
|
|
218
|
+
// Format memories with relevance scores
|
|
219
|
+
const relevantMemories: RelevantMemory[] = results.objects.map((obj: any) => {
|
|
220
|
+
const relevance = 1 - (obj.metadata?.distance ?? 0); // Convert distance to relevance
|
|
221
|
+
const memory: RelevantMemory = {
|
|
222
|
+
id: obj.uuid,
|
|
223
|
+
...obj.properties,
|
|
224
|
+
relevance: Math.max(0, Math.min(1, relevance)), // Clamp to [0, 1]
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// Remove context if not requested
|
|
228
|
+
if (!includeContext) {
|
|
229
|
+
delete memory.context;
|
|
230
|
+
delete memory.location;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return memory;
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Sort by relevance (highest first)
|
|
237
|
+
relevantMemories.sort((a, b) => (b.relevance ?? 0) - (a.relevance ?? 0));
|
|
238
|
+
|
|
239
|
+
logger.info('Query completed', {
|
|
240
|
+
userId,
|
|
241
|
+
query: args.query,
|
|
242
|
+
results: relevantMemories.length,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Format output based on requested format
|
|
246
|
+
let formattedMemories: RelevantMemory[] | string;
|
|
247
|
+
let contextSummary: string | undefined;
|
|
248
|
+
|
|
249
|
+
if (format === 'compact') {
|
|
250
|
+
// Compact format: text summary for easy LLM consumption
|
|
251
|
+
const summaryParts = relevantMemories.map((mem, idx) => {
|
|
252
|
+
const title = mem.title ? `"${mem.title}"` : `Memory ${idx + 1}`;
|
|
253
|
+
const type = mem.type ? ` [${mem.type}]` : '';
|
|
254
|
+
const relevancePercent = Math.round((mem.relevance ?? 0) * 100);
|
|
255
|
+
const content = mem.content || '(no content)';
|
|
256
|
+
const tags = mem.tags && mem.tags.length > 0 ? `\nTags: ${mem.tags.join(', ')}` : '';
|
|
257
|
+
|
|
258
|
+
return `${idx + 1}. ${title}${type} (${relevancePercent}% relevant)\n${content}${tags}`;
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
formattedMemories = summaryParts.join('\n\n---\n\n');
|
|
262
|
+
|
|
263
|
+
contextSummary = `Found ${relevantMemories.length} relevant memories for query: "${args.query}"`;
|
|
264
|
+
} else {
|
|
265
|
+
// Detailed format: full objects
|
|
266
|
+
formattedMemories = relevantMemories;
|
|
267
|
+
|
|
268
|
+
contextSummary = `Retrieved ${relevantMemories.length} memories with relevance >= ${Math.round(minRelevance * 100)}%`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const result: QueryMemoryResult = {
|
|
272
|
+
query: args.query,
|
|
273
|
+
memories: formattedMemories,
|
|
274
|
+
total: relevantMemories.length,
|
|
275
|
+
min_relevance: minRelevance,
|
|
276
|
+
context_summary: contextSummary,
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
return JSON.stringify(result, null, 2);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
logger.error('Failed to query memories:', error);
|
|
282
|
+
throw new Error(`Failed to query memories: ${error instanceof Error ? error.message : String(error)}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* remember_search_relationship tool
|
|
3
|
+
* Search relationships by observation text or type
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Relationship } from '../types/memory.js';
|
|
7
|
+
import { getMemoryCollection } from '../weaviate/schema.js';
|
|
8
|
+
import { logger } from '../utils/logger.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Tool definition for remember_search_relationship
|
|
12
|
+
*/
|
|
13
|
+
export const searchRelationshipTool = {
|
|
14
|
+
name: 'remember_search_relationship',
|
|
15
|
+
description: `Search relationships by observation text or relationship type.
|
|
16
|
+
|
|
17
|
+
Uses semantic search on relationship observations to find connections.
|
|
18
|
+
Can filter by relationship type, strength, and tags.
|
|
19
|
+
Returns relationships with their connected memory IDs.
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
- "Find relationships about inspiration"
|
|
23
|
+
- "Search for contradicting relationships"
|
|
24
|
+
- "Show me all 'caused_by' relationships"
|
|
25
|
+
`,
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
query: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'Search query (semantic search on observation field)',
|
|
32
|
+
},
|
|
33
|
+
relationship_types: {
|
|
34
|
+
type: 'array',
|
|
35
|
+
items: { type: 'string' },
|
|
36
|
+
description: 'Filter by relationship types (e.g., ["inspired_by", "caused_by"])',
|
|
37
|
+
},
|
|
38
|
+
strength_min: {
|
|
39
|
+
type: 'number',
|
|
40
|
+
description: 'Minimum strength (0-1)',
|
|
41
|
+
minimum: 0,
|
|
42
|
+
maximum: 1,
|
|
43
|
+
},
|
|
44
|
+
confidence_min: {
|
|
45
|
+
type: 'number',
|
|
46
|
+
description: 'Minimum confidence (0-1)',
|
|
47
|
+
minimum: 0,
|
|
48
|
+
maximum: 1,
|
|
49
|
+
},
|
|
50
|
+
tags: {
|
|
51
|
+
type: 'array',
|
|
52
|
+
items: { type: 'string' },
|
|
53
|
+
description: 'Filter by tags',
|
|
54
|
+
},
|
|
55
|
+
limit: {
|
|
56
|
+
type: 'number',
|
|
57
|
+
description: 'Maximum number of results (default: 10)',
|
|
58
|
+
minimum: 1,
|
|
59
|
+
maximum: 100,
|
|
60
|
+
},
|
|
61
|
+
offset: {
|
|
62
|
+
type: 'number',
|
|
63
|
+
description: 'Offset for pagination (default: 0)',
|
|
64
|
+
minimum: 0,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
required: ['query'],
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Search relationship arguments
|
|
73
|
+
*/
|
|
74
|
+
export interface SearchRelationshipArgs {
|
|
75
|
+
query: string;
|
|
76
|
+
relationship_types?: string[];
|
|
77
|
+
strength_min?: number;
|
|
78
|
+
confidence_min?: number;
|
|
79
|
+
tags?: string[];
|
|
80
|
+
limit?: number;
|
|
81
|
+
offset?: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Search relationship result
|
|
86
|
+
*/
|
|
87
|
+
export interface SearchRelationshipResult {
|
|
88
|
+
relationships: Relationship[];
|
|
89
|
+
total: number;
|
|
90
|
+
offset: number;
|
|
91
|
+
limit: number;
|
|
92
|
+
message: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Handle remember_search_relationship tool
|
|
97
|
+
*/
|
|
98
|
+
export async function handleSearchRelationship(
|
|
99
|
+
args: SearchRelationshipArgs,
|
|
100
|
+
userId: string
|
|
101
|
+
): Promise<string> {
|
|
102
|
+
try {
|
|
103
|
+
logger.info('Searching relationships', {
|
|
104
|
+
userId,
|
|
105
|
+
query: args.query,
|
|
106
|
+
types: args.relationship_types
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const collection = getMemoryCollection(userId);
|
|
110
|
+
const limit = args.limit ?? 10;
|
|
111
|
+
const offset = args.offset ?? 0;
|
|
112
|
+
|
|
113
|
+
// Build where filter for doc_type and other filters
|
|
114
|
+
const whereFilters: any[] = [
|
|
115
|
+
{
|
|
116
|
+
path: 'doc_type',
|
|
117
|
+
operator: 'Equal',
|
|
118
|
+
valueText: 'relationship',
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
// Add relationship type filter
|
|
123
|
+
if (args.relationship_types && args.relationship_types.length > 0) {
|
|
124
|
+
if (args.relationship_types.length === 1) {
|
|
125
|
+
whereFilters.push({
|
|
126
|
+
path: 'relationship_type',
|
|
127
|
+
operator: 'Equal',
|
|
128
|
+
valueText: args.relationship_types[0],
|
|
129
|
+
});
|
|
130
|
+
} else {
|
|
131
|
+
whereFilters.push({
|
|
132
|
+
operator: 'Or',
|
|
133
|
+
operands: args.relationship_types.map(type => ({
|
|
134
|
+
path: 'relationship_type',
|
|
135
|
+
operator: 'Equal',
|
|
136
|
+
valueText: type,
|
|
137
|
+
})),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Add strength filter
|
|
143
|
+
if (args.strength_min !== undefined) {
|
|
144
|
+
whereFilters.push({
|
|
145
|
+
path: 'strength',
|
|
146
|
+
operator: 'GreaterThanEqual',
|
|
147
|
+
valueNumber: args.strength_min,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Add confidence filter
|
|
152
|
+
if (args.confidence_min !== undefined) {
|
|
153
|
+
whereFilters.push({
|
|
154
|
+
path: 'confidence',
|
|
155
|
+
operator: 'GreaterThanEqual',
|
|
156
|
+
valueNumber: args.confidence_min,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Add tags filter
|
|
161
|
+
if (args.tags && args.tags.length > 0) {
|
|
162
|
+
whereFilters.push({
|
|
163
|
+
path: 'tags',
|
|
164
|
+
operator: 'ContainsAny',
|
|
165
|
+
valueTextArray: args.tags,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Build search options
|
|
170
|
+
const searchOptions: any = {
|
|
171
|
+
alpha: 1.0, // Pure semantic search for relationships
|
|
172
|
+
limit: limit + offset, // Get extra for offset
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Add filters if present
|
|
176
|
+
if (whereFilters.length > 0) {
|
|
177
|
+
searchOptions.filters = whereFilters.length > 1 ? {
|
|
178
|
+
operator: 'And' as const,
|
|
179
|
+
operands: whereFilters,
|
|
180
|
+
} : whereFilters[0];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Perform hybrid search (semantic search on observation field)
|
|
184
|
+
const results = await collection.query.hybrid(args.query, searchOptions);
|
|
185
|
+
|
|
186
|
+
// Apply offset manually (Weaviate v4 doesn't have built-in offset for nearText)
|
|
187
|
+
const paginatedResults = results.objects.slice(offset, offset + limit);
|
|
188
|
+
|
|
189
|
+
// Map results to Relationship type
|
|
190
|
+
const relationships: Relationship[] = paginatedResults.map((obj: any) => ({
|
|
191
|
+
id: obj.uuid,
|
|
192
|
+
user_id: obj.properties.user_id,
|
|
193
|
+
doc_type: 'relationship',
|
|
194
|
+
memory_ids: obj.properties.memory_ids || [],
|
|
195
|
+
relationship_type: obj.properties.relationship_type,
|
|
196
|
+
observation: obj.properties.observation,
|
|
197
|
+
strength: obj.properties.strength,
|
|
198
|
+
confidence: obj.properties.confidence,
|
|
199
|
+
context: obj.properties.context || {
|
|
200
|
+
timestamp: obj.properties.created_at,
|
|
201
|
+
source: { type: 'api', platform: 'mcp' },
|
|
202
|
+
},
|
|
203
|
+
created_at: obj.properties.created_at,
|
|
204
|
+
updated_at: obj.properties.updated_at,
|
|
205
|
+
version: obj.properties.version,
|
|
206
|
+
tags: obj.properties.tags || [],
|
|
207
|
+
}));
|
|
208
|
+
|
|
209
|
+
logger.info('Relationship search completed', {
|
|
210
|
+
userId,
|
|
211
|
+
found: relationships.length,
|
|
212
|
+
total: results.objects.length
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const result: SearchRelationshipResult = {
|
|
216
|
+
relationships,
|
|
217
|
+
total: results.objects.length,
|
|
218
|
+
offset,
|
|
219
|
+
limit,
|
|
220
|
+
message: `Found ${relationships.length} relationship(s) matching query "${args.query}"`,
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
return JSON.stringify(result, null, 2);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
logger.error('Failed to search relationships:', error);
|
|
226
|
+
throw new Error(`Failed to search relationships: ${error instanceof Error ? error.message : String(error)}`);
|
|
227
|
+
}
|
|
228
|
+
}
|