@asifkibria/claude-code-toolkit 1.0.2 → 1.2.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/README.md +165 -214
- package/dist/CLAUDE.md +7 -0
- package/dist/__tests__/dashboard.test.d.ts +2 -0
- package/dist/__tests__/dashboard.test.d.ts.map +1 -0
- package/dist/__tests__/dashboard.test.js +606 -0
- package/dist/__tests__/dashboard.test.js.map +1 -0
- package/dist/__tests__/mcp-validator.test.d.ts +2 -0
- package/dist/__tests__/mcp-validator.test.d.ts.map +1 -0
- package/dist/__tests__/mcp-validator.test.js +217 -0
- package/dist/__tests__/mcp-validator.test.js.map +1 -0
- package/dist/__tests__/scanner.test.js +350 -1
- package/dist/__tests__/scanner.test.js.map +1 -1
- package/dist/__tests__/security.test.d.ts +2 -0
- package/dist/__tests__/security.test.d.ts.map +1 -0
- package/dist/__tests__/security.test.js +375 -0
- package/dist/__tests__/security.test.js.map +1 -0
- package/dist/__tests__/session-recovery.test.d.ts +2 -0
- package/dist/__tests__/session-recovery.test.d.ts.map +1 -0
- package/dist/__tests__/session-recovery.test.js +230 -0
- package/dist/__tests__/session-recovery.test.js.map +1 -0
- package/dist/__tests__/storage.test.d.ts +2 -0
- package/dist/__tests__/storage.test.d.ts.map +1 -0
- package/dist/__tests__/storage.test.js +241 -0
- package/dist/__tests__/storage.test.js.map +1 -0
- package/dist/__tests__/trace.test.d.ts +2 -0
- package/dist/__tests__/trace.test.d.ts.map +1 -0
- package/dist/__tests__/trace.test.js +376 -0
- package/dist/__tests__/trace.test.js.map +1 -0
- package/dist/cli.js +501 -20
- package/dist/cli.js.map +1 -1
- package/dist/index.js +950 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/dashboard-ui.d.ts +2 -0
- package/dist/lib/dashboard-ui.d.ts.map +1 -0
- package/dist/lib/dashboard-ui.js +2075 -0
- package/dist/lib/dashboard-ui.js.map +1 -0
- package/dist/lib/dashboard.d.ts +15 -0
- package/dist/lib/dashboard.d.ts.map +1 -0
- package/dist/lib/dashboard.js +1422 -0
- package/dist/lib/dashboard.js.map +1 -0
- package/dist/lib/logs.d.ts +42 -0
- package/dist/lib/logs.d.ts.map +1 -0
- package/dist/lib/logs.js +166 -0
- package/dist/lib/logs.js.map +1 -0
- package/dist/lib/mcp-validator.d.ts +86 -0
- package/dist/lib/mcp-validator.d.ts.map +1 -0
- package/dist/lib/mcp-validator.js +463 -0
- package/dist/lib/mcp-validator.js.map +1 -0
- package/dist/lib/scanner.d.ts +187 -2
- package/dist/lib/scanner.d.ts.map +1 -1
- package/dist/lib/scanner.js +1224 -14
- package/dist/lib/scanner.js.map +1 -1
- package/dist/lib/security.d.ts +57 -0
- package/dist/lib/security.d.ts.map +1 -0
- package/dist/lib/security.js +423 -0
- package/dist/lib/security.js.map +1 -0
- package/dist/lib/session-recovery.d.ts +60 -0
- package/dist/lib/session-recovery.d.ts.map +1 -0
- package/dist/lib/session-recovery.js +433 -0
- package/dist/lib/session-recovery.js.map +1 -0
- package/dist/lib/storage.d.ts +68 -0
- package/dist/lib/storage.d.ts.map +1 -0
- package/dist/lib/storage.js +500 -0
- package/dist/lib/storage.js.map +1 -0
- package/dist/lib/trace.d.ts +119 -0
- package/dist/lib/trace.d.ts.map +1 -0
- package/dist/lib/trace.js +649 -0
- package/dist/lib/trace.js.map +1 -0
- package/package.json +11 -3
package/dist/lib/scanner.js
CHANGED
|
@@ -48,8 +48,9 @@ export function findBackupFiles(dir) {
|
|
|
48
48
|
walkDir(dir);
|
|
49
49
|
return backups;
|
|
50
50
|
}
|
|
51
|
-
function detectContentType(item) {
|
|
51
|
+
function detectContentType(item, options) {
|
|
52
52
|
const type = item.type;
|
|
53
|
+
const minText = options?.minTextSize ?? MIN_PROBLEMATIC_TEXT_SIZE;
|
|
53
54
|
if (type === "image")
|
|
54
55
|
return "image";
|
|
55
56
|
if (type === "document") {
|
|
@@ -61,7 +62,7 @@ function detectContentType(item) {
|
|
|
61
62
|
}
|
|
62
63
|
if (type === "text") {
|
|
63
64
|
const text = item.text;
|
|
64
|
-
if (text && text.length >
|
|
65
|
+
if (text && text.length > minText)
|
|
65
66
|
return "large_text";
|
|
66
67
|
}
|
|
67
68
|
return "unknown";
|
|
@@ -81,14 +82,16 @@ function getContentSize(item) {
|
|
|
81
82
|
}
|
|
82
83
|
return 0;
|
|
83
84
|
}
|
|
84
|
-
function isProblematicContent(item) {
|
|
85
|
+
function isProblematicContent(item, options) {
|
|
85
86
|
const type = item.type;
|
|
87
|
+
const minBase64 = options?.minBase64Size ?? MIN_PROBLEMATIC_BASE64_SIZE;
|
|
88
|
+
const minText = options?.minTextSize ?? MIN_PROBLEMATIC_TEXT_SIZE;
|
|
86
89
|
// Check images
|
|
87
90
|
if (type === "image") {
|
|
88
91
|
const source = item.source;
|
|
89
92
|
if (source?.type === "base64") {
|
|
90
93
|
const data = source.data;
|
|
91
|
-
return (data?.length || 0) >
|
|
94
|
+
return (data?.length || 0) > minBase64;
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
97
|
// Check documents (PDFs, etc.)
|
|
@@ -96,17 +99,17 @@ function isProblematicContent(item) {
|
|
|
96
99
|
const source = item.source;
|
|
97
100
|
if (source?.type === "base64") {
|
|
98
101
|
const data = source.data;
|
|
99
|
-
return (data?.length || 0) >
|
|
102
|
+
return (data?.length || 0) > minBase64;
|
|
100
103
|
}
|
|
101
104
|
}
|
|
102
105
|
// Check large text content
|
|
103
106
|
if (type === "text") {
|
|
104
107
|
const text = item.text;
|
|
105
|
-
return (text?.length || 0) >
|
|
108
|
+
return (text?.length || 0) > minText;
|
|
106
109
|
}
|
|
107
110
|
return false;
|
|
108
111
|
}
|
|
109
|
-
export function checkContentForIssues(content) {
|
|
112
|
+
export function checkContentForIssues(content, options) {
|
|
110
113
|
if (!Array.isArray(content)) {
|
|
111
114
|
return { hasProblems: false, indices: [], totalSize: 0, contentType: "unknown" };
|
|
112
115
|
}
|
|
@@ -120,9 +123,9 @@ export function checkContentForIssues(content) {
|
|
|
120
123
|
const itemObj = item;
|
|
121
124
|
const size = getContentSize(itemObj);
|
|
122
125
|
totalSize += size;
|
|
123
|
-
if (isProblematicContent(itemObj)) {
|
|
126
|
+
if (isProblematicContent(itemObj, options)) {
|
|
124
127
|
problematicIndices.push(i);
|
|
125
|
-
detectedType = detectContentType(itemObj);
|
|
128
|
+
detectedType = detectContentType(itemObj, options);
|
|
126
129
|
}
|
|
127
130
|
// Check nested content in tool_result
|
|
128
131
|
if (itemObj.type === "tool_result") {
|
|
@@ -134,9 +137,9 @@ export function checkContentForIssues(content) {
|
|
|
134
137
|
const innerObj = innerItem;
|
|
135
138
|
const innerSize = getContentSize(innerObj);
|
|
136
139
|
totalSize += innerSize;
|
|
137
|
-
if (isProblematicContent(innerObj)) {
|
|
140
|
+
if (isProblematicContent(innerObj, options)) {
|
|
138
141
|
problematicIndices.push([i, j]);
|
|
139
|
-
detectedType = detectContentType(innerObj);
|
|
142
|
+
detectedType = detectContentType(innerObj, options);
|
|
140
143
|
}
|
|
141
144
|
}
|
|
142
145
|
}
|
|
@@ -191,7 +194,7 @@ export function fixContentInMessage(content, indices, contentType = "unknown") {
|
|
|
191
194
|
}
|
|
192
195
|
// Keep old function name for backwards compatibility
|
|
193
196
|
export const fixImageInContent = fixContentInMessage;
|
|
194
|
-
export function scanFile(filePath) {
|
|
197
|
+
export function scanFile(filePath, options) {
|
|
195
198
|
const issues = [];
|
|
196
199
|
let content;
|
|
197
200
|
try {
|
|
@@ -215,7 +218,7 @@ export function scanFile(filePath) {
|
|
|
215
218
|
const message = data.message;
|
|
216
219
|
const messageContent = message?.content;
|
|
217
220
|
if (messageContent) {
|
|
218
|
-
const { hasProblems, indices, totalSize, contentType } = checkContentForIssues(messageContent);
|
|
221
|
+
const { hasProblems, indices, totalSize, contentType } = checkContentForIssues(messageContent, options);
|
|
219
222
|
if (hasProblems) {
|
|
220
223
|
issues.push({
|
|
221
224
|
line: lineNum + 1,
|
|
@@ -229,7 +232,7 @@ export function scanFile(filePath) {
|
|
|
229
232
|
if (data.toolUseResult) {
|
|
230
233
|
const toolResult = data.toolUseResult;
|
|
231
234
|
const resultContent = toolResult.content;
|
|
232
|
-
const { hasProblems, indices, totalSize, contentType } = checkContentForIssues(resultContent);
|
|
235
|
+
const { hasProblems, indices, totalSize, contentType } = checkContentForIssues(resultContent, options);
|
|
233
236
|
if (hasProblems) {
|
|
234
237
|
const existingIssue = issues.find((i) => i.line === lineNum + 1);
|
|
235
238
|
if (!existingIssue) {
|
|
@@ -444,4 +447,1211 @@ export function deleteOldBackups(dir, olderThanDays) {
|
|
|
444
447
|
}
|
|
445
448
|
return { deleted, errors };
|
|
446
449
|
}
|
|
450
|
+
function extractTextFromContent(content) {
|
|
451
|
+
const parts = [];
|
|
452
|
+
for (const item of content) {
|
|
453
|
+
if (typeof item !== "object" || item === null)
|
|
454
|
+
continue;
|
|
455
|
+
const itemObj = item;
|
|
456
|
+
if (itemObj.type === "text") {
|
|
457
|
+
parts.push(itemObj.text);
|
|
458
|
+
}
|
|
459
|
+
else if (itemObj.type === "image") {
|
|
460
|
+
parts.push("[Image]");
|
|
461
|
+
}
|
|
462
|
+
else if (itemObj.type === "document") {
|
|
463
|
+
const source = itemObj.source;
|
|
464
|
+
const mediaType = source?.media_type;
|
|
465
|
+
if (mediaType?.includes("pdf")) {
|
|
466
|
+
parts.push("[PDF Document]");
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
parts.push("[Document]");
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
else if (itemObj.type === "tool_use") {
|
|
473
|
+
const name = itemObj.name;
|
|
474
|
+
parts.push(`[Tool: ${name}]`);
|
|
475
|
+
}
|
|
476
|
+
else if (itemObj.type === "tool_result") {
|
|
477
|
+
const innerContent = itemObj.content;
|
|
478
|
+
if (Array.isArray(innerContent)) {
|
|
479
|
+
parts.push(extractTextFromContent(innerContent));
|
|
480
|
+
}
|
|
481
|
+
else if (typeof innerContent === "string") {
|
|
482
|
+
parts.push(innerContent);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return parts.join("\n");
|
|
487
|
+
}
|
|
488
|
+
function parseConversation(filePath) {
|
|
489
|
+
const messages = [];
|
|
490
|
+
let content;
|
|
491
|
+
try {
|
|
492
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
return messages;
|
|
496
|
+
}
|
|
497
|
+
const lines = content.split("\n");
|
|
498
|
+
for (const line of lines) {
|
|
499
|
+
if (!line.trim())
|
|
500
|
+
continue;
|
|
501
|
+
let data;
|
|
502
|
+
try {
|
|
503
|
+
data = JSON.parse(line);
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
const message = data.message;
|
|
509
|
+
if (!message)
|
|
510
|
+
continue;
|
|
511
|
+
const role = message.role;
|
|
512
|
+
const messageContent = message.content;
|
|
513
|
+
let textContent = "";
|
|
514
|
+
const toolUses = [];
|
|
515
|
+
if (typeof messageContent === "string") {
|
|
516
|
+
textContent = messageContent;
|
|
517
|
+
}
|
|
518
|
+
else if (Array.isArray(messageContent)) {
|
|
519
|
+
const textParts = [];
|
|
520
|
+
for (const item of messageContent) {
|
|
521
|
+
if (typeof item !== "object" || item === null)
|
|
522
|
+
continue;
|
|
523
|
+
const itemObj = item;
|
|
524
|
+
if (itemObj.type === "text") {
|
|
525
|
+
textParts.push(itemObj.text);
|
|
526
|
+
}
|
|
527
|
+
else if (itemObj.type === "image") {
|
|
528
|
+
textParts.push("[Image]");
|
|
529
|
+
}
|
|
530
|
+
else if (itemObj.type === "document") {
|
|
531
|
+
const source = itemObj.source;
|
|
532
|
+
const mediaType = source?.media_type;
|
|
533
|
+
if (mediaType?.includes("pdf")) {
|
|
534
|
+
textParts.push("[PDF Document]");
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
textParts.push("[Document]");
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
else if (itemObj.type === "tool_use") {
|
|
541
|
+
toolUses.push({
|
|
542
|
+
name: itemObj.name,
|
|
543
|
+
input: itemObj.input,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
textContent = textParts.join("\n");
|
|
548
|
+
}
|
|
549
|
+
const exportedMessage = {
|
|
550
|
+
role,
|
|
551
|
+
content: textContent,
|
|
552
|
+
};
|
|
553
|
+
if (toolUses.length > 0) {
|
|
554
|
+
exportedMessage.toolUse = toolUses;
|
|
555
|
+
}
|
|
556
|
+
if (data.toolUseResult) {
|
|
557
|
+
const toolResult = data.toolUseResult;
|
|
558
|
+
const resultContent = toolResult.content;
|
|
559
|
+
let resultText = "";
|
|
560
|
+
if (typeof resultContent === "string") {
|
|
561
|
+
resultText = resultContent;
|
|
562
|
+
}
|
|
563
|
+
else if (Array.isArray(resultContent)) {
|
|
564
|
+
resultText = extractTextFromContent(resultContent);
|
|
565
|
+
}
|
|
566
|
+
exportedMessage.toolResults = [{
|
|
567
|
+
name: toolResult.name || "unknown",
|
|
568
|
+
result: resultText.slice(0, 500) + (resultText.length > 500 ? "..." : ""),
|
|
569
|
+
}];
|
|
570
|
+
}
|
|
571
|
+
if (data.timestamp) {
|
|
572
|
+
exportedMessage.timestamp = data.timestamp;
|
|
573
|
+
}
|
|
574
|
+
messages.push(exportedMessage);
|
|
575
|
+
}
|
|
576
|
+
return messages;
|
|
577
|
+
}
|
|
578
|
+
function formatAsMarkdown(messages, options) {
|
|
579
|
+
const lines = [];
|
|
580
|
+
lines.push("# Conversation Export");
|
|
581
|
+
lines.push("");
|
|
582
|
+
lines.push(`Exported: ${new Date().toISOString()}`);
|
|
583
|
+
lines.push(`Messages: ${messages.length}`);
|
|
584
|
+
lines.push("");
|
|
585
|
+
lines.push("---");
|
|
586
|
+
lines.push("");
|
|
587
|
+
for (const msg of messages) {
|
|
588
|
+
const roleLabel = msg.role === "user" ? "👤 User" : msg.role === "assistant" ? "🤖 Assistant" : "⚙️ System";
|
|
589
|
+
lines.push(`## ${roleLabel}`);
|
|
590
|
+
if (options.includeTimestamps && msg.timestamp) {
|
|
591
|
+
lines.push(`*${msg.timestamp}*`);
|
|
592
|
+
}
|
|
593
|
+
lines.push("");
|
|
594
|
+
if (msg.content) {
|
|
595
|
+
lines.push(msg.content);
|
|
596
|
+
lines.push("");
|
|
597
|
+
}
|
|
598
|
+
if (msg.toolUse && msg.toolUse.length > 0) {
|
|
599
|
+
lines.push("**Tool calls:**");
|
|
600
|
+
for (const tool of msg.toolUse) {
|
|
601
|
+
lines.push(`- \`${tool.name}\``);
|
|
602
|
+
}
|
|
603
|
+
lines.push("");
|
|
604
|
+
}
|
|
605
|
+
if (options.includeToolResults && msg.toolResults && msg.toolResults.length > 0) {
|
|
606
|
+
lines.push("**Tool results:**");
|
|
607
|
+
for (const result of msg.toolResults) {
|
|
608
|
+
lines.push(`<details>`);
|
|
609
|
+
lines.push(`<summary>${result.name}</summary>`);
|
|
610
|
+
lines.push("");
|
|
611
|
+
lines.push("```");
|
|
612
|
+
lines.push(result.result);
|
|
613
|
+
lines.push("```");
|
|
614
|
+
lines.push("</details>");
|
|
615
|
+
}
|
|
616
|
+
lines.push("");
|
|
617
|
+
}
|
|
618
|
+
lines.push("---");
|
|
619
|
+
lines.push("");
|
|
620
|
+
}
|
|
621
|
+
return lines.join("\n");
|
|
622
|
+
}
|
|
623
|
+
function formatAsJson(messages, options) {
|
|
624
|
+
const exportData = {
|
|
625
|
+
exportedAt: new Date().toISOString(),
|
|
626
|
+
messageCount: messages.length,
|
|
627
|
+
options: {
|
|
628
|
+
includeToolResults: options.includeToolResults,
|
|
629
|
+
includeTimestamps: options.includeTimestamps,
|
|
630
|
+
},
|
|
631
|
+
messages: messages.map(msg => {
|
|
632
|
+
const result = {
|
|
633
|
+
role: msg.role,
|
|
634
|
+
content: msg.content,
|
|
635
|
+
};
|
|
636
|
+
if (options.includeTimestamps && msg.timestamp) {
|
|
637
|
+
result.timestamp = msg.timestamp;
|
|
638
|
+
}
|
|
639
|
+
if (msg.toolUse && msg.toolUse.length > 0) {
|
|
640
|
+
result.toolUse = msg.toolUse;
|
|
641
|
+
}
|
|
642
|
+
if (options.includeToolResults && msg.toolResults && msg.toolResults.length > 0) {
|
|
643
|
+
result.toolResults = msg.toolResults;
|
|
644
|
+
}
|
|
645
|
+
return result;
|
|
646
|
+
}),
|
|
647
|
+
};
|
|
648
|
+
return JSON.stringify(exportData, null, 2);
|
|
649
|
+
}
|
|
650
|
+
export function exportConversation(filePath, options) {
|
|
651
|
+
const messages = parseConversation(filePath);
|
|
652
|
+
let content;
|
|
653
|
+
if (options.format === "markdown") {
|
|
654
|
+
content = formatAsMarkdown(messages, options);
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
content = formatAsJson(messages, options);
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
file: filePath,
|
|
661
|
+
format: options.format,
|
|
662
|
+
messageCount: messages.length,
|
|
663
|
+
content,
|
|
664
|
+
exportedAt: new Date(),
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
export function exportConversationToFile(sourcePath, outputPath, options) {
|
|
668
|
+
try {
|
|
669
|
+
const result = exportConversation(sourcePath, options);
|
|
670
|
+
fs.writeFileSync(outputPath, result.content, "utf-8");
|
|
671
|
+
return {
|
|
672
|
+
success: true,
|
|
673
|
+
outputPath,
|
|
674
|
+
messageCount: result.messageCount,
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
catch (e) {
|
|
678
|
+
return {
|
|
679
|
+
success: false,
|
|
680
|
+
outputPath,
|
|
681
|
+
messageCount: 0,
|
|
682
|
+
error: `Export failed: ${e}`,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
// Context size estimation constants
|
|
687
|
+
const CHARS_PER_TOKEN = 4; // Approximate for English text
|
|
688
|
+
const IMAGE_BASE_TOKENS = 85; // Minimum tokens for any image
|
|
689
|
+
const IMAGE_TOKENS_PER_MEGAPIXEL = 1334; // Approximate scaling
|
|
690
|
+
const TOOL_OVERHEAD_TOKENS = 50; // Overhead per tool call/result
|
|
691
|
+
function estimateTokensFromText(text) {
|
|
692
|
+
if (!text)
|
|
693
|
+
return 0;
|
|
694
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
695
|
+
}
|
|
696
|
+
function estimateTokensFromBase64Image(base64Length) {
|
|
697
|
+
// Base64 size roughly correlates with pixel count
|
|
698
|
+
// 1 byte of base64 ≈ 0.75 bytes of data
|
|
699
|
+
// For JPEG, roughly 1 byte per pixel after compression
|
|
700
|
+
const estimatedPixels = (base64Length * 0.75);
|
|
701
|
+
const megapixels = estimatedPixels / 1_000_000;
|
|
702
|
+
return Math.max(IMAGE_BASE_TOKENS, Math.ceil(IMAGE_TOKENS_PER_MEGAPIXEL * megapixels));
|
|
703
|
+
}
|
|
704
|
+
function estimateTokensFromContent(content) {
|
|
705
|
+
const result = { text: 0, image: 0, document: 0, toolUse: 0 };
|
|
706
|
+
if (typeof content === "string") {
|
|
707
|
+
result.text = estimateTokensFromText(content);
|
|
708
|
+
return result;
|
|
709
|
+
}
|
|
710
|
+
if (!Array.isArray(content))
|
|
711
|
+
return result;
|
|
712
|
+
for (const item of content) {
|
|
713
|
+
if (typeof item !== "object" || item === null)
|
|
714
|
+
continue;
|
|
715
|
+
const itemObj = item;
|
|
716
|
+
if (itemObj.type === "text") {
|
|
717
|
+
result.text += estimateTokensFromText(itemObj.text);
|
|
718
|
+
}
|
|
719
|
+
else if (itemObj.type === "image") {
|
|
720
|
+
const source = itemObj.source;
|
|
721
|
+
if (source?.type === "base64") {
|
|
722
|
+
const data = source.data;
|
|
723
|
+
result.image += estimateTokensFromBase64Image(data?.length || 0);
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
result.image += IMAGE_BASE_TOKENS;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
else if (itemObj.type === "document") {
|
|
730
|
+
const source = itemObj.source;
|
|
731
|
+
if (source?.type === "base64") {
|
|
732
|
+
const data = source.data;
|
|
733
|
+
// Documents are converted to text, estimate based on base64 size
|
|
734
|
+
result.document += Math.ceil((data?.length || 0) * 0.75 / CHARS_PER_TOKEN);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
else if (itemObj.type === "tool_use") {
|
|
738
|
+
result.toolUse += TOOL_OVERHEAD_TOKENS;
|
|
739
|
+
const input = itemObj.input;
|
|
740
|
+
if (input) {
|
|
741
|
+
result.toolUse += estimateTokensFromText(JSON.stringify(input));
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
else if (itemObj.type === "tool_result") {
|
|
745
|
+
const innerContent = itemObj.content;
|
|
746
|
+
if (Array.isArray(innerContent)) {
|
|
747
|
+
const inner = estimateTokensFromContent(innerContent);
|
|
748
|
+
result.text += inner.text;
|
|
749
|
+
result.image += inner.image;
|
|
750
|
+
result.document += inner.document;
|
|
751
|
+
}
|
|
752
|
+
else if (typeof innerContent === "string") {
|
|
753
|
+
result.text += estimateTokensFromText(innerContent);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return result;
|
|
758
|
+
}
|
|
759
|
+
export function estimateContextSize(filePath) {
|
|
760
|
+
const breakdown = {
|
|
761
|
+
userTokens: 0,
|
|
762
|
+
assistantTokens: 0,
|
|
763
|
+
systemTokens: 0,
|
|
764
|
+
toolUseTokens: 0,
|
|
765
|
+
toolResultTokens: 0,
|
|
766
|
+
imageTokens: 0,
|
|
767
|
+
documentTokens: 0,
|
|
768
|
+
};
|
|
769
|
+
const warnings = [];
|
|
770
|
+
let messageCount = 0;
|
|
771
|
+
let largestMessage = null;
|
|
772
|
+
let content;
|
|
773
|
+
try {
|
|
774
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
775
|
+
}
|
|
776
|
+
catch {
|
|
777
|
+
return {
|
|
778
|
+
file: filePath,
|
|
779
|
+
totalTokens: 0,
|
|
780
|
+
breakdown,
|
|
781
|
+
messageCount: 0,
|
|
782
|
+
largestMessage: null,
|
|
783
|
+
warnings: ["Could not read file"],
|
|
784
|
+
estimatedAt: new Date(),
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
const lines = content.split("\n");
|
|
788
|
+
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
789
|
+
const line = lines[lineNum];
|
|
790
|
+
if (!line.trim())
|
|
791
|
+
continue;
|
|
792
|
+
let data;
|
|
793
|
+
try {
|
|
794
|
+
data = JSON.parse(line);
|
|
795
|
+
}
|
|
796
|
+
catch {
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
const message = data.message;
|
|
800
|
+
if (!message)
|
|
801
|
+
continue;
|
|
802
|
+
messageCount++;
|
|
803
|
+
const role = message.role;
|
|
804
|
+
const messageContent = message.content;
|
|
805
|
+
const contentTokens = estimateTokensFromContent(messageContent);
|
|
806
|
+
const messageTotal = contentTokens.text + contentTokens.image + contentTokens.document + contentTokens.toolUse;
|
|
807
|
+
if (role === "user") {
|
|
808
|
+
breakdown.userTokens += contentTokens.text;
|
|
809
|
+
}
|
|
810
|
+
else if (role === "assistant") {
|
|
811
|
+
breakdown.assistantTokens += contentTokens.text;
|
|
812
|
+
}
|
|
813
|
+
else if (role === "system") {
|
|
814
|
+
breakdown.systemTokens += contentTokens.text;
|
|
815
|
+
}
|
|
816
|
+
breakdown.toolUseTokens += contentTokens.toolUse;
|
|
817
|
+
breakdown.imageTokens += contentTokens.image;
|
|
818
|
+
breakdown.documentTokens += contentTokens.document;
|
|
819
|
+
// Track largest message
|
|
820
|
+
if (!largestMessage || messageTotal > largestMessage.tokens) {
|
|
821
|
+
largestMessage = { line: lineNum + 1, tokens: messageTotal, role };
|
|
822
|
+
}
|
|
823
|
+
// Process tool results
|
|
824
|
+
if (data.toolUseResult) {
|
|
825
|
+
const toolResult = data.toolUseResult;
|
|
826
|
+
const resultContent = toolResult.content;
|
|
827
|
+
const resultTokens = estimateTokensFromContent(resultContent);
|
|
828
|
+
breakdown.toolResultTokens += resultTokens.text + TOOL_OVERHEAD_TOKENS;
|
|
829
|
+
breakdown.imageTokens += resultTokens.image;
|
|
830
|
+
breakdown.documentTokens += resultTokens.document;
|
|
831
|
+
if (resultTokens.text + resultTokens.image > 10000) {
|
|
832
|
+
warnings.push(`Line ${lineNum + 1}: Large tool result (~${resultTokens.text + resultTokens.image} tokens)`);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
const totalTokens = breakdown.userTokens + breakdown.assistantTokens + breakdown.systemTokens +
|
|
837
|
+
breakdown.toolUseTokens + breakdown.toolResultTokens + breakdown.imageTokens + breakdown.documentTokens;
|
|
838
|
+
// Add warnings for high context usage
|
|
839
|
+
if (totalTokens > 150000) {
|
|
840
|
+
warnings.push("Context exceeds 150K tokens - may hit limits on some models");
|
|
841
|
+
}
|
|
842
|
+
else if (totalTokens > 100000) {
|
|
843
|
+
warnings.push("Context exceeds 100K tokens - consider archiving older messages");
|
|
844
|
+
}
|
|
845
|
+
if (breakdown.imageTokens > totalTokens * 0.5) {
|
|
846
|
+
warnings.push("Images account for >50% of context - consider removing unused images");
|
|
847
|
+
}
|
|
848
|
+
return {
|
|
849
|
+
file: filePath,
|
|
850
|
+
totalTokens,
|
|
851
|
+
breakdown,
|
|
852
|
+
messageCount,
|
|
853
|
+
largestMessage,
|
|
854
|
+
warnings,
|
|
855
|
+
estimatedAt: new Date(),
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
export function formatContextEstimate(estimate) {
|
|
859
|
+
const lines = [];
|
|
860
|
+
const b = estimate.breakdown;
|
|
861
|
+
lines.push(`Context Size Estimate`);
|
|
862
|
+
lines.push(`${"─".repeat(40)}`);
|
|
863
|
+
lines.push(`Total: ~${estimate.totalTokens.toLocaleString()} tokens`);
|
|
864
|
+
lines.push(`Messages: ${estimate.messageCount}`);
|
|
865
|
+
lines.push("");
|
|
866
|
+
lines.push("Breakdown:");
|
|
867
|
+
lines.push(` User messages: ${b.userTokens.toLocaleString()} tokens`);
|
|
868
|
+
lines.push(` Assistant messages: ${b.assistantTokens.toLocaleString()} tokens`);
|
|
869
|
+
if (b.systemTokens > 0) {
|
|
870
|
+
lines.push(` System messages: ${b.systemTokens.toLocaleString()} tokens`);
|
|
871
|
+
}
|
|
872
|
+
lines.push(` Tool calls: ${b.toolUseTokens.toLocaleString()} tokens`);
|
|
873
|
+
lines.push(` Tool results: ${b.toolResultTokens.toLocaleString()} tokens`);
|
|
874
|
+
if (b.imageTokens > 0) {
|
|
875
|
+
lines.push(` Images: ${b.imageTokens.toLocaleString()} tokens`);
|
|
876
|
+
}
|
|
877
|
+
if (b.documentTokens > 0) {
|
|
878
|
+
lines.push(` Documents: ${b.documentTokens.toLocaleString()} tokens`);
|
|
879
|
+
}
|
|
880
|
+
if (estimate.largestMessage) {
|
|
881
|
+
lines.push("");
|
|
882
|
+
lines.push(`Largest message: Line ${estimate.largestMessage.line} (${estimate.largestMessage.role})`);
|
|
883
|
+
lines.push(` ~${estimate.largestMessage.tokens.toLocaleString()} tokens`);
|
|
884
|
+
}
|
|
885
|
+
if (estimate.warnings.length > 0) {
|
|
886
|
+
lines.push("");
|
|
887
|
+
lines.push("Warnings:");
|
|
888
|
+
for (const warning of estimate.warnings) {
|
|
889
|
+
lines.push(` ⚠ ${warning}`);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return lines.join("\n");
|
|
893
|
+
}
|
|
894
|
+
function getProjectFromPath(filePath) {
|
|
895
|
+
// Extract project name from path like ~/.claude/projects/-Users-me-myproject/conversation.jsonl
|
|
896
|
+
const parts = filePath.split(path.sep);
|
|
897
|
+
const projectsIdx = parts.indexOf("projects");
|
|
898
|
+
if (projectsIdx >= 0 && projectsIdx + 1 < parts.length) {
|
|
899
|
+
return parts[projectsIdx + 1];
|
|
900
|
+
}
|
|
901
|
+
return path.dirname(filePath);
|
|
902
|
+
}
|
|
903
|
+
function getDateFromTimestamp(timestamp) {
|
|
904
|
+
if (!timestamp)
|
|
905
|
+
return null;
|
|
906
|
+
try {
|
|
907
|
+
const date = new Date(timestamp);
|
|
908
|
+
return date.toISOString().split("T")[0];
|
|
909
|
+
}
|
|
910
|
+
catch {
|
|
911
|
+
return null;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
export function generateUsageAnalytics(projectsDir, days = 30) {
|
|
915
|
+
const files = findAllJsonlFiles(projectsDir);
|
|
916
|
+
const dailyMap = new Map();
|
|
917
|
+
const projectMap = new Map();
|
|
918
|
+
const toolMap = new Map();
|
|
919
|
+
let totalMessages = 0;
|
|
920
|
+
let totalTokens = 0;
|
|
921
|
+
let totalSize = 0;
|
|
922
|
+
let totalImages = 0;
|
|
923
|
+
let totalDocuments = 0;
|
|
924
|
+
let problematicContent = 0;
|
|
925
|
+
let totalToolUses = 0;
|
|
926
|
+
for (const file of files) {
|
|
927
|
+
const project = getProjectFromPath(file);
|
|
928
|
+
const stats = getConversationStats(file);
|
|
929
|
+
const contextEst = estimateContextSize(file);
|
|
930
|
+
totalSize += stats.fileSizeBytes;
|
|
931
|
+
totalMessages += stats.totalMessages;
|
|
932
|
+
totalTokens += contextEst.totalTokens;
|
|
933
|
+
totalImages += stats.imageCount;
|
|
934
|
+
totalDocuments += stats.documentCount;
|
|
935
|
+
problematicContent += stats.problematicContent;
|
|
936
|
+
// Update project stats
|
|
937
|
+
const existing = projectMap.get(project) || {
|
|
938
|
+
project,
|
|
939
|
+
conversations: 0,
|
|
940
|
+
messages: 0,
|
|
941
|
+
tokens: 0,
|
|
942
|
+
lastActive: new Date(0),
|
|
943
|
+
};
|
|
944
|
+
existing.conversations++;
|
|
945
|
+
existing.messages += stats.totalMessages;
|
|
946
|
+
existing.tokens += contextEst.totalTokens;
|
|
947
|
+
if (stats.lastModified > existing.lastActive) {
|
|
948
|
+
existing.lastActive = stats.lastModified;
|
|
949
|
+
}
|
|
950
|
+
projectMap.set(project, existing);
|
|
951
|
+
// Parse file for daily activity and tool usage
|
|
952
|
+
let content;
|
|
953
|
+
try {
|
|
954
|
+
content = fs.readFileSync(file, "utf-8");
|
|
955
|
+
}
|
|
956
|
+
catch {
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
const lines = content.split("\n");
|
|
960
|
+
for (const line of lines) {
|
|
961
|
+
if (!line.trim())
|
|
962
|
+
continue;
|
|
963
|
+
let data;
|
|
964
|
+
try {
|
|
965
|
+
data = JSON.parse(line);
|
|
966
|
+
}
|
|
967
|
+
catch {
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
// Track daily activity
|
|
971
|
+
const timestamp = data.timestamp;
|
|
972
|
+
const dateStr = getDateFromTimestamp(timestamp);
|
|
973
|
+
if (dateStr) {
|
|
974
|
+
const daily = dailyMap.get(dateStr) || { date: dateStr, messages: 0, tokens: 0, conversations: 0 };
|
|
975
|
+
daily.messages++;
|
|
976
|
+
dailyMap.set(dateStr, daily);
|
|
977
|
+
}
|
|
978
|
+
// Track tool usage
|
|
979
|
+
const message = data.message;
|
|
980
|
+
if (message?.content && Array.isArray(message.content)) {
|
|
981
|
+
for (const item of message.content) {
|
|
982
|
+
if (typeof item === "object" && item !== null) {
|
|
983
|
+
const itemObj = item;
|
|
984
|
+
if (itemObj.type === "tool_use") {
|
|
985
|
+
const toolName = itemObj.name;
|
|
986
|
+
toolMap.set(toolName, (toolMap.get(toolName) || 0) + 1);
|
|
987
|
+
totalToolUses++;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
// Fill in missing dates for last N days
|
|
995
|
+
for (let i = 0; i < days; i++) {
|
|
996
|
+
const date = new Date();
|
|
997
|
+
date.setDate(date.getDate() - i);
|
|
998
|
+
const dateStr = date.toISOString().split("T")[0];
|
|
999
|
+
if (!dailyMap.has(dateStr)) {
|
|
1000
|
+
dailyMap.set(dateStr, { date: dateStr, messages: 0, tokens: 0, conversations: 0 });
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
// Sort daily activity by date
|
|
1004
|
+
const dailyActivity = Array.from(dailyMap.values())
|
|
1005
|
+
.sort((a, b) => a.date.localeCompare(b.date))
|
|
1006
|
+
.slice(-days);
|
|
1007
|
+
// Sort projects by activity (messages)
|
|
1008
|
+
const topProjects = Array.from(projectMap.values())
|
|
1009
|
+
.sort((a, b) => b.messages - a.messages)
|
|
1010
|
+
.slice(0, 10);
|
|
1011
|
+
// Sort tools by usage
|
|
1012
|
+
const toolUsage = Array.from(toolMap.entries())
|
|
1013
|
+
.sort((a, b) => b[1] - a[1])
|
|
1014
|
+
.slice(0, 15)
|
|
1015
|
+
.map(([name, count]) => ({
|
|
1016
|
+
name,
|
|
1017
|
+
count,
|
|
1018
|
+
percentage: totalToolUses > 0 ? Math.round((count / totalToolUses) * 100) : 0,
|
|
1019
|
+
}));
|
|
1020
|
+
return {
|
|
1021
|
+
overview: {
|
|
1022
|
+
totalConversations: files.length,
|
|
1023
|
+
totalMessages,
|
|
1024
|
+
totalTokens,
|
|
1025
|
+
totalSize,
|
|
1026
|
+
activeProjects: projectMap.size,
|
|
1027
|
+
avgMessagesPerConversation: files.length > 0 ? Math.round(totalMessages / files.length) : 0,
|
|
1028
|
+
avgTokensPerConversation: files.length > 0 ? Math.round(totalTokens / files.length) : 0,
|
|
1029
|
+
},
|
|
1030
|
+
dailyActivity,
|
|
1031
|
+
topProjects,
|
|
1032
|
+
toolUsage,
|
|
1033
|
+
mediaStats: {
|
|
1034
|
+
totalImages,
|
|
1035
|
+
totalDocuments,
|
|
1036
|
+
problematicContent,
|
|
1037
|
+
},
|
|
1038
|
+
generatedAt: new Date(),
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
function createAsciiBar(value, max, width = 20) {
|
|
1042
|
+
const filled = max > 0 ? Math.round((value / max) * width) : 0;
|
|
1043
|
+
return "█".repeat(filled) + "░".repeat(width - filled);
|
|
1044
|
+
}
|
|
1045
|
+
export function formatUsageAnalytics(analytics) {
|
|
1046
|
+
const lines = [];
|
|
1047
|
+
const o = analytics.overview;
|
|
1048
|
+
lines.push("╔══════════════════════════════════════════════════════════════╗");
|
|
1049
|
+
lines.push("║ USAGE ANALYTICS DASHBOARD ║");
|
|
1050
|
+
lines.push("╚══════════════════════════════════════════════════════════════╝");
|
|
1051
|
+
lines.push("");
|
|
1052
|
+
// Overview section
|
|
1053
|
+
lines.push("📊 OVERVIEW");
|
|
1054
|
+
lines.push("─".repeat(50));
|
|
1055
|
+
lines.push(` Conversations: ${o.totalConversations.toLocaleString()}`);
|
|
1056
|
+
lines.push(` Total Messages: ${o.totalMessages.toLocaleString()}`);
|
|
1057
|
+
lines.push(` Total Tokens: ~${o.totalTokens.toLocaleString()}`);
|
|
1058
|
+
lines.push(` Total Size: ${formatBytesForAnalytics(o.totalSize)}`);
|
|
1059
|
+
lines.push(` Active Projects: ${o.activeProjects}`);
|
|
1060
|
+
lines.push(` Avg Msgs/Conv: ${o.avgMessagesPerConversation}`);
|
|
1061
|
+
lines.push(` Avg Tokens/Conv: ~${o.avgTokensPerConversation.toLocaleString()}`);
|
|
1062
|
+
lines.push("");
|
|
1063
|
+
// Activity chart (last 7 days)
|
|
1064
|
+
const last7Days = analytics.dailyActivity.slice(-7);
|
|
1065
|
+
const maxMessages = Math.max(...last7Days.map(d => d.messages), 1);
|
|
1066
|
+
lines.push("📈 ACTIVITY (Last 7 days)");
|
|
1067
|
+
lines.push("─".repeat(50));
|
|
1068
|
+
for (const day of last7Days) {
|
|
1069
|
+
const dayName = new Date(day.date).toLocaleDateString("en-US", { weekday: "short" });
|
|
1070
|
+
const bar = createAsciiBar(day.messages, maxMessages, 25);
|
|
1071
|
+
lines.push(` ${dayName} │${bar}│ ${day.messages}`);
|
|
1072
|
+
}
|
|
1073
|
+
lines.push("");
|
|
1074
|
+
// Top projects
|
|
1075
|
+
if (analytics.topProjects.length > 0) {
|
|
1076
|
+
lines.push("🏆 TOP PROJECTS (by messages)");
|
|
1077
|
+
lines.push("─".repeat(50));
|
|
1078
|
+
const maxProjMsgs = analytics.topProjects[0]?.messages || 1;
|
|
1079
|
+
for (const proj of analytics.topProjects.slice(0, 5)) {
|
|
1080
|
+
const shortName = proj.project.length > 25 ? "..." + proj.project.slice(-22) : proj.project;
|
|
1081
|
+
const bar = createAsciiBar(proj.messages, maxProjMsgs, 15);
|
|
1082
|
+
lines.push(` ${shortName.padEnd(25)} │${bar}│ ${proj.messages}`);
|
|
1083
|
+
}
|
|
1084
|
+
lines.push("");
|
|
1085
|
+
}
|
|
1086
|
+
// Tool usage
|
|
1087
|
+
if (analytics.toolUsage.length > 0) {
|
|
1088
|
+
lines.push("🔧 TOP TOOLS");
|
|
1089
|
+
lines.push("─".repeat(50));
|
|
1090
|
+
for (const tool of analytics.toolUsage.slice(0, 8)) {
|
|
1091
|
+
const shortName = tool.name.length > 20 ? tool.name.slice(0, 17) + "..." : tool.name;
|
|
1092
|
+
lines.push(` ${shortName.padEnd(20)} ${tool.count.toString().padStart(6)} (${tool.percentage}%)`);
|
|
1093
|
+
}
|
|
1094
|
+
lines.push("");
|
|
1095
|
+
}
|
|
1096
|
+
// Media stats
|
|
1097
|
+
const m = analytics.mediaStats;
|
|
1098
|
+
lines.push("🖼️ MEDIA");
|
|
1099
|
+
lines.push("─".repeat(50));
|
|
1100
|
+
lines.push(` Images: ${m.totalImages}`);
|
|
1101
|
+
lines.push(` Documents: ${m.totalDocuments}`);
|
|
1102
|
+
if (m.problematicContent > 0) {
|
|
1103
|
+
lines.push(` ⚠️ Oversized: ${m.problematicContent}`);
|
|
1104
|
+
}
|
|
1105
|
+
lines.push("");
|
|
1106
|
+
lines.push(`Generated: ${analytics.generatedAt.toISOString()}`);
|
|
1107
|
+
return lines.join("\n");
|
|
1108
|
+
}
|
|
1109
|
+
function formatBytesForAnalytics(bytes) {
|
|
1110
|
+
if (bytes < 1024)
|
|
1111
|
+
return `${bytes} B`;
|
|
1112
|
+
if (bytes < 1024 * 1024)
|
|
1113
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1114
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
1115
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1116
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
1117
|
+
}
|
|
1118
|
+
function simpleHash(str) {
|
|
1119
|
+
let hash = 0;
|
|
1120
|
+
for (let i = 0; i < str.length; i++) {
|
|
1121
|
+
const char = str.charCodeAt(i);
|
|
1122
|
+
hash = ((hash << 5) - hash) + char;
|
|
1123
|
+
hash = hash & hash;
|
|
1124
|
+
}
|
|
1125
|
+
return Math.abs(hash).toString(36);
|
|
1126
|
+
}
|
|
1127
|
+
function contentFingerprint(content, maxLen = 10000) {
|
|
1128
|
+
const normalized = content.slice(0, maxLen);
|
|
1129
|
+
return simpleHash(normalized);
|
|
1130
|
+
}
|
|
1131
|
+
export function findDuplicates(projectsDir) {
|
|
1132
|
+
const files = findAllJsonlFiles(projectsDir);
|
|
1133
|
+
const contentHashes = new Map();
|
|
1134
|
+
const conversationHashes = new Map();
|
|
1135
|
+
for (const file of files) {
|
|
1136
|
+
let content;
|
|
1137
|
+
try {
|
|
1138
|
+
content = fs.readFileSync(file, "utf-8");
|
|
1139
|
+
}
|
|
1140
|
+
catch {
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
const fileStats = fs.statSync(file);
|
|
1144
|
+
const convHash = contentFingerprint(content, 50000);
|
|
1145
|
+
const convLocations = conversationHashes.get(convHash) || [];
|
|
1146
|
+
convLocations.push({
|
|
1147
|
+
file,
|
|
1148
|
+
type: "text",
|
|
1149
|
+
size: fileStats.size,
|
|
1150
|
+
});
|
|
1151
|
+
conversationHashes.set(convHash, convLocations);
|
|
1152
|
+
const lines = content.split("\n");
|
|
1153
|
+
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
1154
|
+
const line = lines[lineNum];
|
|
1155
|
+
if (!line.trim())
|
|
1156
|
+
continue;
|
|
1157
|
+
let data;
|
|
1158
|
+
try {
|
|
1159
|
+
data = JSON.parse(line);
|
|
1160
|
+
}
|
|
1161
|
+
catch {
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
const processContent = (contentArray, location) => {
|
|
1165
|
+
if (!Array.isArray(contentArray))
|
|
1166
|
+
return;
|
|
1167
|
+
for (const item of contentArray) {
|
|
1168
|
+
if (typeof item !== "object" || item === null)
|
|
1169
|
+
continue;
|
|
1170
|
+
const itemObj = item;
|
|
1171
|
+
if (itemObj.type === "image" || itemObj.type === "document") {
|
|
1172
|
+
const source = itemObj.source;
|
|
1173
|
+
if (source?.type === "base64") {
|
|
1174
|
+
const data = source.data;
|
|
1175
|
+
if (data && data.length > 1000) {
|
|
1176
|
+
const hash = contentFingerprint(data);
|
|
1177
|
+
const contentType = itemObj.type;
|
|
1178
|
+
const size = data.length;
|
|
1179
|
+
const locations = contentHashes.get(hash) || [];
|
|
1180
|
+
locations.push({
|
|
1181
|
+
file,
|
|
1182
|
+
line: lineNum + 1,
|
|
1183
|
+
type: contentType,
|
|
1184
|
+
size,
|
|
1185
|
+
});
|
|
1186
|
+
contentHashes.set(hash, locations);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
if (itemObj.type === "tool_result") {
|
|
1191
|
+
const innerContent = itemObj.content;
|
|
1192
|
+
if (Array.isArray(innerContent)) {
|
|
1193
|
+
processContent(innerContent, `${location}->tool_result`);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
const message = data.message;
|
|
1199
|
+
if (message?.content) {
|
|
1200
|
+
processContent(message.content, "message");
|
|
1201
|
+
}
|
|
1202
|
+
const toolUseResult = data.toolUseResult;
|
|
1203
|
+
if (toolUseResult?.content) {
|
|
1204
|
+
processContent(toolUseResult.content, "toolUseResult");
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
const conversationDuplicates = [];
|
|
1209
|
+
const contentDuplicates = [];
|
|
1210
|
+
let duplicateImages = 0;
|
|
1211
|
+
let duplicateDocuments = 0;
|
|
1212
|
+
let duplicateTextBlocks = 0;
|
|
1213
|
+
let totalWastedSize = 0;
|
|
1214
|
+
for (const [hash, locations] of conversationHashes) {
|
|
1215
|
+
if (locations.length > 1) {
|
|
1216
|
+
const totalSize = locations.reduce((sum, loc) => sum + loc.size, 0);
|
|
1217
|
+
const wastedSize = totalSize - locations[0].size;
|
|
1218
|
+
conversationDuplicates.push({
|
|
1219
|
+
hash,
|
|
1220
|
+
type: "conversation",
|
|
1221
|
+
contentType: "conversation",
|
|
1222
|
+
locations,
|
|
1223
|
+
totalSize,
|
|
1224
|
+
wastedSize,
|
|
1225
|
+
});
|
|
1226
|
+
totalWastedSize += wastedSize;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
for (const [hash, locations] of contentHashes) {
|
|
1230
|
+
if (locations.length > 1) {
|
|
1231
|
+
const totalSize = locations.reduce((sum, loc) => sum + loc.size, 0);
|
|
1232
|
+
const wastedSize = totalSize - locations[0].size;
|
|
1233
|
+
const contentType = locations[0].type;
|
|
1234
|
+
contentDuplicates.push({
|
|
1235
|
+
hash,
|
|
1236
|
+
type: "content",
|
|
1237
|
+
contentType,
|
|
1238
|
+
locations,
|
|
1239
|
+
totalSize,
|
|
1240
|
+
wastedSize,
|
|
1241
|
+
});
|
|
1242
|
+
totalWastedSize += wastedSize;
|
|
1243
|
+
if (contentType === "image") {
|
|
1244
|
+
duplicateImages += locations.length - 1;
|
|
1245
|
+
}
|
|
1246
|
+
else if (contentType === "document") {
|
|
1247
|
+
duplicateDocuments += locations.length - 1;
|
|
1248
|
+
}
|
|
1249
|
+
else {
|
|
1250
|
+
duplicateTextBlocks += locations.length - 1;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
conversationDuplicates.sort((a, b) => b.wastedSize - a.wastedSize);
|
|
1255
|
+
contentDuplicates.sort((a, b) => b.wastedSize - a.wastedSize);
|
|
1256
|
+
return {
|
|
1257
|
+
totalDuplicateGroups: conversationDuplicates.length + contentDuplicates.length,
|
|
1258
|
+
totalWastedSize,
|
|
1259
|
+
conversationDuplicates,
|
|
1260
|
+
contentDuplicates,
|
|
1261
|
+
summary: {
|
|
1262
|
+
duplicateImages,
|
|
1263
|
+
duplicateDocuments,
|
|
1264
|
+
duplicateTextBlocks,
|
|
1265
|
+
potentialSavings: totalWastedSize,
|
|
1266
|
+
},
|
|
1267
|
+
generatedAt: new Date(),
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
export function formatDuplicateReport(report) {
|
|
1271
|
+
const lines = [];
|
|
1272
|
+
lines.push("╔══════════════════════════════════════════════════════════════╗");
|
|
1273
|
+
lines.push("║ DUPLICATE DETECTION REPORT ║");
|
|
1274
|
+
lines.push("╚══════════════════════════════════════════════════════════════╝");
|
|
1275
|
+
lines.push("");
|
|
1276
|
+
lines.push("📊 SUMMARY");
|
|
1277
|
+
lines.push("─".repeat(50));
|
|
1278
|
+
lines.push(` Duplicate groups: ${report.totalDuplicateGroups}`);
|
|
1279
|
+
lines.push(` Duplicate images: ${report.summary.duplicateImages}`);
|
|
1280
|
+
lines.push(` Duplicate documents: ${report.summary.duplicateDocuments}`);
|
|
1281
|
+
lines.push(` Wasted space: ${formatBytesForAnalytics(report.totalWastedSize)}`);
|
|
1282
|
+
lines.push("");
|
|
1283
|
+
if (report.conversationDuplicates.length > 0) {
|
|
1284
|
+
lines.push("📁 DUPLICATE CONVERSATIONS");
|
|
1285
|
+
lines.push("─".repeat(50));
|
|
1286
|
+
for (const group of report.conversationDuplicates.slice(0, 5)) {
|
|
1287
|
+
lines.push(` [${group.locations.length} copies] Wasted: ${formatBytesForAnalytics(group.wastedSize)}`);
|
|
1288
|
+
for (const loc of group.locations.slice(0, 3)) {
|
|
1289
|
+
const shortPath = loc.file.length > 45 ? "..." + loc.file.slice(-42) : loc.file;
|
|
1290
|
+
lines.push(` - ${shortPath}`);
|
|
1291
|
+
}
|
|
1292
|
+
if (group.locations.length > 3) {
|
|
1293
|
+
lines.push(` ... and ${group.locations.length - 3} more`);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
if (report.conversationDuplicates.length > 5) {
|
|
1297
|
+
lines.push(` ... and ${report.conversationDuplicates.length - 5} more duplicate groups`);
|
|
1298
|
+
}
|
|
1299
|
+
lines.push("");
|
|
1300
|
+
}
|
|
1301
|
+
if (report.contentDuplicates.length > 0) {
|
|
1302
|
+
lines.push("🖼️ DUPLICATE CONTENT");
|
|
1303
|
+
lines.push("─".repeat(50));
|
|
1304
|
+
for (const group of report.contentDuplicates.slice(0, 10)) {
|
|
1305
|
+
const typeIcon = group.contentType === "image" ? "🖼️" : group.contentType === "document" ? "📄" : "📝";
|
|
1306
|
+
lines.push(` ${typeIcon} ${group.contentType} [${group.locations.length} copies] ~${formatBytesForAnalytics(group.wastedSize)} wasted`);
|
|
1307
|
+
for (const loc of group.locations.slice(0, 2)) {
|
|
1308
|
+
const shortPath = loc.file.length > 35 ? "..." + loc.file.slice(-32) : loc.file;
|
|
1309
|
+
lines.push(` - ${shortPath}:${loc.line}`);
|
|
1310
|
+
}
|
|
1311
|
+
if (group.locations.length > 2) {
|
|
1312
|
+
lines.push(` ... and ${group.locations.length - 2} more locations`);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
if (report.contentDuplicates.length > 10) {
|
|
1316
|
+
lines.push(` ... and ${report.contentDuplicates.length - 10} more duplicate content groups`);
|
|
1317
|
+
}
|
|
1318
|
+
lines.push("");
|
|
1319
|
+
}
|
|
1320
|
+
if (report.totalDuplicateGroups === 0) {
|
|
1321
|
+
lines.push("✓ No duplicates found!");
|
|
1322
|
+
lines.push("");
|
|
1323
|
+
}
|
|
1324
|
+
else {
|
|
1325
|
+
lines.push("💡 RECOMMENDATIONS");
|
|
1326
|
+
lines.push("─".repeat(50));
|
|
1327
|
+
if (report.conversationDuplicates.length > 0) {
|
|
1328
|
+
lines.push(" - Review duplicate conversations and consider removing copies");
|
|
1329
|
+
}
|
|
1330
|
+
if (report.summary.duplicateImages > 0) {
|
|
1331
|
+
lines.push(" - Same images appear multiple times in your conversations");
|
|
1332
|
+
}
|
|
1333
|
+
if (report.summary.duplicateDocuments > 0) {
|
|
1334
|
+
lines.push(" - Same documents appear multiple times in your conversations");
|
|
1335
|
+
}
|
|
1336
|
+
lines.push("");
|
|
1337
|
+
}
|
|
1338
|
+
lines.push(`Generated: ${report.generatedAt.toISOString()}`);
|
|
1339
|
+
return lines.join("\n");
|
|
1340
|
+
}
|
|
1341
|
+
export function findArchiveCandidates(projectsDir, options = {}) {
|
|
1342
|
+
const { minDaysInactive = 30, minMessages = 0 } = options;
|
|
1343
|
+
const files = findAllJsonlFiles(projectsDir);
|
|
1344
|
+
const candidates = [];
|
|
1345
|
+
const now = new Date();
|
|
1346
|
+
for (const file of files) {
|
|
1347
|
+
try {
|
|
1348
|
+
const stats = getConversationStats(file);
|
|
1349
|
+
const daysSince = Math.floor((now.getTime() - stats.lastModified.getTime()) / (1000 * 60 * 60 * 24));
|
|
1350
|
+
if (daysSince >= minDaysInactive && stats.totalMessages >= minMessages) {
|
|
1351
|
+
candidates.push({
|
|
1352
|
+
file,
|
|
1353
|
+
lastModified: stats.lastModified,
|
|
1354
|
+
messageCount: stats.totalMessages,
|
|
1355
|
+
sizeBytes: stats.fileSizeBytes,
|
|
1356
|
+
daysSinceActivity: daysSince,
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
catch {
|
|
1361
|
+
continue;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
candidates.sort((a, b) => b.daysSinceActivity - a.daysSinceActivity);
|
|
1365
|
+
return candidates;
|
|
1366
|
+
}
|
|
1367
|
+
export function archiveConversations(projectsDir, options = {}) {
|
|
1368
|
+
const { minDaysInactive = 30, dryRun = false } = options;
|
|
1369
|
+
const archiveDir = path.join(path.dirname(projectsDir), "archive");
|
|
1370
|
+
const candidates = findArchiveCandidates(projectsDir, { minDaysInactive });
|
|
1371
|
+
const archived = [];
|
|
1372
|
+
const skipped = [];
|
|
1373
|
+
let totalSize = 0;
|
|
1374
|
+
if (!dryRun && candidates.length > 0 && !fs.existsSync(archiveDir)) {
|
|
1375
|
+
fs.mkdirSync(archiveDir, { recursive: true });
|
|
1376
|
+
}
|
|
1377
|
+
for (const candidate of candidates) {
|
|
1378
|
+
const relativePath = path.relative(projectsDir, candidate.file);
|
|
1379
|
+
const archivePath = path.join(archiveDir, relativePath);
|
|
1380
|
+
if (dryRun) {
|
|
1381
|
+
archived.push(candidate.file);
|
|
1382
|
+
totalSize += candidate.sizeBytes;
|
|
1383
|
+
continue;
|
|
1384
|
+
}
|
|
1385
|
+
try {
|
|
1386
|
+
const archiveSubDir = path.dirname(archivePath);
|
|
1387
|
+
if (!fs.existsSync(archiveSubDir)) {
|
|
1388
|
+
fs.mkdirSync(archiveSubDir, { recursive: true });
|
|
1389
|
+
}
|
|
1390
|
+
fs.renameSync(candidate.file, archivePath);
|
|
1391
|
+
archived.push(candidate.file);
|
|
1392
|
+
totalSize += candidate.sizeBytes;
|
|
1393
|
+
}
|
|
1394
|
+
catch {
|
|
1395
|
+
skipped.push(candidate.file);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
return { archived, skipped, totalSize, archiveDir };
|
|
1399
|
+
}
|
|
1400
|
+
export function formatArchiveReport(candidates, result, dryRun = false) {
|
|
1401
|
+
const lines = [];
|
|
1402
|
+
lines.push("╔══════════════════════════════════════════════════════════════╗");
|
|
1403
|
+
lines.push("║ CONVERSATION ARCHIVE REPORT ║");
|
|
1404
|
+
lines.push("╚══════════════════════════════════════════════════════════════╝");
|
|
1405
|
+
lines.push("");
|
|
1406
|
+
if (candidates.length === 0) {
|
|
1407
|
+
lines.push("✓ No conversations eligible for archiving.");
|
|
1408
|
+
lines.push("");
|
|
1409
|
+
return lines.join("\n");
|
|
1410
|
+
}
|
|
1411
|
+
const totalSize = candidates.reduce((sum, c) => sum + c.sizeBytes, 0);
|
|
1412
|
+
lines.push("📊 SUMMARY");
|
|
1413
|
+
lines.push("─".repeat(50));
|
|
1414
|
+
lines.push(` Eligible conversations: ${candidates.length}`);
|
|
1415
|
+
lines.push(` Total size: ${formatBytesForAnalytics(totalSize)}`);
|
|
1416
|
+
lines.push("");
|
|
1417
|
+
lines.push("📁 ARCHIVE CANDIDATES");
|
|
1418
|
+
lines.push("─".repeat(50));
|
|
1419
|
+
for (const c of candidates.slice(0, 10)) {
|
|
1420
|
+
const shortPath = c.file.length > 45 ? "..." + c.file.slice(-42) : c.file;
|
|
1421
|
+
lines.push(` ${shortPath}`);
|
|
1422
|
+
lines.push(` ${c.daysSinceActivity} days inactive, ${c.messageCount} msgs, ${formatBytesForAnalytics(c.sizeBytes)}`);
|
|
1423
|
+
}
|
|
1424
|
+
if (candidates.length > 10) {
|
|
1425
|
+
lines.push(` ... and ${candidates.length - 10} more`);
|
|
1426
|
+
}
|
|
1427
|
+
lines.push("");
|
|
1428
|
+
if (result) {
|
|
1429
|
+
if (dryRun) {
|
|
1430
|
+
lines.push("📋 DRY RUN - No changes made");
|
|
1431
|
+
lines.push("─".repeat(50));
|
|
1432
|
+
lines.push(` Would archive: ${result.archived.length} conversations`);
|
|
1433
|
+
lines.push(` Would free: ${formatBytesForAnalytics(result.totalSize)}`);
|
|
1434
|
+
lines.push(` Archive to: ${result.archiveDir}`);
|
|
1435
|
+
}
|
|
1436
|
+
else {
|
|
1437
|
+
lines.push("✓ ARCHIVED");
|
|
1438
|
+
lines.push("─".repeat(50));
|
|
1439
|
+
lines.push(` Archived: ${result.archived.length} conversations`);
|
|
1440
|
+
lines.push(` Freed: ${formatBytesForAnalytics(result.totalSize)}`);
|
|
1441
|
+
lines.push(` Archive at: ${result.archiveDir}`);
|
|
1442
|
+
if (result.skipped.length > 0) {
|
|
1443
|
+
lines.push(` Skipped: ${result.skipped.length} (errors)`);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
lines.push("");
|
|
1447
|
+
}
|
|
1448
|
+
return lines.join("\n");
|
|
1449
|
+
}
|
|
1450
|
+
const DEFAULT_CONFIG = {
|
|
1451
|
+
autoFix: true,
|
|
1452
|
+
autoCleanupDays: 7,
|
|
1453
|
+
autoArchiveDays: 60,
|
|
1454
|
+
enabled: false,
|
|
1455
|
+
};
|
|
1456
|
+
export function getMaintenanceConfigPath() {
|
|
1457
|
+
return path.join(path.dirname(path.dirname(process.env.HOME || "")), ".claude", "maintenance.json");
|
|
1458
|
+
}
|
|
1459
|
+
export function loadMaintenanceConfig(configPath) {
|
|
1460
|
+
const filePath = configPath || path.join(process.env.HOME || "", ".claude", "maintenance.json");
|
|
1461
|
+
try {
|
|
1462
|
+
if (fs.existsSync(filePath)) {
|
|
1463
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
1464
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
catch {
|
|
1468
|
+
// Use defaults
|
|
1469
|
+
}
|
|
1470
|
+
return DEFAULT_CONFIG;
|
|
1471
|
+
}
|
|
1472
|
+
export function saveMaintenanceConfig(config, configPath) {
|
|
1473
|
+
const filePath = configPath || path.join(process.env.HOME || "", ".claude", "maintenance.json");
|
|
1474
|
+
const dir = path.dirname(filePath);
|
|
1475
|
+
if (!fs.existsSync(dir)) {
|
|
1476
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1477
|
+
}
|
|
1478
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2), "utf-8");
|
|
1479
|
+
}
|
|
1480
|
+
export function runMaintenance(projectsDir, options = {}) {
|
|
1481
|
+
const { dryRun = true, config = DEFAULT_CONFIG } = options;
|
|
1482
|
+
const actions = [];
|
|
1483
|
+
const recommendations = [];
|
|
1484
|
+
// Check for issues to fix
|
|
1485
|
+
const files = findAllJsonlFiles(projectsDir);
|
|
1486
|
+
let issueCount = 0;
|
|
1487
|
+
let issueSize = 0;
|
|
1488
|
+
for (const file of files) {
|
|
1489
|
+
const scanResult = scanFile(file);
|
|
1490
|
+
if (scanResult.issues.length > 0) {
|
|
1491
|
+
issueCount += scanResult.issues.length;
|
|
1492
|
+
for (const issue of scanResult.issues) {
|
|
1493
|
+
issueSize += issue.estimatedSize;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
if (issueCount > 0) {
|
|
1498
|
+
actions.push({
|
|
1499
|
+
type: "fix",
|
|
1500
|
+
description: "Oversized content detected",
|
|
1501
|
+
count: issueCount,
|
|
1502
|
+
sizeBytes: issueSize,
|
|
1503
|
+
});
|
|
1504
|
+
if (config.autoFix && !dryRun) {
|
|
1505
|
+
for (const file of files) {
|
|
1506
|
+
fixFile(file, true);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
// Check for old backups
|
|
1511
|
+
const backups = findBackupFiles(projectsDir);
|
|
1512
|
+
const oldBackups = backups.filter(b => {
|
|
1513
|
+
try {
|
|
1514
|
+
const stat = fs.statSync(b);
|
|
1515
|
+
const daysOld = Math.floor((Date.now() - stat.mtime.getTime()) / (1000 * 60 * 60 * 24));
|
|
1516
|
+
return daysOld > config.autoCleanupDays;
|
|
1517
|
+
}
|
|
1518
|
+
catch {
|
|
1519
|
+
return false;
|
|
1520
|
+
}
|
|
1521
|
+
});
|
|
1522
|
+
if (oldBackups.length > 0) {
|
|
1523
|
+
const backupSize = oldBackups.reduce((sum, b) => {
|
|
1524
|
+
try {
|
|
1525
|
+
return sum + fs.statSync(b).size;
|
|
1526
|
+
}
|
|
1527
|
+
catch {
|
|
1528
|
+
return sum;
|
|
1529
|
+
}
|
|
1530
|
+
}, 0);
|
|
1531
|
+
actions.push({
|
|
1532
|
+
type: "cleanup",
|
|
1533
|
+
description: `Backups older than ${config.autoCleanupDays} days`,
|
|
1534
|
+
count: oldBackups.length,
|
|
1535
|
+
sizeBytes: backupSize,
|
|
1536
|
+
});
|
|
1537
|
+
if (!dryRun) {
|
|
1538
|
+
deleteOldBackups(projectsDir, config.autoCleanupDays);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
// Check for archive candidates
|
|
1542
|
+
const archiveCandidates = findArchiveCandidates(projectsDir, {
|
|
1543
|
+
minDaysInactive: config.autoArchiveDays,
|
|
1544
|
+
});
|
|
1545
|
+
if (archiveCandidates.length > 0) {
|
|
1546
|
+
const archiveSize = archiveCandidates.reduce((sum, c) => sum + c.sizeBytes, 0);
|
|
1547
|
+
actions.push({
|
|
1548
|
+
type: "archive",
|
|
1549
|
+
description: `Conversations inactive for ${config.autoArchiveDays}+ days`,
|
|
1550
|
+
count: archiveCandidates.length,
|
|
1551
|
+
sizeBytes: archiveSize,
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
// Determine status
|
|
1555
|
+
let status = "healthy";
|
|
1556
|
+
if (issueCount > 0) {
|
|
1557
|
+
status = "critical";
|
|
1558
|
+
recommendations.push("Run 'cct fix' to remove oversized content");
|
|
1559
|
+
}
|
|
1560
|
+
else if (oldBackups.length > 5 || archiveCandidates.length > 10) {
|
|
1561
|
+
status = "needs_attention";
|
|
1562
|
+
}
|
|
1563
|
+
if (oldBackups.length > 0) {
|
|
1564
|
+
recommendations.push(`Run 'cct cleanup --days ${config.autoCleanupDays}' to remove old backups`);
|
|
1565
|
+
}
|
|
1566
|
+
if (archiveCandidates.length > 0) {
|
|
1567
|
+
recommendations.push(`Run 'cct archive --days ${config.autoArchiveDays}' to archive inactive conversations`);
|
|
1568
|
+
}
|
|
1569
|
+
const nextRun = new Date();
|
|
1570
|
+
nextRun.setDate(nextRun.getDate() + 7);
|
|
1571
|
+
return {
|
|
1572
|
+
status,
|
|
1573
|
+
actions,
|
|
1574
|
+
recommendations,
|
|
1575
|
+
nextRecommendedRun: nextRun,
|
|
1576
|
+
generatedAt: new Date(),
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
export function formatMaintenanceReport(report, dryRun = true) {
|
|
1580
|
+
const lines = [];
|
|
1581
|
+
lines.push("╔══════════════════════════════════════════════════════════════╗");
|
|
1582
|
+
lines.push("║ MAINTENANCE REPORT ║");
|
|
1583
|
+
lines.push("╚══════════════════════════════════════════════════════════════╝");
|
|
1584
|
+
lines.push("");
|
|
1585
|
+
const statusIcon = report.status === "healthy" ? "✓" : report.status === "critical" ? "✗" : "⚠";
|
|
1586
|
+
const statusColor = report.status === "healthy" ? "Healthy" : report.status === "critical" ? "Critical" : "Needs Attention";
|
|
1587
|
+
lines.push("📊 STATUS");
|
|
1588
|
+
lines.push("─".repeat(50));
|
|
1589
|
+
lines.push(` ${statusIcon} ${statusColor}`);
|
|
1590
|
+
lines.push("");
|
|
1591
|
+
if (report.actions.length > 0) {
|
|
1592
|
+
lines.push(dryRun ? "📋 PENDING ACTIONS (dry run)" : "📋 ACTIONS TAKEN");
|
|
1593
|
+
lines.push("─".repeat(50));
|
|
1594
|
+
for (const action of report.actions) {
|
|
1595
|
+
const icon = action.type === "fix" ? "🔧" : action.type === "cleanup" ? "🗑️" : "📦";
|
|
1596
|
+
const sizeStr = action.sizeBytes ? ` (~${formatBytesForAnalytics(action.sizeBytes)})` : "";
|
|
1597
|
+
lines.push(` ${icon} ${action.description}`);
|
|
1598
|
+
lines.push(` ${action.count} item(s)${sizeStr}`);
|
|
1599
|
+
}
|
|
1600
|
+
lines.push("");
|
|
1601
|
+
}
|
|
1602
|
+
if (report.recommendations.length > 0) {
|
|
1603
|
+
lines.push("💡 RECOMMENDATIONS");
|
|
1604
|
+
lines.push("─".repeat(50));
|
|
1605
|
+
for (const rec of report.recommendations) {
|
|
1606
|
+
lines.push(` - ${rec}`);
|
|
1607
|
+
}
|
|
1608
|
+
lines.push("");
|
|
1609
|
+
}
|
|
1610
|
+
if (report.actions.length === 0 && report.recommendations.length === 0) {
|
|
1611
|
+
lines.push("✓ Everything looks good! No maintenance needed.");
|
|
1612
|
+
lines.push("");
|
|
1613
|
+
}
|
|
1614
|
+
lines.push(`Generated: ${report.generatedAt.toISOString()}`);
|
|
1615
|
+
return lines.join("\n");
|
|
1616
|
+
}
|
|
1617
|
+
export function generateCronSchedule() {
|
|
1618
|
+
return `# Claude Code Toolkit Maintenance
|
|
1619
|
+
# Run weekly on Sunday at 3am
|
|
1620
|
+
0 3 * * 0 npx @asifkibria/claude-code-toolkit maintenance --auto
|
|
1621
|
+
|
|
1622
|
+
# Or add to your shell profile to run on terminal start:
|
|
1623
|
+
# npx @asifkibria/claude-code-toolkit maintenance --check
|
|
1624
|
+
`;
|
|
1625
|
+
}
|
|
1626
|
+
export function generateLaunchdPlist() {
|
|
1627
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1628
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1629
|
+
<plist version="1.0">
|
|
1630
|
+
<dict>
|
|
1631
|
+
<key>Label</key>
|
|
1632
|
+
<string>com.claude-code-toolkit.maintenance</string>
|
|
1633
|
+
<key>ProgramArguments</key>
|
|
1634
|
+
<array>
|
|
1635
|
+
<string>/usr/local/bin/npx</string>
|
|
1636
|
+
<string>@asifkibria/claude-code-toolkit</string>
|
|
1637
|
+
<string>maintenance</string>
|
|
1638
|
+
<string>--auto</string>
|
|
1639
|
+
</array>
|
|
1640
|
+
<key>StartCalendarInterval</key>
|
|
1641
|
+
<dict>
|
|
1642
|
+
<key>Weekday</key>
|
|
1643
|
+
<integer>0</integer>
|
|
1644
|
+
<key>Hour</key>
|
|
1645
|
+
<integer>3</integer>
|
|
1646
|
+
<key>Minute</key>
|
|
1647
|
+
<integer>0</integer>
|
|
1648
|
+
</dict>
|
|
1649
|
+
<key>StandardOutPath</key>
|
|
1650
|
+
<string>/tmp/claude-code-toolkit.log</string>
|
|
1651
|
+
<key>StandardErrorPath</key>
|
|
1652
|
+
<string>/tmp/claude-code-toolkit.error.log</string>
|
|
1653
|
+
</dict>
|
|
1654
|
+
</plist>
|
|
1655
|
+
`;
|
|
1656
|
+
}
|
|
447
1657
|
//# sourceMappingURL=scanner.js.map
|