@prmichaelsen/remember-mcp 0.2.5 → 0.2.7

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.
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Preferences Database Service
3
+ * Handles all Firestore operations for user preferences
4
+ */
5
+
6
+ import { getDocument, setDocument } from '../firestore/init.js';
7
+ import { getUserPreferencesPath } from '../firestore/paths.js';
8
+ import { logger } from '../utils/logger.js';
9
+ import {
10
+ UserPreferences,
11
+ DEFAULT_PREFERENCES,
12
+ } from '../types/preferences.js';
13
+
14
+ export class PreferencesDatabaseService {
15
+ /**
16
+ * Get user preferences
17
+ * Returns defaults if preferences don't exist
18
+ */
19
+ static async getPreferences(userId: string): Promise<UserPreferences> {
20
+ try {
21
+ const pathParts = getUserPreferencesPath(userId).split('/');
22
+ const docId = pathParts.pop()!;
23
+ const collectionPath = pathParts.join('/');
24
+
25
+ const doc = await getDocument(collectionPath, docId);
26
+
27
+ if (!doc) {
28
+ // Return defaults with user_id
29
+ const now = new Date().toISOString();
30
+ return {
31
+ user_id: userId,
32
+ ...DEFAULT_PREFERENCES,
33
+ created_at: now,
34
+ updated_at: now,
35
+ };
36
+ }
37
+
38
+ return doc as UserPreferences;
39
+ } catch (error) {
40
+ logger.error('Failed to get preferences:', error);
41
+ throw new Error(`Failed to get preferences: ${error instanceof Error ? error.message : String(error)}`);
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Update user preferences (partial update with merge)
47
+ * Creates with defaults if preferences don't exist
48
+ */
49
+ static async updatePreferences(
50
+ userId: string,
51
+ updates: Partial<Omit<UserPreferences, 'user_id' | 'created_at'>>
52
+ ): Promise<UserPreferences> {
53
+ try {
54
+ const pathParts = getUserPreferencesPath(userId).split('/');
55
+ const docId = pathParts.pop()!;
56
+ const collectionPath = pathParts.join('/');
57
+
58
+ const now = new Date().toISOString();
59
+ const doc = await getDocument(collectionPath, docId);
60
+
61
+ if (!doc) {
62
+ // Create with defaults + updates
63
+ const newPrefs: UserPreferences = {
64
+ user_id: userId,
65
+ ...DEFAULT_PREFERENCES,
66
+ ...updates,
67
+ created_at: now,
68
+ updated_at: now,
69
+ };
70
+
71
+ await setDocument(collectionPath, docId, newPrefs);
72
+ logger.info('Preferences created with defaults', { userId });
73
+ return newPrefs;
74
+ }
75
+
76
+ // Update existing preferences with merge
77
+ const updateData = {
78
+ ...updates,
79
+ updated_at: now,
80
+ };
81
+
82
+ await setDocument(collectionPath, docId, updateData, { merge: true });
83
+ logger.info('Preferences updated', { userId });
84
+
85
+ // Return updated preferences
86
+ const updatedDoc = await getDocument(collectionPath, docId);
87
+ return updatedDoc as UserPreferences;
88
+ } catch (error) {
89
+ logger.error('Failed to update preferences:', error);
90
+ throw new Error(`Failed to update preferences: ${error instanceof Error ? error.message : String(error)}`);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Create preferences with defaults
96
+ */
97
+ static async createPreferences(userId: string): Promise<UserPreferences> {
98
+ try {
99
+ const pathParts = getUserPreferencesPath(userId).split('/');
100
+ const docId = pathParts.pop()!;
101
+ const collectionPath = pathParts.join('/');
102
+
103
+ const now = new Date().toISOString();
104
+ const preferences: UserPreferences = {
105
+ user_id: userId,
106
+ ...DEFAULT_PREFERENCES,
107
+ created_at: now,
108
+ updated_at: now,
109
+ };
110
+
111
+ await setDocument(collectionPath, docId, preferences);
112
+ logger.info('Preferences created', { userId });
113
+
114
+ return preferences;
115
+ } catch (error) {
116
+ logger.error('Failed to create preferences:', error);
117
+ throw new Error(`Failed to create preferences: ${error instanceof Error ? error.message : String(error)}`);
118
+ }
119
+ }
120
+ }
@@ -38,6 +38,7 @@ export const createMemoryTool = {
38
38
  type: {
39
39
  type: 'string',
40
40
  description: getContentTypeDescription(),
41
+
41
42
  default: DEFAULT_CONTENT_TYPE,
42
43
  },
43
44
  weight: {
@@ -0,0 +1,111 @@
1
+ /**
2
+ * remember_get_preferences tool
3
+ * Retrieve user preferences with defaults
4
+ */
5
+
6
+ import { PreferencesDatabaseService } from '../services/preferences-database.service.js';
7
+ import { logger } from '../utils/logger.js';
8
+ import {
9
+ UserPreferences,
10
+ PreferenceCategory,
11
+ PREFERENCE_CATEGORIES,
12
+ getPreferenceDescription,
13
+ } from '../types/preferences.js';
14
+
15
+ /**
16
+ * Tool definition for remember_get_preferences
17
+ */
18
+ export const getPreferencesTool = {
19
+ name: 'remember_get_preferences',
20
+ description: `Get current user preferences.
21
+
22
+ Use this to understand user's current settings before suggesting changes
23
+ or to explain why system is behaving a certain way.
24
+
25
+ Returns the complete preferences object or filtered by category.
26
+ If preferences don't exist, returns defaults.
27
+
28
+ ${getPreferenceDescription()}
29
+ `,
30
+ inputSchema: {
31
+ type: 'object',
32
+ properties: {
33
+ category: {
34
+ type: 'string',
35
+ enum: ['templates', 'search', 'location', 'privacy', 'notifications', 'display'],
36
+ description: 'Optional category to filter preferences',
37
+ },
38
+ },
39
+ },
40
+ };
41
+
42
+ /**
43
+ * Get preferences arguments
44
+ */
45
+ export interface GetPreferencesArgs {
46
+ category?: PreferenceCategory;
47
+ }
48
+
49
+ /**
50
+ * Get preferences result
51
+ */
52
+ export interface GetPreferencesResult {
53
+ preferences: UserPreferences | Partial<UserPreferences>;
54
+ is_default: boolean;
55
+ message: string;
56
+ }
57
+
58
+ /**
59
+ * Handle remember_get_preferences tool
60
+ */
61
+ export async function handleGetPreferences(
62
+ args: GetPreferencesArgs,
63
+ userId: string
64
+ ): Promise<string> {
65
+ try {
66
+ const { category } = args;
67
+
68
+ logger.info('Getting preferences', { userId, category });
69
+
70
+ // Get preferences using service layer
71
+ const preferences = await PreferencesDatabaseService.getPreferences(userId);
72
+
73
+ // Check if these are defaults (no created_at means they were just generated)
74
+ const isDefault = !preferences.created_at || preferences.created_at === preferences.updated_at;
75
+
76
+ // Filter by category if requested
77
+ let result: UserPreferences | Partial<UserPreferences>;
78
+ let message: string;
79
+
80
+ if (category) {
81
+ if (!PREFERENCE_CATEGORIES.includes(category)) {
82
+ throw new Error(`Invalid category: ${category}. Valid categories: ${PREFERENCE_CATEGORIES.join(', ')}`);
83
+ }
84
+
85
+ result = {
86
+ [category]: preferences[category],
87
+ };
88
+ message = isDefault
89
+ ? `Showing default ${category} preferences (user has not customized preferences yet).`
90
+ : `Showing current ${category} preferences.`;
91
+ } else {
92
+ result = preferences;
93
+ message = isDefault
94
+ ? 'Showing default preferences (user has not customized preferences yet).'
95
+ : 'Showing current user preferences.';
96
+ }
97
+
98
+ const response: GetPreferencesResult = {
99
+ preferences: result,
100
+ is_default: isDefault,
101
+ message,
102
+ };
103
+
104
+ logger.info('Preferences retrieved successfully', { userId, category, isDefault });
105
+
106
+ return JSON.stringify(response, null, 2);
107
+ } catch (error) {
108
+ logger.error('Failed to get preferences:', error);
109
+ throw new Error(`Failed to get preferences: ${error instanceof Error ? error.message : String(error)}`);
110
+ }
111
+ }
@@ -6,6 +6,7 @@
6
6
  import type { Memory, SearchFilters } from '../types/memory.js';
7
7
  import { getMemoryCollection } from '../weaviate/schema.js';
8
8
  import { logger } from '../utils/logger.js';
9
+ import { buildCombinedSearchFilters } from '../utils/weaviate-filters.js';
9
10
 
10
11
  /**
11
12
  * Tool definition for remember_query_memory
@@ -144,58 +145,8 @@ export async function handleQueryMemory(
144
145
  const includeContext = args.include_context ?? true;
145
146
  const format = args.format ?? 'detailed';
146
147
 
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
- }
148
+ // Build filters using v3 API - search both memories and relationships
149
+ const filters = buildCombinedSearchFilters(collection, args.filters);
199
150
 
200
151
  // Build search options
201
152
  const searchOptions: any = {
@@ -205,11 +156,8 @@ export async function handleQueryMemory(
205
156
  };
206
157
 
207
158
  // 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];
159
+ if (filters) {
160
+ searchOptions.filters = filters;
213
161
  }
214
162
 
215
163
  // Perform semantic search using nearText
@@ -1,30 +1,34 @@
1
1
  /**
2
2
  * remember_search_memory tool
3
- * Search memories using hybrid semantic + keyword search
3
+ * Search memories AND relationships using hybrid semantic + keyword search
4
4
  */
5
5
 
6
- import type { Memory, SearchOptions, SearchResult, SearchFilters } from '../types/memory.js';
6
+ import type { Memory, Relationship, SearchOptions, SearchResult, SearchFilters } from '../types/memory.js';
7
7
  import { getMemoryCollection } from '../weaviate/schema.js';
8
8
  import { logger } from '../utils/logger.js';
9
+ import { buildCombinedSearchFilters, buildMemoryOnlyFilters } from '../utils/weaviate-filters.js';
9
10
 
10
11
  /**
11
12
  * Tool definition for remember_search_memory
12
13
  */
13
14
  export const searchMemoryTool = {
14
15
  name: 'remember_search_memory',
15
- description: `Search memories using hybrid semantic and keyword search.
16
+ description: `Search memories AND relationships using hybrid semantic and keyword search.
17
+
18
+ By default, searches BOTH memories and relationships to provide comprehensive results.
19
+ Relationships contain valuable context in their observations.
16
20
 
17
21
  Supports:
18
- - Semantic search (meaning-based)
22
+ - Semantic search (meaning-based) across memory content and relationship observations
19
23
  - Keyword search (exact matches)
20
24
  - Hybrid search (balanced with alpha parameter)
21
25
  - Filtering by type, tags, weight, trust, date range
22
- - Location-based search
26
+ - Returns both memories and relationships in separate arrays
23
27
 
24
28
  Examples:
25
- - "Find memories about camping trips"
26
- - "Search for recipes I saved"
27
- - "Show me notes from last week"
29
+ - "Find memories about camping trips" → returns memories + relationships about camping
30
+ - "Search for recipes I saved" → returns recipe memories + related relationships
31
+ - "Show me notes from last week" → returns notes + any relationships created that week
28
32
  `,
29
33
  inputSchema: {
30
34
  type: 'object',
@@ -87,8 +91,8 @@ export const searchMemoryTool = {
87
91
  },
88
92
  include_relationships: {
89
93
  type: 'boolean',
90
- description: 'Include relationships in results. Default: false',
91
- default: false,
94
+ description: 'Include relationships in results. Default: true (searches both memories and relationships)',
95
+ default: true,
92
96
  },
93
97
  },
94
98
  required: ['query'],
@@ -103,65 +107,24 @@ export async function handleSearchMemory(
103
107
  userId: string
104
108
  ): Promise<string> {
105
109
  try {
106
- logger.info('Searching memories', { userId, query: args.query });
110
+ const includeRelationships = args.include_relationships !== false; // Default true
111
+
112
+ logger.info('Searching memories and relationships', {
113
+ userId,
114
+ query: args.query,
115
+ includeRelationships
116
+ });
107
117
 
108
118
  const collection = getMemoryCollection(userId);
109
119
  const alpha = args.alpha ?? 0.7;
110
120
  const limit = args.limit ?? 10;
111
121
  const offset = args.offset ?? 0;
112
122
 
113
- // Build where filter
114
- const whereFilters: any[] = [
115
- {
116
- path: 'doc_type',
117
- operator: 'Equal',
118
- valueText: 'memory',
119
- },
120
- ];
121
-
122
- // Add type filter
123
- if (args.filters?.types && args.filters.types.length > 0) {
124
- whereFilters.push({
125
- path: 'type',
126
- operator: 'ContainsAny',
127
- valueTextArray: args.filters.types,
128
- });
129
- }
130
-
131
- // Add weight filter
132
- if (args.filters?.weight_min !== undefined) {
133
- whereFilters.push({
134
- path: 'weight',
135
- operator: 'GreaterThanEqual',
136
- valueNumber: args.filters.weight_min,
137
- });
138
- }
139
-
140
- // Add trust filter
141
- if (args.filters?.trust_min !== undefined) {
142
- whereFilters.push({
143
- path: 'trust',
144
- operator: 'GreaterThanEqual',
145
- valueNumber: args.filters.trust_min,
146
- });
147
- }
148
-
149
- // Add date range filters
150
- if (args.filters?.date_from) {
151
- whereFilters.push({
152
- path: 'created_at',
153
- operator: 'GreaterThanEqual',
154
- valueDate: new Date(args.filters.date_from),
155
- });
156
- }
157
-
158
- if (args.filters?.date_to) {
159
- whereFilters.push({
160
- path: 'created_at',
161
- operator: 'LessThanEqual',
162
- valueDate: new Date(args.filters.date_to),
163
- });
164
- }
123
+ // Build filters using v3 API
124
+ // Use OR logic to search both memories and relationships
125
+ const filters = includeRelationships
126
+ ? buildCombinedSearchFilters(collection, args.filters)
127
+ : buildMemoryOnlyFilters(collection, args.filters);
165
128
 
166
129
  // Build search options
167
130
  const searchOptions: any = {
@@ -170,11 +133,8 @@ export async function handleSearchMemory(
170
133
  };
171
134
 
172
135
  // Add filters if present
173
- if (whereFilters.length > 0) {
174
- searchOptions.filters = whereFilters.length > 1 ? {
175
- operator: 'And' as const,
176
- operands: whereFilters,
177
- } : whereFilters[0];
136
+ if (filters) {
137
+ searchOptions.filters = filters;
178
138
  }
179
139
 
180
140
  // Perform hybrid search with Weaviate v3 API
@@ -183,29 +143,38 @@ export async function handleSearchMemory(
183
143
  // Apply offset
184
144
  const paginatedResults = results.objects.slice(offset);
185
145
 
186
- // Format memories
187
- const memories: Partial<Memory>[] = paginatedResults.map((obj: any) => ({
188
- id: obj.uuid,
189
- ...obj.properties,
190
- }));
146
+ // Separate memories and relationships
147
+ const memories: Partial<Memory>[] = [];
148
+ const relationships: Partial<Relationship>[] = [];
149
+
150
+ for (const obj of paginatedResults) {
151
+ const doc: any = {
152
+ id: obj.uuid,
153
+ ...obj.properties,
154
+ };
155
+
156
+ if (doc.doc_type === 'memory') {
157
+ memories.push(doc as Memory);
158
+ } else if (doc.doc_type === 'relationship') {
159
+ relationships.push(doc as Relationship);
160
+ }
161
+ }
191
162
 
192
163
  // Build result
193
164
  const searchResult: SearchResult = {
194
165
  memories: memories as Memory[],
195
- total: memories.length,
166
+ relationships: includeRelationships ? (relationships as Relationship[]) : undefined,
167
+ total: memories.length + relationships.length,
196
168
  offset: offset,
197
169
  limit: limit,
198
170
  };
199
171
 
200
- // TODO: Include relationships if requested
201
- if (args.include_relationships) {
202
- searchResult.relationships = [];
203
- }
204
-
205
- logger.info('Search completed', {
206
- userId,
207
- query: args.query,
208
- results: memories.length
172
+ logger.info('Search completed', {
173
+ userId,
174
+ query: args.query,
175
+ memoriesFound: memories.length,
176
+ relationshipsFound: relationships.length,
177
+ total: searchResult.total
209
178
  });
210
179
 
211
180
  return JSON.stringify(searchResult, null, 2);
@@ -0,0 +1,145 @@
1
+ /**
2
+ * remember_set_preference tool
3
+ * Update user preferences through natural conversation
4
+ */
5
+
6
+ import { PreferencesDatabaseService } from '../services/preferences-database.service.js';
7
+ import { logger } from '../utils/logger.js';
8
+ import {
9
+ UserPreferences,
10
+ getPreferenceDescription,
11
+ getPreferencesSchema,
12
+ } from '../types/preferences.js';
13
+
14
+ /**
15
+ * Tool definition for remember_set_preference
16
+ */
17
+ export const setPreferenceTool = {
18
+ name: 'remember_set_preference',
19
+ description: `Update user preferences for system behavior through natural conversation.
20
+
21
+ This tool allows bulk updates to user preferences. Provide a partial preferences object
22
+ with only the fields you want to update. All updates are merged with existing preferences.
23
+
24
+ ${getPreferenceDescription()}
25
+
26
+ Common examples:
27
+ - Disable template suggestions: { templates: { auto_suggest: false } }
28
+ - Change search defaults: { search: { default_limit: 20, default_alpha: 0.8 } }
29
+ - Update privacy: { privacy: { default_trust_level: 0.8 } }
30
+ - Suppress categories: { templates: { suppressed_categories: ["work", "personal"] } }
31
+ `,
32
+ inputSchema: {
33
+ type: 'object',
34
+ properties: {
35
+ preferences: {
36
+ ...getPreferencesSchema(),
37
+ description: 'Partial preferences object with fields to update',
38
+ },
39
+ },
40
+ required: ['preferences'],
41
+ },
42
+ };
43
+
44
+ /**
45
+ * Set preference arguments
46
+ */
47
+ export interface SetPreferenceArgs {
48
+ preferences: Partial<Omit<UserPreferences, 'user_id' | 'created_at' | 'updated_at'>>;
49
+ }
50
+
51
+ /**
52
+ * Set preference result
53
+ */
54
+ export interface SetPreferenceResult {
55
+ success: boolean;
56
+ updated_preferences: UserPreferences;
57
+ message: string;
58
+ error?: string;
59
+ }
60
+
61
+ /**
62
+ * Format a user-friendly message about the preference changes
63
+ */
64
+ function formatPreferenceChangeMessage(updates: Partial<UserPreferences>): string {
65
+ const changes: string[] = [];
66
+
67
+ if (updates.templates) {
68
+ if (updates.templates.auto_suggest !== undefined) {
69
+ changes.push(
70
+ updates.templates.auto_suggest
71
+ ? 'Template suggestions enabled'
72
+ : 'Template suggestions disabled'
73
+ );
74
+ }
75
+ if (updates.templates.suppressed_categories) {
76
+ changes.push(`Suppressed categories: ${updates.templates.suppressed_categories.join(', ')}`);
77
+ }
78
+ }
79
+
80
+ if (updates.search) {
81
+ if (updates.search.default_limit !== undefined) {
82
+ changes.push(`Search limit set to ${updates.search.default_limit}`);
83
+ }
84
+ if (updates.search.default_alpha !== undefined) {
85
+ changes.push(`Search alpha set to ${updates.search.default_alpha}`);
86
+ }
87
+ }
88
+
89
+ if (updates.privacy) {
90
+ if (updates.privacy.default_trust_level !== undefined) {
91
+ changes.push(`Default trust level set to ${updates.privacy.default_trust_level}`);
92
+ }
93
+ }
94
+
95
+ if (updates.location) {
96
+ if (updates.location.auto_capture !== undefined) {
97
+ changes.push(
98
+ updates.location.auto_capture
99
+ ? 'Location auto-capture enabled'
100
+ : 'Location auto-capture disabled'
101
+ );
102
+ }
103
+ }
104
+
105
+ if (changes.length === 0) {
106
+ return 'Preferences updated successfully';
107
+ }
108
+
109
+ return `Preferences updated: ${changes.join(', ')}`;
110
+ }
111
+
112
+ /**
113
+ * Handle remember_set_preference tool
114
+ */
115
+ export async function handleSetPreference(
116
+ args: SetPreferenceArgs,
117
+ userId: string
118
+ ): Promise<string> {
119
+ try {
120
+ const { preferences } = args;
121
+
122
+ logger.info('Setting preferences', { userId, updates: Object.keys(preferences) });
123
+
124
+ // Update preferences using service layer
125
+ const updatedPreferences = await PreferencesDatabaseService.updatePreferences(
126
+ userId,
127
+ preferences
128
+ );
129
+
130
+ const message = formatPreferenceChangeMessage(preferences);
131
+
132
+ const result: SetPreferenceResult = {
133
+ success: true,
134
+ updated_preferences: updatedPreferences,
135
+ message,
136
+ };
137
+
138
+ logger.info('Preferences set successfully', { userId });
139
+
140
+ return JSON.stringify(result, null, 2);
141
+ } catch (error) {
142
+ logger.error('Failed to set preferences:', error);
143
+ throw new Error(`Failed to set preferences: ${error instanceof Error ? error.message : String(error)}`);
144
+ }
145
+ }