@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.
- package/README.md +396 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +27 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +243 -0
- package/dist/index.js.map +1 -0
- package/dist/intelligence/context-optimizer.d.ts +93 -0
- package/dist/intelligence/context-optimizer.d.ts.map +1 -0
- package/dist/intelligence/context-optimizer.js +481 -0
- package/dist/intelligence/context-optimizer.js.map +1 -0
- package/dist/intelligence/error-analyzer.d.ts +49 -0
- package/dist/intelligence/error-analyzer.d.ts.map +1 -0
- package/dist/intelligence/error-analyzer.js +765 -0
- package/dist/intelligence/error-analyzer.js.map +1 -0
- package/dist/intelligence/gap-filler.d.ts +56 -0
- package/dist/intelligence/gap-filler.d.ts.map +1 -0
- package/dist/intelligence/gap-filler.js +410 -0
- package/dist/intelligence/gap-filler.js.map +1 -0
- package/dist/intelligence/guidance-generator.d.ts +43 -0
- package/dist/intelligence/guidance-generator.d.ts.map +1 -0
- package/dist/intelligence/guidance-generator.js +314 -0
- package/dist/intelligence/guidance-generator.js.map +1 -0
- package/dist/intelligence/index.d.ts +132 -0
- package/dist/intelligence/index.d.ts.map +1 -0
- package/dist/intelligence/index.js +683 -0
- package/dist/intelligence/index.js.map +1 -0
- package/dist/server-http.d.ts +9 -0
- package/dist/server-http.d.ts.map +1 -0
- package/dist/server-http.js +141 -0
- package/dist/server-http.js.map +1 -0
- package/dist/services/api-key-service.d.ts +68 -0
- package/dist/services/api-key-service.d.ts.map +1 -0
- package/dist/services/api-key-service.js +298 -0
- package/dist/services/api-key-service.js.map +1 -0
- package/dist/services/llm-client.d.ts +66 -0
- package/dist/services/llm-client.d.ts.map +1 -0
- package/dist/services/llm-client.js +141 -0
- package/dist/services/llm-client.js.map +1 -0
- package/dist/services/model-registry.d.ts +135 -0
- package/dist/services/model-registry.d.ts.map +1 -0
- package/dist/services/model-registry.js +276 -0
- package/dist/services/model-registry.js.map +1 -0
- package/dist/services/visualprd-client.d.ts +191 -0
- package/dist/services/visualprd-client.d.ts.map +1 -0
- package/dist/services/visualprd-client.js +805 -0
- package/dist/services/visualprd-client.js.map +1 -0
- package/dist/tools/index.d.ts +803 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +570 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/types/index.d.ts +497 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- 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
|