@prmichaelsen/remember-mcp 3.12.0 → 3.14.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/agent/milestones/milestone-17-remember-core-migration.md +140 -0
  3. package/agent/progress.yaml +123 -6
  4. package/agent/tasks/milestone-17-remember-core-migration/task-193-foundation-setup.md +58 -0
  5. package/agent/tasks/milestone-17-remember-core-migration/task-194-migrate-relationship-tools.md +47 -0
  6. package/agent/tasks/milestone-17-remember-core-migration/task-195-migrate-preference-tools.md +34 -0
  7. package/agent/tasks/milestone-17-remember-core-migration/task-196-migrate-memory-tools.md +46 -0
  8. package/agent/tasks/milestone-17-remember-core-migration/task-197-migrate-space-confirmation-tools.md +49 -0
  9. package/agent/tasks/milestone-17-remember-core-migration/task-198-migrate-space-search-moderate.md +46 -0
  10. package/agent/tasks/milestone-17-remember-core-migration/task-199-migrate-delete-memory.md +43 -0
  11. package/agent/tasks/milestone-17-remember-core-migration/task-200-code-cleanup-verification.md +52 -0
  12. package/dist/core-services.d.ts +25 -0
  13. package/dist/server-factory.js +3578 -4485
  14. package/dist/server.js +3070 -3973
  15. package/dist/tools/confirm-publish-moderation.spec.d.ts +3 -2
  16. package/dist/tools/create-memory.d.ts +1 -1
  17. package/dist/tools/query-space.d.ts +1 -1
  18. package/dist/tools/search-space.d.ts +10 -14
  19. package/jest.config.js +11 -0
  20. package/package.json +2 -1
  21. package/src/core-services.ts +50 -0
  22. package/src/tools/confirm-publish-moderation.spec.ts +120 -176
  23. package/src/tools/confirm.ts +70 -1035
  24. package/src/tools/create-memory.ts +16 -67
  25. package/src/tools/create-relationship.ts +13 -181
  26. package/src/tools/delete-memory.ts +7 -72
  27. package/src/tools/delete-relationship.ts +7 -91
  28. package/src/tools/deny.ts +4 -14
  29. package/src/tools/find-similar.ts +16 -110
  30. package/src/tools/get-preferences.ts +3 -8
  31. package/src/tools/moderate.spec.ts +65 -81
  32. package/src/tools/moderate.ts +18 -121
  33. package/src/tools/publish.ts +7 -204
  34. package/src/tools/query-space.ts +28 -140
  35. package/src/tools/retract.ts +7 -185
  36. package/src/tools/revise.ts +4 -136
  37. package/src/tools/search-relationship.ts +17 -116
  38. package/src/tools/search-space.ts +58 -304
  39. package/src/tools/set-preference.ts +3 -8
  40. package/src/tools/update-memory.ts +22 -190
  41. package/src/tools/update-relationship.ts +16 -90
  42. package/src/v2-smoke.e2e.ts +3 -2
  43. package/dist/collections/composite-ids.d.ts +0 -106
  44. package/dist/collections/core-infrastructure.spec.d.ts +0 -11
  45. package/dist/collections/dot-notation.d.ts +0 -106
  46. package/dist/collections/tracking-arrays.d.ts +0 -176
  47. package/dist/constants/content-types.d.ts +0 -61
  48. package/dist/services/confirmation-token.service.d.ts +0 -99
  49. package/dist/services/confirmation-token.service.spec.d.ts +0 -5
  50. package/dist/services/preferences-database.service.d.ts +0 -22
  51. package/dist/services/space-config.service.d.ts +0 -23
  52. package/dist/services/space-config.service.spec.d.ts +0 -2
  53. package/src/collections/composite-ids.ts +0 -193
  54. package/src/collections/core-infrastructure.spec.ts +0 -353
  55. package/src/collections/dot-notation.ts +0 -212
  56. package/src/collections/tracking-arrays.ts +0 -298
  57. package/src/constants/content-types.ts +0 -490
  58. package/src/services/confirmation-token.service.spec.ts +0 -254
  59. package/src/services/confirmation-token.service.ts +0 -328
  60. package/src/services/preferences-database.service.ts +0 -120
  61. package/src/services/space-config.service.spec.ts +0 -102
  62. package/src/services/space-config.service.ts +0 -79
@@ -8,15 +8,11 @@
8
8
 
9
9
  import type { Tool } from '@modelcontextprotocol/sdk/types.js';
10
10
  import { Filters } from 'weaviate-client';
