@myskyline_ai/ccdebug 0.2.1
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/LICENSE +201 -0
- package/README.md +129 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +674 -0
- package/dist/html-generator.d.ts +24 -0
- package/dist/html-generator.d.ts.map +1 -0
- package/dist/html-generator.js +141 -0
- package/dist/index-generator.d.ts +29 -0
- package/dist/index-generator.d.ts.map +1 -0
- package/dist/index-generator.js +271 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/interceptor-loader.js +59 -0
- package/dist/interceptor.d.ts +46 -0
- package/dist/interceptor.d.ts.map +1 -0
- package/dist/interceptor.js +555 -0
- package/dist/log-file-manager.d.ts +15 -0
- package/dist/log-file-manager.d.ts.map +1 -0
- package/dist/log-file-manager.js +41 -0
- package/dist/shared-conversation-processor.d.ts +114 -0
- package/dist/shared-conversation-processor.d.ts.map +1 -0
- package/dist/shared-conversation-processor.js +663 -0
- package/dist/token-extractor.js +28 -0
- package/dist/types.d.ts +95 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/frontend/dist/index.global.js +1522 -0
- package/frontend/dist/styles.css +985 -0
- package/frontend/template.html +19 -0
- package/package.json +83 -0
- package/web/debug.html +14 -0
- package/web/dist/assets/index-BIP9r3RA.js +48 -0
- package/web/dist/assets/index-BIP9r3RA.js.map +1 -0
- package/web/dist/assets/index-De3gn-G-.css +1 -0
- package/web/dist/favicon.svg +4 -0
- package/web/dist/index.html +15 -0
- package/web/index.html +14 -0
- package/web/package.json +47 -0
- package/web/server/conversation-parser.d.ts +47 -0
- package/web/server/conversation-parser.d.ts.map +1 -0
- package/web/server/conversation-parser.js +564 -0
- package/web/server/conversation-parser.js.map +1 -0
- package/web/server/index.d.ts +16 -0
- package/web/server/index.d.ts.map +1 -0
- package/web/server/index.js +60 -0
- package/web/server/index.js.map +1 -0
- package/web/server/log-file-manager.d.ts +98 -0
- package/web/server/log-file-manager.d.ts.map +1 -0
- package/web/server/log-file-manager.js +512 -0
- package/web/server/log-file-manager.js.map +1 -0
- package/web/server/src/types/index.d.ts +68 -0
- package/web/server/src/types/index.d.ts.map +1 -0
- package/web/server/src/types/index.js +3 -0
- package/web/server/src/types/index.js.map +1 -0
- package/web/server/test-path.js +48 -0
- package/web/server/web-server.d.ts +41 -0
- package/web/server/web-server.d.ts.map +1 -0
- package/web/server/web-server.js +807 -0
- package/web/server/web-server.js.map +1 -0
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ClaudeTrafficLogger = void 0;
|
|
7
|
+
exports.initializeInterceptor = initializeInterceptor;
|
|
8
|
+
exports.getLogger = getLogger;
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const html_generator_1 = require("./html-generator");
|
|
12
|
+
const log_file_manager_1 = require("./log-file-manager");
|
|
13
|
+
class ClaudeTrafficLogger {
|
|
14
|
+
constructor(config = {}) {
|
|
15
|
+
this.pendingRequests = new Map();
|
|
16
|
+
this.pairs = [];
|
|
17
|
+
this.config = {
|
|
18
|
+
logDirectory: ".claude-trace",
|
|
19
|
+
enableRealTimeHTML: false,
|
|
20
|
+
logLevel: "info",
|
|
21
|
+
...config,
|
|
22
|
+
};
|
|
23
|
+
//创建.claude-trace目录
|
|
24
|
+
this.traceHomeDir = this.config.logDirectory;
|
|
25
|
+
if (!fs_1.default.existsSync(this.traceHomeDir)) {
|
|
26
|
+
fs_1.default.mkdirSync(this.traceHomeDir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
//创建tracelog目录
|
|
29
|
+
this.traceLogDir = path_1.default.join(this.traceHomeDir, 'tracelog');
|
|
30
|
+
if (!fs_1.default.existsSync(this.traceLogDir)) {
|
|
31
|
+
fs_1.default.mkdirSync(this.traceLogDir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
// 创建cclog目录
|
|
34
|
+
this.ccLogDir = path_1.default.join(this.traceHomeDir, 'cclog');
|
|
35
|
+
if (!fs_1.default.existsSync(this.ccLogDir)) {
|
|
36
|
+
fs_1.default.mkdirSync(this.ccLogDir, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
this.ccLogFile = '';
|
|
39
|
+
// Generate filenames based on custom name or timestamp
|
|
40
|
+
const logBaseName = config?.logBaseName || process.env.CLAUDE_TRACE_LOG_NAME;
|
|
41
|
+
const fileBaseName = logBaseName || `log-${new Date().toISOString().replace(/[:.]/g, "-").replace("T", "-").slice(0, -5)}`; // Remove milliseconds and Z
|
|
42
|
+
this.traceLogFile = path_1.default.join(this.traceLogDir, `${fileBaseName}.jsonl`);
|
|
43
|
+
this.htmlFile = path_1.default.join(this.traceLogDir, `${fileBaseName}.html`);
|
|
44
|
+
// Initialize HTML generator
|
|
45
|
+
this.htmlGenerator = new html_generator_1.HTMLGenerator();
|
|
46
|
+
// Clear log file
|
|
47
|
+
fs_1.default.writeFileSync(this.traceLogFile, "");
|
|
48
|
+
// Output the actual filenames with absolute paths
|
|
49
|
+
console.log(`Logs will be written to:`);
|
|
50
|
+
console.log(` JSONL: ${path_1.default.resolve(this.traceLogFile)}`);
|
|
51
|
+
console.log(` HTML: ${path_1.default.resolve(this.htmlFile)}`);
|
|
52
|
+
}
|
|
53
|
+
isClaudeAPI(url) {
|
|
54
|
+
const urlString = typeof url === "string" ? url : url.toString();
|
|
55
|
+
const includeAllRequests = process.env.CLAUDE_TRACE_INCLUDE_ALL_REQUESTS === "true";
|
|
56
|
+
// Support custom ANTHROPIC_BASE_URL
|
|
57
|
+
const baseUrl = process.env.ANTHROPIC_BASE_URL || "https://api.anthropic.com";
|
|
58
|
+
const apiHost = new URL(baseUrl).hostname;
|
|
59
|
+
// Check for direct Anthropic API calls
|
|
60
|
+
const isAnthropicAPI = urlString.includes(apiHost);
|
|
61
|
+
// Check for AWS Bedrock Claude API calls
|
|
62
|
+
const isBedrockAPI = urlString.includes("bedrock-runtime.") && urlString.includes(".amazonaws.com");
|
|
63
|
+
if (includeAllRequests) {
|
|
64
|
+
return isAnthropicAPI || isBedrockAPI; // Capture all Claude API requests
|
|
65
|
+
}
|
|
66
|
+
return (isAnthropicAPI && urlString.includes("/v1/messages")) || isBedrockAPI;
|
|
67
|
+
}
|
|
68
|
+
generateRequestId() {
|
|
69
|
+
return `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
70
|
+
}
|
|
71
|
+
redactSensitiveHeaders(headers) {
|
|
72
|
+
const redactedHeaders = { ...headers };
|
|
73
|
+
const sensitiveKeys = [
|
|
74
|
+
"authorization",
|
|
75
|
+
"x-api-key",
|
|
76
|
+
"x-auth-token",
|
|
77
|
+
"cookie",
|
|
78
|
+
"set-cookie",
|
|
79
|
+
"x-session-token",
|
|
80
|
+
"x-access-token",
|
|
81
|
+
"bearer",
|
|
82
|
+
"proxy-authorization",
|
|
83
|
+
];
|
|
84
|
+
for (const key of Object.keys(redactedHeaders)) {
|
|
85
|
+
const lowerKey = key.toLowerCase();
|
|
86
|
+
if (sensitiveKeys.some((sensitive) => lowerKey.includes(sensitive))) {
|
|
87
|
+
// Keep first 10 chars and last 4 chars, redact middle
|
|
88
|
+
const value = redactedHeaders[key];
|
|
89
|
+
if (value && value.length > 14) {
|
|
90
|
+
redactedHeaders[key] = `${value.substring(0, 10)}...${value.slice(-4)}`;
|
|
91
|
+
}
|
|
92
|
+
else if (value && value.length > 4) {
|
|
93
|
+
redactedHeaders[key] = `${value.substring(0, 2)}...${value.slice(-2)}`;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
redactedHeaders[key] = "[REDACTED]";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return redactedHeaders;
|
|
101
|
+
}
|
|
102
|
+
async cloneResponse(response) {
|
|
103
|
+
// Clone the response to avoid consuming the body
|
|
104
|
+
return response.clone();
|
|
105
|
+
}
|
|
106
|
+
async parseRequestBody(body) {
|
|
107
|
+
if (!body)
|
|
108
|
+
return null;
|
|
109
|
+
if (typeof body === "string") {
|
|
110
|
+
try {
|
|
111
|
+
return JSON.parse(body);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return body;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (body instanceof FormData) {
|
|
118
|
+
const formObject = {};
|
|
119
|
+
for (const [key, value] of body.entries()) {
|
|
120
|
+
formObject[key] = value;
|
|
121
|
+
}
|
|
122
|
+
return formObject;
|
|
123
|
+
}
|
|
124
|
+
return body;
|
|
125
|
+
}
|
|
126
|
+
async parseResponseBody(response) {
|
|
127
|
+
const contentType = response.headers.get("content-type") || "";
|
|
128
|
+
try {
|
|
129
|
+
if (contentType.includes("application/json")) {
|
|
130
|
+
const body = await response.json();
|
|
131
|
+
return { body };
|
|
132
|
+
}
|
|
133
|
+
else if (contentType.includes("text/event-stream")) {
|
|
134
|
+
const body_raw = await response.text();
|
|
135
|
+
return { body_raw };
|
|
136
|
+
}
|
|
137
|
+
else if (contentType.includes("text/")) {
|
|
138
|
+
const body_raw = await response.text();
|
|
139
|
+
return { body_raw };
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
// For other types, try to read as text
|
|
143
|
+
const body_raw = await response.text();
|
|
144
|
+
return { body_raw };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
// Silent error handling during runtime
|
|
149
|
+
return {};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
instrumentAll() {
|
|
153
|
+
this.instrumentFetch();
|
|
154
|
+
this.instrumentNodeHTTP();
|
|
155
|
+
}
|
|
156
|
+
instrumentFetch() {
|
|
157
|
+
if (!global.fetch) {
|
|
158
|
+
// Silent - fetch not available
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// Check if already instrumented by checking for our marker
|
|
162
|
+
if (global.fetch.__claudeTraceInstrumented) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const originalFetch = global.fetch;
|
|
166
|
+
const logger = this;
|
|
167
|
+
global.fetch = async function (input, init = {}) {
|
|
168
|
+
// Convert input to URL for consistency
|
|
169
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
170
|
+
// Only intercept Claude API calls
|
|
171
|
+
if (!logger.isClaudeAPI(url)) {
|
|
172
|
+
return originalFetch(input, init);
|
|
173
|
+
}
|
|
174
|
+
const requestId = logger.generateRequestId();
|
|
175
|
+
const requestTimestamp = Date.now();
|
|
176
|
+
// Capture request details
|
|
177
|
+
const requestData = {
|
|
178
|
+
timestamp: requestTimestamp / 1000, // Convert to seconds (like Python version)
|
|
179
|
+
method: init.method || "GET",
|
|
180
|
+
url: url,
|
|
181
|
+
headers: logger.redactSensitiveHeaders(Object.fromEntries(new Headers(init.headers || {}).entries())),
|
|
182
|
+
body: await logger.parseRequestBody(init.body),
|
|
183
|
+
};
|
|
184
|
+
// Store pending request
|
|
185
|
+
logger.pendingRequests.set(requestId, requestData);
|
|
186
|
+
try {
|
|
187
|
+
// Make the actual request
|
|
188
|
+
const response = await originalFetch(input, init);
|
|
189
|
+
const responseTimestamp = Date.now();
|
|
190
|
+
// Clone response to avoid consuming the body
|
|
191
|
+
const clonedResponse = await logger.cloneResponse(response);
|
|
192
|
+
// Parse response body
|
|
193
|
+
const responseBodyData = await logger.parseResponseBody(clonedResponse);
|
|
194
|
+
// Create response data
|
|
195
|
+
const responseData = {
|
|
196
|
+
timestamp: responseTimestamp / 1000,
|
|
197
|
+
status_code: response.status,
|
|
198
|
+
headers: logger.redactSensitiveHeaders(Object.fromEntries(response.headers.entries())),
|
|
199
|
+
...responseBodyData,
|
|
200
|
+
};
|
|
201
|
+
// Create paired request-response object
|
|
202
|
+
const pair = {
|
|
203
|
+
request: requestData,
|
|
204
|
+
response: responseData,
|
|
205
|
+
logged_at: new Date().toISOString(),
|
|
206
|
+
};
|
|
207
|
+
// Remove from pending and add to pairs
|
|
208
|
+
logger.pendingRequests.delete(requestId);
|
|
209
|
+
logger.pairs.push(pair);
|
|
210
|
+
// Write to log file
|
|
211
|
+
await logger.writePairToLog(pair);
|
|
212
|
+
// Generate HTML if enabled
|
|
213
|
+
if (logger.config.enableRealTimeHTML) {
|
|
214
|
+
await logger.generateHTML();
|
|
215
|
+
}
|
|
216
|
+
return response;
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
// Remove from pending requests on error
|
|
220
|
+
logger.pendingRequests.delete(requestId);
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
// Mark fetch as instrumented
|
|
225
|
+
global.fetch.__claudeTraceInstrumented = true;
|
|
226
|
+
// Silent initialization
|
|
227
|
+
}
|
|
228
|
+
instrumentNodeHTTP() {
|
|
229
|
+
try {
|
|
230
|
+
const http = require("http");
|
|
231
|
+
const https = require("https");
|
|
232
|
+
const logger = this;
|
|
233
|
+
// Instrument http.request
|
|
234
|
+
if (http.request && !http.request.__claudeTraceInstrumented) {
|
|
235
|
+
const originalHttpRequest = http.request;
|
|
236
|
+
http.request = function (options, callback) {
|
|
237
|
+
return logger.interceptNodeRequest(originalHttpRequest, options, callback, false);
|
|
238
|
+
};
|
|
239
|
+
http.request.__claudeTraceInstrumented = true;
|
|
240
|
+
}
|
|
241
|
+
// Instrument http.get
|
|
242
|
+
if (http.get && !http.get.__claudeTraceInstrumented) {
|
|
243
|
+
const originalHttpGet = http.get;
|
|
244
|
+
http.get = function (options, callback) {
|
|
245
|
+
return logger.interceptNodeRequest(originalHttpGet, options, callback, false);
|
|
246
|
+
};
|
|
247
|
+
http.get.__claudeTraceInstrumented = true;
|
|
248
|
+
}
|
|
249
|
+
// Instrument https.request
|
|
250
|
+
if (https.request && !https.request.__claudeTraceInstrumented) {
|
|
251
|
+
const originalHttpsRequest = https.request;
|
|
252
|
+
https.request = function (options, callback) {
|
|
253
|
+
return logger.interceptNodeRequest(originalHttpsRequest, options, callback, true);
|
|
254
|
+
};
|
|
255
|
+
https.request.__claudeTraceInstrumented = true;
|
|
256
|
+
}
|
|
257
|
+
// Instrument https.get
|
|
258
|
+
if (https.get && !https.get.__claudeTraceInstrumented) {
|
|
259
|
+
const originalHttpsGet = https.get;
|
|
260
|
+
https.get = function (options, callback) {
|
|
261
|
+
return logger.interceptNodeRequest(originalHttpsGet, options, callback, true);
|
|
262
|
+
};
|
|
263
|
+
https.get.__claudeTraceInstrumented = true;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
// Silent error handling
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
interceptNodeRequest(originalRequest, options, callback, isHttps) {
|
|
271
|
+
// Parse URL from options
|
|
272
|
+
const url = this.parseNodeRequestURL(options, isHttps);
|
|
273
|
+
if (!this.isClaudeAPI(url)) {
|
|
274
|
+
return originalRequest.call(this, options, callback);
|
|
275
|
+
}
|
|
276
|
+
const requestId = this.generateRequestId();
|
|
277
|
+
const requestTimestamp = Date.now();
|
|
278
|
+
let requestBody = "";
|
|
279
|
+
// Create the request
|
|
280
|
+
const req = originalRequest.call(this, options, (res) => {
|
|
281
|
+
const responseTimestamp = Date.now();
|
|
282
|
+
let responseBody = "";
|
|
283
|
+
// Capture response data
|
|
284
|
+
res.on("data", (chunk) => {
|
|
285
|
+
responseBody += chunk;
|
|
286
|
+
});
|
|
287
|
+
res.on("end", async () => {
|
|
288
|
+
// Process the captured request/response
|
|
289
|
+
const requestData = {
|
|
290
|
+
timestamp: requestTimestamp / 1000,
|
|
291
|
+
method: options.method || "GET",
|
|
292
|
+
url: url,
|
|
293
|
+
headers: this.redactSensitiveHeaders(options.headers || {}),
|
|
294
|
+
body: requestBody ? await this.parseRequestBody(requestBody) : null,
|
|
295
|
+
};
|
|
296
|
+
const responseData = {
|
|
297
|
+
timestamp: responseTimestamp / 1000,
|
|
298
|
+
status_code: res.statusCode,
|
|
299
|
+
headers: this.redactSensitiveHeaders(res.headers || {}),
|
|
300
|
+
...(await this.parseResponseBodyFromString(responseBody, res.headers["content-type"])),
|
|
301
|
+
};
|
|
302
|
+
const pair = {
|
|
303
|
+
request: requestData,
|
|
304
|
+
response: responseData,
|
|
305
|
+
logged_at: new Date().toISOString(),
|
|
306
|
+
};
|
|
307
|
+
this.pairs.push(pair);
|
|
308
|
+
await this.writePairToLog(pair);
|
|
309
|
+
if (this.config.enableRealTimeHTML) {
|
|
310
|
+
await this.generateHTML();
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
// Call original callback if provided
|
|
314
|
+
if (callback) {
|
|
315
|
+
callback(res);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
// Capture request body
|
|
319
|
+
const originalWrite = req.write;
|
|
320
|
+
req.write = function (chunk) {
|
|
321
|
+
if (chunk) {
|
|
322
|
+
requestBody += chunk;
|
|
323
|
+
}
|
|
324
|
+
return originalWrite.call(this, chunk);
|
|
325
|
+
};
|
|
326
|
+
return req;
|
|
327
|
+
}
|
|
328
|
+
parseNodeRequestURL(options, isHttps) {
|
|
329
|
+
if (typeof options === "string") {
|
|
330
|
+
return options;
|
|
331
|
+
}
|
|
332
|
+
const protocol = isHttps ? "https:" : "http:";
|
|
333
|
+
const hostname = options.hostname || options.host || "localhost";
|
|
334
|
+
const port = options.port ? `:${options.port}` : "";
|
|
335
|
+
const path = options.path || "/";
|
|
336
|
+
return `${protocol}//${hostname}${port}${path}`;
|
|
337
|
+
}
|
|
338
|
+
async parseResponseBodyFromString(body, contentType) {
|
|
339
|
+
try {
|
|
340
|
+
if (contentType && contentType.includes("application/json")) {
|
|
341
|
+
return { body: JSON.parse(body) };
|
|
342
|
+
}
|
|
343
|
+
else if (contentType && contentType.includes("text/event-stream")) {
|
|
344
|
+
return { body_raw: body };
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
return { body_raw: body };
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
return { body_raw: body };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
async writePairToLog(pair) {
|
|
355
|
+
try {
|
|
356
|
+
const jsonLine = JSON.stringify(pair) + "\n";
|
|
357
|
+
fs_1.default.appendFileSync(this.traceLogFile, jsonLine);
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
// Silent error handling during runtime
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async generateHTML() {
|
|
364
|
+
try {
|
|
365
|
+
const includeAllRequests = process.env.CLAUDE_TRACE_INCLUDE_ALL_REQUESTS === "true";
|
|
366
|
+
await this.htmlGenerator.generateHTML(this.pairs, this.htmlFile, {
|
|
367
|
+
title: `${this.pairs.length} API Calls`,
|
|
368
|
+
timestamp: new Date().toISOString().replace("T", " ").slice(0, -5),
|
|
369
|
+
includeAllRequests,
|
|
370
|
+
});
|
|
371
|
+
// Silent HTML generation
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
// Silent error handling during runtime
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
cleanup() {
|
|
378
|
+
console.log("Cleaning up orphaned requests...");
|
|
379
|
+
for (const [, requestData] of this.pendingRequests.entries()) {
|
|
380
|
+
const orphanedPair = {
|
|
381
|
+
request: requestData,
|
|
382
|
+
response: null,
|
|
383
|
+
note: "ORPHANED_REQUEST - No matching response received",
|
|
384
|
+
logged_at: new Date().toISOString(),
|
|
385
|
+
};
|
|
386
|
+
try {
|
|
387
|
+
const jsonLine = JSON.stringify(orphanedPair) + "\n";
|
|
388
|
+
fs_1.default.appendFileSync(this.traceLogFile, jsonLine);
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
console.log(`Error writing orphaned request: ${error}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
this.pendingRequests.clear();
|
|
395
|
+
console.log(`Cleanup complete. Logged ${this.pairs.length} pairs`);
|
|
396
|
+
//获取cc会话的sessionid
|
|
397
|
+
let sessionId = this.getSessionIdFromLog();
|
|
398
|
+
if (sessionId != '') {
|
|
399
|
+
//将当前会话对应的cc日志文件,拷贝到.claude-trace/cclog目录
|
|
400
|
+
this.copyCClogFile(sessionId);
|
|
401
|
+
// Rename log file based on sessionid from first record
|
|
402
|
+
this.renameTraceLogFileBySessionId(sessionId);
|
|
403
|
+
}
|
|
404
|
+
// Open browser if requested
|
|
405
|
+
// const shouldOpenBrowser = process.env.CLAUDE_TRACE_OPEN_BROWSER === "true";
|
|
406
|
+
// if (shouldOpenBrowser && fs.existsSync(this.htmlFile)) {
|
|
407
|
+
// try {
|
|
408
|
+
// spawn("open", [this.htmlFile], { detached: true, stdio: "ignore" }).unref();
|
|
409
|
+
// console.log(`Opening ${this.htmlFile} in browser`);
|
|
410
|
+
// } catch (error) {
|
|
411
|
+
// console.log(`Failed to open browser: ${error}`);
|
|
412
|
+
// }
|
|
413
|
+
// }
|
|
414
|
+
}
|
|
415
|
+
getSessionIdFromLog() {
|
|
416
|
+
// Check if log file exists
|
|
417
|
+
if (!fs_1.default.existsSync(this.traceLogFile)) {
|
|
418
|
+
console.log("获取sessionId错误:Log file does not exist");
|
|
419
|
+
return '';
|
|
420
|
+
}
|
|
421
|
+
// Read the first line of the JSONL file
|
|
422
|
+
const fileContent = fs_1.default.readFileSync(this.traceLogFile, 'utf-8');
|
|
423
|
+
const lines = fileContent.split('\n').filter(line => line.trim());
|
|
424
|
+
if (lines.length === 0) {
|
|
425
|
+
console.log("获取sessionId错误:Log file is empty");
|
|
426
|
+
return '';
|
|
427
|
+
}
|
|
428
|
+
// 循环读取日志,直到找到user_id为止
|
|
429
|
+
let userId = null;
|
|
430
|
+
for (const line of lines) {
|
|
431
|
+
const record = JSON.parse(line);
|
|
432
|
+
userId = record?.request?.body?.metadata?.user_id;
|
|
433
|
+
if (userId) {
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (!userId) {
|
|
438
|
+
console.log("获取sessionId错误:No user_id found in any record");
|
|
439
|
+
return '';
|
|
440
|
+
}
|
|
441
|
+
// Extract sessionid from user_id (format: xxxx_session_{sessionid})
|
|
442
|
+
const sessionMatch = userId.match(/_session_([^_]+)$/);
|
|
443
|
+
if (!sessionMatch || !sessionMatch[1]) {
|
|
444
|
+
console.log(`获取sessionId错误:No sessionid found in user_id: ${userId}`);
|
|
445
|
+
return '';
|
|
446
|
+
}
|
|
447
|
+
return sessionMatch[1];
|
|
448
|
+
}
|
|
449
|
+
copyCClogFile(sessionId) {
|
|
450
|
+
//将当前会话对应的cc日志文件,拷贝到.claude-trace/cclog目录
|
|
451
|
+
try {
|
|
452
|
+
// 创建LogFileManager实例
|
|
453
|
+
const logFileManager = new log_file_manager_1.LogFileManager();
|
|
454
|
+
// 获取当前工作目录作为项目路径
|
|
455
|
+
const currentProjectPath = process.cwd();
|
|
456
|
+
// 通过LogFileManager解析源日志目录
|
|
457
|
+
const sourceLogDir = logFileManager.resolveLogDirectory(currentProjectPath);
|
|
458
|
+
// 构建源文件路径(假设源文件名为sessionId.jsonl)
|
|
459
|
+
const sourceFile = path_1.default.join(sourceLogDir, `${sessionId}.jsonl`);
|
|
460
|
+
// 构建目标文件路径
|
|
461
|
+
this.ccLogFile = path_1.default.join(this.ccLogDir, `${sessionId}.jsonl`);
|
|
462
|
+
// 检查源文件是否存在
|
|
463
|
+
if (!fs_1.default.existsSync(sourceFile)) {
|
|
464
|
+
console.log(`源CC日志文件不存在: ${sourceFile}`);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
// 拷贝文件
|
|
468
|
+
fs_1.default.copyFileSync(sourceFile, this.ccLogFile);
|
|
469
|
+
console.log(`CC日志文件已从 ${sourceFile} 拷贝到 ${this.ccLogFile}`);
|
|
470
|
+
// 读取sourceLogDir目录下所有agent_*.jsonl文件,读取第一条记录的sessionId,找到与sessionId变量值相同的文件,拷贝到ccLogDir目录
|
|
471
|
+
const files = fs_1.default.readdirSync(sourceLogDir).filter(file => file.startsWith('agent-') && file.endsWith('.jsonl'));
|
|
472
|
+
for (const file of files) {
|
|
473
|
+
const filePath = path_1.default.join(sourceLogDir, file);
|
|
474
|
+
const content = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
475
|
+
const lines = content.split('\n').filter(line => line.trim());
|
|
476
|
+
if (lines.length > 0) {
|
|
477
|
+
try {
|
|
478
|
+
const firstRecord = JSON.parse(lines[0]);
|
|
479
|
+
const recordSessionId = firstRecord?.sessionId;
|
|
480
|
+
if (recordSessionId === sessionId) {
|
|
481
|
+
// 构建目标文件路径
|
|
482
|
+
const ccAgentLogFile = path_1.default.join(this.ccLogDir, file);
|
|
483
|
+
// 拷贝文件
|
|
484
|
+
fs_1.default.copyFileSync(filePath, ccAgentLogFile);
|
|
485
|
+
console.log(`SubAgent的CC日志文件已从 ${filePath} 拷贝到 ${ccAgentLogFile}`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
catch (parseError) {
|
|
489
|
+
// 静默处理解析错误,继续下一个文件
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
catch (error) {
|
|
496
|
+
console.log(`拷贝CC日志文件时出错: ${error}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
renameTraceLogFileBySessionId(sessionId) {
|
|
500
|
+
try {
|
|
501
|
+
const logDir = path_1.default.dirname(this.traceLogFile);
|
|
502
|
+
const newLogFile = path_1.default.join(logDir, `${sessionId}.jsonl`);
|
|
503
|
+
// Rename the file
|
|
504
|
+
fs_1.default.renameSync(this.traceLogFile, newLogFile);
|
|
505
|
+
console.log(`Log file renamed from ${path_1.default.basename(this.traceLogFile)} to ${sessionId}.jsonl`);
|
|
506
|
+
// Update the logFile path for future reference
|
|
507
|
+
this.traceLogFile = newLogFile;
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
console.log(`Error renaming log file: ${error}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
getStats() {
|
|
514
|
+
return {
|
|
515
|
+
totalPairs: this.pairs.length,
|
|
516
|
+
pendingRequests: this.pendingRequests.size,
|
|
517
|
+
logFile: this.traceLogFile,
|
|
518
|
+
htmlFile: this.htmlFile,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
exports.ClaudeTrafficLogger = ClaudeTrafficLogger;
|
|
523
|
+
// Global logger instance
|
|
524
|
+
let globalLogger = null;
|
|
525
|
+
// Track if event listeners have been set up
|
|
526
|
+
let eventListenersSetup = false;
|
|
527
|
+
function initializeInterceptor(config) {
|
|
528
|
+
if (globalLogger) {
|
|
529
|
+
console.warn("Interceptor already initialized");
|
|
530
|
+
return globalLogger;
|
|
531
|
+
}
|
|
532
|
+
globalLogger = new ClaudeTrafficLogger(config);
|
|
533
|
+
globalLogger.instrumentAll();
|
|
534
|
+
// Setup cleanup on process exit only once
|
|
535
|
+
if (!eventListenersSetup) {
|
|
536
|
+
const cleanup = () => {
|
|
537
|
+
if (globalLogger) {
|
|
538
|
+
globalLogger.cleanup();
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
process.on("exit", cleanup);
|
|
542
|
+
process.on("SIGINT", cleanup);
|
|
543
|
+
process.on("SIGTERM", cleanup);
|
|
544
|
+
process.on("uncaughtException", (error) => {
|
|
545
|
+
console.error("Uncaught exception:", error);
|
|
546
|
+
cleanup();
|
|
547
|
+
process.exit(1);
|
|
548
|
+
});
|
|
549
|
+
eventListenersSetup = true;
|
|
550
|
+
}
|
|
551
|
+
return globalLogger;
|
|
552
|
+
}
|
|
553
|
+
function getLogger() {
|
|
554
|
+
return globalLogger;
|
|
555
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare class LogFileManager {
|
|
2
|
+
private logDir;
|
|
3
|
+
private fileWatcher;
|
|
4
|
+
constructor(logDir?: string);
|
|
5
|
+
private getUserHome;
|
|
6
|
+
private getClaudeProjectsDir;
|
|
7
|
+
/**
|
|
8
|
+
* 根据项目路径解析对应的日志目录
|
|
9
|
+
* @param projectPath 项目路径
|
|
10
|
+
* @returns 日志目录路径
|
|
11
|
+
*/
|
|
12
|
+
resolveLogDirectory(projectPath: string): string;
|
|
13
|
+
private generateProjectId;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=log-file-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log-file-manager.d.ts","sourceRoot":"","sources":["../src/log-file-manager.ts"],"names":[],"mappings":"AAIA,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,WAAW,CAAa;gBAEpB,MAAM,CAAC,EAAE,MAAM;IAI3B,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,oBAAoB;IAI5B;;;;OAIG;IACH,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM;IAUhD,OAAO,CAAC,iBAAiB;CAQ1B"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LogFileManager = void 0;
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
class LogFileManager {
|
|
8
|
+
constructor(logDir) {
|
|
9
|
+
this.fileWatcher = null;
|
|
10
|
+
this.logDir = logDir || '';
|
|
11
|
+
}
|
|
12
|
+
getUserHome() {
|
|
13
|
+
return os.homedir();
|
|
14
|
+
}
|
|
15
|
+
getClaudeProjectsDir() {
|
|
16
|
+
return path.join(this.getUserHome(), '.claude', 'projects');
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 根据项目路径解析对应的日志目录
|
|
20
|
+
* @param projectPath 项目路径
|
|
21
|
+
* @returns 日志目录路径
|
|
22
|
+
*/
|
|
23
|
+
resolveLogDirectory(projectPath) {
|
|
24
|
+
// 1. 获取项目目录的绝对路径
|
|
25
|
+
const absoluteProjectPath = path.resolve(projectPath);
|
|
26
|
+
// 2. 根据实际规律生成项目目录标识
|
|
27
|
+
const projectId = this.generateProjectId(absoluteProjectPath);
|
|
28
|
+
// 3. 构建对应的日志目录路径
|
|
29
|
+
const logDir = path.join(this.getClaudeProjectsDir(), projectId);
|
|
30
|
+
return logDir;
|
|
31
|
+
}
|
|
32
|
+
generateProjectId(projectPath) {
|
|
33
|
+
// 根据实际规律:将路径中的所有 '/'、'\'、'_'、':' 和空格替换为 '-',同时将非 ASCII 字符(如中文)也替换为 '-'
|
|
34
|
+
// 例如:"/Users/ligf/Code/claude-code/ccdebug/ccdemo" -> "-Users-ligf-Code-claude-code-ccdebug-ccdemo"
|
|
35
|
+
// 例如:"/Users/ligf/Code/项目/测试" -> "-Users-ligf-Code------"
|
|
36
|
+
// 例如:"D:\mysoft\CC客服" -> "-D--mysoft-CC--"
|
|
37
|
+
// 例如:"C:\Program Files\MyApp" -> "-C--Program-Files-MyApp"
|
|
38
|
+
return projectPath.replace(/[\/\\_:\s]/g, '-').replace(/[^\x00-\x7F]/g, '-');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
exports.LogFileManager = LogFileManager;
|