@prmichaelsen/remember-mcp 2.2.1 → 2.3.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 (61) hide show
  1. package/AGENT.md +4 -4
  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.proceed.md +311 -0
  6. package/agent/commands/acp.status.md +280 -0
  7. package/agent/commands/acp.version-check-for-updates.md +275 -0
  8. package/agent/commands/acp.version-check.md +190 -0
  9. package/agent/commands/acp.version-update.md +288 -0
  10. package/agent/commands/command.template.md +273 -0
  11. package/agent/design/core-memory-user-profile.md +1253 -0
  12. package/agent/design/ghost-profiles-pseudonymous-identity.md +194 -0
  13. package/agent/design/publish-tools-confirmation-flow.md +922 -0
  14. package/agent/milestones/milestone-10-shared-spaces.md +169 -0
  15. package/agent/progress.yaml +90 -4
  16. package/agent/scripts/install.sh +118 -0
  17. package/agent/scripts/update.sh +22 -10
  18. package/agent/scripts/version.sh +35 -0
  19. package/agent/tasks/task-27-implement-llm-provider-interface.md +51 -0
  20. package/agent/tasks/task-28-implement-llm-provider-factory.md +64 -0
  21. package/agent/tasks/task-29-update-config-for-llm.md +71 -0
  22. package/agent/tasks/task-30-implement-bedrock-provider.md +147 -0
  23. package/agent/tasks/task-31-implement-background-job-service.md +120 -0
  24. package/agent/tasks/task-32-test-llm-provider-integration.md +152 -0
  25. package/agent/tasks/task-34-create-confirmation-token-service.md +191 -0
  26. package/agent/tasks/task-35-create-space-memory-types-schema.md +183 -0
  27. package/agent/tasks/task-36-implement-remember-publish.md +227 -0
  28. package/agent/tasks/task-37-implement-remember-confirm.md +225 -0
  29. package/agent/tasks/task-38-implement-remember-deny.md +161 -0
  30. package/agent/tasks/task-39-implement-remember-search-space.md +188 -0
  31. package/agent/tasks/task-40-implement-remember-query-space.md +193 -0
  32. package/agent/tasks/task-41-configure-firestore-ttl.md +188 -0
  33. package/agent/tasks/task-42-create-tests-shared-spaces.md +216 -0
  34. package/agent/tasks/task-43-update-documentation.md +255 -0
  35. package/dist/llm/types.d.ts +1 -0
  36. package/dist/server-factory.js +914 -1
  37. package/dist/server.js +916 -3
  38. package/dist/services/confirmation-token.service.d.ts +99 -0
  39. package/dist/services/confirmation-token.service.spec.d.ts +5 -0
  40. package/dist/tools/confirm.d.ts +20 -0
  41. package/dist/tools/deny.d.ts +19 -0
  42. package/dist/tools/publish.d.ts +22 -0
  43. package/dist/tools/query-space.d.ts +28 -0
  44. package/dist/tools/search-space.d.ts +29 -0
  45. package/dist/types/space-memory.d.ts +80 -0
  46. package/dist/weaviate/space-schema.d.ts +59 -0
  47. package/dist/weaviate/space-schema.spec.d.ts +5 -0
  48. package/package.json +1 -1
  49. package/src/llm/types.ts +0 -0
  50. package/src/server-factory.ts +33 -0
  51. package/src/server.ts +33 -0
  52. package/src/services/confirmation-token.service.spec.ts +254 -0
  53. package/src/services/confirmation-token.service.ts +232 -0
  54. package/src/tools/confirm.ts +176 -0
  55. package/src/tools/deny.ts +70 -0
  56. package/src/tools/publish.ts +167 -0
  57. package/src/tools/query-space.ts +197 -0
  58. package/src/tools/search-space.ts +189 -0
  59. package/src/types/space-memory.ts +94 -0
  60. package/src/weaviate/space-schema.spec.ts +131 -0
  61. 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,232 @@
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
+ const docRef = await addDocument(collectionPath, request);
72
+
73
+ return { requestId: docRef.id, token };
74
+ }
75
+
76
+ /**
77
+ * Validate and retrieve a confirmation request
78
+ *
79
+ * @param userId - User ID
80
+ * @param token - Confirmation token
81
+ * @returns Request with request_id if valid, null otherwise
82
+ */
83
+ async validateToken(
84
+ userId: string,
85
+ token: string
86
+ ): Promise<(ConfirmationRequest & { request_id: string }) | null> {
87
+ const collectionPath = `users/${userId}/requests`;
88
+
89
+ // Query for the token
90
+ const queryOptions: QueryOptions = {
91
+ where: [
92
+ { field: 'token', op: '==', value: token },
93
+ { field: 'status', op: '==', value: 'pending' },
94
+ ],
95
+ limit: 1,
96
+ };
97
+
98
+ const results = await queryDocuments(collectionPath, queryOptions);
99
+
100
+ if (results.length === 0) {
101
+ return null;
102
+ }
103
+
104
+ const doc = results[0];
105
+ const request = doc.data as ConfirmationRequest;
106
+
107
+ // Check expiry
108
+ const expiresAt = new Date(request.expires_at);
109
+ if (expiresAt.getTime() < Date.now()) {
110
+ await this.updateStatus(userId, doc.id, 'expired');
111
+ return null;
112
+ }
113
+
114
+ return {
115
+ ...request,
116
+ request_id: doc.id,
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Confirm a request
122
+ *
123
+ * @param userId - User ID
124
+ * @param token - Confirmation token
125
+ * @returns Confirmed request if valid, null otherwise
126
+ */
127
+ async confirmRequest(
128
+ userId: string,
129
+ token: string
130
+ ): Promise<(ConfirmationRequest & { request_id: string }) | null> {
131
+ const request = await this.validateToken(userId, token);
132
+ if (!request) {
133
+ return null;
134
+ }
135
+
136
+ await this.updateStatus(userId, request.request_id, 'confirmed');
137
+
138
+ return {
139
+ ...request,
140
+ status: 'confirmed',
141
+ confirmed_at: new Date().toISOString(),
142
+ };
143
+ }
144
+
145
+ /**
146
+ * Deny a request
147
+ *
148
+ * @param userId - User ID
149
+ * @param token - Confirmation token
150
+ * @returns True if denied successfully, false otherwise
151
+ */
152
+ async denyRequest(
153
+ userId: string,
154
+ token: string
155
+ ): Promise<boolean> {
156
+ const request = await this.validateToken(userId, token);
157
+ if (!request) {
158
+ return false;
159
+ }
160
+
161
+ await this.updateStatus(userId, request.request_id, 'denied');
162
+ return true;
163
+ }
164
+
165
+ /**
166
+ * Retract a request
167
+ *
168
+ * @param userId - User ID
169
+ * @param token - Confirmation token
170
+ * @returns True if retracted successfully, false otherwise
171
+ */
172
+ async retractRequest(
173
+ userId: string,
174
+ token: string
175
+ ): Promise<boolean> {
176
+ const request = await this.validateToken(userId, token);
177
+ if (!request) {
178
+ return false;
179
+ }
180
+
181
+ await this.updateStatus(userId, request.request_id, 'retracted');
182
+ return true;
183
+ }
184
+
185
+ /**
186
+ * Update request status
187
+ *
188
+ * @param userId - User ID
189
+ * @param requestId - Request document ID
190
+ * @param status - New status
191
+ */
192
+ private async updateStatus(
193
+ userId: string,
194
+ requestId: string,
195
+ status: ConfirmationRequest['status']
196
+ ): Promise<void> {
197
+ const collectionPath = `users/${userId}/requests`;
198
+
199
+ const updateData: Partial<ConfirmationRequest> = {
200
+ status,
201
+ };
202
+
203
+ if (status === 'confirmed') {
204
+ updateData.confirmed_at = new Date().toISOString();
205
+ }
206
+
207
+ await updateDocument(collectionPath, requestId, updateData);
208
+ }
209
+
210
+ /**
211
+ * Clean up expired requests (optional - Firestore TTL handles deletion)
212
+ *
213
+ * Note: Configure Firestore TTL policy on 'requests' collection group
214
+ * with 'expires_at' field for automatic deletion within 24 hours.
215
+ *
216
+ * This method is optional for immediate cleanup if needed.
217
+ *
218
+ * @returns Count of deleted requests
219
+ */
220
+ async cleanupExpired(): Promise<number> {
221
+ // Note: firebase-admin-sdk-v8 doesn't support collectionGroup queries
222
+ // This would need to be implemented differently or rely on Firestore TTL
223
+ // For now, return 0 and rely on Firestore TTL policy
224
+ console.warn('[ConfirmationTokenService] cleanupExpired not implemented - rely on Firestore TTL');
225
+ return 0;
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Singleton instance of the confirmation token service
231
+ */
232
+ export const confirmationTokenService = new ConfirmationTokenService();
@@ -0,0 +1,176 @@
1
+ /**
2
+ * remember_confirm tool
3
+ *
4
+ * Generic confirmation tool that executes any pending action.
5
+ * This is the second phase of the confirmation workflow.
6
+ */
7
+
8
+ import type { Tool } from '@modelcontextprotocol/sdk/types.js';
9
+ import { confirmationTokenService, type ConfirmationRequest } from '../services/confirmation-token.service.js';
10
+ import { getWeaviateClient, getMemoryCollectionName } from '../weaviate/client.js';
11
+ import { ensureSpaceCollection } from '../weaviate/space-schema.js';
12
+ import { handleToolError } from '../utils/error-handler.js';
13
+
14
+ /**
15
+ * Tool definition for remember_confirm
16
+ */
17
+ export const confirmTool: Tool = {
18
+ name: 'remember_confirm',
19
+ description: 'Confirm and execute a pending action using the token. Works for any action that requires confirmation (publish, delete, etc.).',
20
+ inputSchema: {
21
+ type: 'object',
22
+ properties: {
23
+ token: {
24
+ type: 'string',
25
+ description: 'The confirmation token from the action tool',
26
+ },
27
+ },
28
+ required: ['token'],
29
+ },
30
+ };
31
+
32
+ interface ConfirmArgs {
33
+ token: string;
34
+ }
35
+
36
+ /**
37
+ * Handle remember_confirm tool execution
38
+ */
39
+ export async function handleConfirm(
40
+ args: ConfirmArgs,
41
+ userId: string
42
+ ): Promise<string> {
43
+ try {
44
+ // Validate and confirm token
45
+ const request = await confirmationTokenService.confirmRequest(userId, args.token);
46
+
47
+ if (!request) {
48
+ return JSON.stringify(
49
+ {
50
+ success: false,
51
+ error: 'Invalid or expired token',
52
+ message: 'The confirmation token is invalid, expired, or has already been used.',
53
+ },
54
+ null,
55
+ 2
56
+ );
57
+ }
58
+
59
+ // GENERIC: Execute action based on type
60
+ // This is where the generic pattern delegates to action-specific executors
61
+ if (request.action === 'publish_memory') {
62
+ return await executePublishMemory(request, userId);
63
+ }
64
+
65
+ // Add other action types here as needed
66
+ // if (request.action === 'retract_memory') {
67
+ // return await executeRetractMemory(request, userId);
68
+ // }
69
+
70
+ throw new Error(`Unknown action type: ${request.action}`);
71
+ } catch (error) {
72
+ handleToolError(error, {
73
+ toolName: 'remember_confirm',
74
+ userId,
75
+ operation: 'confirm action',
76
+ token: args.token,
77
+ });
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Execute publish memory action
83
+ */
84
+ async function executePublishMemory(
85
+ request: ConfirmationRequest & { request_id: string },
86
+ userId: string
87
+ ): Promise<string> {
88
+ try {
89
+ // Fetch the memory NOW (during confirmation, not from stored payload)
90
+ const weaviateClient = getWeaviateClient();
91
+ const userCollection = weaviateClient.collections.get(
92
+ getMemoryCollectionName(userId)
93
+ );
94
+
95
+ const originalMemory = await userCollection.query.fetchObjectById(
96
+ request.payload.memory_id
97
+ );
98
+
99
+ if (!originalMemory) {
100
+ return JSON.stringify(
101
+ {
102
+ success: false,
103
+ error: 'Memory not found',
104
+ message: `Original memory ${request.payload.memory_id} no longer exists`,
105
+ },
106
+ null,
107
+ 2
108
+ );
109
+ }
110
+
111
+ // Verify ownership again
112
+ if (originalMemory.properties.user_id !== userId) {
113
+ return JSON.stringify(
114
+ {
115
+ success: false,
116
+ error: 'Permission denied',
117
+ message: 'You can only publish your own memories',
118
+ },
119
+ null,
120
+ 2
121
+ );
122
+ }
123
+
124
+ // Get target collection
125
+ const targetCollection = await ensureSpaceCollection(
126
+ weaviateClient,
127
+ request.target_collection || 'the_void'
128
+ );
129
+
130
+ // Create published memory (copy with modifications)
131
+ const originalTags = Array.isArray(originalMemory.properties.tags)
132
+ ? originalMemory.properties.tags
133
+ : [];
134
+ const additionalTags = Array.isArray(request.payload.additional_tags)
135
+ ? request.payload.additional_tags
136
+ : [];
137
+
138
+ const publishedMemory = {
139
+ ...originalMemory.properties,
140
+ // Override specific fields
141
+ space_id: request.target_collection || 'the_void',
142
+ author_id: userId, // Always attributed
143
+ published_at: new Date().toISOString(),
144
+ discovery_count: 0,
145
+ doc_type: 'space_memory',
146
+ attribution: 'user' as const,
147
+ // Merge additional tags
148
+ tags: [...originalTags, ...additionalTags],
149
+ // Update timestamps
150
+ created_at: new Date().toISOString(),
151
+ updated_at: new Date().toISOString(),
152
+ version: 1,
153
+ };
154
+
155
+ const result = await targetCollection.data.insert({
156
+ properties: publishedMemory as any,
157
+ });
158
+
159
+ // Return minimal response - agent already knows original memory
160
+ return JSON.stringify(
161
+ {
162
+ success: true,
163
+ space_memory_id: result,
164
+ },
165
+ null,
166
+ 2
167
+ );
168
+ } catch (error) {
169
+ handleToolError(error, {
170
+ toolName: 'remember_confirm',
171
+ userId,
172
+ operation: 'execute publish_memory',
173
+ action: 'publish_memory',
174
+ });
175
+ }
176
+ }