@visualprd/mcp-server 1.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.
Files changed (58) hide show
  1. package/README.md +396 -0
  2. package/dist/cli.d.ts +9 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +27 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/index.d.ts +20 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +243 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/intelligence/context-optimizer.d.ts +93 -0
  11. package/dist/intelligence/context-optimizer.d.ts.map +1 -0
  12. package/dist/intelligence/context-optimizer.js +481 -0
  13. package/dist/intelligence/context-optimizer.js.map +1 -0
  14. package/dist/intelligence/error-analyzer.d.ts +49 -0
  15. package/dist/intelligence/error-analyzer.d.ts.map +1 -0
  16. package/dist/intelligence/error-analyzer.js +765 -0
  17. package/dist/intelligence/error-analyzer.js.map +1 -0
  18. package/dist/intelligence/gap-filler.d.ts +56 -0
  19. package/dist/intelligence/gap-filler.d.ts.map +1 -0
  20. package/dist/intelligence/gap-filler.js +410 -0
  21. package/dist/intelligence/gap-filler.js.map +1 -0
  22. package/dist/intelligence/guidance-generator.d.ts +43 -0
  23. package/dist/intelligence/guidance-generator.d.ts.map +1 -0
  24. package/dist/intelligence/guidance-generator.js +314 -0
  25. package/dist/intelligence/guidance-generator.js.map +1 -0
  26. package/dist/intelligence/index.d.ts +132 -0
  27. package/dist/intelligence/index.d.ts.map +1 -0
  28. package/dist/intelligence/index.js +683 -0
  29. package/dist/intelligence/index.js.map +1 -0
  30. package/dist/server-http.d.ts +9 -0
  31. package/dist/server-http.d.ts.map +1 -0
  32. package/dist/server-http.js +141 -0
  33. package/dist/server-http.js.map +1 -0
  34. package/dist/services/api-key-service.d.ts +68 -0
  35. package/dist/services/api-key-service.d.ts.map +1 -0
  36. package/dist/services/api-key-service.js +298 -0
  37. package/dist/services/api-key-service.js.map +1 -0
  38. package/dist/services/llm-client.d.ts +66 -0
  39. package/dist/services/llm-client.d.ts.map +1 -0
  40. package/dist/services/llm-client.js +141 -0
  41. package/dist/services/llm-client.js.map +1 -0
  42. package/dist/services/model-registry.d.ts +135 -0
  43. package/dist/services/model-registry.d.ts.map +1 -0
  44. package/dist/services/model-registry.js +276 -0
  45. package/dist/services/model-registry.js.map +1 -0
  46. package/dist/services/visualprd-client.d.ts +191 -0
  47. package/dist/services/visualprd-client.d.ts.map +1 -0
  48. package/dist/services/visualprd-client.js +805 -0
  49. package/dist/services/visualprd-client.js.map +1 -0
  50. package/dist/tools/index.d.ts +803 -0
  51. package/dist/tools/index.d.ts.map +1 -0
  52. package/dist/tools/index.js +570 -0
  53. package/dist/tools/index.js.map +1 -0
  54. package/dist/types/index.d.ts +497 -0
  55. package/dist/types/index.d.ts.map +1 -0
  56. package/dist/types/index.js +8 -0
  57. package/dist/types/index.js.map +1 -0
  58. package/package.json +48 -0
