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.d.ts +313 -0
- package/dist/index.js +968 -0
- package/dist/index.js.map +1 -0
- package/dist/ui/assets/index-Bhvfo4aN.css +1 -0
- package/dist/ui/assets/index-NhZc47IQ.js +14 -0
- package/dist/ui/index.html +14 -0
- package/dist/ui/vite.svg +1 -0
- package/package.json +55 -0
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
|