11
- import { getWeaviateClient } from '../weaviate/client.js';
12
- import { isValidSpaceId } from '../weaviate/space-schema.js';
13
11
  import { SUPPORTED_SPACES } from '../types/space-memory.js';
14
12
  import { handleToolError } from '../utils/error-handler.js';
15
13
  import { createDebugLogger } from '../utils/debug.js';
16
- import { CollectionType, getCollectionName } from '../collections/dot-notation.js';
17
- import { logger } from '../utils/logger.js';
18
14
  import type { AuthContext } from '../types/auth.js';
19
- import { canModerate, canModerateAny } from '../utils/auth-helpers.js';
15
+ import { createCoreServices } from '../core-services.js';
20
16
 
21
17
  /**
22
18
  * Tool definition for remember_search_space
@@ -123,130 +119,57 @@ Let the search algorithm find ALL relevant memories regardless of type unless ex
123
119
 
124
120
  export type ModerationFilter = 'approved' | 'pending' | 'rejected' | 'removed' | 'all';
125
121
 
126
- interface SearchSpaceArgs {
127
- query: string;
128
- spaces?: string[];
129
- groups?: string[];
130
- search_type?: 'hybrid' | 'bm25' | 'semantic';
131
- content_type?: string;
132
- tags?: string[];
133
- min_weight?: number;
134
- max_weight?: number;
135
- date_from?: string;
136
- date_to?: string;
137
- moderation_filter?: ModerationFilter;
138
- include_comments?: boolean;
139
- limit?: number;
140
- offset?: number;
141
- }
142
-
143
122
  /**
144
123
  * Build the moderation status filter for a Weaviate collection query.
145
- *
146
- * - 'approved' (default): matches approved OR null (backward compat for pre-moderation memories)
147
- * - 'pending'/'rejected'/'removed': matches that specific status
148
- * - 'all': no moderation filter applied
124
+ * @deprecated Kept for test compatibility — logic now lives in remember-core SpaceService
149
125
  */
150
126
  export function buildModerationFilter(collection: any, moderationFilter: ModerationFilter = 'approved'): any | null {
151
- if (moderationFilter === 'all') {
152
- return null;
153
- }
154
-
127
+ if (moderationFilter === 'all') return null;
155
128
  if (moderationFilter === 'approved') {
156
- // Approved OR null (backward compat: existing memories without moderation_status are approved)
157
129
  return Filters.or(
158
130
  collection.filter.byProperty('moderation_status').equal('approved'),
159
131
  collection.filter.byProperty('moderation_status').isNull(true)
160
132
  );
161
133
  }
162
-
163
- // Specific non-approved status
164
134
  return collection.filter.byProperty('moderation_status').equal(moderationFilter);
165
135
  }
166
136
 
167
137
  /**
168
138
  * Build base filters applied to all space/group collection queries.
169
- * Excludes soft-deleted memories and optionally filters by content type, tags, weight, and date.
170
- * Includes moderation status filter (default: approved/null only).
139
+ * @deprecated Kept for test compatibility logic now lives in remember-core SpaceService
171
140
  */
