mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 21:03:08 +00:00
feat: Enhance log parser to support Cursor CLI events
- Added functions to detect and normalize Cursor stream events, including tool calls and system messages. - Updated `parseLogLine` to handle Cursor events and integrate them into the log entry structure. - Marked completion of the log parser integration phase in the project plan.
This commit is contained in:
@@ -3,6 +3,14 @@
|
|||||||
* Parses agent output into structured sections for display
|
* Parses agent output into structured sections for display
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
CursorStreamEvent,
|
||||||
|
CursorSystemEvent,
|
||||||
|
CursorAssistantEvent,
|
||||||
|
CursorToolCallEvent,
|
||||||
|
CursorResultEvent,
|
||||||
|
} from '@automaker/types';
|
||||||
|
|
||||||
export type LogEntryType =
|
export type LogEntryType =
|
||||||
| 'prompt'
|
| 'prompt'
|
||||||
| 'tool_call'
|
| 'tool_call'
|
||||||
@@ -300,6 +308,244 @@ export function generateToolSummary(toolName: string, content: string): string |
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Cursor Event Parsing
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect if a parsed JSON object is a Cursor stream event
|
||||||
|
*/
|
||||||
|
function isCursorEvent(obj: unknown): obj is CursorStreamEvent {
|
||||||
|
return (
|
||||||
|
obj !== null &&
|
||||||
|
typeof obj === 'object' &&
|
||||||
|
'type' in obj &&
|
||||||
|
'session_id' in obj &&
|
||||||
|
['system', 'user', 'assistant', 'tool_call', 'result'].includes(
|
||||||
|
(obj as Record<string, unknown>).type as string
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize Cursor tool call event to log entry
|
||||||
|
*/
|
||||||
|
function normalizeCursorToolCall(
|
||||||
|
event: CursorToolCallEvent,
|
||||||
|
baseEntry: { id: string; timestamp: string }
|
||||||
|
): LogEntry | null {
|
||||||
|
const toolCall = event.tool_call;
|
||||||
|
const isStarted = event.subtype === 'started';
|
||||||
|
const isCompleted = event.subtype === 'completed';
|
||||||
|
|
||||||
|
// Read tool
|
||||||
|
if (toolCall.readToolCall) {
|
||||||
|
const path = toolCall.readToolCall.args.path;
|
||||||
|
const result = toolCall.readToolCall.result?.success;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseEntry,
|
||||||
|
id: `${baseEntry.id}-${event.call_id}`,
|
||||||
|
type: 'tool_call' as LogEntryType,
|
||||||
|
title: isStarted ? `Reading ${path}` : `Read ${path}`,
|
||||||
|
content:
|
||||||
|
isCompleted && result
|
||||||
|
? `${result.totalLines} lines, ${result.totalChars} chars`
|
||||||
|
: `Path: ${path}`,
|
||||||
|
collapsed: true,
|
||||||
|
metadata: {
|
||||||
|
toolName: 'Read',
|
||||||
|
toolCategory: 'read' as ToolCategory,
|
||||||
|
filePath: path,
|
||||||
|
summary: isCompleted ? `Read ${result?.totalLines || 0} lines` : `Reading file...`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write tool
|
||||||
|
if (toolCall.writeToolCall) {
|
||||||
|
const path =
|
||||||
|
toolCall.writeToolCall.args?.path ||
|
||||||
|
toolCall.writeToolCall.result?.success?.path ||
|
||||||
|
'unknown';
|
||||||
|
const result = toolCall.writeToolCall.result?.success;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseEntry,
|
||||||
|
id: `${baseEntry.id}-${event.call_id}`,
|
||||||
|
type: 'tool_call' as LogEntryType,
|
||||||
|
title: isStarted ? `Writing ${path}` : `Wrote ${path}`,
|
||||||
|
content:
|
||||||
|
isCompleted && result
|
||||||
|
? `${result.linesCreated} lines, ${result.fileSize} bytes`
|
||||||
|
: `Path: ${path}`,
|
||||||
|
collapsed: true,
|
||||||
|
metadata: {
|
||||||
|
toolName: 'Write',
|
||||||
|
toolCategory: 'write' as ToolCategory,
|
||||||
|
filePath: path,
|
||||||
|
summary: isCompleted ? `Wrote ${result?.linesCreated || 0} lines` : `Writing file...`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic function tool
|
||||||
|
if (toolCall.function) {
|
||||||
|
const name = toolCall.function.name;
|
||||||
|
const args = toolCall.function.arguments;
|
||||||
|
|
||||||
|
// Determine category based on tool name
|
||||||
|
const category = categorizeToolName(name);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseEntry,
|
||||||
|
id: `${baseEntry.id}-${event.call_id}`,
|
||||||
|
type: 'tool_call' as LogEntryType,
|
||||||
|
title: `${name} ${isStarted ? 'started' : 'completed'}`,
|
||||||
|
content: args || '',
|
||||||
|
collapsed: true,
|
||||||
|
metadata: {
|
||||||
|
toolName: name,
|
||||||
|
toolCategory: category,
|
||||||
|
summary: `${name} ${event.subtype}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize Cursor stream event to log entry
|
||||||
|
*/
|
||||||
|
export function normalizeCursorEvent(event: CursorStreamEvent): LogEntry | null {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const baseEntry = {
|
||||||
|
id: `cursor-${event.session_id}-${Date.now()}`,
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (event.type) {
|
||||||
|
case 'system': {
|
||||||
|
const sysEvent = event as CursorSystemEvent;
|
||||||
|
return {
|
||||||
|
...baseEntry,
|
||||||
|
type: 'info' as LogEntryType,
|
||||||
|
title: 'Session Started',
|
||||||
|
content: `Model: ${sysEvent.model}\nAuth: ${sysEvent.apiKeySource}\nCWD: ${sysEvent.cwd}`,
|
||||||
|
collapsed: true,
|
||||||
|
metadata: {
|
||||||
|
phase: 'init',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'assistant': {
|
||||||
|
const assistEvent = event as CursorAssistantEvent;
|
||||||
|
const text = assistEvent.message.content
|
||||||
|
.filter((c) => c.type === 'text')
|
||||||
|
.map((c) => c.text)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
if (!text.trim()) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseEntry,
|
||||||
|
type: 'info' as LogEntryType,
|
||||||
|
title: 'Assistant',
|
||||||
|
content: text,
|
||||||
|
collapsed: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'tool_call': {
|
||||||
|
const toolEvent = event as CursorToolCallEvent;
|
||||||
|
return normalizeCursorToolCall(toolEvent, baseEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'result': {
|
||||||
|
const resultEvent = event as CursorResultEvent;
|
||||||
|
|
||||||
|
if (resultEvent.is_error) {
|
||||||
|
return {
|
||||||
|
...baseEntry,
|
||||||
|
type: 'error' as LogEntryType,
|
||||||
|
title: 'Error',
|
||||||
|
content: resultEvent.error || resultEvent.result || 'Unknown error',
|
||||||
|
collapsed: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseEntry,
|
||||||
|
type: 'success' as LogEntryType,
|
||||||
|
title: 'Completed',
|
||||||
|
content: `Duration: ${resultEvent.duration_ms}ms`,
|
||||||
|
collapsed: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a single log line into a structured entry
|
||||||
|
* Handles both Cursor JSON events and plain text
|
||||||
|
*/
|
||||||
|
export function parseLogLine(line: string): LogEntry | null {
|
||||||
|
if (!line.trim()) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(line);
|
||||||
|
|
||||||
|
// Check if it's a Cursor stream event
|
||||||
|
if (isCursorEvent(parsed)) {
|
||||||
|
return normalizeCursorEvent(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other JSON, treat as debug info
|
||||||
|
return {
|
||||||
|
id: `json-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||||
|
type: 'debug',
|
||||||
|
title: 'Debug Info',
|
||||||
|
content: line,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
collapsed: true,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
// Non-JSON line - treat as plain text
|
||||||
|
return {
|
||||||
|
id: `text-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
||||||
|
type: 'info',
|
||||||
|
title: 'Output',
|
||||||
|
content: line,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
collapsed: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get provider-specific styling for log entries
|
||||||
|
*/
|
||||||
|
export function getProviderStyle(entry: LogEntry): { badge?: string; icon?: string } {
|
||||||
|
// Check if entry has Cursor session ID pattern
|
||||||
|
if (entry.id.startsWith('cursor-')) {
|
||||||
|
return {
|
||||||
|
badge: 'Cursor',
|
||||||
|
icon: 'terminal',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default (Claude/AutoMaker)
|
||||||
|
return {
|
||||||
|
badge: 'Claude',
|
||||||
|
icon: 'bot',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if an entry should be collapsed by default
|
* Determines if an entry should be collapsed by default
|
||||||
*/
|
*/
|
||||||
@@ -489,6 +735,26 @@ export function parseLogOutput(rawOutput: string): LogEntry[] {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for Cursor stream events (NDJSON lines)
|
||||||
|
// These are complete JSON objects on a single line
|
||||||
|
if (trimmedLine.startsWith('{') && trimmedLine.endsWith('}')) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(trimmedLine);
|
||||||
|
if (isCursorEvent(parsed)) {
|
||||||
|
// Finalize any pending entry before adding Cursor event
|
||||||
|
finalizeEntry();
|
||||||
|
const cursorEntry = normalizeCursorEvent(parsed);
|
||||||
|
if (cursorEntry) {
|
||||||
|
entries.push(cursorEntry);
|
||||||
|
}
|
||||||
|
lineIndex++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not valid JSON, continue with normal parsing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If we're in JSON accumulation mode, keep accumulating until depth returns to 0
|
// If we're in JSON accumulation mode, keep accumulating until depth returns to 0
|
||||||
if (inJsonAccumulation) {
|
if (inJsonAccumulation) {
|
||||||
currentContent.push(line);
|
currentContent.push(line);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
| 2 | [Cursor Provider Implementation](phases/phase-2-provider.md) | `completed` | ✅ |
|
| 2 | [Cursor Provider Implementation](phases/phase-2-provider.md) | `completed` | ✅ |
|
||||||
| 3 | [Provider Factory Integration](phases/phase-3-factory.md) | `completed` | ✅ |
|
| 3 | [Provider Factory Integration](phases/phase-3-factory.md) | `completed` | ✅ |
|
||||||
| 4 | [Setup Routes & Status Endpoints](phases/phase-4-routes.md) | `completed` | ✅ |
|
| 4 | [Setup Routes & Status Endpoints](phases/phase-4-routes.md) | `completed` | ✅ |
|
||||||
| 5 | [Log Parser Integration](phases/phase-5-log-parser.md) | `pending` | - |
|
| 5 | [Log Parser Integration](phases/phase-5-log-parser.md) | `completed` | ✅ |
|
||||||
| 6 | [UI Setup Wizard](phases/phase-6-setup-wizard.md) | `pending` | - |
|
| 6 | [UI Setup Wizard](phases/phase-6-setup-wizard.md) | `pending` | - |
|
||||||
| 7 | [Settings View Provider Tabs](phases/phase-7-settings.md) | `pending` | - |
|
| 7 | [Settings View Provider Tabs](phases/phase-7-settings.md) | `pending` | - |
|
||||||
| 8 | [AI Profiles Integration](phases/phase-8-profiles.md) | `pending` | - |
|
| 8 | [AI Profiles Integration](phases/phase-8-profiles.md) | `pending` | - |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Phase 5: Log Parser Integration
|
# Phase 5: Log Parser Integration
|
||||||
|
|
||||||
**Status:** `pending`
|
**Status:** `completed`
|
||||||
**Dependencies:** Phase 2 (Provider), Phase 3 (Factory)
|
**Dependencies:** Phase 2 (Provider), Phase 3 (Factory)
|
||||||
**Estimated Effort:** Small (parser extension)
|
**Estimated Effort:** Small (parser extension)
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ Update the log parser to recognize and normalize Cursor CLI stream events for di
|
|||||||
|
|
||||||
### Task 5.1: Add Cursor Event Type Detection
|
### Task 5.1: Add Cursor Event Type Detection
|
||||||
|
|
||||||
**Status:** `pending`
|
**Status:** `completed`
|
||||||
|
|
||||||
**File:** `apps/ui/src/lib/log-parser.ts`
|
**File:** `apps/ui/src/lib/log-parser.ts`
|
||||||
|
|
||||||
@@ -216,7 +216,7 @@ function normalizeCursorToolCall(
|
|||||||
|
|
||||||
### Task 5.2: Update parseLogLine Function
|
### Task 5.2: Update parseLogLine Function
|
||||||
|
|
||||||
**Status:** `pending`
|
**Status:** `completed`
|
||||||
|
|
||||||
**File:** `apps/ui/src/lib/log-parser.ts`
|
**File:** `apps/ui/src/lib/log-parser.ts`
|
||||||
|
|
||||||
@@ -255,7 +255,7 @@ export function parseLogLine(line: string): LogEntry | null {
|
|||||||
|
|
||||||
### Task 5.3: Add Cursor-Specific Styling (Optional)
|
### Task 5.3: Add Cursor-Specific Styling (Optional)
|
||||||
|
|
||||||
**Status:** `pending`
|
**Status:** `completed`
|
||||||
|
|
||||||
**File:** `apps/ui/src/lib/log-parser.ts`
|
**File:** `apps/ui/src/lib/log-parser.ts`
|
||||||
|
|
||||||
@@ -348,13 +348,13 @@ console.log('Parsed entries:', entries.length);
|
|||||||
|
|
||||||
Before marking this phase complete:
|
Before marking this phase complete:
|
||||||
|
|
||||||
- [ ] `isCursorEvent()` correctly identifies Cursor events
|
- [x] `isCursorEvent()` correctly identifies Cursor events
|
||||||
- [ ] `normalizeCursorEvent()` handles all event types
|
- [x] `normalizeCursorEvent()` handles all event types
|
||||||
- [ ] Tool calls are categorized correctly
|
- [x] Tool calls are categorized correctly
|
||||||
- [ ] File paths extracted for Read/Write tools
|
- [x] File paths extracted for Read/Write tools
|
||||||
- [ ] Existing Claude event parsing not broken
|
- [x] Existing Claude event parsing not broken
|
||||||
- [ ] Log viewer displays Cursor events correctly
|
- [x] Log viewer displays Cursor events correctly
|
||||||
- [ ] No runtime errors with malformed events
|
- [x] No runtime errors with malformed events
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user