@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.
- package/AGENT.md +4 -4
- package/CHANGELOG.md +45 -0
- package/README.md +43 -3
- package/agent/commands/acp.init.md +376 -0
- package/agent/commands/acp.proceed.md +311 -0
- package/agent/commands/acp.status.md +280 -0
- package/agent/commands/acp.version-check-for-updates.md +275 -0
- package/agent/commands/acp.version-check.md +190 -0
- package/agent/commands/acp.version-update.md +288 -0
- package/agent/commands/command.template.md +273 -0
- package/agent/design/core-memory-user-profile.md +1253 -0
- package/agent/design/ghost-profiles-pseudonymous-identity.md +194 -0
- package/agent/design/publish-tools-confirmation-flow.md +922 -0
- package/agent/milestones/milestone-10-shared-spaces.md +169 -0
- package/agent/progress.yaml +90 -4
- package/agent/scripts/install.sh +118 -0
- package/agent/scripts/update.sh +22 -10
- package/agent/scripts/version.sh +35 -0
- package/agent/tasks/task-27-implement-llm-provider-interface.md +51 -0
- package/agent/tasks/task-28-implement-llm-provider-factory.md +64 -0
- package/agent/tasks/task-29-update-config-for-llm.md +71 -0
- package/agent/tasks/task-30-implement-bedrock-provider.md +147 -0
- package/agent/tasks/task-31-implement-background-job-service.md +120 -0
- package/agent/tasks/task-32-test-llm-provider-integration.md +152 -0
- package/agent/tasks/task-34-create-confirmation-token-service.md +191 -0
- package/agent/tasks/task-35-create-space-memory-types-schema.md +183 -0
- package/agent/tasks/task-36-implement-remember-publish.md +227 -0
- package/agent/tasks/task-37-implement-remember-confirm.md +225 -0
- package/agent/tasks/task-38-implement-remember-deny.md +161 -0
- package/agent/tasks/task-39-implement-remember-search-space.md +188 -0
- package/agent/tasks/task-40-implement-remember-query-space.md +193 -0
- package/agent/tasks/task-41-configure-firestore-ttl.md +188 -0
- package/agent/tasks/task-42-create-tests-shared-spaces.md +216 -0
- package/agent/tasks/task-43-update-documentation.md +255 -0
- package/dist/llm/types.d.ts +1 -0
- package/dist/server-factory.js +914 -1
- package/dist/server.js +916 -3
- package/dist/services/confirmation-token.service.d.ts +99 -0
- package/dist/services/confirmation-token.service.spec.d.ts +5 -0
- package/dist/tools/confirm.d.ts +20 -0
- package/dist/tools/deny.d.ts +19 -0
- package/dist/tools/publish.d.ts +22 -0
- package/dist/tools/query-space.d.ts +28 -0
- package/dist/tools/search-space.d.ts +29 -0
- package/dist/types/space-memory.d.ts +80 -0
- package/dist/weaviate/space-schema.d.ts +59 -0
- package/dist/weaviate/space-schema.spec.d.ts +5 -0
- package/package.json +1 -1
- package/src/llm/types.ts +0 -0
- package/src/server-factory.ts +33 -0
- package/src/server.ts +33 -0
- package/src/services/confirmation-token.service.spec.ts +254 -0
- package/src/services/confirmation-token.service.ts +232 -0
- package/src/tools/confirm.ts +176 -0
- package/src/tools/deny.ts +70 -0
- package/src/tools/publish.ts +167 -0
- package/src/tools/query-space.ts +197 -0
- package/src/tools/search-space.ts +189 -0
- package/src/types/space-memory.ts +94 -0
- package/src/weaviate/space-schema.spec.ts +131 -0
- 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
|
+
}
|