@@ -0,0 +1,805 @@
1
+ "use strict";
2
+ /**
3
+ * VisualPRD API Client
4
+ *
5
+ * Connects to VisualPRD's Firestore backend to fetch project data,
6
+ * update build prompt status, and manage dynamic steps.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.VisualPRDClient = void 0;
10
+ const app_1 = require("firebase-admin/app");
11
+ const firestore_1 = require("firebase-admin/firestore");
12
+ class VisualPRDClient {
13
+ db;
14
+ config;
15
+ projectDataCache = null;
16
+ cacheTimestamp = 0;
17
+ CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
18
+ constructor(config) {
19
+ this.config = config;
20
+ // Initialize Firebase Admin if not already initialized
21
+ let app;
22
+ if ((0, app_1.getApps)().length === 0) {
23
+ // Use service account from environment or config
24
+ const serviceAccount = process.env.GOOGLE_APPLICATION_CREDENTIALS
25
+ ? undefined // Will use default credentials
26
+ : {
27
+ projectId: process.env.FIREBASE_PROJECT_ID || 'visualprd-app',
28
+ privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
29
+ clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
30
+ };
31
+ app = (0, app_1.initializeApp)(serviceAccount
32
+ ? { credential: (0, app_1.cert)(serviceAccount) }
33
+ : { projectId: process.env.FIREBASE_PROJECT_ID || 'visualprd-app' });
34
+ }
35
+ else {
36
+ app = (0, app_1.getApps)()[0];
37
+ }
38
+ this.db = (0, firestore_1.getFirestore)(app);
39
+ }
40
+ // ============================================================================
41
+ // PROJECT DATA LOADING
42
+ // ============================================================================
43
+ /**
44
+ * Load full project data including all entities
45
+ */
46
+ async loadProjectData(forceRefresh = false) {
47
+ // Check cache first
48
+ if (!forceRefresh &&
49
+ this.projectDataCache &&
50
+ Date.now() - this.cacheTimestamp < this.CACHE_TTL_MS) {
51
+ return this.projectDataCache;
52
+ }
53
+ const projectId = this.config.projectId;
54
+ const projectRef = this.db.collection('projects').doc(projectId);
55
+ // Load project document
56
+ const projectDoc = await projectRef.get();
57
+ if (!projectDoc.exists) {
58
+ throw new Error(`Project ${projectId} not found`);
59
+ }
60
+ const projectData = projectDoc.data();
61
+ // Load all subcollections in parallel
62
+ const [pages, schemas, endpoints, techStack, buildPrompts] = await Promise.all([
63
+ this.loadPages(projectRef),
64
+ this.loadSchemas(projectRef),
65
+ this.loadEndpoints(projectRef),
66
+ this.loadTechStack(projectRef),
67
+ this.loadBuildPrompts(projectRef),
68
+ ]);
69
+ // Construct full project data
70
+ const fullData = {
71
+ projectId,
72
+ projectName: projectData.name || projectData.projectName || 'Untitled Project',
73
+ description: projectData.description,
74
+ pages,
75
+ schemas,
76
+ endpoints,
77
+ techStack,
78
+ designSystem: projectData.designSystem || this.extractDesignSystem(projectData),
79
+ buildPrompts,
80
+ version: this.detectVersion(projectData),
81
+ createdAt: projectData.createdAt?.toDate?.() || new Date(),
82
+ updatedAt: projectData.updatedAt?.toDate?.() || new Date(),
83
+ };
84
+ // Update cache
85
+ this.projectDataCache = fullData;
86
+ this.cacheTimestamp = Date.now();
87
+ return fullData;
88
+ }
89
+ async loadPages(projectRef) {
90
+ const pagesSnapshot = await projectRef.collection('pages').get();
91
+ return pagesSnapshot.docs.map((doc) => ({
92
+ id: doc.id,
93
+ ...doc.data(),
94
+ }));
95
+ }
96
+ async loadSchemas(projectRef) {
97
+ const schemasSnapshot = await projectRef.collection('databaseSchemas').get();
98
+ return schemasSnapshot.docs.map((doc) => ({
99
+ id: doc.id,
100
+ ...doc.data(),
101
+ }));
102
+ }
103
+ async loadEndpoints(projectRef) {
104
+ const endpointsSnapshot = await projectRef.collection('apiEndpoints').get();
105
+ return endpointsSnapshot.docs.map((doc) => ({
106
+ id: doc.id,
107
+ ...doc.data(),
108
+ }));
109
+ }
110
+ async loadTechStack(projectRef) {
111
+ const techSnapshot = await projectRef.collection('techStack').get();
112
+ return techSnapshot.docs.map((doc) => ({
113
+ id: doc.id,
114
+ ...doc.data(),
115
+ }));
116
+ }
117
+ async loadBuildPrompts(projectRef) {
118
+ // First, check if this is v9-mega format (category-based structure)
119
+ const projectDoc = await projectRef.get();
120
+ const projectData = projectDoc.data();
121
+ if (projectData?.generationVersion === 'v9-mega' || projectData?.version === 'v9-mega') {
122
+ // V9-mega format: Load from category documents with instructions array
123
+ return this.loadBuildPromptsV9Mega(projectRef);
124
+ }
125
+ // Legacy format: Individual documents ordered by 'order' field
126
+ const promptsSnapshot = await projectRef
127
+ .collection('buildPrompts')
128
+ .orderBy('order', 'asc')
129
+ .get();
130
+ return promptsSnapshot.docs.map((doc) => {
131
+ const data = doc.data();
132
+ return {
133
+ promptId: doc.id,
134
+ order: data.order || 0,
135
+ title: data.title || data.stepTitle || 'Untitled Step',
136
+ category: data.category || 'feature',
137
+ instruction: data.instruction || data.promptText || '',
138
+ expectedOutcome: data.expectedOutcome,
139
+ testCriteria: data.testCriteria || [],
140
+ estimatedMinutes: data.estimatedMinutes,
141
+ completed: data.completed || data.status === 'completed',
142
+ completedAt: data.completedAt?.toDate?.(),
143
+ completionNotes: data.completionNotes,
144
+ filesCreated: data.filesCreated || [],
145
+ dependsOn: data.dependsOn || data.dependencies || [],
146
+ relatedDocuments: {
147
+ pages: data.relatedPages || data.relatedDocuments?.pages || [],
148
+ schemas: data.relatedSchemas || data.relatedDocuments?.schemas || [],
149
+ endpoints: data.relatedEndpoints || data.relatedDocuments?.endpoints || [],
150
+ techStack: data.relatedTechStack || data.relatedDocuments?.techStack || [],
151
+ },
152
+ isInjected: data.isInjected || false,
153
+ injectedBy: data.injectedBy,
154
+ injectedReason: data.injectedReason,
155
+ };
156
+ });
157
+ }
158
+ /**
159
+ * Load build prompts from v9-mega format (category documents with instructions array)
160
+ */
161
+ async loadBuildPromptsV9Mega(projectRef) {
162
+ const categoriesSnapshot = await projectRef
163
+ .collection('buildPrompts')
164
+ .orderBy('order', 'asc')
165
+ .get();
166
+ const allPrompts = [];
167
+ for (const categoryDoc of categoriesSnapshot.docs) {
168
+ const categoryData = categoryDoc.data();
169
+ const categoryId = categoryDoc.id;
170
+ const categoryTitle = categoryData.categoryTitle || categoryData.title || categoryId;
171
+ const instructions = categoryData.instructions || [];
172
+ for (const instruction of instructions) {
173
+ allPrompts.push({
174
+ promptId: instruction.id || `${categoryId}-${instruction.promptNumber}`,
175
+ order: instruction.promptNumber || 0,
176
+ title: instruction.title || 'Untitled Step',
177
+ category: categoryId,
178
+ instruction: instruction.instruction || '',
179
+ expectedOutcome: instruction.expectedOutcome,
180
+ // V2 structured test criteria
181
+ testCriteria: instruction.testCriteria || [],
182
+ estimatedMinutes: instruction.estimatedMinutes,
183
+ completed: instruction.completed || false,
184
+ completedAt: instruction.completedAt?.toDate?.(),
185
+ completionNotes: instruction.completionNotes,
186
+ filesCreated: instruction.filesCreated || [],
187
+ dependsOn: instruction.dependsOn || [],
188
+ relatedDocuments: instruction.relatedDocuments || {
189
+ pages: [],
190
+ schemas: [],
191
+ endpoints: [],
192
+ techStack: [],
193
+ },
194
+ // V2 specific fields
195
+ verificationPrompt: instruction.verificationPrompt,
196
+ categoryTitle: categoryTitle,
197
+ priority: instruction.priority,
198
+ flaggedForReview: instruction.flaggedForReview || false,
199
+ flaggedAt: instruction.flaggedAt?.toDate?.(),
200
+ flaggedReason: instruction.flaggedReason,
201
+ isInjected: instruction.isInjected || false,
202
+ injectedBy: instruction.injectedBy,
203
+ injectedReason: instruction.injectedReason,
204
+ });
205
+ }
206
+ }
207
+ // Sort by promptNumber/order across all categories
208
+ allPrompts.sort((a, b) => a.order - b.order);
209
+ return allPrompts;
210
+ }
211
+ detectVersion(projectData) {
212
+ if (projectData.version === 'v9-mega' || projectData.generationVersion === 'v9-mega') {
213
+ return 'v9-mega';
214
+ }
215
+ if (projectData.version === 'v2' || projectData.hasFeaturePlans) {
216
+ return 'v2';
217
+ }
218
+ return 'v1';
219
+ }
220
+ extractDesignSystem(projectData) {
221
+ // Try to extract design system from various possible locations
222
+ if (projectData.designSystem)
223
+ return projectData.designSystem;
224
+ if (projectData.design?.designSystem)
225
+ return projectData.design.designSystem;
226
+ // Build from individual fields if available
227
+ if (projectData.colorPalette || projectData.typography) {
228
+ return {
229
+ colorPalette: projectData.colorPalette,
230
+ typography: projectData.typography,
231
+ spacing: projectData.spacing,
232
+ borderRadius: projectData.borderRadius,
233
+ shadows: projectData.shadows,
234
+ breakpoints: projectData.breakpoints,
235
+ componentPatterns: projectData.componentPatterns,
236
+ };
237
+ }
238
+ return undefined;
239
+ }
240
+ // ============================================================================
241
+ // BUILD PROMPT OPERATIONS
242
+ // ============================================================================
243
+ /**
244
+ * Get the next incomplete build prompt with all dependencies met
245
+ */
246
+ async getNextBuildPrompt() {
247
+ const projectData = await this.loadProjectData();
248
+ const prompts = projectData.buildPrompts;
249
+ // Find first incomplete prompt with all dependencies met
250
+ for (const prompt of prompts) {
251
+ if (prompt.completed)
252
+ continue;
253
+ const allDepsMet = (prompt.dependsOn || []).every((depId) => {
254
+ const dep = prompts.find((p) => p.promptId === depId || p.title === depId);
255
+ return dep?.completed === true;
256
+ });
257
+ if (allDepsMet) {
258
+ return prompt;
259
+ }
260
+ }
261
+ return null;
262
+ }
263
+ /**
264
+ * Mark a build prompt as complete
265
+ */
266
+ async markPromptComplete(promptId, completionData) {
267
+ const projectRef = this.db.collection('projects').doc(this.config.projectId);
268
+ const promptRef = projectRef.collection('buildPrompts').doc(promptId);
269
+ try {
270
+ await promptRef.update({
271
+ completed: true,
272
+ status: 'completed',
273
+ completedAt: firestore_1.FieldValue.serverTimestamp(),
274
+ completionNotes: completionData.completionNotes || '',
275
+ filesCreated: completionData.filesCreated || [],
276
+ testResults: completionData.testResults,
277
+ updatedAt: firestore_1.FieldValue.serverTimestamp(),
278
+ });
279
+ // Invalidate cache
280
+ this.projectDataCache = null;
281
+ return true;
282
+ }
283
+ catch (error) {
284
+ console.error('Error marking prompt complete:', error);
285
+ return false;
286
+ }
287
+ }
288
+ /**
289
+ * Uncheck/revert a build prompt
290
+ */
291
+ async uncheckPrompt(promptId) {
292
+ const projectRef = this.db.collection('projects').doc(this.config.projectId);
293
+ const promptRef = projectRef.collection('buildPrompts').doc(promptId);
294
+ try {
295
+ await promptRef.update({
296
+ completed: false,
297
+ status: 'pending',
298
+ completedAt: null,
299
+ updatedAt: firestore_1.FieldValue.serverTimestamp(),
300
+ });
301
+ this.projectDataCache = null;
302
+ return true;
303
+ }
304
+ catch (error) {
305
+ console.error('Error unchecking prompt:', error);
306
+ return false;
307
+ }
308
+ }
309
+ /**
310
+ * Inject a new dynamic build prompt
311
+ */
312
+ async injectBuildPrompt(insertAfterPromptId, newPrompt) {
313
+ const projectRef = this.db.collection('projects').doc(this.config.projectId);
314
+ try {
315
+ // Get current prompts to determine order
316
+ const projectData = await this.loadProjectData();
317
+ const insertAfterPrompt = projectData.buildPrompts.find((p) => p.promptId === insertAfterPromptId);
318
+ if (!insertAfterPrompt) {
319
+ throw new Error(`Prompt ${insertAfterPromptId} not found`);
320
+ }
321
+ // Calculate new order (insert between current and next)
322
+ const currentOrder = insertAfterPrompt.order;
323
+ const nextPrompt = projectData.buildPrompts.find((p) => p.order > currentOrder);
324
+ const newOrder = nextPrompt
325
+ ? (currentOrder + nextPrompt.order) / 2
326
+ : currentOrder + 1;
327
+ // Create new prompt document
328
+ const newPromptRef = await projectRef.collection('buildPrompts').add({
329
+ order: newOrder,
330
+ title: newPrompt.title,
331
+ category: newPrompt.category,
332
+ instruction: newPrompt.instruction,
333
+ expectedOutcome: `Complete the ${newPrompt.title} task as described`,
334
+ testCriteria: [],
335
+ estimatedMinutes: newPrompt.estimatedMinutes || 30,
336
+ completed: false,
337
+ status: 'pending',
338
+ dependsOn: newPrompt.dependsOn || [insertAfterPromptId],
339
+ relatedDocuments: newPrompt.relatedDocuments || {},
340
+ isInjected: true,
341
+ injectedBy: 'mcp_intelligence',
342
+ injectedReason: newPrompt.reason,
343
+ createdAt: firestore_1.FieldValue.serverTimestamp(),
344
+ updatedAt: firestore_1.FieldValue.serverTimestamp(),
345
+ });
346
+ // Invalidate cache
347
+ this.projectDataCache = null;
348
+ // Return the created prompt
349
+ const createdDoc = await newPromptRef.get();
350
+ return {
351
+ promptId: newPromptRef.id,
352
+ ...createdDoc.data(),
353
+ };
354
+ }
355
+ catch (error) {
356
+ console.error('Error injecting build prompt:', error);
357
+ return null;
358
+ }
359
+ }
360
+ // ============================================================================
361
+ // ENTITY LOOKUPS
362
+ // ============================================================================
363
+ /**
364
+ * Get a specific page by ID or name
365
+ */
366
+ async getPage(identifier) {
367
+ const projectData = await this.loadProjectData();
368
+ const normalized = this.normalize(identifier);
369
+ return (projectData.pages.find((p) => p.id === identifier ||
370
+ this.normalize(p.pageName) === normalized ||
371
+ this.normalize(p.id) === normalized) || null);
372
+ }
373
+ /**
374
+ * Get a specific schema by ID or collection name
375
+ */
376
+ async getSchema(identifier) {
377
+ const projectData = await this.loadProjectData();
378
+ const normalized = this.normalize(identifier);
379
+ return (projectData.schemas.find((s) => s.id === identifier ||
380
+ this.normalize(s.collectionName) === normalized ||
381
+ this.normalize(s.id) === normalized) || null);
382
+ }
383
+ /**
384
+ * Get a specific endpoint by ID or name
385
+ */
386
+ async getEndpoint(identifier) {
387
+ const projectData = await this.loadProjectData();
388
+ const normalized = this.normalize(identifier);
389
+ return (projectData.endpoints.find((e) => e.id === identifier ||
390
+ this.normalize(e.name) === normalized ||
391
+ this.normalize(e.id) === normalized ||
392
+ e.path === identifier) || null);
393
+ }
394
+ /**
395
+ * Get a specific tech stack item by ID or name
396
+ */
397
+ async getTechStackItem(identifier) {
398
+ const projectData = await this.loadProjectData();
399
+ const normalized = this.normalize(identifier);
400
+ return (projectData.techStack.find((t) => t.id === identifier ||
401
+ this.normalize(t.name) === normalized ||
402
+ this.normalize(t.id) === normalized) || null);
403
+ }
404
+ /**
405
+ * Get multiple entities by their references
406
+ */
407
+ async resolveEntityReferences(refs) {
408
+ const projectData = await this.loadProjectData();
409
+ const resolvePages = (refs?.pages || [])
410
+ .map((ref) => {
411
+ const normalized = this.normalize(ref);
412
+ return projectData.pages.find((p) => p.id === ref ||
413
+ this.normalize(p.pageName) === normalized ||
414
+ this.normalize(p.id) === normalized);
415
+ })
416
+ .filter(Boolean);
417
+ const resolveSchemas = (refs?.schemas || [])
418
+ .map((ref) => {
419
+ const normalized = this.normalize(ref);
420
+ return projectData.schemas.find((s) => s.id === ref ||
421
+ this.normalize(s.collectionName) === normalized ||
422
+ this.normalize(s.id) === normalized);
423
+ })
424
+ .filter(Boolean);
425
+ const resolveEndpoints = (refs?.endpoints || [])
426
+ .map((ref) => {
427
+ const normalized = this.normalize(ref);
428
+ return projectData.endpoints.find((e) => e.id === ref ||
429
+ this.normalize(e.name) === normalized ||
430
+ this.normalize(e.id) === normalized);
431
+ })
432
+ .filter(Boolean);
433
+ // Handle "tech-stack" meaning all tech stack items
434
+ let resolveTechStack = [];
435
+ const techRefs = refs?.techStack || [];
436
+ if (techRefs.some((ref) => this.normalize(ref) === 'tech-stack' || this.normalize(ref) === 'techstack')) {
437
+ resolveTechStack = [...projectData.techStack];
438
+ }
439
+ else {
440
+ resolveTechStack = techRefs
441
+ .map((ref) => {
442
+ const normalized = this.normalize(ref);
443
+ return projectData.techStack.find((t) => t.id === ref ||
444
+ this.normalize(t.name) === normalized ||
445
+ this.normalize(t.id) === normalized);
446
+ })
447
+ .filter(Boolean);
448
+ }
449
+ return {
450
+ pages: resolvePages,
451
+ schemas: resolveSchemas,
452
+ endpoints: resolveEndpoints,
453
+ techStack: resolveTechStack,
454
+ };
455
+ }
456
+ // ============================================================================
457
+ // SESSION MANAGEMENT
458
+ // ============================================================================
459
+ /**
460
+ * Create or update MCP session
461
+ */
462
+ async updateSession(sessionId, data) {
463
+ const sessionRef = this.db
464
+ .collection('projects')
465
+ .doc(this.config.projectId)
466
+ .collection('mcpSessions')
467
+ .doc(sessionId);
468
+ await sessionRef.set({
469
+ ...data,
470
+ lastActivityAt: firestore_1.FieldValue.serverTimestamp(),
471
+ }, { merge: true });
472
+ }
473
+ /**
474
+ * Log a tool call for analytics
475
+ */
476
+ async logToolCall(sessionId, toolName, request, response, durationMs, intelligenceUsed) {
477
+ const sessionRef = this.db
478
+ .collection('projects')
479
+ .doc(this.config.projectId)
480
+ .collection('mcpSessions')
481
+ .doc(sessionId);
482
+ await sessionRef.update({
483
+ toolCalls: firestore_1.FieldValue.arrayUnion({
484
+ toolName,
485
+ timestamp: new Date().toISOString(),
486
+ request,
487
+ response: this.truncateForStorage(response),
488
+ durationMs,
489
+ intelligenceUsed,
490
+ }),
491
+ lastActivityAt: firestore_1.FieldValue.serverTimestamp(),
492
+ });
493
+ }
494
+ /**
495
+ * Log an error for debugging
496
+ */
497
+ async logError(sessionId, promptId, error, diagnosis) {
498
+ const errorLogRef = this.db
499
+ .collection('projects')
500
+ .doc(this.config.projectId)
501
+ .collection('mcpErrorLogs')
502
+ .doc();
503
+ await errorLogRef.set({
504
+ sessionId,
505
+ promptId,
506
+ error,
507
+ diagnosis,
508
+ timestamp: firestore_1.FieldValue.serverTimestamp(),
509
+ });
510
+ }
511
+ // ============================================================================
512
+ // UTILITY METHODS
513
+ // ============================================================================
514
+ normalize(str) {
515
+ return str
516
+ .toLowerCase()
517
+ .replace(/[^a-z0-9]/g, '')
518
+ .trim();
519
+ }
520
+ truncateForStorage(obj, maxLength = 10000) {
521
+ const str = JSON.stringify(obj);
522
+ if (str.length <= maxLength)
523
+ return obj;
524
+ return {
525
+ truncated: true,
526
+ preview: str.substring(0, maxLength),
527
+ originalLength: str.length,
528
+ };
529
+ }
530
+ /**
531
+ * Get build progress statistics
532
+ */
533
+ async getBuildProgress() {
534
+ const projectData = await this.loadProjectData();
535
+ const prompts = projectData.buildPrompts;
536
+ const totalSteps = prompts.length;
537
+ const completedSteps = prompts.filter((p) => p.completed).length;
538
+ const currentStep = completedSteps + 1;
539
+ const percentComplete = totalSteps > 0 ? Math.round((completedSteps / totalSteps) * 100) : 0;
540
+ // Estimate remaining time based on incomplete prompts
541
+ const incompletePrompts = prompts.filter((p) => !p.completed);
542
+ const estimatedTimeRemaining = incompletePrompts.reduce((total, p) => total + (p.estimatedMinutes || 30), 0);
543
+ return {
544
+ totalSteps,
545
+ completedSteps,
546
+ currentStep,
547
+ percentComplete,
548
+ estimatedTimeRemaining,
549
+ };
550
+ }
551
+ /**
552
+ * Get the design system
553
+ */
554
+ async getDesignSystem() {
555
+ const projectData = await this.loadProjectData();
556
+ return projectData.designSystem;
557
+ }
558
+ /**
559
+ * Clear cache (force refresh on next load)
560
+ */
561
+ clearCache() {
562
+ this.projectDataCache = null;
563
+ this.cacheTimestamp = 0;
564
+ }
565
+ // ============================================================================
566
+ // V2 METHODS - Autonomous Validation Workflow
567
+ // ============================================================================
568
+ /**
569
+ * Get next incomplete build instruction with V2 format (structured testCriteria)
570
+ * For v9-mega projects only
571
+ */
572
+ async getNextBuildInstructionV2() {
573
+ const projectData = await this.loadProjectData();
574
+ if (projectData.version !== 'v9-mega') {
575
+ console.warn('getNextBuildInstructionV2 called on non-v9-mega project');
576
+ return null;
577
+ }
578
+ const prompts = projectData.buildPrompts;
579
+ // Find first incomplete, unflagged prompt with all dependencies met
580
+ for (const prompt of prompts) {
581
+ if (prompt.completed)
582
+ continue;
583
+ if (prompt.flaggedForReview)
584
+ continue;
585
+ const allDepsMet = (prompt.dependsOn || []).every((depId) => {
586
+ const dep = prompts.find((p) => p.promptId === depId || p.title === depId);
587
+ return dep?.completed === true;
588
+ });
589
+ if (allDepsMet) {
590
+ // Convert to V2 format
591
+ return this.promptToInstructionV2(prompt);
592
+ }
593
+ }
594
+ return null;
595
+ }
596
+ /**
597
+ * Convert BuildPrompt to BuildInstructionV2 format
598
+ */
599
+ promptToInstructionV2(prompt) {
600
+ return {
601
+ id: prompt.promptId,
602
+ promptNumber: prompt.order,
603
+ title: prompt.title,
604
+ category: prompt.category,
605
+ categoryTitle: prompt.categoryTitle || prompt.category,
606
+ instruction: prompt.instruction,
607
+ verificationPrompt: prompt.verificationPrompt || '',
608
+ testCriteria: this.normalizeTestCriteria(prompt.testCriteria),
609
+ relatedDocuments: {
610
+ pages: prompt.relatedDocuments?.pages || [],
611
+ schemas: prompt.relatedDocuments?.schemas || [],
612
+ endpoints: prompt.relatedDocuments?.endpoints || [],
613
+ techStack: prompt.relatedDocuments?.techStack || [],
614
+ },
615
+ estimatedMinutes: prompt.estimatedMinutes || 30,
616
+ priority: prompt.priority || 'medium',
617
+ completed: prompt.completed,
618
+ completedAt: prompt.completedAt,
619
+ flaggedForReview: prompt.flaggedForReview,
620
+ flaggedAt: prompt.flaggedAt,
621
+ flaggedReason: prompt.flaggedReason,
622
+ };
623
+ }
624
+ /**
625
+ * Normalize test criteria to V2 format
626
+ * Handles both string[] (old format) and TestCriterionV2[] (new format)
627
+ */
628
+ normalizeTestCriteria(criteria) {
629
+ if (!criteria || criteria.length === 0) {
630
+ return [];
631
+ }
632
+ // Check if already in V2 format
633
+ if (typeof criteria[0] === 'object' && 'criterion' in criteria[0]) {
634
+ return criteria;
635
+ }
636
+ // Convert string[] to TestCriterionV2[]
637
+ return criteria.map((c, idx) => ({
638
+ criterion: c,
639
+ type: 'functional',
640
+ verification: `Verify: ${c}`,
641
+ expectedResult: 'Criterion is met',
642
+ }));
643
+ }
644
+ /**
645
+ * Mark an instruction as complete in v9-mega format
646
+ * Updates the instruction within its category document
647
+ */
648
+ async markInstructionCompleteV2(promptId, completionData) {
649
+ const projectRef = this.db.collection('projects').doc(this.config.projectId);
650
+ try {
651
+ // Find which category contains this instruction
652
+ const categoriesSnapshot = await projectRef.collection('buildPrompts').get();
653
+ for (const categoryDoc of categoriesSnapshot.docs) {
654
+ const categoryData = categoryDoc.data();
655
+ const instructions = categoryData.instructions || [];
656
+ const instructionIndex = instructions.findIndex((inst) => inst.id === promptId);
657
+ if (instructionIndex !== -1) {
658
+ // Update the instruction within the array
659
+ instructions[instructionIndex] = {
660
+ ...instructions[instructionIndex],
661
+ completed: true,
662
+ completedAt: new Date(),
663
+ completionNotes: completionData.completionNotes || '',
664
+ filesCreated: completionData.filesCreated || [],
665
+ validationSummary: completionData.validationSummary,
666
+ };
667
+ await categoryDoc.ref.update({
668
+ instructions,
669
+ updatedAt: firestore_1.FieldValue.serverTimestamp(),
670
+ });
671
+ // Invalidate cache
672
+ this.projectDataCache = null;
673
+ return true;
674
+ }
675
+ }
676
+ console.error(`Instruction ${promptId} not found in any category`);
677
+ return false;
678
+ }
679
+ catch (error) {
680
+ console.error('Error marking instruction complete:', error);
681
+ return false;
682
+ }
683
+ }
684
+ /**
685
+ * Flag an instruction for human review (after max retry attempts)
686
+ */
687
+ async flagInstructionForReview(promptId, reason, validationHistory) {
688
+ const projectRef = this.db.collection('projects').doc(this.config.projectId);
689
+ try {
690
+ const categoriesSnapshot = await projectRef.collection('buildPrompts').get();
691
+ for (const categoryDoc of categoriesSnapshot.docs) {
692
+ const categoryData = categoryDoc.data();
693
+ const instructions = categoryData.instructions || [];
694
+ const instructionIndex = instructions.findIndex((inst) => inst.id === promptId);
695
+ if (instructionIndex !== -1) {
696
+ instructions[instructionIndex] = {
697
+ ...instructions[instructionIndex],
698
+ flaggedForReview: true,
699
+ flaggedAt: new Date(),
700
+ flaggedReason: reason,
701
+ validationHistory,
702
+ };
703
+ await categoryDoc.ref.update({
704
+ instructions,
705
+ updatedAt: firestore_1.FieldValue.serverTimestamp(),
706
+ });
707
+ this.projectDataCache = null;
708
+ return true;
709
+ }
710
+ }
711
+ return false;
712
+ }
713
+ catch (error) {
714
+ console.error('Error flagging instruction:', error);
715
+ return false;
716
+ }
717
+ }
718
+ /**
719
+ * Get V2 build progress with flagged counts
720
+ */
721
+ async getBuildProgressV2() {
722
+ const projectData = await this.loadProjectData();
723
+ const prompts = projectData.buildPrompts;
724
+ const total = prompts.length;
725
+ const completed = prompts.filter((p) => p.completed).length;
726
+ const flagged = prompts.filter((p) => p.flaggedForReview).length;
727
+ const remaining = total - completed - flagged;
728
+ const completionPercentage = total > 0 ? Math.round((completed / total) * 100) : 0;
729
+ // Get next step
730
+ let nextStep;
731
+ for (const prompt of prompts) {
732
+ if (prompt.completed || prompt.flaggedForReview)
733
+ continue;
734
+ const allDepsMet = (prompt.dependsOn || []).every((depId) => {
735
+ const dep = prompts.find((p) => p.promptId === depId || p.title === depId);
736
+ return dep?.completed === true;
737
+ });
738
+ if (allDepsMet) {
739
+ nextStep = this.promptToInstructionV2(prompt);
740
+ break;
741
+ }
742
+ }
743
+ let message;
744
+ if (completed === total) {
745
+ message = '🎉 All build steps completed! Project is ready for deployment.';
746
+ }
747
+ else if (flagged > 0) {
748
+ message = `⚠️ ${flagged} step(s) flagged for human review. ${remaining} steps remaining.`;
749
+ }
750
+ else {
751
+ message = `📊 ${completed}/${total} steps completed (${completionPercentage}%). ${remaining} remaining.`;
752
+ }
753
+ return {
754
+ total,
755
+ completed,
756
+ flagged,
757
+ remaining,
758
+ completionPercentage,
759
+ nextStep,
760
+ message,
761
+ };
762
+ }
763
+ /**
764
+ * Log a V2 validation event for analytics
765
+ */
766
+ async logValidationEvent(sessionId, promptId, event) {
767
+ const eventRef = this.db
768
+ .collection('projects')
769
+ .doc(this.config.projectId)
770
+ .collection('mcpValidationLogs')
771
+ .doc();
772
+ await eventRef.set({
773
+ sessionId,
774
+ promptId,
775
+ ...event,
776
+ timestamp: firestore_1.FieldValue.serverTimestamp(),
777
+ });
778
+ }
779
+ /**
780
+ * Get user's BYOK configuration if available
781
+ */
782
+ async getUserBYOKConfig(userId) {
783
+ try {
784
+ const userDoc = await this.db.collection('users').doc(userId).get();
785
+ if (!userDoc.exists)
786
+ return null;
787
+ const userData = userDoc.data();
788
+ const byokConfig = userData?.byokConfig;
789
+ if (!byokConfig?.apiKey) {
790
+ return { hasCustomKey: false };
791
+ }
792
+ return {
793
+ hasCustomKey: true,
794
+ encryptedApiKey: byokConfig.apiKey,
795
+ preferredModel: byokConfig.preferredModel,
796
+ };
797
+ }
798
+ catch (error) {
799
+ console.error('Error fetching BYOK config:', error);
800
+ return null;
801
+ }
802
+ }
803
+ }
804
+ exports.VisualPRDClient = VisualPRDClient;
805
+ //# sourceMappingURL=visualprd-client.js.map