@prmichaelsen/remember-mcp 0.1.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/.env.example +65 -0
- package/AGENT.md +840 -0
- package/README.md +72 -0
- package/agent/design/.gitkeep +0 -0
- package/agent/design/access-control-result-pattern.md +458 -0
- package/agent/design/action-audit-memory-types.md +637 -0
- package/agent/design/common-template-fields.md +282 -0
- package/agent/design/complete-tool-set.md +407 -0
- package/agent/design/content-types-expansion.md +521 -0
- package/agent/design/cross-database-id-strategy.md +358 -0
- package/agent/design/default-template-library.md +423 -0
- package/agent/design/firestore-wrapper-analysis.md +606 -0
- package/agent/design/llm-provider-abstraction.md +691 -0
- package/agent/design/location-handling-architecture.md +523 -0
- package/agent/design/memory-templates-design.md +364 -0
- package/agent/design/permissions-storage-architecture.md +680 -0
- package/agent/design/relationship-storage-strategy.md +361 -0
- package/agent/design/remember-mcp-implementation-tasks.md +417 -0
- package/agent/design/remember-mcp-progress.yaml +141 -0
- package/agent/design/requirements-enhancements.md +468 -0
- package/agent/design/requirements.md +56 -0
- package/agent/design/template-storage-strategy.md +412 -0
- package/agent/design/template-suggestion-system.md +853 -0
- package/agent/design/trust-escalation-prevention.md +343 -0
- package/agent/design/trust-system-implementation.md +592 -0
- package/agent/design/user-preferences.md +683 -0
- package/agent/design/weaviate-collection-strategy.md +461 -0
- package/agent/milestones/.gitkeep +0 -0
- package/agent/milestones/milestone-1-project-foundation.md +121 -0
- package/agent/milestones/milestone-2-core-memory-system.md +150 -0
- package/agent/milestones/milestone-3-relationships-graph.md +116 -0
- package/agent/milestones/milestone-4-user-preferences.md +103 -0
- package/agent/milestones/milestone-5-template-system.md +126 -0
- package/agent/milestones/milestone-6-auth-multi-tenancy.md +124 -0
- package/agent/milestones/milestone-7-trust-permissions.md +133 -0
- package/agent/milestones/milestone-8-testing-quality.md +137 -0
- package/agent/milestones/milestone-9-deployment-documentation.md +147 -0
- package/agent/patterns/.gitkeep +0 -0
- package/agent/patterns/bootstrap.md +1271 -0
- package/agent/patterns/firebase-admin-sdk-v8-usage.md +950 -0
- package/agent/patterns/firestore-users-pattern-best-practices.md +347 -0
- package/agent/patterns/library-services.md +454 -0
- package/agent/patterns/testing-colocated.md +316 -0
- package/agent/progress.yaml +395 -0
- package/agent/tasks/.gitkeep +0 -0
- package/agent/tasks/task-1-initialize-project-structure.md +266 -0
- package/agent/tasks/task-2-install-dependencies.md +199 -0
- package/agent/tasks/task-3-setup-weaviate-client.md +330 -0
- package/agent/tasks/task-4-setup-firestore-client.md +362 -0
- package/agent/tasks/task-5-create-basic-mcp-server.md +114 -0
- package/agent/tasks/task-6-create-integration-tests.md +195 -0
- package/agent/tasks/task-7-finalize-milestone-1.md +363 -0
- package/agent/tasks/task-8-setup-utility-scripts.md +382 -0
- package/agent/tasks/task-9-create-server-factory.md +404 -0
- package/dist/config.d.ts +26 -0
- package/dist/constants/content-types.d.ts +60 -0
- package/dist/firestore/init.d.ts +14 -0
- package/dist/firestore/paths.d.ts +53 -0
- package/dist/firestore/paths.spec.d.ts +2 -0
- package/dist/server-factory.d.ts +40 -0
- package/dist/server-factory.js +1741 -0
- package/dist/server-factory.spec.d.ts +2 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +1690 -0
- package/dist/tools/create-memory.d.ts +94 -0
- package/dist/tools/delete-memory.d.ts +47 -0
- package/dist/tools/search-memory.d.ts +88 -0
- package/dist/types/memory.d.ts +183 -0
- package/dist/utils/logger.d.ts +7 -0
- package/dist/weaviate/client.d.ts +39 -0
- package/dist/weaviate/client.spec.d.ts +2 -0
- package/dist/weaviate/schema.d.ts +29 -0
- package/esbuild.build.js +60 -0
- package/esbuild.watch.js +25 -0
- package/jest.config.js +31 -0
- package/jest.e2e.config.js +17 -0
- package/package.json +68 -0
- package/src/.gitkeep +0 -0
- package/src/config.ts +56 -0
- package/src/constants/content-types.ts +454 -0
- package/src/firestore/init.ts +68 -0
- package/src/firestore/paths.spec.ts +75 -0
- package/src/firestore/paths.ts +124 -0
- package/src/server-factory.spec.ts +60 -0
- package/src/server-factory.ts +215 -0
- package/src/server.ts +243 -0
- package/src/tools/create-memory.ts +198 -0
- package/src/tools/delete-memory.ts +126 -0
- package/src/tools/search-memory.ts +216 -0
- package/src/types/memory.ts +276 -0
- package/src/utils/logger.ts +42 -0
- package/src/weaviate/client.spec.ts +58 -0
- package/src/weaviate/client.ts +114 -0
- package/src/weaviate/schema.ts +288 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# Task 3: Set Up Weaviate Client
|
|
2
|
+
|
|
3
|
+
**Milestone**: M1 - Project Foundation
|
|
4
|
+
**Estimated Time**: 3 hours
|
|
5
|
+
**Dependencies**: Task 2 ✅
|
|
6
|
+
**Status**: ✅ COMPLETED (2026-02-11)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Objective
|
|
11
|
+
|
|
12
|
+
Create Weaviate client wrapper with connection management and user-scoped collection handling.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Steps
|
|
17
|
+
|
|
18
|
+
### 1. Create Weaviate Client Wrapper
|
|
19
|
+
|
|
20
|
+
**src/weaviate/client.ts**:
|
|
21
|
+
```typescript
|
|
22
|
+
import weaviate, { WeaviateClient, ApiKey } from 'weaviate-client';
|
|
23
|
+
import { config } from '../config.js';
|
|
24
|
+
|
|
25
|
+
let client: WeaviateClient | null = null;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Initialize Weaviate client
|
|
29
|
+
*/
|
|
30
|
+
export async function initWeaviateClient(): Promise<WeaviateClient> {
|
|
31
|
+
if (client) {
|
|
32
|
+
return client;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const clientConfig: any = {
|
|
36
|
+
scheme: config.weaviate.url.startsWith('https') ? 'https' : 'http',
|
|
37
|
+
host: config.weaviate.url.replace(/^https?:\/\//, ''),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
if (config.weaviate.apiKey) {
|
|
41
|
+
clientConfig.apiKey = new ApiKey(config.weaviate.apiKey);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
client = await weaviate.client(clientConfig);
|
|
45
|
+
|
|
46
|
+
console.log('[Weaviate] Client initialized');
|
|
47
|
+
return client;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get Weaviate client instance
|
|
52
|
+
*/
|
|
53
|
+
export function getWeaviateClient(): WeaviateClient {
|
|
54
|
+
if (!client) {
|
|
55
|
+
throw new Error('Weaviate client not initialized. Call initWeaviateClient() first.');
|
|
56
|
+
}
|
|
57
|
+
return client;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Test Weaviate connection
|
|
62
|
+
*/
|
|
63
|
+
export async function testWeaviateConnection(): Promise<boolean> {
|
|
64
|
+
try {
|
|
65
|
+
const client = getWeaviateClient();
|
|
66
|
+
const meta = await client.misc.metaGetter().do();
|
|
67
|
+
console.log('[Weaviate] Connection successful:', meta.version);
|
|
68
|
+
return true;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error('[Weaviate] Connection failed:', error);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Sanitize user_id for collection name
|
|
77
|
+
* Weaviate collection names must start with uppercase and contain only alphanumeric
|
|
78
|
+
*/
|
|
79
|
+
export function sanitizeUserId(userId: string): string {
|
|
80
|
+
// Remove special characters, keep alphanumeric
|
|
81
|
+
const sanitized = userId.replace(/[^a-zA-Z0-9]/g, '_');
|
|
82
|
+
// Ensure starts with uppercase
|
|
83
|
+
return sanitized.charAt(0).toUpperCase() + sanitized.slice(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get collection name for user's memories
|
|
88
|
+
*/
|
|
89
|
+
export function getMemoryCollectionName(userId: string): string {
|
|
90
|
+
return `Memory_${sanitizeUserId(userId)}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get collection name for user's templates
|
|
95
|
+
*/
|
|
96
|
+
export function getTemplateCollectionName(userId: string): string {
|
|
97
|
+
return `Template_${sanitizeUserId(userId)}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get collection name for user's audit logs
|
|
102
|
+
*/
|
|
103
|
+
export function getAuditCollectionName(userId: string): string {
|
|
104
|
+
return `Audit_${sanitizeUserId(userId)}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Check if collection exists
|
|
109
|
+
*/
|
|
110
|
+
export async function collectionExists(collectionName: string): Promise<boolean> {
|
|
111
|
+
try {
|
|
112
|
+
const client = getWeaviateClient();
|
|
113
|
+
const schema = await client.schema.getter().do();
|
|
114
|
+
return schema.classes?.some((c: any) => c.class === collectionName) ?? false;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error(`[Weaviate] Error checking collection ${collectionName}:`, error);
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Close Weaviate client connection
|
|
123
|
+
*/
|
|
124
|
+
export async function closeWeaviateClient(): Promise<void> {
|
|
125
|
+
if (client) {
|
|
126
|
+
// Weaviate client doesn't have explicit close method
|
|
127
|
+
client = null;
|
|
128
|
+
console.log('[Weaviate] Client closed');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 2. Create Configuration Module
|
|
134
|
+
|
|
135
|
+
**src/config.ts**:
|
|
136
|
+
```typescript
|
|
137
|
+
import dotenv from 'dotenv';
|
|
138
|
+
|
|
139
|
+
dotenv.config();
|
|
140
|
+
|
|
141
|
+
export const config = {
|
|
142
|
+
// Weaviate
|
|
143
|
+
weaviate: {
|
|
144
|
+
url: process.env.WEAVIATE_URL || 'http://localhost:8080',
|
|
145
|
+
apiKey: process.env.WEAVIATE_API_KEY || '',
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
// OpenAI
|
|
149
|
+
openai: {
|
|
150
|
+
apiKey: process.env.OPENAI_APIKEY || '',
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
// Firebase
|
|
154
|
+
firebase: {
|
|
155
|
+
projectId: process.env.FIREBASE_PROJECT_ID || '',
|
|
156
|
+
credentialsPath: process.env.GOOGLE_APPLICATION_CREDENTIALS || './serviceAccount.json',
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
// Server
|
|
160
|
+
server: {
|
|
161
|
+
port: parseInt(process.env.PORT || '3000', 10),
|
|
162
|
+
nodeEnv: process.env.NODE_ENV || 'development',
|
|
163
|
+
logLevel: process.env.LOG_LEVEL || 'info',
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
// MCP
|
|
167
|
+
mcp: {
|
|
168
|
+
transport: process.env.MCP_TRANSPORT || 'sse',
|
|
169
|
+
},
|
|
170
|
+
} as const;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Validate required configuration
|
|
174
|
+
*/
|
|
175
|
+
export function validateConfig(): void {
|
|
176
|
+
const required = [
|
|
177
|
+
{ key: 'WEAVIATE_URL', value: config.weaviate.url },
|
|
178
|
+
{ key: 'OPENAI_APIKEY', value: config.openai.apiKey },
|
|
179
|
+
{ key: 'FIREBASE_PROJECT_ID', value: config.firebase.projectId },
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
const missing = required.filter((r) => !r.value);
|
|
183
|
+
|
|
184
|
+
if (missing.length > 0) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`Missing required environment variables: ${missing.map((m) => m.key).join(', ')}`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.log('[Config] Configuration validated');
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### 3. Create Logger Utility
|
|
195
|
+
|
|
196
|
+
**src/utils/logger.ts**:
|
|
197
|
+
```typescript
|
|
198
|
+
import { config } from '../config.js';
|
|
199
|
+
|
|
200
|
+
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
201
|
+
|
|
202
|
+
const LOG_LEVELS: Record<LogLevel, number> = {
|
|
203
|
+
debug: 0,
|
|
204
|
+
info: 1,
|
|
205
|
+
warn: 2,
|
|
206
|
+
error: 3,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const currentLevel = LOG_LEVELS[config.server.logLevel as LogLevel] ?? LOG_LEVELS.info;
|
|
210
|
+
|
|
211
|
+
function shouldLog(level: LogLevel): boolean {
|
|
212
|
+
return LOG_LEVELS[level] >= currentLevel;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export const logger = {
|
|
216
|
+
debug: (message: string, ...args: any[]) => {
|
|
217
|
+
if (shouldLog('debug')) {
|
|
218
|
+
console.debug(`[DEBUG] ${message}`, ...args);
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
info: (message: string, ...args: any[]) => {
|
|
223
|
+
if (shouldLog('info')) {
|
|
224
|
+
console.info(`[INFO] ${message}`, ...args);
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
warn: (message: string, ...args: any[]) => {
|
|
229
|
+
if (shouldLog('warn')) {
|
|
230
|
+
console.warn(`[WARN] ${message}`, ...args);
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
error: (message: string, ...args: any[]) => {
|
|
235
|
+
if (shouldLog('error')) {
|
|
236
|
+
console.error(`[ERROR] ${message}`, ...args);
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### 4. Create Test File
|
|
243
|
+
|
|
244
|
+
**tests/unit/weaviate-client.test.ts**:
|
|
245
|
+
```typescript
|
|
246
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
247
|
+
import {
|
|
248
|
+
initWeaviateClient,
|
|
249
|
+
testWeaviateConnection,
|
|
250
|
+
sanitizeUserId,
|
|
251
|
+
getMemoryCollectionName,
|
|
252
|
+
} from '../../src/weaviate/client.js';
|
|
253
|
+
|
|
254
|
+
describe('Weaviate Client', () => {
|
|
255
|
+
beforeAll(async () => {
|
|
256
|
+
await initWeaviateClient();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should initialize client', async () => {
|
|
260
|
+
const result = await testWeaviateConnection();
|
|
261
|
+
expect(result).toBe(true);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should sanitize user IDs', () => {
|
|
265
|
+
expect(sanitizeUserId('user@example.com')).toBe('User_example_com');
|
|
266
|
+
expect(sanitizeUserId('user-123')).toBe('User_123');
|
|
267
|
+
expect(sanitizeUserId('123user')).toBe('_23user');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should generate collection names', () => {
|
|
271
|
+
expect(getMemoryCollectionName('user123')).toBe('Memory_User123');
|
|
272
|
+
expect(getMemoryCollectionName('user@test.com')).toBe('Memory_User_test_com');
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Verification
|
|
280
|
+
|
|
281
|
+
- [x] src/weaviate/client.ts created
|
|
282
|
+
- [x] src/config.ts created
|
|
283
|
+
- [x] src/utils/logger.ts created
|
|
284
|
+
- [x] Tests created (tests/unit/weaviate-client.test.ts)
|
|
285
|
+
- [x] Can initialize Weaviate client (with Weaviate v3 API)
|
|
286
|
+
- [x] Connection test implemented (skipped - requires Weaviate instance)
|
|
287
|
+
- [x] User ID sanitization works (7/7 tests passing)
|
|
288
|
+
- [x] Collection name generation works (all tests passing)
|
|
289
|
+
- [x] Jest configured for ESM support
|
|
290
|
+
|
|
291
|
+
## Completion Notes
|
|
292
|
+
|
|
293
|
+
**Completed**: 2026-02-11
|
|
294
|
+
|
|
295
|
+
**What Was Created**:
|
|
296
|
+
- ✅ [`src/config.ts`](../../src/config.ts) - Configuration management with environment variables
|
|
297
|
+
- ✅ [`src/utils/logger.ts`](../../src/utils/logger.ts) - Logging utility with log levels
|
|
298
|
+
- ✅ [`src/weaviate/client.ts`](../../src/weaviate/client.ts) - Weaviate client wrapper with:
|
|
299
|
+
- Client initialization using Weaviate v3 API
|
|
300
|
+
- Connection testing
|
|
301
|
+
- User ID sanitization for collection names
|
|
302
|
+
- Collection name generators (Memory, Template, Audit)
|
|
303
|
+
- Collection existence checking
|
|
304
|
+
- ✅ [`tests/unit/weaviate-client.test.ts`](../../tests/unit/weaviate-client.test.ts) - Unit tests (7 passing, 1 skipped)
|
|
305
|
+
- ✅ Updated [`jest.config.js`](../../jest.config.js) for ESM support
|
|
306
|
+
|
|
307
|
+
**Test Results**: 7 passed, 1 skipped (connection test requires Weaviate instance)
|
|
308
|
+
|
|
309
|
+
**What's Next**:
|
|
310
|
+
- Task 4: Set up Firestore client wrapper
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Testing
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
# Run tests
|
|
318
|
+
npm test
|
|
319
|
+
|
|
320
|
+
# Test connection manually
|
|
321
|
+
npm run dev
|
|
322
|
+
# Should see: [Weaviate] Client initialized
|
|
323
|
+
# Should see: [Weaviate] Connection successful
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## Next Task
|
|
329
|
+
|
|
330
|
+
Task 4: Set Up Firestore Client
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# Task 4: Set Up Firestore Client
|
|
2
|
+
|
|
3
|
+
**Milestone**: M1 - Project Foundation
|
|
4
|
+
**Estimated Time**: 2 hours
|
|
5
|
+
**Dependencies**: Task 2 ✅
|
|
6
|
+
**Status**: ✅ COMPLETED (2026-02-11)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Objective
|
|
11
|
+
|
|
12
|
+
Create Firestore initialization helper and path utilities using firebase-admin-sdk-v8 (service layer pattern, no wrapper needed per design decision in [`agent/design/firestore-wrapper-analysis.md`](../../agent/design/firestore-wrapper-analysis.md)).
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Steps
|
|
17
|
+
|
|
18
|
+
### 1. Create Firestore Client Wrapper
|
|
19
|
+
|
|
20
|
+
**src/firestore/client.ts**:
|
|
21
|
+
```typescript
|
|
22
|
+
import admin from 'firebase-admin';
|
|
23
|
+
import { Firestore, Timestamp } from 'firebase-admin/firestore';
|
|
24
|
+
import { config } from '../config.js';
|
|
25
|
+
import { readFileSync } from 'fs';
|
|
26
|
+
|
|
27
|
+
let firestore: Firestore | null = null;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Initialize Firebase Admin and Firestore
|
|
31
|
+
*/
|
|
32
|
+
export async function initFirestore(): Promise<Firestore> {
|
|
33
|
+
if (firestore) {
|
|
34
|
+
return firestore;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Read service account key
|
|
39
|
+
const serviceAccount = JSON.parse(
|
|
40
|
+
readFileSync(config.firebase.credentialsPath, 'utf8')
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Initialize Firebase Admin
|
|
44
|
+
admin.initializeApp({
|
|
45
|
+
credential: admin.credential.cert(serviceAccount),
|
|
46
|
+
projectId: config.firebase.projectId,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
firestore = admin.firestore();
|
|
50
|
+
|
|
51
|
+
// Configure Firestore settings
|
|
52
|
+
firestore.settings({
|
|
53
|
+
ignoreUndefinedProperties: true,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
console.log('[Firestore] Client initialized');
|
|
57
|
+
return firestore;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error('[Firestore] Initialization failed:', error);
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get Firestore instance
|
|
66
|
+
*/
|
|
67
|
+
export function getFirestore(): Firestore {
|
|
68
|
+
if (!firestore) {
|
|
69
|
+
throw new Error('Firestore not initialized. Call initFirestore() first.');
|
|
70
|
+
}
|
|
71
|
+
return firestore;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Test Firestore connection
|
|
76
|
+
*/
|
|
77
|
+
export async function testFirestoreConnection(): Promise<boolean> {
|
|
78
|
+
try {
|
|
79
|
+
const db = getFirestore();
|
|
80
|
+
// Try to read a document (will fail gracefully if doesn't exist)
|
|
81
|
+
await db.collection('_health_check').doc('test').get();
|
|
82
|
+
console.log('[Firestore] Connection successful');
|
|
83
|
+
return true;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error('[Firestore] Connection failed:', error);
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get document from Firestore
|
|
92
|
+
*/
|
|
93
|
+
export async function getDocument<T = any>(
|
|
94
|
+
collection: string,
|
|
95
|
+
docId: string
|
|
96
|
+
): Promise<T | null> {
|
|
97
|
+
try {
|
|
98
|
+
const db = getFirestore();
|
|
99
|
+
const doc = await db.collection(collection).doc(docId).get();
|
|
100
|
+
|
|
101
|
+
if (!doc.exists) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return doc.data() as T;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error(`[Firestore] Error getting document ${collection}/${docId}:`, error);
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Set document in Firestore
|
|
114
|
+
*/
|
|
115
|
+
export async function setDocument<T = any>(
|
|
116
|
+
collection: string,
|
|
117
|
+
docId: string,
|
|
118
|
+
data: T
|
|
119
|
+
): Promise<void> {
|
|
120
|
+
try {
|
|
121
|
+
const db = getFirestore();
|
|
122
|
+
await db.collection(collection).doc(docId).set(data);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error(`[Firestore] Error setting document ${collection}/${docId}:`, error);
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Update document in Firestore
|
|
131
|
+
*/
|
|
132
|
+
export async function updateDocument<T = any>(
|
|
133
|
+
collection: string,
|
|
134
|
+
docId: string,
|
|
135
|
+
data: Partial<T>
|
|
136
|
+
): Promise<void> {
|
|
137
|
+
try {
|
|
138
|
+
const db = getFirestore();
|
|
139
|
+
await db.collection(collection).doc(docId).update(data as any);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error(`[Firestore] Error updating document ${collection}/${docId}:`, error);
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Delete document from Firestore
|
|
148
|
+
*/
|
|
149
|
+
export async function deleteDocument(collection: string, docId: string): Promise<void> {
|
|
150
|
+
try {
|
|
151
|
+
const db = getFirestore();
|
|
152
|
+
await db.collection(collection).doc(docId).delete();
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.error(`[Firestore] Error deleting document ${collection}/${docId}:`, error);
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Query documents from Firestore
|
|
161
|
+
*/
|
|
162
|
+
export async function queryDocuments<T = any>(
|
|
163
|
+
collection: string,
|
|
164
|
+
filters?: Array<{ field: string; operator: FirebaseFirestore.WhereFilterOp; value: any }>
|
|
165
|
+
): Promise<T[]> {
|
|
166
|
+
try {
|
|
167
|
+
const db = getFirestore();
|
|
168
|
+
let query: FirebaseFirestore.Query = db.collection(collection);
|
|
169
|
+
|
|
170
|
+
if (filters) {
|
|
171
|
+
for (const filter of filters) {
|
|
172
|
+
query = query.where(filter.field, filter.operator, filter.value);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const snapshot = await query.get();
|
|
177
|
+
return snapshot.docs.map((doc) => doc.data() as T);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error(`[Firestore] Error querying collection ${collection}:`, error);
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Export Timestamp for use in other modules
|
|
186
|
+
*/
|
|
187
|
+
export { Timestamp };
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### 2. Create Firestore Path Helpers
|
|
191
|
+
|
|
192
|
+
**src/firestore/paths.ts**:
|
|
193
|
+
```typescript
|
|
194
|
+
/**
|
|
195
|
+
* Firestore path helpers following users/{user_id}/ pattern
|
|
196
|
+
*/
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get path to user's preferences document
|
|
200
|
+
*/
|
|
201
|
+
export function getUserPreferencesPath(userId: string): string {
|
|
202
|
+
return `users/${userId}/preferences`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get path to user's templates collection
|
|
207
|
+
*/
|
|
208
|
+
export function getUserTemplatesPath(userId: string): string {
|
|
209
|
+
return `users/${userId}/templates`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get path to specific user template
|
|
214
|
+
*/
|
|
215
|
+
export function getUserTemplatePath(userId: string, templateId: string): string {
|
|
216
|
+
return `users/${userId}/templates/${templateId}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get path to user's access logs collection
|
|
221
|
+
*/
|
|
222
|
+
export function getUserAccessLogsPath(userId: string): string {
|
|
223
|
+
return `users/${userId}/access_logs`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get path to user's trust relationships collection
|
|
228
|
+
*/
|
|
229
|
+
export function getUserTrustRelationshipsPath(userId: string): string {
|
|
230
|
+
return `users/${userId}/trust_relationships`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get path to default templates collection
|
|
235
|
+
*/
|
|
236
|
+
export function getDefaultTemplatesPath(): string {
|
|
237
|
+
return 'templates/default';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get path to specific default template
|
|
242
|
+
*/
|
|
243
|
+
export function getDefaultTemplatePath(templateId: string): string {
|
|
244
|
+
return `templates/default/${templateId}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get path to user permissions (cross-user)
|
|
249
|
+
*/
|
|
250
|
+
export function getUserPermissionsPath(ownerUserId: string, accessorUserId: string): string {
|
|
251
|
+
return `user_permissions/${ownerUserId}/allowed_accessors/${accessorUserId}`;
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### 3. Create Test File
|
|
256
|
+
|
|
257
|
+
**tests/unit/firestore-client.test.ts**:
|
|
258
|
+
```typescript
|
|
259
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
260
|
+
import {
|
|
261
|
+
initFirestore,
|
|
262
|
+
testFirestoreConnection,
|
|
263
|
+
setDocument,
|
|
264
|
+
getDocument,
|
|
265
|
+
deleteDocument,
|
|
266
|
+
} from '../../src/firestore/client.js';
|
|
267
|
+
|
|
268
|
+
describe('Firestore Client', () => {
|
|
269
|
+
beforeAll(async () => {
|
|
270
|
+
await initFirestore();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should initialize client', async () => {
|
|
274
|
+
const result = await testFirestoreConnection();
|
|
275
|
+
expect(result).toBe(true);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should set and get document', async () => {
|
|
279
|
+
const testData = { test: 'value', timestamp: new Date().toISOString() };
|
|
280
|
+
|
|
281
|
+
await setDocument('_test', 'test-doc', testData);
|
|
282
|
+
const retrieved = await getDocument('_test', 'test-doc');
|
|
283
|
+
|
|
284
|
+
expect(retrieved).toEqual(testData);
|
|
285
|
+
|
|
286
|
+
// Cleanup
|
|
287
|
+
await deleteDocument('_test', 'test-doc');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('should return null for non-existent document', async () => {
|
|
291
|
+
const result = await getDocument('_test', 'non-existent');
|
|
292
|
+
expect(result).toBeNull();
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## Verification
|
|
300
|
+
|
|
301
|
+
- [x] src/firestore/init.ts created (minimal initialization helper)
|
|
302
|
+
- [x] src/firestore/paths.ts created (collection path helpers)
|
|
303
|
+
- [x] Tests created (tests/unit/firestore-paths.test.ts)
|
|
304
|
+
- [x] Can initialize Firestore (initFirestore function)
|
|
305
|
+
- [x] Connection test implemented (testFirestoreConnection)
|
|
306
|
+
- [x] Re-exports firebase-admin-sdk-v8 functions (no wrapper needed)
|
|
307
|
+
- [x] Path helpers work correctly (7 tests passing, 100% coverage)
|
|
308
|
+
|
|
309
|
+
## Completion Notes
|
|
310
|
+
|
|
311
|
+
**Completed**: 2026-02-11
|
|
312
|
+
|
|
313
|
+
**What Was Created**:
|
|
314
|
+
- ✅ [`src/firestore/init.ts`](../../src/firestore/init.ts) - Minimal initialization helper
|
|
315
|
+
- `initFirestore()` - Initialize firebase-admin-sdk-v8 once
|
|
316
|
+
- `isFirestoreInitialized()` - Check initialization state
|
|
317
|
+
- `testFirestoreConnection()` - Test connection
|
|
318
|
+
- Re-exports all firebase-admin-sdk-v8 functions for convenience
|
|
319
|
+
|
|
320
|
+
- ✅ [`src/firestore/paths.ts`](../../src/firestore/paths.ts) - Collection path helpers
|
|
321
|
+
- `getUserPreferencesPath()` - user_preferences/{userId}
|
|
322
|
+
- `getUserTemplatesPath()` - users/{userId}/templates
|
|
323
|
+
- `getUserPermissionsPath()` - user_permissions/{userId}/allowed_accessors
|
|
324
|
+
- `getUserPermissionPath()` - Specific permission document
|
|
325
|
+
- `getTrustHistoryPath()` - trust_history/{userId}/history
|
|
326
|
+
- `getDefaultTemplatesPath()` - templates/default
|
|
327
|
+
- `getDefaultTemplatePath()` - Specific default template
|
|
328
|
+
|
|
329
|
+
- ✅ [`tests/unit/firestore-paths.test.ts`](../../tests/unit/firestore-paths.test.ts) - Unit tests
|
|
330
|
+
- 7 tests passing
|
|
331
|
+
- 100% code coverage for paths.ts
|
|
332
|
+
|
|
333
|
+
**Design Decision**:
|
|
334
|
+
- ✅ Using firebase-admin-sdk-v8 instead of firebase-admin (edge-compatible)
|
|
335
|
+
- ✅ Service layer pattern (no wrapper) per [`agent/design/firestore-wrapper-analysis.md`](../../agent/design/firestore-wrapper-analysis.md)
|
|
336
|
+
- ✅ Minimal init helper + re-exports (not a full wrapper class)
|
|
337
|
+
- ✅ Path helpers for multi-tenant collection organization
|
|
338
|
+
|
|
339
|
+
**Test Results**: 14 passed, 1 skipped (all Firestore path tests passing)
|
|
340
|
+
|
|
341
|
+
**What's Next**:
|
|
342
|
+
- Task 5: Create basic MCP server
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## Testing
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
# Run tests
|
|
350
|
+
npm test
|
|
351
|
+
|
|
352
|
+
# Test connection manually
|
|
353
|
+
npm run dev
|
|
354
|
+
# Should see: [Firestore] Client initialized
|
|
355
|
+
# Should see: [Firestore] Connection successful
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## Next Task
|
|
361
|
+
|
|
362
|
+
Task 5: Create Basic MCP Server
|