172
141
  export function buildBaseFilters(collection: any, args: SearchSpaceArgs): any[] {
173
142
  const filterList: any[] = [];
174
-
175
- // Exclude soft-deleted memories (requires indexNullState: true on collection)
176
143
  filterList.push(collection.filter.byProperty('deleted_at').isNull(true));
177
-
178
- // Only return memories (not relationships)
179
144
  filterList.push(collection.filter.byProperty('doc_type').equal('memory'));
180
-
181
- // Moderation status filter
182
145
  const moderationFilter = buildModerationFilter(collection, args.moderation_filter);
183
- if (moderationFilter) {
184
- filterList.push(moderationFilter);
185
- }
186
-
187
- // Apply content type filter
188
- if (args.content_type) {
189
- filterList.push(collection.filter.byProperty('content_type').equal(args.content_type));
190
- }
191
-
192
- // Exclude comments and ghost memories by default (unless content_type is explicitly set)
193
- if (!args.include_comments && !args.content_type) {
194
- filterList.push(collection.filter.byProperty('content_type').notEqual('comment'));
195
- }
196
- if (!args.content_type) {
197
- filterList.push(collection.filter.byProperty('content_type').notEqual('ghost'));
198
- }
199
-
200
- // Apply tags filter (AND semantics: memory must have ALL specified tags)
201
- if (args.tags && args.tags.length > 0) {
202
- args.tags.forEach(tag => {
203
- filterList.push(collection.filter.byProperty('tags').containsAny([tag]));
204
- });
205
- }
206
-
207
- // Apply weight filters
208
- if (args.min_weight !== undefined) {
209
- filterList.push(collection.filter.byProperty('weight').greaterOrEqual(args.min_weight));
210
- }
211
- if (args.max_weight !== undefined) {
212
- filterList.push(collection.filter.byProperty('weight').lessOrEqual(args.max_weight));
213
- }
214
-
215
- // Apply date filters (created_at stored as ISO 8601 text, sorts lexicographically)
216
- if (args.date_from) {
217
- filterList.push(collection.filter.byProperty('created_at').greaterOrEqual(new Date(args.date_from)));
218
- }
219
- if (args.date_to) {
220
- filterList.push(collection.filter.byProperty('created_at').lessOrEqual(new Date(args.date_to)));
221
- }
222
-
146
+ if (moderationFilter) filterList.push(moderationFilter);
147
+ if (args.content_type) filterList.push(collection.filter.byProperty('content_type').equal(args.content_type));
148
+ if (!args.include_comments && !args.content_type) filterList.push(collection.filter.byProperty('content_type').notEqual('comment'));
149
+ if (!args.content_type) filterList.push(collection.filter.byProperty('content_type').notEqual('ghost'));
150
+ if (args.tags && args.tags.length > 0) args.tags.forEach(tag => filterList.push(collection.filter.byProperty('tags').containsAny([tag])));
151
+ if (args.min_weight !== undefined) filterList.push(collection.filter.byProperty('weight').greaterOrEqual(args.min_weight));
152
+ if (args.max_weight !== undefined) filterList.push(collection.filter.byProperty('weight').lessOrEqual(args.max_weight));
153
+ if (args.date_from) filterList.push(collection.filter.byProperty('created_at').greaterOrEqual(new Date(args.date_from)));
154
+ if (args.date_to) filterList.push(collection.filter.byProperty('created_at').lessOrEqual(new Date(args.date_to)));
223
155
  return filterList;
224
156
  }
225
157
 
