claude_stream_viewer 1.0.14 → 1.0.16
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 +23 -14
- package/data/claude_events/basic.claude_events.jsonl +99 -0
- package/data/claude_events/video_build_in_public.claude_events.jsonl +4224 -0
- package/data/claude_events/video_generation.claude_events.jsonl +196 -0
- package/demo_vhs/claude_stream_viewer.compressed.gif +0 -0
- package/demo_vhs/claude_stream_viewer.gif +0 -0
- package/demo_vhs/demo.tape +29 -0
- package/demo_vhs/fixture_demo.jsonl +6 -0
- package/dist/claude_stream_viewer copy.d.ts +3 -0
- package/dist/claude_stream_viewer copy.d.ts.map +1 -0
- package/dist/claude_stream_viewer copy.js +228 -0
- package/dist/claude_stream_viewer copy.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +412 -0
- package/dist/cli.js.map +1 -0
- package/package.json +19 -5
- package/src/claude_stream_viewer copy.ts +287 -0
- package/src/cli.ts +468 -0
- package/src/claude_stream_viewer.ts +0 -208
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Reads Claude Code stream-json events line-by-line from stdin and pretty-prints
|
|
4
|
+
// the consolidated event layer (system/assistant/user/result). The fine-grained
|
|
5
|
+
// `stream_event` SSE envelopes are ignored — the consolidated events already
|
|
6
|
+
// carry fully assembled content blocks.
|
|
7
|
+
//
|
|
8
|
+
// Usage:
|
|
9
|
+
// claude --output-format stream-json --verbose --permission-mode auto -p "explain quantum computing like I'm 5" | npx claude_stream_viewer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
import Readline from 'node:readline';
|
|
13
|
+
import Fs from 'node:fs';
|
|
14
|
+
import * as Commander from 'commander';
|
|
15
|
+
import Chalk from 'chalk';
|
|
16
|
+
import Path from 'node:path';
|
|
17
|
+
|
|
18
|
+
const __dirname = new URL('.', import.meta.url).pathname;
|
|
19
|
+
|
|
20
|
+
const MAX_TOOL_RESULT_LINES = 40;
|
|
21
|
+
|
|
22
|
+
type CliOptions = {
|
|
23
|
+
color: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
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
|
+
// Partial typing on purpose: only fields we render are listed. Anything we
|
|
39
|
+
// don't recognize falls through to the unknown-event branch.
|
|
40
|
+
type ClaudeEvent = {
|
|
41
|
+
type?: string;
|
|
42
|
+
subtype?: string;
|
|
43
|
+
cwd?: string;
|
|
44
|
+
model?: string;
|
|
45
|
+
tools?: string[];
|
|
46
|
+
session_id?: string;
|
|
47
|
+
claude_code_version?: string;
|
|
48
|
+
message?: {
|
|
49
|
+
content?: ContentBlock[];
|
|
50
|
+
};
|
|
51
|
+
rate_limit_info?: {
|
|
52
|
+
status?: string;
|
|
53
|
+
};
|
|
54
|
+
result?: string;
|
|
55
|
+
total_cost_usd?: number;
|
|
56
|
+
duration_ms?: number;
|
|
57
|
+
num_turns?: number;
|
|
58
|
+
terminal_reason?: string;
|
|
59
|
+
usage?: {
|
|
60
|
+
input_tokens?: number;
|
|
61
|
+
output_tokens?: number;
|
|
62
|
+
cache_read_input_tokens?: number;
|
|
63
|
+
cache_creation_input_tokens?: number;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const colors = {
|
|
68
|
+
text: Chalk.white,
|
|
69
|
+
tool: Chalk.cyan,
|
|
70
|
+
system: Chalk.gray,
|
|
71
|
+
error: Chalk.red,
|
|
72
|
+
json: Chalk.dim,
|
|
73
|
+
header: Chalk.yellow.bold,
|
|
74
|
+
thinking: Chalk.gray.italic,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
78
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
79
|
+
// class MainHelper
|
|
80
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
81
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
82
|
+
|
|
83
|
+
class MainHelper {
|
|
84
|
+
private toolNamesById = new Map<string, string>();
|
|
85
|
+
|
|
86
|
+
printHeader(label: string) {
|
|
87
|
+
console.log(colors.header(`\n=== ${label} ===`));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
printJSON(obj: unknown) {
|
|
91
|
+
console.log(colors.json(JSON.stringify(obj, null, 2)));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
95
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
96
|
+
// per-event-type renderers
|
|
97
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
98
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
99
|
+
|
|
100
|
+
private renderSystem(event: ClaudeEvent) {
|
|
101
|
+
if (event.subtype !== 'init') {
|
|
102
|
+
this.printHeader(`SYSTEM: ${event.subtype ?? 'unknown'}`);
|
|
103
|
+
this.printJSON(event);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
this.printHeader('SESSION');
|
|
107
|
+
const sessionPrefix = event.session_id !== undefined ? event.session_id.slice(0, 8) : 'unknown';
|
|
108
|
+
const toolsCount = event.tools !== undefined ? event.tools.length : 0;
|
|
109
|
+
console.log(colors.system(`model: ${event.model ?? 'unknown'}`));
|
|
110
|
+
console.log(colors.system(`cwd: ${event.cwd ?? 'unknown'}`));
|
|
111
|
+
console.log(colors.system(`tools: ${toolsCount}`));
|
|
112
|
+
console.log(colors.system(`session: ${sessionPrefix}`));
|
|
113
|
+
if (event.claude_code_version !== undefined) {
|
|
114
|
+
console.log(colors.system(`claude-code: v${event.claude_code_version}`));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private renderAssistant(event: ClaudeEvent) {
|
|
119
|
+
const content = event.message?.content;
|
|
120
|
+
if (content === undefined) return;
|
|
121
|
+
for (const block of content) {
|
|
122
|
+
this.renderAssistantBlock(block);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private renderAssistantBlock(block: ContentBlock) {
|
|
127
|
+
if (block.type === 'thinking') {
|
|
128
|
+
console.log(colors.system('\n--- thinking ---'));
|
|
129
|
+
console.log(colors.thinking(block.thinking ?? ''));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (block.type === 'text') {
|
|
133
|
+
console.log(colors.text(`\n${block.text ?? ''}`));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (block.type === 'tool_use') {
|
|
137
|
+
const name = block.name ?? 'tool';
|
|
138
|
+
if (block.id !== undefined) {
|
|
139
|
+
this.toolNamesById.set(block.id, name);
|
|
140
|
+
}
|
|
141
|
+
console.log(colors.tool(`\n→ ${name}`));
|
|
142
|
+
console.log(colors.json(JSON.stringify(block.input ?? {}, null, 2)));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
console.log(colors.system(`\n[assistant block: ${block.type ?? 'unknown'}]`));
|
|
146
|
+
this.printJSON(block);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private renderUser(event: ClaudeEvent) {
|
|
150
|
+
const content = event.message?.content;
|
|
151
|
+
if (content === undefined) return;
|
|
152
|
+
for (const block of content) {
|
|
153
|
+
if (block.type === 'tool_result') {
|
|
154
|
+
this.renderToolResult(block);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
console.log(colors.system(`\n[user block: ${block.type ?? 'unknown'}]`));
|
|
158
|
+
this.printJSON(block);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private renderToolResult(block: ContentBlock) {
|
|
163
|
+
const toolName = block.tool_use_id !== undefined
|
|
164
|
+
? this.toolNamesById.get(block.tool_use_id) ?? block.tool_use_id.slice(0, 12)
|
|
165
|
+
: 'unknown';
|
|
166
|
+
const isError = block.is_error === true;
|
|
167
|
+
const label = isError ? `← ${toolName} ERROR` : `← ${toolName} result`;
|
|
168
|
+
console.log(colors.tool(`\n${label}`));
|
|
169
|
+
|
|
170
|
+
const text = MainHelper.toolResultToText(block.content);
|
|
171
|
+
const lines = text.split('\n');
|
|
172
|
+
const colorFn = isError ? colors.error : colors.text;
|
|
173
|
+
if (lines.length <= MAX_TOOL_RESULT_LINES) {
|
|
174
|
+
console.log(colorFn(text));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const head = lines.slice(0, MAX_TOOL_RESULT_LINES).join('\n');
|
|
178
|
+
const moreCount = lines.length - MAX_TOOL_RESULT_LINES;
|
|
179
|
+
console.log(colorFn(head));
|
|
180
|
+
console.log(colors.system(`… (truncated, ${moreCount} more lines)`));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private static toolResultToText(content: ContentBlock['content']): string {
|
|
184
|
+
if (content === undefined) return '';
|
|
185
|
+
if (typeof content === 'string') return content;
|
|
186
|
+
return content.map((b) => b.text ?? '').join('');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private renderRateLimit(event: ClaudeEvent) {
|
|
190
|
+
const status = event.rate_limit_info?.status ?? 'unknown';
|
|
191
|
+
console.log(colors.system(`\n[rate_limit] ${status}`));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private renderResult(event: ClaudeEvent) {
|
|
195
|
+
this.printHeader('RESULT');
|
|
196
|
+
if (event.terminal_reason !== undefined) console.log(colors.system(`terminal_reason: ${event.terminal_reason}`));
|
|
197
|
+
if (event.num_turns !== undefined) console.log(colors.system(`turns: ${event.num_turns}`));
|
|
198
|
+
if (event.duration_ms !== undefined) console.log(colors.system(`duration: ${(event.duration_ms / 1000).toFixed(2)}s`));
|
|
199
|
+
if (event.total_cost_usd !== undefined) console.log(colors.system(`cost: $${event.total_cost_usd.toFixed(4)}`));
|
|
200
|
+
const usage = event.usage;
|
|
201
|
+
if (usage !== undefined) {
|
|
202
|
+
console.log(colors.system(
|
|
203
|
+
`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}`,
|
|
204
|
+
));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
209
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
210
|
+
// dispatcher
|
|
211
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
212
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
213
|
+
|
|
214
|
+
handleEvent(claudeEvent: ClaudeEvent) {
|
|
215
|
+
try {
|
|
216
|
+
const type = claudeEvent.type;
|
|
217
|
+
if (type === 'stream_event') return;
|
|
218
|
+
if (type === 'system') return this.renderSystem(claudeEvent);
|
|
219
|
+
if (type === 'assistant') return this.renderAssistant(claudeEvent);
|
|
220
|
+
if (type === 'user') return this.renderUser(claudeEvent);
|
|
221
|
+
if (type === 'rate_limit_event') return this.renderRateLimit(claudeEvent);
|
|
222
|
+
if (type === 'result') return this.renderResult(claudeEvent);
|
|
223
|
+
this.printHeader(`${type ?? 'unknown'}`);
|
|
224
|
+
this.printJSON(claudeEvent);
|
|
225
|
+
} catch (err) {
|
|
226
|
+
console.error(colors.error('Error processing event:'), err);
|
|
227
|
+
this.printJSON(claudeEvent);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
233
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
234
|
+
// function main
|
|
235
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
236
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
237
|
+
|
|
238
|
+
async function main(): Promise<void> {
|
|
239
|
+
|
|
240
|
+
const packageJsonPath = Path.join(__dirname, '..', 'package.json');
|
|
241
|
+
const packageJson: object = JSON.parse(await Fs.promises.readFile(packageJsonPath, 'utf-8'));
|
|
242
|
+
const packageVersion = (packageJson as { version?: string }).version ?? 'unknown';
|
|
243
|
+
|
|
244
|
+
const program = new Commander.Command();
|
|
245
|
+
program
|
|
246
|
+
.name('claude_stream_viewer')
|
|
247
|
+
.description('Pretty-prints Claude Code consolidated stream-json events from stdin with colorized output.')
|
|
248
|
+
.version(packageVersion)
|
|
249
|
+
.option('--no-color', 'disable colored output')
|
|
250
|
+
.parse(process.argv);
|
|
251
|
+
|
|
252
|
+
const options = program.opts<CliOptions>();
|
|
253
|
+
if (options.color === false) {
|
|
254
|
+
Chalk.level = 0;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const helper = new MainHelper();
|
|
258
|
+
const readline = Readline.createInterface({
|
|
259
|
+
input: process.stdin,
|
|
260
|
+
crlfDelay: Infinity,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
readline.on('line', (line) => {
|
|
264
|
+
if (line.trim() === '') return;
|
|
265
|
+
try {
|
|
266
|
+
const parsed = JSON.parse(line);
|
|
267
|
+
helper.handleEvent(parsed);
|
|
268
|
+
} catch (err) {
|
|
269
|
+
console.error(colors.error('Invalid JSON:'), line);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
await new Promise<void>((resolve) => {
|
|
274
|
+
readline.on('close', () => {
|
|
275
|
+
console.log(colors.system('\n[stream ended]'));
|
|
276
|
+
resolve();
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
282
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
283
|
+
//
|
|
284
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
285
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
286
|
+
|
|
287
|
+
await main();
|