Files
Codewalkers/apps/server/agent/providers/parsers/claude.ts
Lukas May 34578d39c6 refactor: Restructure monorepo to apps/server/ and apps/web/ layout
Move src/ → apps/server/ and packages/web/ → apps/web/ to adopt
standard monorepo conventions (apps/ for runnable apps, packages/
for reusable libraries). Update all config files, shared package
imports, test fixtures, and documentation to reflect new paths.

Key fixes:
- Update workspace config to ["apps/*", "packages/*"]
- Update tsconfig.json rootDir/include for apps/server/
- Add apps/web/** to vitest exclude list
- Update drizzle.config.ts schema path
- Fix ensure-schema.ts migration path detection (3 levels up in dev,
  2 levels up in dist)
- Fix tests/integration/cli-server.test.ts import paths
- Update packages/shared imports to apps/server/ paths
- Update all docs/ files with new paths
2026-03-03 11:22:53 +01:00

168 lines
4.5 KiB
TypeScript

/**
* Claude Code Stream Parser
*
* Parses Claude Code CLI `--output-format stream-json` NDJSON output
* into standardized StreamEvents.
*
* Key line types handled:
* - system (subtype=init): session_id
* - stream_event (content_block_delta, text_delta): delta.text
* - stream_event (content_block_start, tool_use): content_block.name, .id
* - stream_event (message_delta): delta.stop_reason
* - result: result, session_id, total_cost_usd
* - any with is_error: true: error message
*/
import type { StreamEvent, StreamParser } from '../stream-types.js';
interface ClaudeSystemEvent {
type: 'system';
subtype?: string;
session_id?: string;
}
interface ClaudeStreamEvent {
type: 'stream_event';
event?: {
type: string;
index?: number;
delta?: {
type?: string;
text?: string;
stop_reason?: string;
};
content_block?: {
type?: string;
id?: string;
name?: string;
};
};
}
interface ClaudeAssistantEvent {
type: 'assistant';
message?: {
content?: Array<{
type: string;
text?: string;
id?: string;
name?: string;
}>;
};
}
interface ClaudeResultEvent {
type: 'result';
result?: string;
session_id?: string;
total_cost_usd?: number;
is_error?: boolean;
}
type ClaudeEvent = ClaudeSystemEvent | ClaudeStreamEvent | ClaudeAssistantEvent | ClaudeResultEvent | { type: string; is_error?: boolean; result?: string };
export class ClaudeStreamParser implements StreamParser {
readonly provider = 'claude';
parseLine(line: string): StreamEvent[] {
const trimmed = line.trim();
if (!trimmed) return [];
let parsed: ClaudeEvent;
try {
parsed = JSON.parse(trimmed);
} catch {
// Not valid JSON, ignore
return [];
}
// Check for error on non-result events (e.g. stream errors)
// Result events with is_error are handled in the 'result' case below
if ('is_error' in parsed && parsed.is_error && 'result' in parsed && parsed.type !== 'result') {
return [{ type: 'error', message: String(parsed.result) }];
}
const events: StreamEvent[] = [];
switch (parsed.type) {
case 'system': {
const sysEvent = parsed as ClaudeSystemEvent;
if (sysEvent.subtype === 'init' && sysEvent.session_id) {
events.push({ type: 'init', sessionId: sysEvent.session_id });
}
break;
}
case 'stream_event': {
const streamEvent = parsed as ClaudeStreamEvent;
const inner = streamEvent.event;
if (!inner) break;
switch (inner.type) {
case 'content_block_delta': {
if (inner.delta?.type === 'text_delta' && inner.delta.text) {
events.push({ type: 'text_delta', text: inner.delta.text });
}
break;
}
case 'content_block_start': {
if (inner.content_block?.type === 'tool_use') {
const name = inner.content_block.name || 'unknown';
const id = inner.content_block.id || '';
events.push({ type: 'tool_use_start', name, id });
}
break;
}
case 'message_delta': {
if (inner.delta?.stop_reason) {
events.push({ type: 'turn_end', stopReason: inner.delta.stop_reason });
}
break;
}
}
break;
}
case 'assistant': {
// Claude CLI stream-json now emits complete assistant messages
// instead of granular stream_event deltas
const assistantEvent = parsed as ClaudeAssistantEvent;
const content = assistantEvent.message?.content;
if (Array.isArray(content)) {
for (const block of content) {
if (block.type === 'text' && block.text) {
events.push({ type: 'text_delta', text: block.text });
} else if (block.type === 'tool_use' && block.name) {
events.push({ type: 'tool_use_start', name: block.name, id: block.id || '' });
}
}
}
break;
}
case 'result': {
const resultEvent = parsed as ClaudeResultEvent;
events.push({
type: 'result',
text: resultEvent.result || '',
sessionId: resultEvent.session_id,
costUsd: resultEvent.total_cost_usd,
isError: resultEvent.is_error === true,
});
break;
}
// Ignore: message_start, content_block_stop, message_stop, user
}
return events;
}
end(): StreamEvent[] {
// Claude emits a result event, so nothing needed at end
return [];
}
}