@asifkibria/claude-code-toolkit 1.0.2 → 1.0.7

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