cc-wiretap 1.0.0

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/dist/index.js ADDED
@@ -0,0 +1,968 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+ import chalk7 from "chalk";
6
+
7
+ // src/ca.ts
8
+ import { homedir } from "os";
9
+ import { join } from "path";
10
+ import { mkdir, readFile, writeFile, access, constants } from "fs/promises";
11
+ import { generateCACertificate } from "mockttp";
12
+ import chalk from "chalk";
13
+ var CA_DIR = join(homedir(), ".cc-wiretap");
14
+ var CA_CERT_PATH = join(CA_DIR, "ca.pem");
15
+ var CA_KEY_PATH = join(CA_DIR, "ca-key.pem");
16
+ async function fileExists(path) {
17
+ try {
18
+ await access(path, constants.F_OK);
19
+ return true;
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+ async function ensureCADirectory() {
25
+ try {
26
+ await mkdir(CA_DIR, { recursive: true });
27
+ } catch (error) {
28
+ if (error.code !== "EEXIST") {
29
+ throw error;
30
+ }
31
+ }
32
+ }
33
+ async function loadOrGenerateCA() {
34
+ await ensureCADirectory();
35
+ const certExists = await fileExists(CA_CERT_PATH);
36
+ const keyExists = await fileExists(CA_KEY_PATH);
37
+ if (certExists && keyExists) {
38
+ console.log(chalk.green("\u2713"), "Using existing CA certificate from", chalk.cyan(CA_DIR));
39
+ const cert2 = await readFile(CA_CERT_PATH, "utf-8");
40
+ const key2 = await readFile(CA_KEY_PATH, "utf-8");
41
+ return {
42
+ certPath: CA_CERT_PATH,
43
+ keyPath: CA_KEY_PATH,
44
+ cert: cert2,
45
+ key: key2
46
+ };
47
+ }
48
+ console.log(chalk.yellow("\u2699"), "Generating new CA certificate...");
49
+ const { cert, key } = await generateCACertificate({
50
+ commonName: "CC Wiretap CA",
51
+ organizationName: "CC Wiretap"
52
+ });
53
+ await writeFile(CA_CERT_PATH, cert);
54
+ await writeFile(CA_KEY_PATH, key);
55
+ console.log(chalk.green("\u2713"), "CA certificate generated at", chalk.cyan(CA_DIR));
56
+ console.log();
57
+ console.log(chalk.yellow("To trust the CA certificate, run:"));
58
+ console.log();
59
+ console.log(chalk.gray(" # macOS:"));
60
+ console.log(chalk.white(` sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${CA_CERT_PATH}"`));
61
+ console.log();
62
+ console.log(chalk.gray(" # Linux (Debian/Ubuntu):"));
63
+ console.log(chalk.white(` sudo cp "${CA_CERT_PATH}" /usr/local/share/ca-certificates/cc-wiretap.crt`));
64
+ console.log(chalk.white(" sudo update-ca-certificates"));
65
+ console.log();
66
+ console.log(chalk.gray(" # For Node.js/Claude Code, use:"));
67
+ console.log(chalk.white(` NODE_EXTRA_CA_CERTS="${CA_CERT_PATH}"`));
68
+ console.log();
69
+ return {
70
+ certPath: CA_CERT_PATH,
71
+ keyPath: CA_KEY_PATH,
72
+ cert,
73
+ key
74
+ };
75
+ }
76
+ function getCAPath() {
77
+ return CA_CERT_PATH;
78
+ }
79
+
80
+ // src/proxy.ts
81
+ import * as mockttp from "mockttp";
82
+ import chalk3 from "chalk";
83
+ import { gunzipSync, brotliDecompressSync } from "zlib";
84
+
85
+ // src/interceptor.ts
86
+ import { randomUUID } from "crypto";
87
+ import chalk2 from "chalk";
88
+
89
+ // src/parser.ts
90
+ function parseSSELine(line) {
91
+ if (!line.startsWith("data: ")) {
92
+ return null;
93
+ }
94
+ const jsonStr = line.slice(6).trim();
95
+ if (!jsonStr || jsonStr === "[DONE]") {
96
+ return null;
97
+ }
98
+ try {
99
+ return JSON.parse(jsonStr);
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+ function parseSSEChunk(chunk) {
105
+ const events = [];
106
+ const lines = chunk.split("\n");
107
+ for (const line of lines) {
108
+ const trimmed = line.trim();
109
+ if (trimmed.startsWith("data: ")) {
110
+ const event = parseSSELine(trimmed);
111
+ if (event) {
112
+ events.push(event);
113
+ }
114
+ }
115
+ }
116
+ return events;
117
+ }
118
+ var SSEStreamParser = class {
119
+ buffer = "";
120
+ events = [];
121
+ /**
122
+ * Feed data to the parser
123
+ */
124
+ feed(data) {
125
+ this.buffer += data;
126
+ const newEvents = [];
127
+ const parts = this.buffer.split("\n\n");
128
+ this.buffer = parts.pop() || "";
129
+ for (const part of parts) {
130
+ const lines = part.split("\n");
131
+ for (const line of lines) {
132
+ const trimmed = line.trim();
133
+ if (trimmed.startsWith("data: ")) {
134
+ const event = parseSSELine(trimmed);
135
+ if (event) {
136
+ newEvents.push(event);
137
+ this.events.push(event);
138
+ }
139
+ }
140
+ }
141
+ }
142
+ return newEvents;
143
+ }
144
+ /**
145
+ * Flush any remaining buffered data
146
+ */
147
+ flush() {
148
+ if (!this.buffer.trim()) {
149
+ return [];
150
+ }
151
+ const events = parseSSEChunk(this.buffer);
152
+ this.events.push(...events);
153
+ this.buffer = "";
154
+ return events;
155
+ }
156
+ /**
157
+ * Get all parsed events
158
+ */
159
+ getAllEvents() {
160
+ return [...this.events];
161
+ }
162
+ /**
163
+ * Reset the parser
164
+ */
165
+ reset() {
166
+ this.buffer = "";
167
+ this.events = [];
168
+ }
169
+ };
170
+ function reconstructResponseFromEvents(events) {
171
+ let messageStart = null;
172
+ let messageDelta = null;
173
+ const contentBlocks = /* @__PURE__ */ new Map();
174
+ const textDeltas = /* @__PURE__ */ new Map();
175
+ const jsonDeltas = /* @__PURE__ */ new Map();
176
+ for (const event of events) {
177
+ switch (event.type) {
178
+ case "message_start":
179
+ messageStart = event;
180
+ break;
181
+ case "content_block_start": {
182
+ const startEvent = event;
183
+ contentBlocks.set(startEvent.index, {
184
+ type: startEvent.content_block.type,
185
+ content: { ...startEvent.content_block }
186
+ });
187
+ if (startEvent.content_block.type === "text") {
188
+ textDeltas.set(startEvent.index, []);
189
+ } else if (startEvent.content_block.type === "tool_use") {
190
+ jsonDeltas.set(startEvent.index, []);
191
+ }
192
+ break;
193
+ }
194
+ case "content_block_delta": {
195
+ const deltaEvent = event;
196
+ if (deltaEvent.delta.type === "text_delta") {
197
+ const deltas = textDeltas.get(deltaEvent.index) || [];
198
+ deltas.push(deltaEvent.delta.text);
199
+ textDeltas.set(deltaEvent.index, deltas);
200
+ } else if (deltaEvent.delta.type === "input_json_delta") {
201
+ const deltas = jsonDeltas.get(deltaEvent.index) || [];
202
+ deltas.push(deltaEvent.delta.partial_json);
203
+ jsonDeltas.set(deltaEvent.index, deltas);
204
+ }
205
+ break;
206
+ }
207
+ case "message_delta":
208
+ messageDelta = event;
209
+ break;
210
+ }
211
+ }
212
+ if (!messageStart) {
213
+ return null;
214
+ }
215
+ const content = [];
216
+ const sortedIndices = Array.from(contentBlocks.keys()).sort((a, b) => a - b);
217
+ for (const index of sortedIndices) {
218
+ const block = contentBlocks.get(index);
219
+ if (block.type === "text") {
220
+ const text = (textDeltas.get(index) || []).join("");
221
+ content.push({
222
+ type: "text",
223
+ text
224
+ });
225
+ } else if (block.type === "tool_use") {
226
+ const jsonStr = (jsonDeltas.get(index) || []).join("");
227
+ let input = {};
228
+ try {
229
+ input = jsonStr ? JSON.parse(jsonStr) : {};
230
+ } catch {
231
+ }
232
+ content.push({
233
+ type: "tool_use",
234
+ id: block.content.id || "",
235
+ name: block.content.name || "",
236
+ input
237
+ });
238
+ }
239
+ }
240
+ const usage = {
241
+ input_tokens: messageStart.message.usage.input_tokens,
242
+ output_tokens: messageDelta?.usage.output_tokens || messageStart.message.usage.output_tokens,
243
+ cache_creation_input_tokens: messageStart.message.usage.cache_creation_input_tokens,
244
+ cache_read_input_tokens: messageStart.message.usage.cache_read_input_tokens
245
+ };
246
+ return {
247
+ id: messageStart.message.id,
248
+ type: "message",
249
+ role: "assistant",
250
+ content,
251
+ model: messageStart.message.model,
252
+ stop_reason: messageDelta?.delta.stop_reason || null,
253
+ stop_sequence: messageDelta?.delta.stop_sequence || null,
254
+ usage
255
+ };
256
+ }
257
+
258
+ // src/interceptor.ts
259
+ var CLAUDE_API_HOSTS = [
260
+ "api.anthropic.com",
261
+ "api.claude.ai"
262
+ ];
263
+ var CLAUDE_MESSAGES_PATH = "/v1/messages";
264
+ var ClaudeInterceptor = class {
265
+ wsServer;
266
+ activeRequests = /* @__PURE__ */ new Map();
267
+ constructor(wsServer) {
268
+ this.wsServer = wsServer;
269
+ }
270
+ isClaudeRequest(request) {
271
+ const host = request.headers.host || new URL(request.url).host;
272
+ const path = new URL(request.url).pathname;
273
+ return CLAUDE_API_HOSTS.some((h) => host.includes(h)) && path.includes(CLAUDE_MESSAGES_PATH) && request.method === "POST";
274
+ }
275
+ async handleRequest(request) {
276
+ if (!this.isClaudeRequest(request)) {
277
+ return null;
278
+ }
279
+ const requestId = randomUUID();
280
+ const timestamp = Date.now();
281
+ let requestBody;
282
+ try {
283
+ const bodyBuffer = request.body.buffer;
284
+ if (bodyBuffer.length > 0) {
285
+ const bodyText = bodyBuffer.toString("utf-8");
286
+ requestBody = JSON.parse(bodyText);
287
+ }
288
+ } catch (error) {
289
+ console.error(chalk2.yellow("\u26A0"), "Failed to parse request body:", error);
290
+ }
291
+ const intercepted = {
292
+ id: requestId,
293
+ timestamp,
294
+ method: request.method,
295
+ url: request.url,
296
+ requestHeaders: this.headersToRecord(request.headers),
297
+ requestBody,
298
+ sseEvents: []
299
+ };
300
+ this.activeRequests.set(requestId, {
301
+ request: intercepted,
302
+ parser: new SSEStreamParser()
303
+ });
304
+ this.wsServer.addRequest(intercepted);
305
+ this.wsServer.broadcast({
306
+ type: "request_start",
307
+ requestId,
308
+ timestamp,
309
+ method: request.method,
310
+ url: request.url,
311
+ headers: intercepted.requestHeaders
312
+ });
313
+ if (requestBody) {
314
+ this.wsServer.broadcast({
315
+ type: "request_body",
316
+ requestId,
317
+ body: requestBody
318
+ });
319
+ const model = requestBody.model || "unknown";
320
+ const messageCount = requestBody.messages?.length || 0;
321
+ const hasTools = requestBody.tools && requestBody.tools.length > 0;
322
+ const isStreaming = requestBody.stream === true;
323
+ console.log(
324
+ chalk2.cyan("\u2192"),
325
+ chalk2.white(`[${requestId.slice(0, 8)}]`),
326
+ chalk2.green(model),
327
+ `${messageCount} messages`,
328
+ hasTools ? chalk2.yellow(`+ ${requestBody.tools.length} tools`) : "",
329
+ isStreaming ? chalk2.magenta("streaming") : ""
330
+ );
331
+ }
332
+ return requestId;
333
+ }
334
+ async handleResponseStart(requestId, statusCode, headers) {
335
+ const active = this.activeRequests.get(requestId);
336
+ if (!active) {
337
+ return;
338
+ }
339
+ const timestamp = Date.now();
340
+ active.request.responseStartTime = timestamp;
341
+ active.request.statusCode = statusCode;
342
+ active.request.responseHeaders = headers;
343
+ this.wsServer.broadcast({
344
+ type: "response_start",
345
+ requestId,
346
+ timestamp,
347
+ statusCode,
348
+ headers
349
+ });
350
+ }
351
+ handleResponseChunk(requestId, chunk) {
352
+ const active = this.activeRequests.get(requestId);
353
+ if (!active) {
354
+ return;
355
+ }
356
+ const data = typeof chunk === "string" ? chunk : chunk.toString("utf-8");
357
+ const events = active.parser.feed(data);
358
+ for (const event of events) {
359
+ active.request.sseEvents.push(event);
360
+ this.wsServer.broadcast({
361
+ type: "response_chunk",
362
+ requestId,
363
+ event
364
+ });
365
+ if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
366
+ process.stdout.write(chalk2.gray("."));
367
+ }
368
+ }
369
+ }
370
+ async handleResponseComplete(requestId) {
371
+ const active = this.activeRequests.get(requestId);
372
+ if (!active) {
373
+ return;
374
+ }
375
+ const remainingEvents = active.parser.flush();
376
+ for (const event of remainingEvents) {
377
+ active.request.sseEvents.push(event);
378
+ this.wsServer.broadcast({
379
+ type: "response_chunk",
380
+ requestId,
381
+ event
382
+ });
383
+ }
384
+ const response = reconstructResponseFromEvents(active.request.sseEvents);
385
+ const timestamp = Date.now();
386
+ const durationMs = timestamp - active.request.timestamp;
387
+ active.request.response = response || void 0;
388
+ active.request.durationMs = durationMs;
389
+ if (response) {
390
+ this.wsServer.broadcast({
391
+ type: "response_complete",
392
+ requestId,
393
+ timestamp,
394
+ response,
395
+ durationMs
396
+ });
397
+ console.log();
398
+ console.log(
399
+ chalk2.green("\u2713"),
400
+ chalk2.white(`[${requestId.slice(0, 8)}]`),
401
+ `${response.usage.input_tokens} in / ${response.usage.output_tokens} out`,
402
+ chalk2.gray(`(${durationMs}ms)`),
403
+ response.stop_reason === "tool_use" ? chalk2.yellow("\u2192 tool_use") : ""
404
+ );
405
+ }
406
+ this.activeRequests.delete(requestId);
407
+ }
408
+ handleResponseError(requestId, error) {
409
+ const active = this.activeRequests.get(requestId);
410
+ if (!active) {
411
+ return;
412
+ }
413
+ active.request.error = error.message;
414
+ this.wsServer.broadcast({
415
+ type: "error",
416
+ requestId,
417
+ error: error.message,
418
+ timestamp: Date.now()
419
+ });
420
+ console.log(
421
+ chalk2.red("\u2717"),
422
+ chalk2.white(`[${requestId.slice(0, 8)}]`),
423
+ error.message
424
+ );
425
+ this.activeRequests.delete(requestId);
426
+ }
427
+ handleNonStreamingResponse(requestId, _statusCode, bodyText) {
428
+ const active = this.activeRequests.get(requestId);
429
+ if (!active) {
430
+ return;
431
+ }
432
+ try {
433
+ if (bodyText) {
434
+ const claudeResponse = JSON.parse(bodyText);
435
+ const timestamp = Date.now();
436
+ const durationMs = timestamp - active.request.timestamp;
437
+ active.request.response = claudeResponse;
438
+ active.request.durationMs = durationMs;
439
+ this.wsServer.broadcast({
440
+ type: "response_complete",
441
+ requestId,
442
+ timestamp,
443
+ response: claudeResponse,
444
+ durationMs
445
+ });
446
+ if (claudeResponse.type === "message") {
447
+ console.log(
448
+ chalk2.green("\u2713"),
449
+ chalk2.white(`[${requestId.slice(0, 8)}]`),
450
+ `${claudeResponse.usage.input_tokens} in / ${claudeResponse.usage.output_tokens} out`,
451
+ chalk2.gray(`(${durationMs}ms)`),
452
+ claudeResponse.stop_reason === "tool_use" ? chalk2.yellow("\u2192 tool_use") : ""
453
+ );
454
+ } else if (claudeResponse.type === "error") {
455
+ console.log(
456
+ chalk2.yellow("\u26A0"),
457
+ chalk2.white(`[${requestId.slice(0, 8)}]`),
458
+ chalk2.red(claudeResponse.error.message),
459
+ chalk2.gray(`(${durationMs}ms)`)
460
+ );
461
+ }
462
+ }
463
+ } catch (error) {
464
+ console.error(chalk2.yellow("\u26A0"), "Failed to parse response body:", error);
465
+ }
466
+ this.activeRequests.delete(requestId);
467
+ }
468
+ headersToRecord(headers) {
469
+ const result = {};
470
+ for (const [key, value] of Object.entries(headers)) {
471
+ if (value !== void 0) {
472
+ result[key] = Array.isArray(value) ? value.join(", ") : value;
473
+ }
474
+ }
475
+ return result;
476
+ }
477
+ getActiveRequestCount() {
478
+ return this.activeRequests.size;
479
+ }
480
+ };
481
+
482
+ // src/proxy.ts
483
+ function decompressBody(buffer, contentEncoding) {
484
+ if (!buffer.length) return "";
485
+ try {
486
+ if (contentEncoding === "gzip") {
487
+ return gunzipSync(buffer).toString("utf-8");
488
+ }
489
+ if (contentEncoding === "br") {
490
+ return brotliDecompressSync(buffer).toString("utf-8");
491
+ }
492
+ } catch {
493
+ }
494
+ return buffer.toString("utf-8");
495
+ }
496
+ function isAnthropicHost(url) {
497
+ try {
498
+ const host = new URL(url).host;
499
+ return CLAUDE_API_HOSTS.some((h) => host.includes(h));
500
+ } catch {
501
+ return false;
502
+ }
503
+ }
504
+ async function createProxy(options) {
505
+ const { port, ca, wsServer } = options;
506
+ const server = mockttp.getLocal({
507
+ https: {
508
+ cert: ca.cert,
509
+ key: ca.key
510
+ }
511
+ });
512
+ const interceptor = new ClaudeInterceptor(wsServer);
513
+ const requestIds = /* @__PURE__ */ new Map();
514
+ await server.forAnyRequest().thenPassThrough({
515
+ beforeRequest: async (request) => {
516
+ if (!isAnthropicHost(request.url)) {
517
+ return {};
518
+ }
519
+ const requestId = await interceptor.handleRequest(request);
520
+ if (requestId) {
521
+ requestIds.set(request.id, requestId);
522
+ }
523
+ return {};
524
+ },
525
+ beforeResponse: async (response) => {
526
+ const requestId = requestIds.get(response.id);
527
+ if (!requestId) {
528
+ return {};
529
+ }
530
+ await interceptor.handleResponseStart(requestId, response.statusCode, response.headers);
531
+ const contentType = response.headers["content-type"] || "";
532
+ const isStreaming = contentType.includes("text/event-stream");
533
+ if (isStreaming) {
534
+ const bodyBuffer = response.body.buffer;
535
+ if (bodyBuffer.length > 0) {
536
+ const bodyText = bodyBuffer.toString("utf-8");
537
+ interceptor.handleResponseChunk(requestId, bodyText);
538
+ }
539
+ await interceptor.handleResponseComplete(requestId);
540
+ } else {
541
+ const bodyBuffer = response.body.buffer;
542
+ const contentEncoding = response.headers["content-encoding"];
543
+ const bodyText = decompressBody(bodyBuffer, contentEncoding);
544
+ interceptor.handleNonStreamingResponse(requestId, response.statusCode, bodyText);
545
+ }
546
+ requestIds.delete(response.id);
547
+ return {};
548
+ }
549
+ });
550
+ await server.start(port);
551
+ console.log(chalk3.green("\u2713"), `Proxy server started on port ${chalk3.cyan(port)}`);
552
+ console.log(chalk3.gray(" Intercepting:"), CLAUDE_API_HOSTS.join(", "));
553
+ console.log(chalk3.gray(" All other traffic: transparent passthrough"));
554
+ return {
555
+ server,
556
+ interceptor,
557
+ stop: async () => {
558
+ await server.stop();
559
+ console.log(chalk3.gray("\u25CB"), "Proxy server stopped");
560
+ }
561
+ };
562
+ }
563
+
564
+ // src/websocket.ts
565
+ import { WebSocketServer, WebSocket } from "ws";
566
+ import chalk4 from "chalk";
567
+ var WiretapWebSocketServer = class {
568
+ wss;
569
+ clients = /* @__PURE__ */ new Set();
570
+ requests = /* @__PURE__ */ new Map();
571
+ constructor(options = {}) {
572
+ if (options.server) {
573
+ this.wss = new WebSocketServer({ server: options.server });
574
+ } else {
575
+ this.wss = new WebSocketServer({ port: options.port || 8081 });
576
+ }
577
+ this.wss.on("connection", (ws, req) => {
578
+ const clientIp = req.socket.remoteAddress || "unknown";
579
+ console.log(chalk4.blue("\u2B24"), `UI client connected from ${clientIp}`);
580
+ this.clients.add(ws);
581
+ this.sendCurrentState(ws);
582
+ ws.on("message", (data) => {
583
+ try {
584
+ const message = JSON.parse(data.toString());
585
+ if (message.type === "clear_all") {
586
+ console.log(chalk4.yellow("\u27F2"), "Clearing all requests");
587
+ this.requests.clear();
588
+ this.broadcast({ type: "clear_all" });
589
+ }
590
+ } catch (error) {
591
+ console.error(chalk4.red("\u2717"), `Failed to parse client message: ${error}`);
592
+ }
593
+ });
594
+ ws.on("close", () => {
595
+ console.log(chalk4.gray("\u25CB"), `UI client disconnected from ${clientIp}`);
596
+ this.clients.delete(ws);
597
+ });
598
+ ws.on("error", (error) => {
599
+ console.error(chalk4.red("\u2717"), `WebSocket error: ${error.message}`);
600
+ this.clients.delete(ws);
601
+ });
602
+ });
603
+ this.wss.on("error", (error) => {
604
+ console.error(chalk4.red("\u2717"), `WebSocket server error: ${error.message}`);
605
+ });
606
+ }
607
+ sendCurrentState(ws) {
608
+ if (this.requests.size > 0) {
609
+ this.sendToClient(ws, {
610
+ type: "history_sync",
611
+ requests: Array.from(this.requests.values())
612
+ });
613
+ }
614
+ }
615
+ sendToClient(ws, message) {
616
+ if (ws.readyState === WebSocket.OPEN) {
617
+ ws.send(JSON.stringify(message));
618
+ }
619
+ }
620
+ broadcast(message) {
621
+ const data = JSON.stringify(message);
622
+ for (const client of this.clients) {
623
+ if (client.readyState === WebSocket.OPEN) {
624
+ client.send(data);
625
+ }
626
+ }
627
+ }
628
+ // Request management
629
+ addRequest(request) {
630
+ this.requests.set(request.id, request);
631
+ }
632
+ getRequest(requestId) {
633
+ return this.requests.get(requestId);
634
+ }
635
+ // Stats
636
+ getClientCount() {
637
+ return this.clients.size;
638
+ }
639
+ getRequestCount() {
640
+ return this.requests.size;
641
+ }
642
+ // Lifecycle
643
+ close() {
644
+ return new Promise((resolve, reject) => {
645
+ for (const client of this.clients) {
646
+ client.close();
647
+ }
648
+ this.clients.clear();
649
+ this.wss.close((err) => {
650
+ if (err) {
651
+ reject(err);
652
+ } else {
653
+ resolve();
654
+ }
655
+ });
656
+ });
657
+ }
658
+ getPort() {
659
+ const address = this.wss.address();
660
+ if (address && typeof address === "object") {
661
+ return address.port;
662
+ }
663
+ return void 0;
664
+ }
665
+ };
666
+
667
+ // src/setup-server.ts
668
+ import { createServer } from "http";
669
+ import chalk5 from "chalk";
670
+ var SETUP_PORT = 8082;
671
+ function generateSetupScript(proxyPort) {
672
+ const caPath = getCAPath();
673
+ return `#!/bin/bash
674
+ # CC Wiretap - Terminal Setup Script
675
+ # This script configures your terminal session to route traffic through the proxy
676
+
677
+ # Proxy settings (for most HTTP clients)
678
+ export HTTP_PROXY="http://localhost:${proxyPort}"
679
+ export HTTPS_PROXY="http://localhost:${proxyPort}"
680
+ export http_proxy="http://localhost:${proxyPort}"
681
+ export https_proxy="http://localhost:${proxyPort}"
682
+
683
+ # Node.js CA certificate
684
+ export NODE_EXTRA_CA_CERTS="${caPath}"
685
+
686
+ # Python/OpenSSL CA certificates
687
+ export SSL_CERT_FILE="${caPath}"
688
+ export REQUESTS_CA_BUNDLE="${caPath}"
689
+
690
+ # curl CA certificate
691
+ export CURL_CA_BUNDLE="${caPath}"
692
+
693
+ # Ruby CA certificate
694
+ export SSL_CERT_DIR=""
695
+
696
+ # Git CA certificate (for HTTPS remotes)
697
+ export GIT_SSL_CAINFO="${caPath}"
698
+
699
+ # AWS CLI
700
+ export AWS_CA_BUNDLE="${caPath}"
701
+
702
+ # Disable proxy for localhost (prevents loops)
703
+ export NO_PROXY="localhost,127.0.0.1,::1"
704
+ export no_proxy="localhost,127.0.0.1,::1"
705
+
706
+ # Visual indicator that proxy is active
707
+ export WIRETAP_ACTIVE="1"
708
+
709
+ # Update PS1 to show proxy is active (optional - uncomment if desired)
710
+ # export PS1="[wiretap] $PS1"
711
+
712
+ echo ""
713
+ echo " \u2713 CC Wiretap proxy configured for this terminal"
714
+ echo ""
715
+ echo " Proxy: http://localhost:${proxyPort}"
716
+ echo " CA: ${caPath}"
717
+ echo ""
718
+ echo " All HTTP/HTTPS traffic from this terminal will be intercepted."
719
+ echo " Run 'unset-wiretap' to disable."
720
+ echo ""
721
+
722
+ # Create unset function
723
+ unset-wiretap() {
724
+ unset HTTP_PROXY HTTPS_PROXY http_proxy https_proxy
725
+ unset NODE_EXTRA_CA_CERTS SSL_CERT_FILE REQUESTS_CA_BUNDLE
726
+ unset CURL_CA_BUNDLE SSL_CERT_DIR GIT_SSL_CAINFO AWS_CA_BUNDLE
727
+ unset NO_PROXY no_proxy WIRETAP_ACTIVE
728
+ echo "\u2713 Wiretap proxy disabled for this terminal"
729
+ }
730
+ export -f unset-wiretap 2>/dev/null || true
731
+ `;
732
+ }
733
+ function generateFishScript(proxyPort) {
734
+ const caPath = getCAPath();
735
+ return `# CC Wiretap - Fish Shell Setup Script
736
+
737
+ set -gx HTTP_PROXY "http://localhost:${proxyPort}"
738
+ set -gx HTTPS_PROXY "http://localhost:${proxyPort}"
739
+ set -gx http_proxy "http://localhost:${proxyPort}"
740
+ set -gx https_proxy "http://localhost:${proxyPort}"
741
+ set -gx NODE_EXTRA_CA_CERTS "${caPath}"
742
+ set -gx SSL_CERT_FILE "${caPath}"
743
+ set -gx REQUESTS_CA_BUNDLE "${caPath}"
744
+ set -gx CURL_CA_BUNDLE "${caPath}"
745
+ set -gx GIT_SSL_CAINFO "${caPath}"
746
+ set -gx AWS_CA_BUNDLE "${caPath}"
747
+ set -gx NO_PROXY "localhost,127.0.0.1,::1"
748
+ set -gx no_proxy "localhost,127.0.0.1,::1"
749
+ set -gx WIRETAP_ACTIVE "1"
750
+
751
+ echo ""
752
+ echo " \u2713 CC Wiretap proxy configured for this terminal"
753
+ echo ""
754
+ echo " Proxy: http://localhost:${proxyPort}"
755
+ echo " CA: ${caPath}"
756
+ echo ""
757
+
758
+ function unset-wiretap
759
+ set -e HTTP_PROXY HTTPS_PROXY http_proxy https_proxy
760
+ set -e NODE_EXTRA_CA_CERTS SSL_CERT_FILE REQUESTS_CA_BUNDLE
761
+ set -e CURL_CA_BUNDLE GIT_SSL_CAINFO AWS_CA_BUNDLE
762
+ set -e NO_PROXY no_proxy WIRETAP_ACTIVE
763
+ echo "\u2713 Wiretap proxy disabled"
764
+ end
765
+ `;
766
+ }
767
+ function createSetupServer(proxyPort) {
768
+ const server = createServer((req, res) => {
769
+ const url = new URL(req.url || "/", `http://localhost:${SETUP_PORT}`);
770
+ res.setHeader("Access-Control-Allow-Origin", "*");
771
+ res.setHeader("Access-Control-Allow-Methods", "GET");
772
+ if (url.pathname === "/" || url.pathname === "/setup") {
773
+ const shell = url.searchParams.get("shell") || "bash";
774
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
775
+ if (shell === "fish") {
776
+ res.end(generateFishScript(proxyPort));
777
+ } else {
778
+ res.end(generateSetupScript(proxyPort));
779
+ }
780
+ } else if (url.pathname === "/status") {
781
+ res.setHeader("Content-Type", "application/json");
782
+ res.end(JSON.stringify({
783
+ active: true,
784
+ proxyPort,
785
+ caPath: getCAPath()
786
+ }));
787
+ } else {
788
+ res.statusCode = 404;
789
+ res.end("Not found");
790
+ }
791
+ });
792
+ server.listen(SETUP_PORT, () => {
793
+ console.log(chalk5.green("\u2713"), `Setup server started on port ${chalk5.cyan(SETUP_PORT)}`);
794
+ });
795
+ return server;
796
+ }
797
+ function getSetupCommand() {
798
+ return `eval "$(curl -s http://localhost:${SETUP_PORT}/setup)"`;
799
+ }
800
+
801
+ // src/ui-server.ts
802
+ import { createServer as createServer2 } from "http";
803
+ import { createReadStream, existsSync, statSync } from "fs";
804
+ import { join as join2, extname } from "path";
805
+ import { fileURLToPath } from "url";
806
+ import chalk6 from "chalk";
807
+ var __dirname2 = fileURLToPath(new URL(".", import.meta.url));
808
+ var MIME_TYPES = {
809
+ ".html": "text/html; charset=utf-8",
810
+ ".js": "application/javascript; charset=utf-8",
811
+ ".mjs": "application/javascript; charset=utf-8",
812
+ ".css": "text/css; charset=utf-8",
813
+ ".json": "application/json; charset=utf-8",
814
+ ".png": "image/png",
815
+ ".jpg": "image/jpeg",
816
+ ".jpeg": "image/jpeg",
817
+ ".gif": "image/gif",
818
+ ".svg": "image/svg+xml",
819
+ ".ico": "image/x-icon",
820
+ ".woff": "font/woff",
821
+ ".woff2": "font/woff2",
822
+ ".ttf": "font/ttf",
823
+ ".eot": "application/vnd.ms-fontobject"
824
+ };
825
+ function getUIDistPath() {
826
+ return join2(__dirname2, "ui");
827
+ }
828
+ function serveFile(res, filePath) {
829
+ const ext = extname(filePath).toLowerCase();
830
+ const contentType = MIME_TYPES[ext] || "application/octet-stream";
831
+ res.setHeader("Content-Type", contentType);
832
+ res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
833
+ const stream = createReadStream(filePath);
834
+ stream.pipe(res);
835
+ stream.on("error", () => {
836
+ res.statusCode = 500;
837
+ res.end("Internal Server Error");
838
+ });
839
+ }
840
+ function handleRequest(req, res, uiPath) {
841
+ const url = new URL(req.url || "/", "http://localhost");
842
+ let pathname = url.pathname;
843
+ let relativePath = decodeURIComponent(pathname.slice(1));
844
+ if (relativePath === "" || relativePath === "/") {
845
+ relativePath = "index.html";
846
+ }
847
+ const filePath = join2(uiPath, relativePath);
848
+ if (!filePath.startsWith(uiPath)) {
849
+ res.statusCode = 403;
850
+ res.end("Forbidden");
851
+ return;
852
+ }
853
+ if (existsSync(filePath) && statSync(filePath).isFile()) {
854
+ serveFile(res, filePath);
855
+ return;
856
+ }
857
+ const indexPath = join2(uiPath, "index.html");
858
+ if (existsSync(indexPath)) {
859
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
860
+ res.setHeader("Cache-Control", "no-cache");
861
+ createReadStream(indexPath).pipe(res);
862
+ return;
863
+ }
864
+ res.statusCode = 404;
865
+ res.end("Not Found");
866
+ }
867
+ function createUIServer(options) {
868
+ const { port } = options;
869
+ const uiPath = getUIDistPath();
870
+ if (!existsSync(uiPath) || !existsSync(join2(uiPath, "index.html"))) {
871
+ console.log(chalk6.yellow("!"), "UI not bundled. Run in dev mode or build first.");
872
+ return null;
873
+ }
874
+ const server = createServer2((req, res) => {
875
+ res.setHeader("Access-Control-Allow-Origin", "*");
876
+ res.setHeader("Access-Control-Allow-Methods", "GET");
877
+ handleRequest(req, res, uiPath);
878
+ });
879
+ server.listen(port, () => {
880
+ console.log(chalk6.green("\u2713"), `UI server started on port ${chalk6.cyan(port)}`);
881
+ });
882
+ return server;
883
+ }
884
+
885
+ // src/index.ts
886
+ var VERSION = "1.0.0";
887
+ var BANNER = `
888
+ ${chalk7.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557")}
889
+ ${chalk7.cyan("\u2551")} ${chalk7.cyan("\u2551")}
890
+ ${chalk7.cyan("\u2551")} ${chalk7.bold.white("CC Wiretap")} ${chalk7.gray("v" + VERSION)} ${chalk7.cyan("\u2551")}
891
+ ${chalk7.cyan("\u2551")} ${chalk7.gray("HTTP/HTTPS proxy for Claude Code traffic inspection")} ${chalk7.cyan("\u2551")}
892
+ ${chalk7.cyan("\u2551")} ${chalk7.cyan("\u2551")}
893
+ ${chalk7.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D")}
894
+ `;
895
+ async function main() {
896
+ const program = new Command();
897
+ program.name("cc-wiretap").description("HTTP/HTTPS proxy for intercepting and visualizing Claude Code traffic").version(VERSION).option("-p, --port <port>", "Proxy server port", "8080").option("-w, --ws-port <port>", "WebSocket server port for UI", "8081").option("-u, --ui-port <port>", "UI dashboard server port", "3000").option("-q, --quiet", "Suppress banner and verbose output", false).action(async (options) => {
898
+ if (!options.quiet) {
899
+ console.log(BANNER);
900
+ }
901
+ const proxyPort = parseInt(options.port, 10);
902
+ const wsPort = parseInt(options.wsPort, 10);
903
+ const uiPort = parseInt(options.uiPort, 10);
904
+ try {
905
+ const ca = await loadOrGenerateCA();
906
+ const wsServer = new WiretapWebSocketServer({ port: wsPort });
907
+ console.log(chalk7.green("\u2713"), `WebSocket server started on port ${chalk7.cyan(wsPort)}`);
908
+ const proxy = await createProxy({
909
+ port: proxyPort,
910
+ ca,
911
+ wsServer
912
+ });
913
+ const setupServer = createSetupServer(proxyPort);
914
+ const uiServer = createUIServer({ port: uiPort });
915
+ console.log();
916
+ console.log(chalk7.white("Ready to intercept Claude API traffic."));
917
+ console.log();
918
+ console.log(chalk7.yellow.bold("Quick setup - run this in any terminal:"));
919
+ console.log();
920
+ console.log(chalk7.white.bold(` ${getSetupCommand()}`));
921
+ console.log();
922
+ console.log(chalk7.gray("Or manually:"));
923
+ console.log();
924
+ console.log(chalk7.gray(` NODE_EXTRA_CA_CERTS="${getCAPath()}" \\`));
925
+ console.log(chalk7.gray(` HTTPS_PROXY=http://localhost:${proxyPort} \\`));
926
+ console.log(chalk7.gray(" claude"));
927
+ console.log();
928
+ console.log(chalk7.gray("UI:"), chalk7.cyan(`http://localhost:${uiPort}`));
929
+ console.log();
930
+ console.log(chalk7.gray("\u2500".repeat(60)));
931
+ console.log();
932
+ const shutdown = async () => {
933
+ console.log();
934
+ console.log(chalk7.yellow("Shutting down..."));
935
+ await proxy.stop();
936
+ await wsServer.close();
937
+ setupServer.close();
938
+ uiServer?.close();
939
+ process.exit(0);
940
+ };
941
+ process.on("SIGINT", shutdown);
942
+ process.on("SIGTERM", shutdown);
943
+ } catch (error) {
944
+ console.error(chalk7.red("\u2717"), "Failed to start:", error);
945
+ process.exit(1);
946
+ }
947
+ });
948
+ program.parse();
949
+ }
950
+ main().catch((error) => {
951
+ console.error(chalk7.red("Fatal error:"), error);
952
+ process.exit(1);
953
+ });
954
+ export {
955
+ CLAUDE_API_HOSTS,
956
+ ClaudeInterceptor,
957
+ SSEStreamParser,
958
+ WiretapWebSocketServer,
959
+ createProxy,
960
+ createSetupServer,
961
+ createUIServer,
962
+ getCAPath,
963
+ getSetupCommand,
964
+ loadOrGenerateCA,
965
+ parseSSEChunk,
966
+ reconstructResponseFromEvents
967
+ };
968
+ //# sourceMappingURL=index.js.map