claude_stream_viewer 1.0.16 → 1.0.18

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.
Files changed (59) hide show
  1. package/README.md +38 -10
  2. package/demo_vhs/claude_stream_viewer.gif +0 -0
  3. package/dist/cli.js +46 -364
  4. package/dist/cli.js.map +1 -1
  5. package/dist/libs/colors.d.ts +15 -0
  6. package/dist/libs/colors.d.ts.map +1 -0
  7. package/dist/libs/colors.js +16 -0
  8. package/dist/libs/colors.js.map +1 -0
  9. package/dist/libs/terminal_output.d.ts +8 -0
  10. package/dist/libs/terminal_output.d.ts.map +1 -0
  11. package/dist/libs/terminal_output.js +13 -0
  12. package/dist/libs/terminal_output.js.map +1 -0
  13. package/dist/log_viewer/log_renderer.d.ts +28 -0
  14. package/dist/log_viewer/log_renderer.d.ts.map +1 -0
  15. package/dist/log_viewer/log_renderer.js +163 -0
  16. package/dist/log_viewer/log_renderer.js.map +1 -0
  17. package/dist/log_viewer/tool_input_formatter.d.ts +31 -0
  18. package/dist/log_viewer/tool_input_formatter.d.ts.map +1 -0
  19. package/dist/log_viewer/tool_input_formatter.js +190 -0
  20. package/dist/log_viewer/tool_input_formatter.js.map +1 -0
  21. package/dist/stats/stats_collector.d.ts +28 -0
  22. package/dist/stats/stats_collector.d.ts.map +1 -0
  23. package/dist/stats/stats_collector.js +165 -0
  24. package/dist/stats/stats_collector.js.map +1 -0
  25. package/dist/stats/stats_renderer.d.ts +13 -0
  26. package/dist/stats/stats_renderer.d.ts.map +1 -0
  27. package/dist/stats/stats_renderer.js +193 -0
  28. package/dist/stats/stats_renderer.js.map +1 -0
  29. package/dist/types/claude_event.d.ts +75 -0
  30. package/dist/types/claude_event.d.ts.map +1 -0
  31. package/dist/types/claude_event.js +2 -0
  32. package/dist/types/claude_event.js.map +1 -0
  33. package/dist/types/stats_report.d.ts +68 -0
  34. package/dist/types/stats_report.d.ts.map +1 -0
  35. package/dist/types/stats_report.js +2 -0
  36. package/dist/types/stats_report.js.map +1 -0
  37. package/package.json +4 -5
  38. package/src/cli.ts +53 -409
  39. package/src/libs/colors.ts +16 -0
  40. package/src/libs/terminal_output.ts +14 -0
  41. package/src/log_viewer/log_renderer.ts +160 -0
  42. package/src/log_viewer/tool_input_formatter.ts +183 -0
  43. package/src/stats/stats_collector.ts +175 -0
  44. package/src/stats/stats_renderer.ts +185 -0
  45. package/src/types/claude_event.ts +74 -0
  46. package/src/types/stats_report.ts +66 -0
  47. package/dist/claude-stream-viewer.d.ts +0 -3
  48. package/dist/claude-stream-viewer.d.ts.map +0 -1
  49. package/dist/claude-stream-viewer.js +0 -100
  50. package/dist/claude-stream-viewer.js.map +0 -1
  51. package/dist/claude_stream_viewer copy.d.ts +0 -3
  52. package/dist/claude_stream_viewer copy.d.ts.map +0 -1
  53. package/dist/claude_stream_viewer copy.js +0 -228
  54. package/dist/claude_stream_viewer copy.js.map +0 -1
  55. package/dist/claude_stream_viewer.d.ts +0 -3
  56. package/dist/claude_stream_viewer.d.ts.map +0 -1
  57. package/dist/claude_stream_viewer.js +0 -155
  58. package/dist/claude_stream_viewer.js.map +0 -1
  59. package/src/claude_stream_viewer copy.ts +0 -287
package/src/cli.ts CHANGED
@@ -5,419 +5,45 @@
5
5
  // `stream_event` SSE envelopes are ignored — the consolidated events already
