@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.
Files changed (69) hide show
  1. package/README.md +165 -214
  2. package/dist/CLAUDE.md +7 -0
  3. package/dist/__tests__/dashboard.test.d.ts +2 -0
  4. package/dist/__tests__/dashboard.test.d.ts.map +1 -0
  5. package/dist/__tests__/dashboard.test.js +606 -0
  6. package/dist/__tests__/dashboard.test.js.map +1 -0
  7. package/dist/__tests__/mcp-validator.test.d.ts +2 -0
  8. package/dist/__tests__/mcp-validator.test.d.ts.map +1 -0
  9. package/dist/__tests__/mcp-validator.test.js +217 -0
  10. package/dist/__tests__/mcp-validator.test.js.map +1 -0
  11. package/dist/__tests__/scanner.test.js +350 -1
  12. package/dist/__tests__/scanner.test.js.map +1 -1
  13. package/dist/__tests__/security.test.d.ts +2 -0
  14. package/dist/__tests__/security.test.d.ts.map +1 -0
  15. package/dist/__tests__/security.test.js +375 -0
  16. package/dist/__tests__/security.test.js.map +1 -0
  17. package/dist/__tests__/session-recovery.test.d.ts +2 -0
  18. package/dist/__tests__/session-recovery.test.d.ts.map +1 -0
  19. package/dist/__tests__/session-recovery.test.js +230 -0
  20. package/dist/__tests__/session-recovery.test.js.map +1 -0
  21. package/dist/__tests__/storage.test.d.ts +2 -0
  22. package/dist/__tests__/storage.test.d.ts.map +1 -0
  23. package/dist/__tests__/storage.test.js +241 -0
  24. package/dist/__tests__/storage.test.js.map +1 -0
  25. package/dist/__tests__/trace.test.d.ts +2 -0
  26. package/dist/__tests__/trace.test.d.ts.map +1 -0
  27. package/dist/__tests__/trace.test.js +376 -0
  28. package/dist/__tests__/trace.test.js.map +1 -0
  29. package/dist/cli.js +501 -20
  30. package/dist/cli.js.map +1 -1
  31. package/dist/index.js +950 -3
  32. package/dist/index.js.map +1 -1
  33. package/dist/lib/dashboard-ui.d.ts +2 -0
  34. package/dist/lib/dashboard-ui.d.ts.map +1 -0
  35. package/dist/lib/dashboard-ui.js +2075 -0
  36. package/dist/lib/dashboard-ui.js.map +1 -0
  37. package/dist/lib/dashboard.d.ts +15 -0
  38. package/dist/lib/dashboard.d.ts.map +1 -0
  39. package/dist/lib/dashboard.js +1422 -0
  40. package/dist/lib/dashboard.js.map +1 -0
  41. package/dist/lib/logs.d.ts +42 -0
  42. package/dist/lib/logs.d.ts.map +1 -0
  43. package/dist/lib/logs.js +166 -0
  44. package/dist/lib/logs.js.map +1 -0
  45. package/dist/lib/mcp-validator.d.ts +86 -0
  46. package/dist/lib/mcp-validator.d.ts.map +1 -0
  47. package/dist/lib/mcp-validator.js +463 -0
  48. package/dist/lib/mcp-validator.js.map +1 -0
  49. package/dist/lib/scanner.d.ts +187 -2
  50. package/dist/lib/scanner.d.ts.map +1 -1
  51. package/dist/lib/scanner.js +1224 -14
  52. package/dist/lib/scanner.js.map +1 -1
  53. package/dist/lib/security.d.ts +57 -0
  54. package/dist/lib/security.d.ts.map +1 -0
  55. package/dist/lib/security.js +423 -0
  56. package/dist/lib/security.js.map +1 -0
  57. package/dist/lib/session-recovery.d.ts +60 -0
  58. package/dist/lib/session-recovery.d.ts.map +1 -0
  59. package/dist/lib/session-recovery.js +433 -0
  60. package/dist/lib/session-recovery.js.map +1 -0
  61. package/dist/lib/storage.d.ts +68 -0
  62. package/dist/lib/storage.d.ts.map +1 -0
  63. package/dist/lib/storage.js +500 -0
  64. package/dist/lib/storage.js.map +1 -0
  65. package/dist/lib/trace.d.ts +119 -0
  66. package/dist/lib/trace.d.ts.map +1 -0
  67. package/dist/lib/trace.js +649 -0
  68. package/dist/lib/trace.js.map +1 -0
  69. package/package.json +11 -3
@@ -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 > MIN_PROBLEMATIC_TEXT_SIZE)
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) > MIN_PROBLEMATIC_BASE64_SIZE;
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) > MIN_PROBLEMATIC_BASE64_SIZE;
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) > MIN_PROBLEMATIC_TEXT_SIZE;
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