Jake Shore 96e52666c5 MCPEngine full sync — studio scaffold, factory v2, server updates, state.json — 2026-02-12
=== NEW ===
- studio/ — MCPEngine Studio scaffold (Next.js monorepo, build plan)
- docs/FACTORY-V2.md — Factory v2 architecture doc
- docs/CALENDLY_MCP_BUILD_SUMMARY.md — Calendly MCP build report

=== UPDATED SERVERS ===
- fieldedge: Added jobs-tools, UI build script, main entry update
- lightspeed: Updated main + server entry points
- squarespace: Added collection-browser + page-manager apps
- toast: Added main + server entry points

=== INFRA ===
- infra/command-center/state.json — Updated pipeline state
- infra/command-center/FACTORY-V2.md — Factory v2 operator playbook
2026-02-12 17:58:33 -05:00

184 lines
5.2 KiB
TypeScript

// Stream Parser — extracts structured PipelineEvents from Claude's streaming text
import type {
PipelineEvent,
ToolDefinition,
} from '../types';
/**
* Parse Claude streaming text into PipelineEvent objects.
* Scans for JSON blocks, code blocks, and status markers in the accumulated text.
*
* @param text - Accumulated text from Claude stream so far
* @param phase - Current pipeline phase (analysis, generate, design, test)
* @returns Array of events found in the text (deduplicated by caller)
*/
export function parseStreamEvents(
text: string,
phase: 'analysis' | 'generate' | 'design' | 'test'
): PipelineEvent[] {
const events: PipelineEvent[] = [];
switch (phase) {
case 'analysis':
events.push(...parseAnalysisEvents(text));
break;
case 'generate':
events.push(...parseGenerateEvents(text));
break;
case 'design':
events.push(...parseDesignEvents(text));
break;
case 'test':
events.push(...parseTestEvents(text));
break;
}
return events;
}
/** Look for tool definitions appearing in analysis stream */
function parseAnalysisEvents(text: string): PipelineEvent[] {
const events: PipelineEvent[] = [];
const toolPatterns = [
// Match tool objects in JSON arrays
/\{\s*"name"\s*:\s*"([^"]+)"\s*,\s*"description"\s*:\s*"([^"]+)"[^}]*"endpoint"\s*:\s*"([^"]+)"[^}]*"method"\s*:\s*"([^"]+)"/g,
];
for (const pattern of toolPatterns) {
let match;
while ((match = pattern.exec(text)) !== null) {
const partialTool: Partial<ToolDefinition> = {
name: match[1],
description: match[2],
endpoint: match[3],
method: match[4],
};
events.push({
type: 'analysis:tool_found',
tool: partialTool as ToolDefinition,
});
}
}
// Check for progress markers
const progressMarkers = [
{ pattern: /analyzing\s+endpoint/i, step: 'Analyzing endpoints', percent: 20 },
{ pattern: /auth(?:entication|orization)\s+(?:flow|config|type)/i, step: 'Detecting auth flow', percent: 40 },
{ pattern: /tool\s+(?:group|definition|mapping)/i, step: 'Mapping tools', percent: 60 },
{ pattern: /app\s+candidate/i, step: 'Identifying app candidates', percent: 75 },
{ pattern: /rate\s+limit/i, step: 'Checking rate limits', percent: 85 },
];
for (const marker of progressMarkers) {
if (marker.pattern.test(text)) {
events.push({
type: 'analysis:progress',
step: marker.step,
percent: marker.percent,
});
}
}
return events;
}
/** Look for file blocks in generation stream */
function parseGenerateEvents(text: string): PipelineEvent[] {
const events: PipelineEvent[] = [];
// Match completed file code blocks
const fileBlockRegex = /```(\w+)\s*\/\/\s*path:\s*(\S+)\s*\n([\s\S]*?)```/g;
let match;
while ((match = fileBlockRegex.exec(text)) !== null) {
events.push({
type: 'generate:file_ready',
path: match[2].trim(),
content: match[3].trim(),
});
}
return events;
}
/** Look for HTML output in design stream */
function parseDesignEvents(text: string): PipelineEvent[] {
const events: PipelineEvent[] = [];
// Check if HTML block is being written
if (text.includes('```html') && !text.includes('```html\n')) {
// Still writing the opening
return events;
}
const progressMarkers = [
{ pattern: /<!DOCTYPE|<html/i, step: 'Generating HTML structure', percent: 30 },
{ pattern: /<style|tailwind/i, step: 'Applying styles', percent: 50 },
{ pattern: /mcpRequest|data-mcp-tool/i, step: 'Wiring tool bindings', percent: 70 },
{ pattern: /<\/html>/i, step: 'Finalizing app', percent: 90 },
];
for (const marker of progressMarkers) {
if (marker.pattern.test(text)) {
events.push({
type: 'design:progress',
app: 'current',
percent: marker.percent,
});
}
}
return events;
}
/** Look for test result markers */
function parseTestEvents(text: string): PipelineEvent[] {
const events: PipelineEvent[] = [];
// Check for pass/fail indicators
const passPattern = /✓|✅|PASS|passed/gi;
const failPattern = /✗|❌|FAIL|failed/gi;
const passes = (text.match(passPattern) || []).length;
const failures = (text.match(failPattern) || []).length;
if (passes + failures > 0) {
// We can infer progress from test count
events.push({
type: 'analysis:progress',
step: `Tests running: ${passes} passed, ${failures} failed`,
percent: Math.min(80, (passes + failures) * 10),
});
}
return events;
}
/**
* Extract a JSON object from text, handling code blocks and raw JSON.
*/
export function extractJSON<T = unknown>(text: string): T | null {
// Try code block first
const codeBlockMatch = text.match(/```(?:json)?\s*\n([\s\S]*?)\n```/);
if (codeBlockMatch) {
try {
return JSON.parse(codeBlockMatch[1]) as T;
} catch {
// fall through
}
}
// Try raw JSON (find outermost braces)
const braceStart = text.indexOf('{');
const braceEnd = text.lastIndexOf('}');
if (braceStart !== -1 && braceEnd > braceStart) {
try {
return JSON.parse(text.slice(braceStart, braceEnd + 1)) as T;
} catch {
return null;
}
}
return null;
}