6
6
  // carry fully assembled content blocks.
7
7
  //
8
+ // With --stats/-s (or --json / --markdown) the streamed log is suppressed and
9
+ // only a summary report is printed at the end.
10
+ //
8
11
  // Usage:
9
- // claude --output-format stream-json --verbose --permission-mode auto -p "explain quantum computing like I'm 5" | npx claude_stream_viewer
10
-
12
+ // claude --output-format stream-json --verbose -p "explain quantum computing" | npx claude_stream_viewer
13
+ // … | npx claude_stream_viewer --stats
14
+ // … | npx claude_stream_viewer --json
11
15
 
12
16
  import Readline from 'node:readline';
13
17
  import Fs from 'node:fs';
18
+ import Path from 'node:path';
14
19
  import * as Commander from 'commander';
15
20
  import Chalk from 'chalk';
16
- import Path from 'node:path';
17
21
 
18
- const __dirname = new URL('.', import.meta.url).pathname;
22
+ import { colors } from './libs/colors.js';
23
+ import { TerminalOutput } from './libs/terminal_output.js';
24
+ import { LogRenderer } from './log_viewer/log_renderer.js';
25
+ import { StatsCollector } from './stats/stats_collector.js';
26
+ import { StatsRenderer } from './stats/stats_renderer.js';
27
+ import type { ClaudeEvent } from './types/claude_event.js';
28
+ import type { StatsFormat } from './types/stats_report.js';
19
29
 
20
- const MAX_TOOL_RESULT_LINES = 40;
30
+ const __dirname = new URL('.', import.meta.url).pathname;
21
31
 
32
+ /** Parsed command-line options (see the Commander definitions in {@link main}). */
22
33
  type CliOptions = {
23
34
  color: boolean;
35
+ stats: boolean;
36
+ json: boolean;
37
+ markdown: boolean;
24
38
  };
25
39
 
