@prmichaelsen/remember-mcp 2.2.1 → 2.3.1

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 (69) hide show
  1. package/AGENT.md +98 -5
  2. package/CHANGELOG.md +45 -0
  3. package/README.md +43 -3
  4. package/agent/commands/acp.init.md +376 -0
  5. package/agent/commands/acp.package-install.md +347 -0
  6. package/agent/commands/acp.proceed.md +311 -0
  7. package/agent/commands/acp.report.md +392 -0
  8. package/agent/commands/acp.status.md +280 -0
  9. package/agent/commands/acp.sync.md +323 -0
  10. package/agent/commands/acp.update.md +301 -0
  11. package/agent/commands/acp.validate.md +385 -0
  12. package/agent/commands/acp.version-check-for-updates.md +275 -0
  13. package/agent/commands/acp.version-check.md +190 -0
  14. package/agent/commands/acp.version-update.md +288 -0
  15. package/agent/commands/command.template.md +273 -0
  16. package/agent/design/core-memory-user-profile.md +1253 -0
  17. package/agent/design/ghost-profiles-pseudonymous-identity.md +194 -0
  18. package/agent/design/publish-tools-confirmation-flow.md +922 -0
  19. package/agent/milestones/milestone-10-shared-spaces.md +169 -0
  20. package/agent/progress.yaml +90 -4
  21. package/agent/scripts/install.sh +118 -0
  22. package/agent/scripts/update.sh +22 -10
  23. package/agent/scripts/version.sh +35 -0
  24. package/agent/tasks/task-27-implement-llm-provider-interface.md +51 -0
  25. package/agent/tasks/task-28-implement-llm-provider-factory.md +64 -0
  26. package/agent/tasks/task-29-update-config-for-llm.md +71 -0
  27. package/agent/tasks/task-30-implement-bedrock-provider.md +147 -0
  28. package/agent/tasks/task-31-implement-background-job-service.md +120 -0
  29. package/agent/tasks/task-32-test-llm-provider-integration.md +152 -0
  30. package/agent/tasks/task-34-create-confirmation-token-service.md +191 -0
  31. package/agent/tasks/task-35-create-space-memory-types-schema.md +183 -0
  32. package/agent/tasks/task-36-implement-remember-publish.md +227 -0
  33. package/agent/tasks/task-37-implement-remember-confirm.md +225 -0
  34. package/agent/tasks/task-38-implement-remember-deny.md +161 -0
  35. package/agent/tasks/task-39-implement-remember-search-space.md +188 -0
  36. package/agent/tasks/task-40-implement-remember-query-space.md +193 -0
  37. package/agent/tasks/task-41-configure-firestore-ttl.md +188 -0
  38. package/agent/tasks/task-42-create-tests-shared-spaces.md +216 -0
  39. package/agent/tasks/task-43-update-documentation.md +255 -0
  40. package/agent/tasks/task-44-implement-remember-retract.md +263 -0
  41. package/agent/tasks/task-45-fix-publish-false-success-bug.md +230 -0
  42. package/dist/llm/types.d.ts +1 -0
  43. package/dist/server-factory.js +1000 -1
  44. package/dist/server.js +1002 -3
  45. package/dist/services/confirmation-token.service.d.ts +99 -0
  46. package/dist/services/confirmation-token.service.spec.d.ts +5 -0
  47. package/dist/tools/confirm.d.ts +20 -0
  48. package/dist/tools/deny.d.ts +19 -0
  49. package/dist/tools/publish.d.ts +22 -0
  50. package/dist/tools/query-space.d.ts +28 -0
  51. package/dist/tools/search-space.d.ts +29 -0
  52. package/dist/types/space-memory.d.ts +80 -0
  53. package/dist/weaviate/space-schema.d.ts +59 -0
  54. package/dist/weaviate/space-schema.spec.d.ts +5 -0
  55. package/package.json +1 -1
  56. package/src/llm/types.ts +0 -0
  57. package/src/server-factory.ts +33 -0
  58. package/src/server.ts +33 -0
  59. package/src/services/confirmation-token.service.spec.ts +254 -0
  60. package/src/services/confirmation-token.service.ts +265 -0
  61. package/src/tools/confirm.ts +219 -0
  62. package/src/tools/create-memory.ts +7 -0
  63. package/src/tools/deny.ts +70 -0
  64. package/src/tools/publish.ts +190 -0
  65. package/src/tools/query-space.ts +197 -0
  66. package/src/tools/search-space.ts +189 -0
  67. package/src/types/space-memory.ts +94 -0
  68. package/src/weaviate/space-schema.spec.ts +131 -0
  69. package/src/weaviate/space-schema.ts +275 -0
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Unit tests for Confirmation Token Service
3
+ */
4
+
5
+ import { ConfirmationTokenService, type ConfirmationRequest } from '../../src/services/confirmation-token.service';
6
+ import * as firestoreInit from '../../src/firestore/init';
7
+
8
+ // Mock Firestore functions
9
+ jest.mock('../../src/firestore/init', () => ({
10
+ getDocument: jest.fn(),
11
+ addDocument: jest.fn(),
12
+ updateDocument: jest.fn(),
13
+ queryDocuments: jest.fn(),
14
+ }));
15
+
16
+ describe('ConfirmationTokenService', () => {
17
+ let service: ConfirmationTokenService;
18
+ const mockUserId = 'test-user-123';
19
+ const mockToken = '550e8400-e29b-41d4-a716-446655440000';
20
+
21
+ beforeEach(() => {
22
+ service = new ConfirmationTokenService();
23
+ jest.clearAllMocks();
24
+ });
25
+
26
+ afterEach(() => {
27
+ jest.restoreAllMocks();
28
+ });
29
+
30
+ describe('createRequest', () => {
31
+ it('should create a new confirmation request with token', async () => {
32
+ const mockDocRef = { id: 'request-123', path: 'users/test-user-123/requests/request-123' };
33
+ (firestoreInit.addDocument as jest.Mock).mockResolvedValue(mockDocRef);
34
+
35
+ const payload = { memory_id: 'mem-123', additional_tags: [] };
36
+ const result = await service.createRequest(mockUserId, 'publish_memory', payload, 'the_void');
37
+
38
+ expect(result.requestId).toBe('request-123');
39
+ expect(result.token).toBeDefined();
40
+ expect(typeof result.token).toBe('string');
41
+ expect(result.token.length).toBeGreaterThan(0);
42
+ expect(firestoreInit.addDocument).toHaveBeenCalledWith(
43
+ 'users/test-user-123/requests',
44
+ expect.objectContaining({
45
+ user_id: mockUserId,
46
+ action: 'publish_memory',
47
+ target_collection: 'the_void',
48
+ payload,
49
+ status: 'pending',
50
+ })
51
+ );
52
+ });
53
+
54
+ it('should set expiry to 5 minutes from now', async () => {
55
+ const mockDocRef = { id: 'request-123', path: 'path' };
56
+ (firestoreInit.addDocument as jest.Mock).mockResolvedValue(mockDocRef);
57
+
58
+ const beforeTime = Date.now();
59
+ await service.createRequest(mockUserId, 'publish_memory', {});
60
+ const afterTime = Date.now();
61
+
62
+ const call = (firestoreInit.addDocument as jest.Mock).mock.calls[0][1];
63
+ const expiresAt = new Date(call.expires_at).getTime();
64
+ const createdAt = new Date(call.created_at).getTime();
65
+
66
+ // Should be 5 minutes (300000ms) after creation
67
+ const expectedExpiry = createdAt + 5 * 60 * 1000;
68
+ expect(expiresAt).toBe(expectedExpiry);
69
+ expect(createdAt).toBeGreaterThanOrEqual(beforeTime);
70
+ expect(createdAt).toBeLessThanOrEqual(afterTime);
71
+ });
72
+ });
73
+
74
+ describe('validateToken', () => {
75
+ it('should return request if token is valid and not expired', async () => {
76
+ const mockRequest: ConfirmationRequest = {
77
+ user_id: mockUserId,
78
+ token: mockToken,
79
+ action: 'publish_memory',
80
+ payload: { memory_id: 'mem-123' },
81
+ created_at: new Date().toISOString(),
82
+ expires_at: new Date(Date.now() + 60000).toISOString(), // 1 minute from now
83
+ status: 'pending',
84
+ };
85
+
86
+ (firestoreInit.queryDocuments as jest.Mock).mockResolvedValue([
87
+ { id: 'request-123', data: mockRequest }
88
+ ]);
89
+
90
+ const result = await service.validateToken(mockUserId, mockToken);
91
+
92
+ expect(result).toEqual({
93
+ ...mockRequest,
94
+ request_id: 'request-123',
95
+ });
96
+ });
97
+
98
+ it('should return null if token not found', async () => {
99
+ (firestoreInit.queryDocuments as jest.Mock).mockResolvedValue([]);
100
+
101
+ const result = await service.validateToken(mockUserId, mockToken);
102
+
103
+ expect(result).toBeNull();
104
+ });
105
+
106
+ it('should return null and mark expired if token is expired', async () => {
107
+ const mockRequest: ConfirmationRequest = {
108
+ user_id: mockUserId,
109
+ token: mockToken,
110
+ action: 'publish_memory',
111
+ payload: {},
112
+ created_at: new Date(Date.now() - 10 * 60 * 1000).toISOString(), // 10 minutes ago
113
+ expires_at: new Date(Date.now() - 5 * 60 * 1000).toISOString(), // 5 minutes ago (expired)
114
+ status: 'pending',
115
+ };
116
+
117
+ (firestoreInit.queryDocuments as jest.Mock).mockResolvedValue([
118
+ { id: 'request-123', data: mockRequest }
119
+ ]);
120
+ (firestoreInit.updateDocument as jest.Mock).mockResolvedValue(undefined);
121
+
122
+ const result = await service.validateToken(mockUserId, mockToken);
123
+
124
+ expect(result).toBeNull();
125
+ expect(firestoreInit.updateDocument).toHaveBeenCalledWith(
126
+ 'users/test-user-123/requests',
127
+ 'request-123',
128
+ { status: 'expired' }
129
+ );
130
+ });
131
+ });
132
+
133
+ describe('confirmRequest', () => {
134
+ it('should confirm a valid request', async () => {
135
+ const mockRequest: ConfirmationRequest = {
136
+ user_id: mockUserId,
137
+ token: mockToken,
138
+ action: 'publish_memory',
139
+ payload: { memory_id: 'mem-123' },
140
+ created_at: new Date().toISOString(),
141
+ expires_at: new Date(Date.now() + 60000).toISOString(),
142
+ status: 'pending',
143
+ };
144
+
145
+ (firestoreInit.queryDocuments as jest.Mock).mockResolvedValue([
146
+ { id: 'request-123', data: mockRequest }
147
+ ]);
148
+ (firestoreInit.updateDocument as jest.Mock).mockResolvedValue(undefined);
149
+
150
+ const result = await service.confirmRequest(mockUserId, mockToken);
151
+
152
+ expect(result).not.toBeNull();
153
+ expect(result?.status).toBe('confirmed');
154
+ expect(result?.confirmed_at).toBeDefined();
155
+ expect(firestoreInit.updateDocument).toHaveBeenCalledWith(
156
+ 'users/test-user-123/requests',
157
+ 'request-123',
158
+ expect.objectContaining({
159
+ status: 'confirmed',
160
+ confirmed_at: expect.any(String),
161
+ })
162
+ );
163
+ });
164
+
165
+ it('should return null if token is invalid', async () => {
166
+ (firestoreInit.queryDocuments as jest.Mock).mockResolvedValue([]);
167
+
168
+ const result = await service.confirmRequest(mockUserId, mockToken);
169
+
170
+ expect(result).toBeNull();
171
+ expect(firestoreInit.updateDocument).not.toHaveBeenCalled();
172
+ });
173
+ });
174
+
175
+ describe('denyRequest', () => {
176
+ it('should deny a valid request', async () => {
177
+ const mockRequest: ConfirmationRequest = {
178
+ user_id: mockUserId,
179
+ token: mockToken,
180
+ action: 'publish_memory',
181
+ payload: {},
182
+ created_at: new Date().toISOString(),
183
+ expires_at: new Date(Date.now() + 60000).toISOString(),
184
+ status: 'pending',
185
+ };
186
+
187
+ (firestoreInit.queryDocuments as jest.Mock).mockResolvedValue([
188
+ { id: 'request-123', data: mockRequest }
189
+ ]);
190
+ (firestoreInit.updateDocument as jest.Mock).mockResolvedValue(undefined);
191
+
192
+ const result = await service.denyRequest(mockUserId, mockToken);
193
+
194
+ expect(result).toBe(true);
195
+ expect(firestoreInit.updateDocument).toHaveBeenCalledWith(
196
+ 'users/test-user-123/requests',
197
+ 'request-123',
198
+ { status: 'denied' }
199
+ );
200
+ });
201
+
202
+ it('should return false if token is invalid', async () => {
203
+ (firestoreInit.queryDocuments as jest.Mock).mockResolvedValue([]);
204
+
205
+ const result = await service.denyRequest(mockUserId, mockToken);
206
+
207
+ expect(result).toBe(false);
208
+ });
209
+ });
210
+
211
+ describe('retractRequest', () => {
212
+ it('should retract a valid request', async () => {
213
+ const mockRequest: ConfirmationRequest = {
214
+ user_id: mockUserId,
215
+ token: mockToken,
216
+ action: 'publish_memory',
217
+ payload: {},
218
+ created_at: new Date().toISOString(),
219
+ expires_at: new Date(Date.now() + 60000).toISOString(),
220
+ status: 'pending',
221
+ };
222
+
223
+ (firestoreInit.queryDocuments as jest.Mock).mockResolvedValue([
224
+ { id: 'request-123', data: mockRequest }
225
+ ]);
226
+ (firestoreInit.updateDocument as jest.Mock).mockResolvedValue(undefined);
227
+
228
+ const result = await service.retractRequest(mockUserId, mockToken);
229
+
230
+ expect(result).toBe(true);
231
+ expect(firestoreInit.updateDocument).toHaveBeenCalledWith(
232
+ 'users/test-user-123/requests',
233
+ 'request-123',
234
+ { status: 'retracted' }
235
+ );
236
+ });
237
+
238
+ it('should return false if token is invalid', async () => {
239
+ (firestoreInit.queryDocuments as jest.Mock).mockResolvedValue([]);
240
+
241
+ const result = await service.retractRequest(mockUserId, mockToken);
242
+
243
+ expect(result).toBe(false);
244
+ });
245
+ });
246
+
247
+ describe('cleanupExpired', () => {
248
+ it('should return 0 (not implemented - relies on Firestore TTL)', async () => {
249
+ const result = await service.cleanupExpired();
250
+
251
+ expect(result).toBe(0);
252
+ });
253
+ });
254
+ });
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Confirmation Token Service
3
+ *
4
+ * Manages confirmation tokens for sensitive operations like publishing memories.
5
+ * Tokens are one-time use with 5-minute expiry.
6
+ */
7
+
8
+ import { randomUUID } from 'crypto';
9
+ import {
10
+ getDocument,
11
+ addDocument,
12
+ updateDocument,
13
+ queryDocuments,
14
+ type QueryOptions
15
+ } from '../firestore/init.js';
16
+
17
+ /**
18
+ * Confirmation request stored in Firestore
19
+ */
20
+ export interface ConfirmationRequest {
21
+ user_id: string;
22
+ token: string;
23
+ action: string;
24
+ target_collection?: string;
25
+ payload: any;
26
+ created_at: string; // ISO 8601 timestamp
27
+ expires_at: string; // ISO 8601 timestamp
28
+ status: 'pending' | 'confirmed' | 'denied' | 'expired' | 'retracted';
29
+ confirmed_at?: string; // ISO 8601 timestamp
30
+ }
31
+
32
+ /**
33
+ * Service for managing confirmation tokens
34
+ */
35
+ export class ConfirmationTokenService {
36
+ private readonly EXPIRY_MINUTES = 5;
37
+
38
+ /**
39
+ * Create a new confirmation request
40
+ *
41
+ * @param userId - User ID who initiated the request
42
+ * @param action - Action type (e.g., 'publish_memory')
43
+ * @param payload - Data to store with the request
44
+ * @param targetCollection - Optional target collection (e.g., 'the_void')
45
+ * @returns Request ID and token
46
+ */
47
+ async createRequest(
48
+ userId: string,
49
+ action: string,
50
+ payload: any,
51
+ targetCollection?: string
52
+ ): Promise<{ requestId: string; token: string }> {
53
+ const token = randomUUID();
54
+
55
+ const now = new Date();
56
+ const expiresAt = new Date(now.getTime() + this.EXPIRY_MINUTES * 60 * 1000);
57
+
58
+ const request: ConfirmationRequest = {
59
+ user_id: userId,
60
+ token,
61
+ action,
62
+ target_collection: targetCollection,
63
+ payload,
64
+ created_at: now.toISOString(),
65
+ expires_at: expiresAt.toISOString(),
66
+ status: 'pending',
67
+ };
68
+
69
+ // Add document to Firestore (auto-generates ID)
70
+ const collectionPath = `users/${userId}/requests`;
71
+ console.log('[ConfirmationTokenService] Creating request:', {
72
+ userId,
73
+ action,
74
+ targetCollection,
75
+ collectionPath,
76
+ });
77
+
78
+ const docRef = await addDocument(collectionPath, request);
79
+
80
+ console.log('[ConfirmationTokenService] Request created:', {
81
+ requestId: docRef.id,
82
+ token,
83
+ expiresAt: request.expires_at,
84
+ });
85
+
86
+ return { requestId: docRef.id, token };
87
+ }
88
+
89
+ /**
90
+ * Validate and retrieve a confirmation request
91
+ *
92
+ * @param userId - User ID
93
+ * @param token - Confirmation token
94
+ * @returns Request with request_id if valid, null otherwise
95
+ */
96
+ async validateToken(
97
+ userId: string,
98
+ token: string
99
+ ): Promise<(ConfirmationRequest & { request_id: string }) | null> {
100
+ const collectionPath = `users/${userId}/requests`;
101
+
102
+ console.log('[ConfirmationTokenService] Validating token:', {
103
+ userId,
104
+ token,
105
+ collectionPath,
106
+ });
107
+
108
+ // Query for the token
109
+ const queryOptions: QueryOptions = {
110
+ where: [
111
+ { field: 'token', op: '==', value: token },
112
+ { field: 'status', op: '==', value: 'pending' },
113
+ ],
114
+ limit: 1,
115
+ };
116
+
117
+ const results = await queryDocuments(collectionPath, queryOptions);
118
+
119
+ console.log('[ConfirmationTokenService] Query results:', {
120
+ resultsFound: results.length,
121
+ hasResults: results.length > 0,
122
+ });
123
+
124
+ if (results.length === 0) {
125
+ console.log('[ConfirmationTokenService] Token not found or not pending');
126
+ return null;
127
+ }
128
+
129
+ const doc = results[0];
130
+ const request = doc.data as ConfirmationRequest;
131
+
132
+ console.log('[ConfirmationTokenService] Request found:', {
133
+ requestId: doc.id,
134
+ action: request.action,
135
+ status: request.status,
136
+ expiresAt: request.expires_at,
137
+ });
138
+
139
+ // Check expiry
140
+ const expiresAt = new Date(request.expires_at);
141
+ if (expiresAt.getTime() < Date.now()) {
142
+ console.log('[ConfirmationTokenService] Token expired');
143
+ await this.updateStatus(userId, doc.id, 'expired');
144
+ return null;
145
+ }
146
+
147
+ return {
148
+ ...request,
149
+ request_id: doc.id,
150
+ };
151
+ }
152
+
153
+ /**
154
+ * Confirm a request
155
+ *
156
+ * @param userId - User ID
157
+ * @param token - Confirmation token
158
+ * @returns Confirmed request if valid, null otherwise
159
+ */
160
+ async confirmRequest(
161
+ userId: string,
162
+ token: string
163
+ ): Promise<(ConfirmationRequest & { request_id: string }) | null> {
164
+ const request = await this.validateToken(userId, token);
165
+ if (!request) {
166
+ return null;
167
+ }
168
+
169
+ await this.updateStatus(userId, request.request_id, 'confirmed');
170
+
171
+ return {
172
+ ...request,
173
+ status: 'confirmed',
174
+ confirmed_at: new Date().toISOString(),
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Deny a request
180
+ *
181
+ * @param userId - User ID
182
+ * @param token - Confirmation token
183
+ * @returns True if denied successfully, false otherwise
184
+ */
185
+ async denyRequest(
186
+ userId: string,
187
+ token: string
188
+ ): Promise<boolean> {
189
+ const request = await this.validateToken(userId, token);
190
+ if (!request) {
191
+ return false;
192
+ }
193
+
194
+ await this.updateStatus(userId, request.request_id, 'denied');
195
+ return true;
196
+ }
197
+
198
+ /**
199
+ * Retract a request
200
+ *
201
+ * @param userId - User ID
202
+ * @param token - Confirmation token
203
+ * @returns True if retracted successfully, false otherwise
204
+ */
205
+ async retractRequest(
206
+ userId: string,
207
+ token: string
208
+ ): Promise<boolean> {
209
+ const request = await this.validateToken(userId, token);
210
+ if (!request) {
211
+ return false;
212
+ }
213
+
214
+ await this.updateStatus(userId, request.request_id, 'retracted');
215
+ return true;
216
+ }
217
+
218
+ /**
219
+ * Update request status
220
+ *
221
+ * @param userId - User ID
222
+ * @param requestId - Request document ID
223
+ * @param status - New status
224
+ */
225
+ private async updateStatus(
226
+ userId: string,
227
+ requestId: string,
228
+ status: ConfirmationRequest['status']
229
+ ): Promise<void> {
230
+ const collectionPath = `users/${userId}/requests`;
231
+
232
+ const updateData: Partial<ConfirmationRequest> = {
233
+ status,
234
+ };
235
+
236
+ if (status === 'confirmed') {
237
+ updateData.confirmed_at = new Date().toISOString();
238
+ }
239
+
240
+ await updateDocument(collectionPath, requestId, updateData);
241
+ }
242
+
243
+ /**
244
+ * Clean up expired requests (optional - Firestore TTL handles deletion)
245
+ *
246
+ * Note: Configure Firestore TTL policy on 'requests' collection group
247
+ * with 'expires_at' field for automatic deletion within 24 hours.
248
+ *
249
+ * This method is optional for immediate cleanup if needed.
250
+ *
251
+ * @returns Count of deleted requests
252
+ */
253
+ async cleanupExpired(): Promise<number> {
254
+ // Note: firebase-admin-sdk-v8 doesn't support collectionGroup queries
255
+ // This would need to be implemented differently or rely on Firestore TTL
256
+ // For now, return 0 and rely on Firestore TTL policy
257
+ console.warn('[ConfirmationTokenService] cleanupExpired not implemented - rely on Firestore TTL');
258
+ return 0;
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Singleton instance of the confirmation token service
264
+ */
265
+ export const confirmationTokenService = new ConfirmationTokenService();