@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,24 @@
|
|
|
1
|
+
import { RawPair } from "./types";
|
|
2
|
+
export declare class HTMLGenerator {
|
|
3
|
+
private frontendDir;
|
|
4
|
+
private templatePath;
|
|
5
|
+
private bundlePath;
|
|
6
|
+
constructor();
|
|
7
|
+
private ensureFrontendBuilt;
|
|
8
|
+
private loadTemplateFiles;
|
|
9
|
+
private filterClaudeAPIPairs;
|
|
10
|
+
private filterShortConversations;
|
|
11
|
+
private prepareDataForInjection;
|
|
12
|
+
private escapeHtml;
|
|
13
|
+
generateHTML(pairs: RawPair[], outputFile: string, options?: {
|
|
14
|
+
title?: string;
|
|
15
|
+
timestamp?: string;
|
|
16
|
+
includeAllRequests?: boolean;
|
|
17
|
+
}): Promise<void>;
|
|
18
|
+
generateHTMLFromJSONL(jsonlFile: string, outputFile?: string, includeAllRequests?: boolean): Promise<string>;
|
|
19
|
+
getTemplatePaths(): {
|
|
20
|
+
templatePath: string;
|
|
21
|
+
bundlePath: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=html-generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html-generator.d.ts","sourceRoot":"","sources":["../src/html-generator.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAkC,MAAM,SAAS,CAAC;AAElE,qBAAa,aAAa;IACzB,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,UAAU,CAAS;;IAQ3B,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,oBAAoB;IAQ5B,OAAO,CAAC,wBAAwB;IAQhC,OAAO,CAAC,uBAAuB;IAgB/B,OAAO,CAAC,UAAU;IASL,YAAY,CACxB,KAAK,EAAE,OAAO,EAAE,EAChB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE;QACR,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,kBAAkB,CAAC,EAAE,OAAO,CAAC;KACxB,GACJ,OAAO,CAAC,IAAI,CAAC;IAoDH,qBAAqB,CACjC,SAAS,EAAE,MAAM,EACjB,UAAU,CAAC,EAAE,MAAM,EACnB,kBAAkB,GAAE,OAAc,GAChC,OAAO,CAAC,MAAM,CAAC;IAoCX,gBAAgB,IAAI;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE;CAMvE"}
|
|
@@ -0,0 +1,141 @@
|
|
|
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.HTMLGenerator = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
class HTMLGenerator {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.frontendDir = path_1.default.join(__dirname, "..", "frontend");
|
|
12
|
+
this.templatePath = path_1.default.join(this.frontendDir, "template.html");
|
|
13
|
+
this.bundlePath = path_1.default.join(this.frontendDir, "dist", "index.global.js");
|
|
14
|
+
}
|
|
15
|
+
ensureFrontendBuilt() {
|
|
16
|
+
if (!fs_1.default.existsSync(this.bundlePath)) {
|
|
17
|
+
throw new Error(`Frontend bundle not found at ${this.bundlePath}. ` + `Run 'npm run build' in frontend directory first.`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
loadTemplateFiles() {
|
|
21
|
+
this.ensureFrontendBuilt();
|
|
22
|
+
const htmlTemplate = fs_1.default.readFileSync(this.templatePath, "utf-8");
|
|
23
|
+
const jsBundle = fs_1.default.readFileSync(this.bundlePath, "utf-8");
|
|
24
|
+
return { htmlTemplate, jsBundle };
|
|
25
|
+
}
|
|
26
|
+
filterClaudeAPIPairs(pairs) {
|
|
27
|
+
return pairs.filter((pair) => {
|
|
28
|
+
const url = pair.request.url;
|
|
29
|
+
// Include both Anthropic API and Bedrock API calls
|
|
30
|
+
return url.includes("/v1/messages") || url.includes("bedrock-runtime.amazonaws.com");
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
filterShortConversations(pairs) {
|
|
34
|
+
return pairs.filter((pair) => {
|
|
35
|
+
const messages = pair.request?.body?.messages;
|
|
36
|
+
if (!Array.isArray(messages))
|
|
37
|
+
return true;
|
|
38
|
+
return messages.length > 2;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
prepareDataForInjection(data) {
|
|
42
|
+
const claudeData = {
|
|
43
|
+
rawPairs: data.rawPairs,
|
|
44
|
+
timestamp: data.timestamp,
|
|
45
|
+
metadata: {
|
|
46
|
+
includeAllRequests: data.includeAllRequests || false,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
// Convert to JSON with minimal whitespace
|
|
50
|
+
const dataJson = JSON.stringify(claudeData, null, 0);
|
|
51
|
+
// Base64 encode to avoid all escaping issues
|
|
52
|
+
return Buffer.from(dataJson, "utf-8").toString("base64");
|
|
53
|
+
}
|
|
54
|
+
escapeHtml(text) {
|
|
55
|
+
return text
|
|
56
|
+
.replace(/&/g, "&")
|
|
57
|
+
.replace(/</g, "<")
|
|
58
|
+
.replace(/>/g, ">")
|
|
59
|
+
.replace(/"/g, """)
|
|
60
|
+
.replace(/'/g, "'");
|
|
61
|
+
}
|
|
62
|
+
async generateHTML(pairs, outputFile, options = {}) {
|
|
63
|
+
try {
|
|
64
|
+
let filteredPairs = pairs;
|
|
65
|
+
// Remove filtering entirely - show all data
|
|
66
|
+
// Previously filtered to only include v1/messages pairs with messages.length >= 2
|
|
67
|
+
// but this was too aggressive and excluded valid data
|
|
68
|
+
// Load template and bundle files
|
|
69
|
+
const { htmlTemplate, jsBundle } = this.loadTemplateFiles();
|
|
70
|
+
// Prepare data for injection
|
|
71
|
+
const htmlData = {
|
|
72
|
+
rawPairs: filteredPairs,
|
|
73
|
+
timestamp: options.timestamp || new Date().toISOString().replace("T", " ").slice(0, -5),
|
|
74
|
+
includeAllRequests: options.includeAllRequests || false,
|
|
75
|
+
};
|
|
76
|
+
const dataJsonEscaped = this.prepareDataForInjection(htmlData);
|
|
77
|
+
// BIZARRE BUT NECESSARY: Use split() instead of replace() for bundle injection
|
|
78
|
+
//
|
|
79
|
+
// Why this weird approach? Using replace instead of split() for some reason duplicates
|
|
80
|
+
// the htmlTemplate itself inside the new string! Maybe a bug in Node's String.replace?
|
|
81
|
+
const templateParts = htmlTemplate.split("__CLAUDE_LOGGER_BUNDLE_REPLACEMENT_UNIQUE_9487__");
|
|
82
|
+
if (templateParts.length !== 2) {
|
|
83
|
+
throw new Error("Template bundle replacement marker not found or found multiple times");
|
|
84
|
+
}
|
|
85
|
+
// Reconstruct the template with the bundle injected between the split parts
|
|
86
|
+
let htmlContent = templateParts[0] + jsBundle + templateParts[1];
|
|
87
|
+
htmlContent = htmlContent
|
|
88
|
+
.replace("__CLAUDE_LOGGER_DATA_REPLACEMENT_UNIQUE_9487__", dataJsonEscaped)
|
|
89
|
+
.replace("__CLAUDE_LOGGER_TITLE_REPLACEMENT_UNIQUE_9487__", this.escapeHtml(options.title || `${filteredPairs.length} API Calls`));
|
|
90
|
+
// Ensure output directory exists
|
|
91
|
+
const outputDir = path_1.default.dirname(outputFile);
|
|
92
|
+
if (!fs_1.default.existsSync(outputDir)) {
|
|
93
|
+
fs_1.default.mkdirSync(outputDir, { recursive: true });
|
|
94
|
+
}
|
|
95
|
+
// Write HTML file
|
|
96
|
+
fs_1.default.writeFileSync(outputFile, htmlContent, "utf-8");
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
console.error(`Error generating HTML: ${error}`);
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async generateHTMLFromJSONL(jsonlFile, outputFile, includeAllRequests = true) {
|
|
104
|
+
if (!fs_1.default.existsSync(jsonlFile)) {
|
|
105
|
+
throw new Error(`File '${jsonlFile}' not found.`);
|
|
106
|
+
}
|
|
107
|
+
// Load all pairs from the JSONL file
|
|
108
|
+
const pairs = [];
|
|
109
|
+
const fileContent = fs_1.default.readFileSync(jsonlFile, "utf-8");
|
|
110
|
+
const lines = fileContent.split("\n");
|
|
111
|
+
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
112
|
+
const line = lines[lineNum].trim();
|
|
113
|
+
if (line) {
|
|
114
|
+
try {
|
|
115
|
+
const pair = JSON.parse(line);
|
|
116
|
+
pairs.push(pair);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
console.warn(`Warning: Skipping invalid JSON on line ${lineNum + 1}: ${line.slice(0, 100)}...`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (pairs.length === 0) {
|
|
125
|
+
throw new Error(`No valid data found in '${jsonlFile}'.`);
|
|
126
|
+
}
|
|
127
|
+
// Determine output file
|
|
128
|
+
if (!outputFile) {
|
|
129
|
+
outputFile = jsonlFile.replace(/\.jsonl$/, ".html");
|
|
130
|
+
}
|
|
131
|
+
await this.generateHTML(pairs, outputFile, { includeAllRequests });
|
|
132
|
+
return outputFile;
|
|
133
|
+
}
|
|
134
|
+
getTemplatePaths() {
|
|
135
|
+
return {
|
|
136
|
+
templatePath: this.templatePath,
|
|
137
|
+
bundlePath: this.bundlePath,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
exports.HTMLGenerator = HTMLGenerator;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface ConversationSummary {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
summary: string;
|
|
5
|
+
startTime: string;
|
|
6
|
+
messageCount: number;
|
|
7
|
+
models: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface LogSummary {
|
|
10
|
+
logFile: string;
|
|
11
|
+
htmlFile: string;
|
|
12
|
+
generated: string;
|
|
13
|
+
conversations: ConversationSummary[];
|
|
14
|
+
}
|
|
15
|
+
export declare class IndexGenerator {
|
|
16
|
+
private traceDir;
|
|
17
|
+
private htmlGenerator;
|
|
18
|
+
private conversationProcessor;
|
|
19
|
+
constructor();
|
|
20
|
+
generateIndex(): Promise<void>;
|
|
21
|
+
private findLogFiles;
|
|
22
|
+
private processLogFile;
|
|
23
|
+
private extractConversations;
|
|
24
|
+
private summarizeConversation;
|
|
25
|
+
private conversationToText;
|
|
26
|
+
private callClaude;
|
|
27
|
+
private generateIndexHTML;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=index-generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-generator.d.ts","sourceRoot":"","sources":["../src/index-generator.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,mBAAmB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,mBAAmB,EAAE,CAAC;CACrC;AAED,qBAAa,cAAc;IAC1B,OAAO,CAAC,QAAQ,CAA2B;IAC3C,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,qBAAqB,CAA8B;;IAOrD,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAiCpC,OAAO,CAAC,YAAY;YAON,cAAc;YA6Dd,oBAAoB;YAgBpB,qBAAqB;IA2CnC,OAAO,CAAC,kBAAkB;YAkCZ,UAAU;YAkCV,iBAAiB;CAkE/B"}
|
|
@@ -0,0 +1,271 @@
|
|
|
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.IndexGenerator = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const html_generator_1 = require("./html-generator");
|
|
11
|
+
const shared_conversation_processor_1 = require("./shared-conversation-processor");
|
|
12
|
+
class IndexGenerator {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.traceDir = ".claude-trace";
|
|
15
|
+
this.htmlGenerator = new html_generator_1.HTMLGenerator();
|
|
16
|
+
this.conversationProcessor = new shared_conversation_processor_1.SharedConversationProcessor();
|
|
17
|
+
}
|
|
18
|
+
async generateIndex() {
|
|
19
|
+
console.log("Generating conversation index...");
|
|
20
|
+
console.log(`Looking in: ${this.traceDir}/`);
|
|
21
|
+
if (!fs_1.default.existsSync(this.traceDir)) {
|
|
22
|
+
console.log(`Directory ${this.traceDir} not found`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
// Find all log files
|
|
26
|
+
const logFiles = this.findLogFiles();
|
|
27
|
+
console.log(`Found ${logFiles.length} log files`);
|
|
28
|
+
if (logFiles.length === 0) {
|
|
29
|
+
console.log("No log files found");
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
// Process each log file
|
|
33
|
+
const allSummaries = [];
|
|
34
|
+
for (const logFile of logFiles) {
|
|
35
|
+
console.log(`\nProcessing ${logFile}...`);
|
|
36
|
+
const summary = await this.processLogFile(logFile);
|
|
37
|
+
if (summary) {
|
|
38
|
+
allSummaries.push(summary);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Generate index.html
|
|
42
|
+
await this.generateIndexHTML(allSummaries);
|
|
43
|
+
console.log(`\nIndex generated: ${this.traceDir}/index.html`);
|
|
44
|
+
}
|
|
45
|
+
findLogFiles() {
|
|
46
|
+
const files = fs_1.default.readdirSync(this.traceDir);
|
|
47
|
+
return files
|
|
48
|
+
.filter((file) => file.match(/^log-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}\.jsonl$/))
|
|
49
|
+
.sort((a, b) => b.localeCompare(a)); // newest first
|
|
50
|
+
}
|
|
51
|
+
async processLogFile(logFile) {
|
|
52
|
+
const logPath = path_1.default.join(this.traceDir, logFile);
|
|
53
|
+
const timestamp = logFile.match(/log-(\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2})\.jsonl$/)?.[1];
|
|
54
|
+
if (!timestamp)
|
|
55
|
+
return null;
|
|
56
|
+
const summaryFile = `summary-${timestamp}.json`;
|
|
57
|
+
const summaryPath = path_1.default.join(this.traceDir, summaryFile);
|
|
58
|
+
const htmlFile = `log-${timestamp}.html`;
|
|
59
|
+
const htmlPath = path_1.default.join(this.traceDir, htmlFile);
|
|
60
|
+
// Check if summary needs regeneration
|
|
61
|
+
const logStat = fs_1.default.statSync(logPath);
|
|
62
|
+
let needsRegeneration = !fs_1.default.existsSync(summaryPath);
|
|
63
|
+
if (!needsRegeneration) {
|
|
64
|
+
const summaryStat = fs_1.default.statSync(summaryPath);
|
|
65
|
+
needsRegeneration = summaryStat.mtime < logStat.mtime;
|
|
66
|
+
}
|
|
67
|
+
if (needsRegeneration) {
|
|
68
|
+
console.log(` Generating summary (${needsRegeneration ? "missing or outdated" : "up to date"})...`);
|
|
69
|
+
// Ensure HTML file exists
|
|
70
|
+
if (!fs_1.default.existsSync(htmlPath)) {
|
|
71
|
+
console.log(` Generating HTML file...`);
|
|
72
|
+
await this.htmlGenerator.generateHTMLFromJSONL(logPath, htmlPath);
|
|
73
|
+
}
|
|
74
|
+
// Process conversations
|
|
75
|
+
const conversations = await this.extractConversations(logPath);
|
|
76
|
+
const summaries = [];
|
|
77
|
+
// Summarize non-compacted conversations with more than 2 messages
|
|
78
|
+
const nonCompactedConversations = conversations.filter((conv) => !conv.compacted && conv.messages.length > 2);
|
|
79
|
+
console.log(` Found ${nonCompactedConversations.length} non-compacted conversations (>2 messages)`);
|
|
80
|
+
for (const conversation of nonCompactedConversations) {
|
|
81
|
+
console.log(` Summarizing conversation ${conversation.id}...`);
|
|
82
|
+
const summary = await this.summarizeConversation(conversation);
|
|
83
|
+
if (summary) {
|
|
84
|
+
summaries.push(summary);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Save summary file
|
|
88
|
+
const logSummary = {
|
|
89
|
+
logFile,
|
|
90
|
+
htmlFile,
|
|
91
|
+
generated: new Date().toISOString(),
|
|
92
|
+
conversations: summaries,
|
|
93
|
+
};
|
|
94
|
+
fs_1.default.writeFileSync(summaryPath, JSON.stringify(logSummary, null, 2));
|
|
95
|
+
console.log(` Summary saved: ${summaryFile}`);
|
|
96
|
+
return logSummary;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
console.log(` Using existing summary`);
|
|
100
|
+
return JSON.parse(fs_1.default.readFileSync(summaryPath, "utf-8"));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async extractConversations(logPath) {
|
|
104
|
+
// Read and parse JSONL file
|
|
105
|
+
const content = fs_1.default.readFileSync(logPath, "utf-8");
|
|
106
|
+
const lines = content
|
|
107
|
+
.trim()
|
|
108
|
+
.split("\n")
|
|
109
|
+
.filter((line) => line.trim());
|
|
110
|
+
const rawPairs = lines.map((line) => JSON.parse(line));
|
|
111
|
+
// Process pairs using shared implementation
|
|
112
|
+
const processedPairs = this.conversationProcessor.processRawPairs(rawPairs);
|
|
113
|
+
// Extract conversations
|
|
114
|
+
return this.conversationProcessor.mergeConversations(processedPairs);
|
|
115
|
+
}
|
|
116
|
+
async summarizeConversation(conversation) {
|
|
117
|
+
try {
|
|
118
|
+
// Convert conversation to text for summarization
|
|
119
|
+
const conversationText = this.conversationToText(conversation);
|
|
120
|
+
// Prepare prompt for Claude
|
|
121
|
+
const prompt = `Please analyze this conversation and provide:
|
|
122
|
+
1. A concise title (max 10 words)
|
|
123
|
+
2. A summary in 1-3 paragraphs describing what was accomplished
|
|
124
|
+
|
|
125
|
+
Conversation:
|
|
126
|
+
${conversationText}
|
|
127
|
+
|
|
128
|
+
Format your response as:
|
|
129
|
+
TITLE: [title]
|
|
130
|
+
SUMMARY: [summary]`;
|
|
131
|
+
// Call Claude CLI
|
|
132
|
+
const claudeResponse = await this.callClaude(prompt);
|
|
133
|
+
// Parse response
|
|
134
|
+
const titleMatch = claudeResponse.match(/TITLE:\s*(.+)/);
|
|
135
|
+
const summaryMatch = claudeResponse.match(/SUMMARY:\s*([\s\S]+)/);
|
|
136
|
+
if (!titleMatch || !summaryMatch) {
|
|
137
|
+
console.log(` Failed to parse Claude response for conversation ${conversation.id}`);
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
id: conversation.id,
|
|
142
|
+
title: titleMatch[1].trim(),
|
|
143
|
+
summary: summaryMatch[1].trim(),
|
|
144
|
+
startTime: conversation.metadata.startTime,
|
|
145
|
+
messageCount: conversation.messages.length,
|
|
146
|
+
models: Array.from(conversation.models),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
console.log(` Failed to summarize conversation ${conversation.id}: ${error}`);
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
conversationToText(conversation) {
|
|
155
|
+
let text = "";
|
|
156
|
+
// Add system prompt (stripped)
|
|
157
|
+
if (conversation.system) {
|
|
158
|
+
const systemText = typeof conversation.system === "string"
|
|
159
|
+
? conversation.system
|
|
160
|
+
: conversation.system.map((block) => (block.type === "text" ? block.text : "[non-text]")).join(" ");
|
|
161
|
+
text += `SYSTEM: ${systemText.substring(0, 500)}...\n\n`;
|
|
162
|
+
}
|
|
163
|
+
// Add messages (without tool results for brevity)
|
|
164
|
+
for (const message of conversation.messages) {
|
|
165
|
+
if (message.hide)
|
|
166
|
+
continue;
|
|
167
|
+
text += `${message.role.toUpperCase()}: `;
|
|
168
|
+
if (typeof message.content === "string") {
|
|
169
|
+
text += message.content.substring(0, 1000);
|
|
170
|
+
}
|
|
171
|
+
else if (Array.isArray(message.content)) {
|
|
172
|
+
const textBlocks = message.content
|
|
173
|
+
.filter((block) => block.type === "text")
|
|
174
|
+
.map((block) => block.text)
|
|
175
|
+
.join(" ");
|
|
176
|
+
text += textBlocks.substring(0, 1000);
|
|
177
|
+
}
|
|
178
|
+
text += "\n\n";
|
|
179
|
+
}
|
|
180
|
+
return text;
|
|
181
|
+
}
|
|
182
|
+
async callClaude(prompt) {
|
|
183
|
+
return new Promise((resolve, reject) => {
|
|
184
|
+
console.log(" Calling Claude CLI for summarization...");
|
|
185
|
+
console.log(" This will incur additional token usage");
|
|
186
|
+
const child = (0, child_process_1.spawn)("claude", ["-p", prompt], {
|
|
187
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
188
|
+
});
|
|
189
|
+
let stdout = "";
|
|
190
|
+
let stderr = "";
|
|
191
|
+
child.stdout.on("data", (data) => {
|
|
192
|
+
stdout += data.toString();
|
|
193
|
+
});
|
|
194
|
+
child.stderr.on("data", (data) => {
|
|
195
|
+
stderr += data.toString();
|
|
196
|
+
});
|
|
197
|
+
child.on("close", (code) => {
|
|
198
|
+
if (code === 0) {
|
|
199
|
+
resolve(stdout.trim());
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
reject(new Error(`Claude CLI exited with code ${code}: ${stderr}`));
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
child.on("error", (error) => {
|
|
206
|
+
reject(new Error(`Failed to spawn claude CLI: ${error.message}`));
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
async generateIndexHTML(summaries) {
|
|
211
|
+
// We'll create a simple index template for now
|
|
212
|
+
// Later we can enhance this with the lazy loading frontend
|
|
213
|
+
const indexPath = path_1.default.join(this.traceDir, "index.html");
|
|
214
|
+
let html = `<!DOCTYPE html>
|
|
215
|
+
<html lang="en">
|
|
216
|
+
<head>
|
|
217
|
+
<meta charset="UTF-8">
|
|
218
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
219
|
+
<title>CCDebug - Conversation Index</title>
|
|
220
|
+
<style>
|
|
221
|
+
body { font-family: system-ui, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; }
|
|
222
|
+
.header { border-bottom: 1px solid #ddd; padding-bottom: 20px; margin-bottom: 30px; }
|
|
223
|
+
.log-section { margin-bottom: 40px; }
|
|
224
|
+
.log-header { background: #f5f5f5; padding: 15px; border-radius: 8px; margin-bottom: 20px; }
|
|
225
|
+
.conversation { border: 1px solid #e0e0e0; border-radius: 8px; padding: 20px; margin-bottom: 15px; }
|
|
226
|
+
.conversation h3 { margin-top: 0; color: #2563eb; }
|
|
227
|
+
.conversation-meta { color: #666; font-size: 14px; margin-bottom: 10px; }
|
|
228
|
+
.models { display: flex; gap: 8px; margin-top: 10px; }
|
|
229
|
+
.model-tag { background: #e5f3ff; color: #0066cc; padding: 2px 8px; border-radius: 4px; font-size: 12px; }
|
|
230
|
+
a { color: #2563eb; text-decoration: none; }
|
|
231
|
+
a:hover { text-decoration: underline; }
|
|
232
|
+
</style>
|
|
233
|
+
</head>
|
|
234
|
+
<body>
|
|
235
|
+
<div class="header">
|
|
236
|
+
<h1>CCDebug - Conversation Index</h1>
|
|
237
|
+
<p>Generated: ${new Date().toISOString().replace("T", " ").slice(0, -5)}</p>
|
|
238
|
+
<p>Total logs: ${summaries.length}</p>
|
|
239
|
+
</div>
|
|
240
|
+
`;
|
|
241
|
+
for (const summary of summaries) {
|
|
242
|
+
html += `
|
|
243
|
+
<div class="log-section">
|
|
244
|
+
<div class="log-header">
|
|
245
|
+
<h2><a href="${summary.htmlFile}">${summary.logFile}</a></h2>
|
|
246
|
+
<p>Generated: ${summary.generated.replace("T", " ").slice(0, -5)} | Conversations: ${summary.conversations.length}</p>
|
|
247
|
+
</div>
|
|
248
|
+
`;
|
|
249
|
+
for (const conv of summary.conversations) {
|
|
250
|
+
html += `
|
|
251
|
+
<div class="conversation">
|
|
252
|
+
<h3>${conv.title}</h3>
|
|
253
|
+
<div class="conversation-meta">
|
|
254
|
+
${conv.startTime.replace("T", " ").slice(0, -5)} | ${conv.messageCount} messages
|
|
255
|
+
</div>
|
|
256
|
+
<p>${conv.summary}</p>
|
|
257
|
+
<div class="models">
|
|
258
|
+
${conv.models.map((model) => `<span class="model-tag">${model}</span>`).join("")}
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
`;
|
|
262
|
+
}
|
|
263
|
+
html += ` </div>`;
|
|
264
|
+
}
|
|
265
|
+
html += `
|
|
266
|
+
</body>
|
|
267
|
+
</html>`;
|
|
268
|
+
fs_1.default.writeFileSync(indexPath, html);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
exports.IndexGenerator = IndexGenerator;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { ClaudeTrafficLogger, initializeInterceptor, getLogger, InterceptorConfig } from "./interceptor";
|
|
2
|
+
export { HTMLGenerator } from "./html-generator";
|
|
3
|
+
export { RawPair, ClaudeData, HTMLGenerationData, TemplateReplacements } from "./types";
|
|
4
|
+
export * from "./interceptor";
|
|
5
|
+
export * from "./html-generator";
|
|
6
|
+
export * from "./types";
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACzG,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAGxF,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,SAAS,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.HTMLGenerator = exports.getLogger = exports.initializeInterceptor = exports.ClaudeTrafficLogger = void 0;
|
|
18
|
+
// Main exports for the package
|
|
19
|
+
var interceptor_1 = require("./interceptor");
|
|
20
|
+
Object.defineProperty(exports, "ClaudeTrafficLogger", { enumerable: true, get: function () { return interceptor_1.ClaudeTrafficLogger; } });
|
|
21
|
+
Object.defineProperty(exports, "initializeInterceptor", { enumerable: true, get: function () { return interceptor_1.initializeInterceptor; } });
|
|
22
|
+
Object.defineProperty(exports, "getLogger", { enumerable: true, get: function () { return interceptor_1.getLogger; } });
|
|
23
|
+
var html_generator_1 = require("./html-generator");
|
|
24
|
+
Object.defineProperty(exports, "HTMLGenerator", { enumerable: true, get: function () { return html_generator_1.HTMLGenerator; } });
|
|
25
|
+
// Re-export everything for convenience
|
|
26
|
+
__exportStar(require("./interceptor"), exports);
|
|
27
|
+
__exportStar(require("./html-generator"), exports);
|
|
28
|
+
__exportStar(require("./types"), exports);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// CommonJS loader for interceptor
|
|
2
|
+
try {
|
|
3
|
+
// Try to load the compiled JS version first
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
|
|
7
|
+
// Determine the correct paths based on current location
|
|
8
|
+
const isRunningFromDist = __dirname.includes('dist');
|
|
9
|
+
const isRunningFromSrc = __dirname.includes('src');
|
|
10
|
+
|
|
11
|
+
let jsPath, tsPath;
|
|
12
|
+
|
|
13
|
+
if (isRunningFromDist) {
|
|
14
|
+
// Running from dist directory - interceptor.js is in the same directory
|
|
15
|
+
jsPath = path.join(__dirname, "interceptor.js");
|
|
16
|
+
tsPath = path.join(__dirname, "..", "src", "interceptor.ts");
|
|
17
|
+
} else if (isRunningFromSrc) {
|
|
18
|
+
// Running from src directory - interceptor.js is in dist directory
|
|
19
|
+
jsPath = path.join(__dirname, "..", "dist", "interceptor.js");
|
|
20
|
+
tsPath = path.join(__dirname, "interceptor.ts");
|
|
21
|
+
} else {
|
|
22
|
+
// Fallback - try common locations
|
|
23
|
+
jsPath = path.join(__dirname, "interceptor.js");
|
|
24
|
+
tsPath = path.join(__dirname, "interceptor.ts");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (fs.existsSync(jsPath)) {
|
|
28
|
+
// Use compiled JavaScript
|
|
29
|
+
const interceptorModule = require(jsPath);
|
|
30
|
+
|
|
31
|
+
// Export the function for wrapper script
|
|
32
|
+
module.exports = {
|
|
33
|
+
initializeInterceptor: interceptorModule.initializeInterceptor
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Initialize the interceptor
|
|
37
|
+
interceptorModule.initializeInterceptor();
|
|
38
|
+
} else if (fs.existsSync(tsPath)) {
|
|
39
|
+
// Use TypeScript via tsx
|
|
40
|
+
require("tsx/cjs/api").register();
|
|
41
|
+
const interceptorModule = require(tsPath);
|
|
42
|
+
|
|
43
|
+
// Export the function for wrapper script
|
|
44
|
+
module.exports = {
|
|
45
|
+
initializeInterceptor: interceptorModule.initializeInterceptor
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Initialize the interceptor
|
|
49
|
+
interceptorModule.initializeInterceptor();
|
|
50
|
+
} else {
|
|
51
|
+
console.error("Could not find interceptor file");
|
|
52
|
+
console.error("Tried JS path:", jsPath);
|
|
53
|
+
console.error("Tried TS path:", tsPath);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error("Error loading interceptor:", error.message);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface InterceptorConfig {
|
|
2
|
+
logDirectory?: string;
|
|
3
|
+
logBaseName?: string;
|
|
4
|
+
enableRealTimeHTML?: boolean;
|
|
5
|
+
logLevel?: "debug" | "info" | "warn" | "error";
|
|
6
|
+
}
|
|
7
|
+
export declare class ClaudeTrafficLogger {
|
|
8
|
+
private traceHomeDir;
|
|
9
|
+
private traceLogDir;
|
|
10
|
+
private traceLogFile;
|
|
11
|
+
private ccLogDir;
|
|
12
|
+
private ccLogFile;
|
|
13
|
+
private htmlFile;
|
|
14
|
+
private pendingRequests;
|
|
15
|
+
private pairs;
|
|
16
|
+
private config;
|
|
17
|
+
private htmlGenerator;
|
|
18
|
+
constructor(config?: InterceptorConfig);
|
|
19
|
+
private isClaudeAPI;
|
|
20
|
+
private generateRequestId;
|
|
21
|
+
private redactSensitiveHeaders;
|
|
22
|
+
private cloneResponse;
|
|
23
|
+
private parseRequestBody;
|
|
24
|
+
private parseResponseBody;
|
|
25
|
+
instrumentAll(): void;
|
|
26
|
+
instrumentFetch(): void;
|
|
27
|
+
instrumentNodeHTTP(): void;
|
|
28
|
+
private interceptNodeRequest;
|
|
29
|
+
private parseNodeRequestURL;
|
|
30
|
+
private parseResponseBodyFromString;
|
|
31
|
+
private writePairToLog;
|
|
32
|
+
private generateHTML;
|
|
33
|
+
cleanup(): void;
|
|
34
|
+
private getSessionIdFromLog;
|
|
35
|
+
private copyCClogFile;
|
|
36
|
+
private renameTraceLogFileBySessionId;
|
|
37
|
+
getStats(): {
|
|
38
|
+
totalPairs: number;
|
|
39
|
+
pendingRequests: number;
|
|
40
|
+
logFile: string;
|
|
41
|
+
htmlFile: string;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export declare function initializeInterceptor(config?: InterceptorConfig): ClaudeTrafficLogger;
|
|
45
|
+
export declare function getLogger(): ClaudeTrafficLogger | null;
|
|
46
|
+
//# sourceMappingURL=interceptor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interceptor.d.ts","sourceRoot":"","sources":["../src/interceptor.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,iBAAiB;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;CAC/C;AAED,qBAAa,mBAAmB;IAC/B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,eAAe,CAA+B;IACtD,OAAO,CAAC,KAAK,CAAiB;IAC9B,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,aAAa,CAAgB;gBAEzB,MAAM,GAAE,iBAAsB;IA6C1C,OAAO,CAAC,WAAW;IAqBnB,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,sBAAsB;YAgChB,aAAa;YAKb,gBAAgB;YAsBhB,iBAAiB;IAwBxB,aAAa,IAAI,IAAI;IAKrB,eAAe,IAAI,IAAI;IA0FvB,kBAAkB,IAAI,IAAI;IA8CjC,OAAO,CAAC,oBAAoB;IAuE5B,OAAO,CAAC,mBAAmB;YAab,2BAA2B;YAiB3B,cAAc;YASd,YAAY;IAcnB,OAAO,IAAI,IAAI;IA4CtB,OAAO,CAAC,mBAAmB;IA0C3B,OAAO,CAAC,aAAa;IA4DrB,OAAO,CAAC,6BAA6B;IAkB9B,QAAQ;;;;;;CAQf;AAQD,wBAAgB,qBAAqB,CAAC,MAAM,CAAC,EAAE,iBAAiB,GAAG,mBAAmB,CA8BrF;AAED,wBAAgB,SAAS,IAAI,mBAAmB,GAAG,IAAI,CAEtD"}
|