226
- /**
227
- * Execute a search against a Weaviate collection using the specified search type.
228
- */
229
- async function executeSearch(
230
- collection: any,
231
- query: string,
232
- searchType: 'hybrid' | 'bm25' | 'semantic',
233
- whereFilter: any,
234
- limit: number
235
- ): Promise<any[]> {
236
- const opts = {
237
- limit,
238
- ...(whereFilter && { where: whereFilter }),
239
- };
240
-
241
- switch (searchType) {
242
- case 'bm25':
243
- return (await collection.query.bm25(query, opts)).objects;
244
- case 'semantic':
245
- return (await collection.query.nearText([query], opts)).objects;
246
- case 'hybrid':
247
- default:
248
- return (await collection.query.hybrid(query, opts)).objects;
249
- }
158
+ interface SearchSpaceArgs {
159
+ query: string;
160
+ spaces?: string[];
161
+ groups?: string[];
162
+ search_type?: 'hybrid' | 'bm25' | 'semantic';
163
+ content_type?: string;
164
+ tags?: string[];
165
+ min_weight?: number;
166
+ max_weight?: number;
167
+ date_from?: string;
168
+ date_to?: string;
169
+ moderation_filter?: ModerationFilter;
170
+ include_comments?: boolean;
171
+ limit?: number;
172
+ offset?: number;
250
173
  }
251
174
 
252
175
  /**
@@ -267,212 +190,43 @@ export async function handleSearchSpace(
267
190
  debug.info('Tool invoked');
268
191
  debug.trace('Arguments', { args });
269
192
 
270
- const spaces = args.spaces || [];
271
- const groups = args.groups || [];
272
- const searchType = args.search_type || 'hybrid';
273
- const limit = args.limit || 10;
274
- const offset = args.offset || 0;
275
-
276
- // Validate space IDs
277
- if (spaces.length > 0) {
278
- const invalidSpaces = spaces.filter(s => !isValidSpaceId(s));
279
- if (invalidSpaces.length > 0) {
280
- return JSON.stringify(
281
- {
282
- success: false,
283
- error: 'Invalid space IDs',
284
- message: `Invalid spaces: ${invalidSpaces.join(', ')}. Supported spaces: ${SUPPORTED_SPACES.join(', ')}`,
285
- context: {
286
- invalid_spaces: invalidSpaces,
287
- provided_spaces: spaces,
288
- supported_spaces: SUPPORTED_SPACES,
289
- },
290
- },
291
- null,
292
- 2
293
- );
294
- }
295
- }
296
-
297
- // Validate group IDs
298
- if (groups.length > 0) {
299
- const invalidGroups = groups.filter(g => !g || g.includes('.') || g.trim() === '');
300
- if (invalidGroups.length > 0) {
301
- return JSON.stringify(
302
- {
303
- success: false,
304
- error: 'Invalid group IDs',
305
- message: 'Group IDs cannot be empty or contain dots',
306
- context: { invalid_groups: invalidGroups },
307
- },
308
- null,
309
- 2
310
- );
311
- }
312
- }
313
-
314
- // Permission check: non-approved moderation filters require can_moderate
315
- const moderationFilter = args.moderation_filter || 'approved';
316
- if (moderationFilter !== 'approved') {
317
- // For group searches: check can_moderate per group
318
- for (const groupId of groups) {
319
- if (!canModerate(authContext, groupId)) {
320
- return JSON.stringify(
321
- {
322
- success: false,
323
- error: 'Permission denied',
324
- message: `Moderator access required to view ${moderationFilter} memories in group ${groupId}`,
325
- },
326
- null,
327
- 2
328
- );
329
- }
330
- }
331
- // For space searches: check can_moderate on any group
332
- if ((spaces.length > 0 || groups.length === 0) && !canModerateAny(authContext)) {
333
- return JSON.stringify(
334
- {
335
- success: false,
336
- error: 'Permission denied',
337
- message: `Moderator access required to view ${moderationFilter} memories in spaces`,
338
- },
339
- null,
340
- 2
341
- );
342
- }
343
- }
344
-
345
- const weaviateClient = getWeaviateClient();
346
- // Fetch enough results before pagination so we can deduplicate across sources
347
- const fetchLimit = (limit + offset) * Math.max(1, groups.length + (spaces.length > 0 || groups.length === 0 ? 1 : 0));
348
- const allObjects: any[] = [];
349
-
350
- logger.info('Starting space/group search', {
351
- tool: 'remember_search_space',
352
- userId,
353
- spaces,
354
- groups,
355
- searchType,
356
- query: args.query,
357
- });
358
-
359
- // --- Space collection search ---
360
- // Runs when spaces are specified, OR when neither spaces nor groups are specified (all-public)
361
- if (spaces.length > 0 || groups.length === 0) {
362
- const spacesCollectionName = getCollectionName(CollectionType.SPACES);
363
- const spacesCollection = weaviateClient.collections.get(spacesCollectionName);
364
-
365
- const filterList = buildBaseFilters(spacesCollection, args);
366
-
367
- // Filter by space_ids array when specific spaces are requested
368
- if (spaces.length > 0) {
369
- filterList.push(spacesCollection.filter.byProperty('space_ids').containsAny(spaces));
370
- }
371
- // When spaces.length === 0 and groups.length === 0: no space_ids filter → all-public search
372
-
373
- const whereFilter = filterList.length > 0 ? Filters.and(...filterList) : undefined;
374
-
375
- debug.debug('Searching Memory_spaces_public', {
376
- filterCount: filterList.length,
377
- spaces,
378
- allPublic: spaces.length === 0,
379
- searchType,
380
- });
381
-
382
- const spaceObjects = await debug.time('Space collection search', async () => {
383
- return await executeSearch(spacesCollection, args.query, searchType, whereFilter, fetchLimit);
384
- });
385
-
386
- allObjects.push(...spaceObjects);
387
-
388
- logger.info('Space collection search complete', {
389
- tool: 'remember_search_space',
390
- collectionName: spacesCollectionName,
391
- resultCount: spaceObjects.length,
392
- });
393
- }
394
-
395
- // --- Group collection searches ---
396
- for (const groupId of groups) {
397
- const groupCollectionName = getCollectionName(CollectionType.GROUPS, groupId);
398
-
399
- // Skip if the group collection doesn't exist yet
400
- const exists = await weaviateClient.collections.exists(groupCollectionName);
401
- if (!exists) {
402
- debug.warn('Group collection not found, skipping', { groupId, groupCollectionName });
403
- continue;
404
- }
405
-
406
- const groupCollection = weaviateClient.collections.get(groupCollectionName);
407
- const filterList = buildBaseFilters(groupCollection, args);
408
- const whereFilter = filterList.length > 0 ? Filters.and(...filterList) : undefined;
409
-
410
- debug.debug('Searching group collection', {
411
- groupId,
412
- groupCollectionName,
413
- filterCount: filterList.length,
414
- searchType,
415
- });
416
-
417
- const groupObjects = await debug.time(`Group collection search: ${groupId}`, async () => {
418
- return await executeSearch(groupCollection, args.query, searchType, whereFilter, fetchLimit);
419
- });
420
-
421
- allObjects.push(...groupObjects);
422
-
423
- logger.info('Group collection search complete', {
424
- tool: 'remember_search_space',
425
- groupId,
426
- collectionName: groupCollectionName,
427
- resultCount: groupObjects.length,
428
- });
429
- }
430
-
431
- // --- Deduplicate by UUID (composite ID) ---
432
- const seen = new Set<string>();
433
- const deduplicated = allObjects.filter(obj => {
434
- if (seen.has(obj.uuid)) return false;
435
- seen.add(obj.uuid);
436
- return true;
437
- });
438
-
439
- // --- Sort by relevance score descending ---
440
- deduplicated.sort((a, b) => {
441
- const scoreA = a.metadata?.score ?? 0;
442
- const scoreB = b.metadata?.score ?? 0;
443
- return scoreB - scoreA;
444
- });
445
-
446
- // --- Apply pagination ---
447
- const paginated = deduplicated.slice(offset, offset + limit);
448
-
449
- // Format results
450
- const memories = paginated.map(obj => ({
451
- id: obj.uuid,
452
- ...obj.properties,
453
- _score: obj.metadata?.score,
454
- }));
455
-
456
- const isAllPublic = spaces.length === 0 && groups.length === 0;
193
+ const { space } = createCoreServices(userId);
194
+ const result = await space.search(
195
+ {
196
+ query: args.query,
197
+ spaces: args.spaces,
198
+ groups: args.groups,
199
+ search_type: args.search_type,
200
+ content_type: args.content_type,
201
+ tags: args.tags,
202
+ min_weight: args.min_weight,
203
+ max_weight: args.max_weight,
204
+ date_from: args.date_from,
205
+ date_to: args.date_to,
206
+ moderation_filter: args.moderation_filter as any,
207
+ include_comments: args.include_comments,
208
+ limit: args.limit,
209
+ offset: args.offset,
210
+ },
211
+ authContext as any
212
+ );
457
213
 
458
- const result = {
459
- spaces_searched: isAllPublic ? 'all_public' : spaces,
460
- groups_searched: groups,
214
+ const response = {
215
+ spaces_searched: result.spaces_searched,
216
+ groups_searched: result.groups_searched,
461
217
  query: args.query,
462
- search_type: searchType,
463
- memories,
464
- total: memories.length,
465
- offset,
466
- limit,
218
+ search_type: args.search_type || 'hybrid',
219
+ memories: result.memories,
220
+ total: result.total,
221
+ offset: result.offset,
222
+ limit: result.limit,
467
223
  };
468
224
 
469
225
  debug.info('Tool completed successfully', {
470
- resultCount: memories.length,
471
- spaces,
472
- groups,
226
+ resultCount: result.total,
473
227
  });
474
228
 
475
- return JSON.stringify(result, null, 2);
229
+ return JSON.stringify(response, null, 2);
476
230
  } catch (error) {
477
231
  debug.error('Tool failed', {
478
232
  error: error instanceof Error ? error.message : String(error),
@@ -3,8 +3,6 @@
3
3
  * Update user preferences through natural conversation
4
4
  */
5
5
 
6
- import { PreferencesDatabaseService } from '../services/preferences-database.service.js';
7
- import { logger } from '../utils/logger.js';
8
6
  import { handleToolError } from '../utils/error-handler.js';
9
7
  import { createDebugLogger } from '../utils/debug.js';
10
8
  import {
@@ -13,6 +11,7 @@ import {
13
11
  getPreferencesSchema,
14
12
  } from '../types/preferences.js';
15
13
  import type { AuthContext } from '../types/auth.js';
14
+ import { createCoreServices } from '../core-services.js';
16
15
 
17
16
  /**
18
17
  * Tool definition for remember_set_preference
@@ -128,10 +127,8 @@ export async function handleSetPreference(
128
127
 
129
128
  const { preferences } = args;
130
129
 
131
- logger.info('Setting preferences', { userId, updates: Object.keys(preferences) });
132
-
133
- // Update preferences using service layer
134
- const updatedPreferences = await PreferencesDatabaseService.updatePreferences(
130
+ const { preferences: preferencesService } = createCoreServices(userId);
131
+ const updatedPreferences = await preferencesService.updatePreferences(
135
132
  userId,
136
133
  preferences
137
134
  );
@@ -144,8 +141,6 @@ export async function handleSetPreference(
144
141
  message,
145
142
  };
146
143
 
147
- logger.info('Preferences set successfully', { userId });
148
-
149
144
  return JSON.stringify(result, null, 2);
150
145
  } catch (error) {
151
146
  debug.error('Tool failed', { error: error instanceof Error ? error.message : String(error) });