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.
- package/README.md +38 -10
- package/demo_vhs/claude_stream_viewer.gif +0 -0
- package/dist/cli.js +46 -364
- package/dist/cli.js.map +1 -1
- package/dist/libs/colors.d.ts +15 -0
- package/dist/libs/colors.d.ts.map +1 -0
- package/dist/libs/colors.js +16 -0
- package/dist/libs/colors.js.map +1 -0
- package/dist/libs/terminal_output.d.ts +8 -0
- package/dist/libs/terminal_output.d.ts.map +1 -0
- package/dist/libs/terminal_output.js +13 -0
- package/dist/libs/terminal_output.js.map +1 -0
- package/dist/log_viewer/log_renderer.d.ts +28 -0
- package/dist/log_viewer/log_renderer.d.ts.map +1 -0
- package/dist/log_viewer/log_renderer.js +163 -0
- package/dist/log_viewer/log_renderer.js.map +1 -0
- package/dist/log_viewer/tool_input_formatter.d.ts +31 -0
- package/dist/log_viewer/tool_input_formatter.d.ts.map +1 -0
- package/dist/log_viewer/tool_input_formatter.js +190 -0
- package/dist/log_viewer/tool_input_formatter.js.map +1 -0
- package/dist/stats/stats_collector.d.ts +28 -0
- package/dist/stats/stats_collector.d.ts.map +1 -0
- package/dist/stats/stats_collector.js +165 -0
- package/dist/stats/stats_collector.js.map +1 -0
- package/dist/stats/stats_renderer.d.ts +13 -0
- package/dist/stats/stats_renderer.d.ts.map +1 -0
- package/dist/stats/stats_renderer.js +193 -0
- package/dist/stats/stats_renderer.js.map +1 -0
- package/dist/types/claude_event.d.ts +75 -0
- package/dist/types/claude_event.d.ts.map +1 -0
- package/dist/types/claude_event.js +2 -0
- package/dist/types/claude_event.js.map +1 -0
- package/dist/types/stats_report.d.ts +68 -0
- package/dist/types/stats_report.d.ts.map +1 -0
- package/dist/types/stats_report.js +2 -0
- package/dist/types/stats_report.js.map +1 -0
- package/package.json +4 -5
- package/src/cli.ts +53 -409
- package/src/libs/colors.ts +16 -0
- package/src/libs/terminal_output.ts +14 -0
- package/src/log_viewer/log_renderer.ts +160 -0
- package/src/log_viewer/tool_input_formatter.ts +183 -0
- package/src/stats/stats_collector.ts +175 -0
- package/src/stats/stats_renderer.ts +185 -0
- package/src/types/claude_event.ts +74 -0
- package/src/types/stats_report.ts +66 -0
- package/dist/claude-stream-viewer.d.ts +0 -3
- package/dist/claude-stream-viewer.d.ts.map +0 -1
- package/dist/claude-stream-viewer.js +0 -100
- package/dist/claude-stream-viewer.js.map +0 -1
- package/dist/claude_stream_viewer copy.d.ts +0 -3
- package/dist/claude_stream_viewer copy.d.ts.map +0 -1
- package/dist/claude_stream_viewer copy.js +0 -228
- package/dist/claude_stream_viewer copy.js.map +0 -1
- package/dist/claude_stream_viewer.d.ts +0 -3
- package/dist/claude_stream_viewer.d.ts.map +0 -1
- package/dist/claude_stream_viewer.js +0 -155
- package/dist/claude_stream_viewer.js.map +0 -1
- 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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|