=== 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
184 lines
5.2 KiB
TypeScript
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;
|
|
}
|