26
- type ContentBlock = {
27
- type?: string;
28
- text?: string;
29
- thinking?: string;
30
- id?: string;
31
- name?: string;
32
- input?: unknown;
33
- tool_use_id?: string;
34
- content?: string | Array<{ type?: string; text?: string }>;
35
- is_error?: boolean;
36
- };
37
-
38
- type ToolInput = Record<string, unknown>;
39
-
40
- // Partial typing on purpose: only fields we render are listed. Anything we
41
- // don't recognize falls through to the unknown-event branch.
42
- type ClaudeEvent = {
43
- type?: string;
44
- subtype?: string;
45
- cwd?: string;
46
- model?: string;
47
- tools?: string[];
48
- session_id?: string;
49
- claude_code_version?: string;
50
- message?: {
51
- content?: ContentBlock[];
52
- };
53
- rate_limit_info?: {
54
- status?: string;
55
- };
56
- result?: string;
57
- total_cost_usd?: number;
58
- duration_ms?: number;
59
- num_turns?: number;
60
- terminal_reason?: string;
61
- usage?: {
62
- input_tokens?: number;
63
- output_tokens?: number;
64
- cache_read_input_tokens?: number;
65
- cache_creation_input_tokens?: number;
66
- };
67
- };
68
-
69
- const colors = {
70
- text: Chalk.white,
71
- tool: Chalk.cyan,
72
- system: Chalk.gray,
73
- error: Chalk.red,
74
- json: Chalk.dim,
75
- header: Chalk.yellow.bold,
76
- thinking: Chalk.gray.italic,
77
- };
78
-
79
- ///////////////////////////////////////////////////////////////////////////////
80
- ///////////////////////////////////////////////////////////////////////////////
81
- // class MainHelper
82
- ///////////////////////////////////////////////////////////////////////////////
83
- ///////////////////////////////////////////////////////////////////////////////
84
-
85
- class MainHelper {
86
- private toolNamesById = new Map<string, string>();
87
-
88
- printHeader(label: string) {
89
- console.log(colors.header(`\n=== ${label} ===`));
90
- }
91
-
92
- printJSON(obj: unknown) {
93
- console.log(colors.json(JSON.stringify(obj, null, 2)));
94
- }
95
-
96
- ///////////////////////////////////////////////////////////////////////////////
97
- ///////////////////////////////////////////////////////////////////////////////
98
- // per-event-type renderers
99
- ///////////////////////////////////////////////////////////////////////////////
100
- ///////////////////////////////////////////////////////////////////////////////
101
-
102
- private renderSystem(event: ClaudeEvent) {
103
- if (event.subtype !== 'init') {
104
- this.printHeader(`SYSTEM: ${event.subtype ?? 'unknown'}`);
105
- this.printJSON(event);
106
- return;
107
- }
108
- this.printHeader('SESSION');
109
- const sessionPrefix = event.session_id !== undefined ? event.session_id.slice(0, 8) : 'unknown';
110
- const toolsCount = event.tools !== undefined ? event.tools.length : 0;
111
- console.log(colors.system(`model: ${event.model ?? 'unknown'}`));
112
- console.log(colors.system(`cwd: ${event.cwd ?? 'unknown'}`));
113
- console.log(colors.system(`tools: ${toolsCount}`));
114
- console.log(colors.system(`session: ${sessionPrefix}`));
115
- if (event.claude_code_version !== undefined) {
116
- console.log(colors.system(`claude-code: v${event.claude_code_version}`));
117
- }
118
- }
119
-
120
- private renderAssistant(event: ClaudeEvent) {
121
- const content = event.message?.content;
122
- if (content === undefined) return;
123
- for (const block of content) {
124
- this.renderAssistantBlock(block);
125
- }
126
- }
127
-
128
- private renderAssistantBlock(block: ContentBlock) {
129
- if (block.type === 'thinking') {
130
- console.log(colors.system('\n--- thinking ---'));
131
- console.log(colors.thinking(block.thinking ?? ''));
132
- return;
133
- }
134
- if (block.type === 'text') {
135
- console.log(colors.text(`\n${block.text ?? ''}`));
136
- return;
137
- }
138
- if (block.type === 'tool_use') {
139
- this.renderToolUse(block);
140
- return;
141
- }
142
- console.log(colors.system(`\n[assistant block: ${block.type ?? 'unknown'}]`));
143
- this.printJSON(block);
144
- }
145
-
146
- private renderToolUse(block: ContentBlock) {
147
- const name = block.name ?? 'tool';
148
- if (block.id !== undefined) {
149
- this.toolNamesById.set(block.id, name);
150
- }
151
- console.log(colors.tool(`\n→ ${name}`));
152
-
153
- const rawInput = block.input;
154
- const input: ToolInput = (rawInput !== null && typeof rawInput === 'object')
155
- ? rawInput as ToolInput
156
- : {};
157
- const bodyLines = MainHelper.formatToolBody(name, input);
158
- for (const line of bodyLines) {
159
- console.log(` ${line}`);
160
- }
161
- }
162
-
163
- private static formatToolBody(name: string, input: ToolInput): string[] {
164
- if (name === 'Bash') return MainHelper.formatBashInput(input);
165
- if (name === 'Read') return MainHelper.formatReadInput(input);
166
- if (name === 'Write') return MainHelper.formatWriteInput(input);
167
- if (name === 'Edit') return MainHelper.formatEditInput(input);
168
- if (name === 'Grep') return MainHelper.formatGrepInput(input);
169
- if (name === 'Glob') return MainHelper.formatGlobInput(input);
170
- if (name === 'TodoWrite') return MainHelper.formatTodoWriteInput(input);
171
- if (name === 'WebFetch') return MainHelper.formatWebFetchInput(input);
172
- if (name === 'WebSearch') return MainHelper.formatWebSearchInput(input);
173
- if (name === 'Task' || name === 'Agent') return MainHelper.formatTaskInput(input);
174
- return MainHelper.formatFallbackInput(input);
175
- }
176
-
177
- private static formatBashInput(input: ToolInput): string[] {
178
- const command = MainHelper.readString(input, 'command') ?? '';
179
- const description = MainHelper.readString(input, 'description');
180
- const lines: string[] = [];
181
- const cmdLines = command.split('\n');
182
- lines.push(colors.text(`$ ${cmdLines[0] ?? ''}`));
183
- for (let i = 1; i < cmdLines.length; i++) {
184
- lines.push(colors.text(cmdLines[i]));
185
- }
186
- if (description !== undefined) {
187
- lines.push(colors.system(`# ${description}`));
188
- }
189
- return lines;
190
- }
191
-
192
- private static formatReadInput(input: ToolInput): string[] {
193
- const filePath = MainHelper.readString(input, 'file_path') ?? '';
194
- const offset = MainHelper.readNumber(input, 'offset');
195
- const limit = MainHelper.readNumber(input, 'limit');
196
- let suffix = '';
197
- if (offset !== undefined && limit !== undefined) {
198
- suffix = `:${offset}-${offset + limit - 1}`;
199
- } else if (offset !== undefined) {
200
- suffix = `:${offset}+`;
201
- }
202
- return [colors.text(`${filePath}${suffix}`)];
203
- }
204
-
205
- private static formatWriteInput(input: ToolInput): string[] {
206
- const filePath = MainHelper.readString(input, 'file_path') ?? '';
207
- return [colors.text(filePath)];
208
- }
209
-
210
- private static formatEditInput(input: ToolInput): string[] {
211
- const filePath = MainHelper.readString(input, 'file_path') ?? '';
212
- const lines = [colors.text(filePath)];
213
- if (MainHelper.readBoolean(input, 'replace_all') === true) {
214
- lines.push(colors.system('replace_all=true'));
215
- }
216
- return lines;
217
- }
218
-
219
- private static formatGrepInput(input: ToolInput): string[] {
220
- const pattern = MainHelper.readString(input, 'pattern') ?? '';
221
- const path = MainHelper.readString(input, 'path');
222
- const head = path !== undefined ? `"${pattern}" in ${path}` : `"${pattern}"`;
223
- const flags: string[] = [];
224
- const outputMode = MainHelper.readString(input, 'output_mode');
225
- if (outputMode !== undefined) flags.push(`output_mode=${outputMode}`);
226
- if (MainHelper.readBoolean(input, '-n') === true) flags.push('-n');
227
- if (MainHelper.readBoolean(input, '-i') === true) flags.push('-i');
228
- const headLimit = MainHelper.readNumber(input, 'head_limit');
229
- if (headLimit !== undefined) flags.push(`head_limit=${headLimit}`);
230
- const glob = MainHelper.readString(input, 'glob');
231
- if (glob !== undefined) flags.push(`glob=${glob}`);
232
- const fileType = MainHelper.readString(input, 'type');
233
- if (fileType !== undefined) flags.push(`type=${fileType}`);
234
- if (flags.length === 0) {
235
- return [colors.text(head)];
236
- }
237
- return [`${colors.text(head)} ${colors.system(`(${flags.join(', ')})`)}`];
238
- }
239
-
240
- private static formatGlobInput(input: ToolInput): string[] {
241
- const pattern = MainHelper.readString(input, 'pattern') ?? '';
242
- const path = MainHelper.readString(input, 'path');
243
- const text = path !== undefined ? `${pattern} in ${path}` : pattern;
244
- return [colors.text(text)];
245
- }
246
-
247
- private static formatTodoWriteInput(input: ToolInput): string[] {
248
- const todos = input['todos'];
249
- if (Array.isArray(todos) === false) {
250
- return [colors.system('(no todos)')];
251
- }
252
- return todos.map((todo) => {
253
- if (typeof todo !== 'object' || todo === null) {
254
- return colors.text(String(todo));
255
- }
256
- const t = todo as Record<string, unknown>;
257
- const status = typeof t['status'] === 'string' ? t['status'] : '?';
258
- const content = typeof t['content'] === 'string' ? t['content'] : '';
259
- let mark = '?';
260
- if (status === 'pending') mark = ' ';
261
- else if (status === 'in_progress') mark = '~';
262
- else if (status === 'completed') mark = 'x';
263
- return colors.text(`[${mark}] ${content}`);
264
- });
265
- }
266
-
267
- private static formatWebFetchInput(input: ToolInput): string[] {
268
- const url = MainHelper.readString(input, 'url') ?? '';
269
- const prompt = MainHelper.readString(input, 'prompt');
270
- const lines = [colors.text(url)];
271
- if (prompt !== undefined) {
272
- lines.push(colors.system(`prompt: ${prompt}`));
273
- }
274
- return lines;
275
- }
276
-
277
- private static formatWebSearchInput(input: ToolInput): string[] {
278
- const query = MainHelper.readString(input, 'query') ?? '';
279
- const flags: string[] = [];
280
- const allowed = input['allowed_domains'];
281
- if (Array.isArray(allowed) && allowed.length > 0) {
282
- flags.push(`allowed_domains=[${allowed.map((d) => String(d)).join(', ')}]`);
283
- }
284
- const blocked = input['blocked_domains'];
285
- if (Array.isArray(blocked) && blocked.length > 0) {
286
- flags.push(`blocked_domains=[${blocked.map((d) => String(d)).join(', ')}]`);
287
- }
288
- const head = colors.text(`"${query}"`);
289
- if (flags.length === 0) return [head];
290
- return [`${head} ${colors.system(`(${flags.join(', ')})`)}`];
291
- }
292
-
293
- private static formatTaskInput(input: ToolInput): string[] {
294
- const description = MainHelper.readString(input, 'description') ?? '';
295
- const subagentType = MainHelper.readString(input, 'subagent_type') ?? 'agent';
296
- const prompt = MainHelper.readString(input, 'prompt');
297
- const lines = [colors.text(`[${subagentType}] ${description}`)];
298
- if (prompt !== undefined) {
299
- const PROMPT_PREVIEW_LIMIT = 200;
300
- if (prompt.length <= PROMPT_PREVIEW_LIMIT) {
301
- lines.push(colors.system(`prompt: ${prompt}`));
302
- } else {
303
- const head = prompt.slice(0, PROMPT_PREVIEW_LIMIT);
304
- lines.push(colors.system(`prompt: ${head}… (${prompt.length} chars)`));
305
- }
306
- }
307
- return lines;
308
- }
309
-
310
- private static formatFallbackInput(input: ToolInput): string[] {
311
- const json = JSON.stringify(input, null, 2);
312
- return json.split('\n').map((line) => colors.json(line));
313
- }
314
-
315
- private static readString(input: ToolInput, key: string): string | undefined {
316
- const value = input[key];
317
- return typeof value === 'string' ? value : undefined;
318
- }
319
-
320
- private static readNumber(input: ToolInput, key: string): number | undefined {
321
- const value = input[key];
322
- return typeof value === 'number' ? value : undefined;
323
- }
324
-
325
- private static readBoolean(input: ToolInput, key: string): boolean | undefined {
326
- const value = input[key];
327
- return typeof value === 'boolean' ? value : undefined;
328
- }
329
-
330
- private renderUser(event: ClaudeEvent) {
331
- const content = event.message?.content;
332
- if (content === undefined) return;
333
- for (const block of content) {
334
- if (block.type === 'tool_result') {
335
- this.renderToolResult(block);
336
- continue;
337
- }
338
- console.log(colors.system(`\n[user block: ${block.type ?? 'unknown'}]`));
339
- this.printJSON(block);
340
- }
341
- }
342
-
343
- private renderToolResult(block: ContentBlock) {
344
- const toolName = block.tool_use_id !== undefined
345
- ? this.toolNamesById.get(block.tool_use_id) ?? block.tool_use_id.slice(0, 12)
346
- : 'unknown';
347
- const isError = block.is_error === true;
348
- const label = isError ? `← ${toolName} ERROR` : `← ${toolName} result`;
349
- console.log(colors.tool(`\n${label}`));
350
-
351
- const text = MainHelper.toolResultToText(block.content);
352
- const lines = text.split('\n');
353
- const colorFn = isError ? colors.error : colors.text;
354
- if (lines.length <= MAX_TOOL_RESULT_LINES) {
355
- console.log(colorFn(text));
356
- return;
357
- }
358
- const head = lines.slice(0, MAX_TOOL_RESULT_LINES).join('\n');
359
- const moreCount = lines.length - MAX_TOOL_RESULT_LINES;
360
- console.log(colorFn(head));
361
- console.log(colors.system(`… (truncated, ${moreCount} more lines)`));
362
- }
363
-
364
- private static toolResultToText(content: ContentBlock['content']): string {
365
- if (content === undefined) return '';
366
- if (typeof content === 'string') return content;
367
- return content.map((b) => b.text ?? '').join('');
368
- }
369
-
370
- private renderRateLimit(event: ClaudeEvent) {
371
- const status = event.rate_limit_info?.status ?? 'unknown';
372
- console.log(colors.system(`\n[rate_limit] ${status}`));
373
- }
374
-
375
- private renderResult(event: ClaudeEvent) {
376
- this.printHeader('RESULT');
377
- if (event.terminal_reason !== undefined) console.log(colors.system(`terminal_reason: ${event.terminal_reason}`));
378
- if (event.num_turns !== undefined) console.log(colors.system(`turns: ${event.num_turns}`));
379
- if (event.duration_ms !== undefined) console.log(colors.system(`duration: ${(event.duration_ms / 1000).toFixed(2)}s`));
380
- if (event.total_cost_usd !== undefined) console.log(colors.system(`cost: $${event.total_cost_usd.toFixed(4)}`));
381
- const usage = event.usage;
382
- if (usage !== undefined) {
383
- console.log(colors.system(
384
- `tokens: in=${usage.input_tokens ?? 0} out=${usage.output_tokens ?? 0} cache_read=${usage.cache_read_input_tokens ?? 0} cache_create=${usage.cache_creation_input_tokens ?? 0}`,
385
- ));
386
- }
387
- }
388
-
389
- ///////////////////////////////////////////////////////////////////////////////
390
- ///////////////////////////////////////////////////////////////////////////////
391
- // dispatcher
392
- ///////////////////////////////////////////////////////////////////////////////
393
- ///////////////////////////////////////////////////////////////////////////////
394
-
395
- handleEvent(claudeEvent: ClaudeEvent) {
396
- try {
397
- const type = claudeEvent.type;
398
- if (type === 'stream_event') return;
399
- if (type === 'system') return this.renderSystem(claudeEvent);
400
- if (type === 'assistant') return this.renderAssistant(claudeEvent);
401
- if (type === 'user') return this.renderUser(claudeEvent);
402
- if (type === 'rate_limit_event') return this.renderRateLimit(claudeEvent);
403
- if (type === 'result') return this.renderResult(claudeEvent);
404
- this.printHeader(`${type ?? 'unknown'}`);
405
- this.printJSON(claudeEvent);
406
- } catch (err) {
407
- console.error(colors.error('Error processing event:'), err);
408
- this.printJSON(claudeEvent);
409
- }
410
- }
411
- }
412
-
413
- ///////////////////////////////////////////////////////////////////////////////
414
- ///////////////////////////////////////////////////////////////////////////////
415
- // function main
416
- ///////////////////////////////////////////////////////////////////////////////
417
- ///////////////////////////////////////////////////////////////////////////////
418
-
40
+ /**
41
+ * Entry point: parses options, then reads stream-json events line-by-line from
42
+ * stdin. Every event is fed to the {@link StatsCollector}; unless a stats-only
43
+ * mode is active it is also rendered by the {@link LogRenderer}. On stdin close
44
+ * it prints either the stats summary or the `[stream ended]` marker.
45
+ */
419
46
  async function main(): Promise<void> {
420
-
421
47
  const packageJsonPath = Path.join(__dirname, '..', 'package.json');
422
48
  const packageJson: object = JSON.parse(await Fs.promises.readFile(packageJsonPath, 'utf-8'));
423
49
  const packageVersion = (packageJson as { version?: string }).version ?? 'unknown';
@@ -428,6 +54,9 @@ async function main(): Promise<void> {
428
54
  .description('Pretty-prints Claude Code consolidated stream-json events from stdin with colorized output.')
429
55
  .version(packageVersion)
430
56
  .option('--no-color', 'disable colored output')
57
+ .option('-s, --stats', 'output only the summary stats (latency, tokens, context, tools); suppresses the streamed log')
58
+ .option('--json', 'output stats as JSON (implies --stats)')
59
+ .option('--markdown', 'output stats as Markdown (implies --stats)')
431
60
  .parse(process.argv);
432
61
 
433
62
  const options = program.opts<CliOptions>();
@@ -435,7 +64,12 @@ async function main(): Promise<void> {
435
64
  Chalk.level = 0;
436
65
  }
437
66
 
438
- const helper = new MainHelper();
67
+ const format: StatsFormat = options.json === true ? 'json' : options.markdown === true ? 'markdown' : 'text';
68
+ const statsMode = options.stats === true || options.json === true || options.markdown === true;
69
+
70
+ const collector = new StatsCollector();
71
+ const logRenderer = new LogRenderer();
72
+
439
73
  const readline = Readline.createInterface({
440
74
  input: process.stdin,
441
75
  crlfDelay: Infinity,
@@ -443,26 +77,36 @@ async function main(): Promise<void> {
443
77
 
444
78
  readline.on('line', (line) => {
445
79
  if (line.trim() === '') return;
80
+ let event: ClaudeEvent;
446
81
  try {
447
- const parsed = JSON.parse(line);
448
- helper.handleEvent(parsed);
82
+ event = JSON.parse(line);
449
83
  } catch (err) {
450
84
  console.error(colors.error('Invalid JSON:'), line);
85
+ return;
86
+ }
87
+ try {
88
+ collector.track(event);
89
+ if (statsMode === false) {
90
+ logRenderer.render(event);
91
+ }
92
+ } catch (err) {
93
+ console.error(colors.error('Error processing event:'), err);
94
+ if (statsMode === false) {
95
+ console.log(TerminalOutput.json(event));
96
+ }
451
97
  }
452
98
  });
453
99
 
454
100
  await new Promise<void>((resolve) => {
455
101
  readline.on('close', () => {
456
- console.log(colors.system('\n[stream ended]'));
102
+ if (statsMode === true) {
103
+ console.log(StatsRenderer.render(collector.buildReport(), format));
104
+ } else {
105
+ console.log(colors.system('\n[stream ended]'));
106
+ }
457
107
  resolve();
458
108
  });
459
109
  });
460
110
  }
461
111
 
462
- ///////////////////////////////////////////////////////////////////////////////
463
- ///////////////////////////////////////////////////////////////////////////////
464
- //
465
- ///////////////////////////////////////////////////////////////////////////////
466
- ///////////////////////////////////////////////////////////////////////////////
467
-
468
112
  await main();
@@ -0,0 +1,16 @@
1
+ import Chalk from 'chalk';
2
+
3
+ /**
4
+ * Chalk color palette shared by every renderer. Colors are disabled globally by
5
+ * setting `Chalk.level = 0` (via `--no-color`), which these bound functions
6
+ * honor at call time.
7
+ */
8
+ export const colors = {
9
+ text: Chalk.white,
10
+ tool: Chalk.cyan,
11
+ system: Chalk.gray,
12
+ error: Chalk.red,
13
+ json: Chalk.dim,
14
+ header: Chalk.yellow.bold,
15
+ thinking: Chalk.gray.italic,
16
+ };
@@ -0,0 +1,14 @@
1
+ import { colors } from './colors.js';
2
+
3
+ /** Builds the colorized string primitives shared by the log and stats renderers. */
4
+ export class TerminalOutput {
5
+ /** Returns a colorized `=== LABEL ===` section header, preceded by a blank line. */
6
+ static header(label: string): string {
7
+ return colors.header(`\n=== ${label} ===`);
8
+ }
9
+
10
+ /** Returns a dimmed, pretty-printed (2-space) JSON rendering of `obj`. */
11
+ static json(obj: unknown): string {
12
+ return colors.json(JSON.stringify(obj, null, 2));
13
+ }
14
+ }