claude-highscore
Version:
Claude Code Transcript Time Analyzer - Analyze response times from claude.ai transcripts
1,350 lines (1,333 loc) • 52.7 kB
JavaScript
#!/usr/bin/env node
import { createRequire } from "node:module";
var __require = /* @__PURE__ */ createRequire(import.meta.url);
// src/cli.tsx
import { Command } from "commander";
// src/config.ts
import { promises as fs } from "fs";
import { homedir } from "os";
import { join } from "path";
var DEFAULT_CONFIG = {
defaultOutputFormat: "table",
defaultExportPath: "./cctime-export",
theme: {
primaryColor: "cyan",
accentColor: "yellow",
chartStyle: "line"
},
analysis: {
includeSystemMessages: false,
groupBySession: true,
timeFormat: "24h"
},
export: {
includeMetadata: true,
prettyPrint: true
}
};
class ConfigManager {
config;
configPath;
constructor(config, configPath) {
this.config = config;
this.configPath = configPath || join(homedir(), ".cctime", "config.json");
}
get(key) {
return this.config[key];
}
set(key, value) {
this.config[key] = value;
}
getConfig() {
return { ...this.config };
}
async save() {
const dir = join(homedir(), ".cctime");
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(this.configPath, JSON.stringify(this.config, null, 2), "utf-8");
}
merge(partial) {
this.config = deepMerge(this.config, partial);
}
}
async function loadConfig(configPath) {
const resolvedPath = configPath?.replace("~", homedir()) || join(homedir(), ".cctime", "config.json");
try {
const configData = await fs.readFile(resolvedPath, "utf-8");
const userConfig = JSON.parse(configData);
validateConfig(userConfig);
return deepMerge(DEFAULT_CONFIG, userConfig);
} catch (error) {
if (error.code === "ENOENT") {
return { ...DEFAULT_CONFIG };
}
throw new Error(`Failed to load config: ${error.message}`);
}
}
function validateConfig(config) {
if (config.defaultOutputFormat && !["json", "csv", "table"].includes(config.defaultOutputFormat)) {
throw new Error(`Invalid output format: ${config.defaultOutputFormat}`);
}
if (config.theme?.chartStyle && !["line", "bar"].includes(config.theme.chartStyle)) {
throw new Error(`Invalid chart style: ${config.theme.chartStyle}`);
}
if (config.analysis?.timeFormat && !["12h", "24h"].includes(config.analysis.timeFormat)) {
throw new Error(`Invalid time format: ${config.analysis.timeFormat}`);
}
}
function deepMerge(target, source) {
const result = { ...target };
for (const key in source) {
if (source[key] !== undefined) {
if (typeof source[key] === "object" && source[key] !== null && !Array.isArray(source[key])) {
result[key] = deepMerge(result[key], source[key]);
} else {
result[key] = source[key];
}
}
}
return result;
}
// package.json
var version = "1.8.1";
// src/export/index.ts
import * as fs2 from "fs/promises";
import * as path from "path";
function formatDate(dateStr) {
return new Date(dateStr).toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric"
});
}
function formatDuration(ms) {
if (ms < 1000)
return `${ms}ms`;
if (ms < 60000)
return `${(ms / 1000).toFixed(1)}s`;
return `${(ms / 60000).toFixed(1)}m`;
}
function toSerializable(data) {
const daily = Array.from(data.daily.entries()).map(([date, metrics]) => ({
date,
totalResponseTimeMs: metrics.totalResponseTimeMs,
averageResponseTimeMs: metrics.averageResponseTimeMs,
responseCount: metrics.responseCount,
sessionCount: metrics.sessions.size,
sessions: Array.from(metrics.sessions),
percentiles: metrics.percentiles
}));
const sessions = Array.from(data.sessions.entries()).map(([id, session]) => ({
sessionId: session.sessionId,
projectPath: session.projectPath,
totalResponses: session.totalResponses,
totalResponseTimeMs: session.totalResponseTimeMs,
averageResponseTimeMs: session.averageResponseTimeMs,
firstMessage: session.firstMessage,
lastMessage: session.lastMessage
}));
return {
summary: data.summary,
daily,
sessions
};
}
async function exportJSON(data, outputPath, includeStats) {
try {
const exportData = toSerializable(data);
if (!includeStats) {
delete exportData.sessions;
}
const jsonContent = JSON.stringify(exportData, null, 2);
await fs2.writeFile(outputPath, jsonContent, "utf-8");
return { success: true, filePath: outputPath };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error during JSON export"
};
}
}
async function exportCSV(data, outputPath, includeStats) {
try {
const headers = [
"Date",
"Total Response Time (ms)",
"Average Response Time (ms)",
"Response Count",
"Session Count",
"P50 (ms)",
"P90 (ms)",
"P99 (ms)"
];
const rows = [headers.join(",")];
const dailyData = Array.from(data.daily.entries()).sort(([a], [b]) => a.localeCompare(b));
for (const [date, metrics] of dailyData) {
const row = [
date,
metrics.totalResponseTimeMs.toString(),
metrics.averageResponseTimeMs.toFixed(2),
metrics.responseCount.toString(),
metrics.sessions.size.toString(),
metrics.percentiles.p50.toFixed(2),
metrics.percentiles.p90.toFixed(2),
metrics.percentiles.p99.toFixed(2)
];
rows.push(row.map((cell) => `"${cell}"`).join(","));
}
if (includeStats) {
rows.push("");
rows.push("Summary Statistics");
rows.push(`Total Response Time,${data.summary.totalResponseTimeMs}`);
rows.push(`Total Responses,${data.summary.totalResponses}`);
rows.push(`Average Response Time,${data.summary.averageResponseTimeMs.toFixed(2)}`);
rows.push(`Unique Sessions,${data.summary.uniqueSessions}`);
rows.push(`Date Range,"${data.summary.dateRange.from} to ${data.summary.dateRange.to}"`);
}
const csvContent = rows.join(`
`);
await fs2.writeFile(outputPath, csvContent, "utf-8");
return { success: true, filePath: outputPath };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error during CSV export"
};
}
}
async function exportMarkdown(data, outputPath, includeStats) {
try {
const lines = ["# Claude Code Response Time Analysis"];
lines.push("");
lines.push("## Summary Statistics");
lines.push("");
lines.push(`- **Total Response Time**: ${formatDuration(data.summary.totalResponseTimeMs)}`);
lines.push(`- **Total Responses**: ${data.summary.totalResponses}`);
lines.push(`- **Average Response Time**: ${formatDuration(data.summary.averageResponseTimeMs)}`);
lines.push(`- **Unique Sessions**: ${data.summary.uniqueSessions}`);
lines.push(`- **Date Range**: ${formatDate(data.summary.dateRange.from)} to ${formatDate(data.summary.dateRange.to)}`);
lines.push("");
lines.push("## Daily Metrics");
lines.push("");
lines.push("| Date | Total Time | Avg Time | Responses | Sessions | P50 | P90 | P99 |");
lines.push("|------|------------|----------|-----------|----------|-----|-----|-----|");
const dailyData = Array.from(data.daily.entries()).sort(([a], [b]) => a.localeCompare(b));
for (const [date, metrics] of dailyData) {
lines.push(`| ${formatDate(date)} | ${formatDuration(metrics.totalResponseTimeMs)} | ${formatDuration(metrics.averageResponseTimeMs)} | ${metrics.responseCount} | ${metrics.sessions.size} | ${formatDuration(metrics.percentiles.p50)} | ${formatDuration(metrics.percentiles.p90)} | ${formatDuration(metrics.percentiles.p99)} |`);
}
if (includeStats && data.sessions.size > 0) {
lines.push("");
lines.push("## Session Details");
lines.push("");
lines.push("| Session ID | Project | Responses | Total Time | Avg Time |");
lines.push("|------------|---------|-----------|------------|----------|");
const sessions = Array.from(data.sessions.values()).sort((a, b) => b.totalResponseTimeMs - a.totalResponseTimeMs).slice(0, 20);
for (const session of sessions) {
const sessionId = session.sessionId.substring(0, 8) + "...";
const projectName = session.projectPath.split("/").pop() || "Unknown";
lines.push(`| ${sessionId} | ${projectName} | ${session.totalResponses} | ${formatDuration(session.totalResponseTimeMs)} | ${formatDuration(session.averageResponseTimeMs)} |`);
}
if (data.sessions.size > 20) {
lines.push("");
lines.push(`*Showing top 20 sessions out of ${data.sessions.size} total*`);
}
}
const markdownContent = lines.join(`
`);
await fs2.writeFile(outputPath, markdownContent, "utf-8");
return { success: true, filePath: outputPath };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error during Markdown export"
};
}
}
async function exportData(data, options) {
if (!data || !data.daily || !data.sessions || !data.summary) {
return { success: false, error: "Invalid or missing data to export" };
}
if (options.toClipboard) {
try {
const serialized = toSerializable(data);
let content;
switch (options.format) {
case "json":
content = JSON.stringify(serialized, null, 2);
break;
case "csv":
const headers = ["Date", "Total Time", "Avg Time", "Responses", "Sessions"];
const rows = [headers.join(",")];
Array.from(data.daily.entries()).forEach(([date, metrics]) => {
rows.push([
date,
metrics.totalResponseTimeMs,
metrics.averageResponseTimeMs.toFixed(2),
metrics.responseCount,
metrics.sessions.size
].join(","));
});
content = rows.join(`
`);
break;
case "markdown":
content = `# Response Time Summary
Total: ${formatDuration(data.summary.totalResponseTimeMs)}
Average: ${formatDuration(data.summary.averageResponseTimeMs)}
Responses: ${data.summary.totalResponses}`;
break;
default:
return { success: false, error: "Unsupported format for clipboard" };
}
return {
success: true,
error: "Clipboard export not implemented. Content prepared but not copied."
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Error preparing clipboard content"
};
}
}
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, -5);
const defaultFilename = `cctime-export-${timestamp}`;
let outputPath = options.outputPath;
if (!outputPath) {
const extension = options.format === "markdown" ? "md" : options.format;
outputPath = path.join(process.cwd(), `${defaultFilename}.${extension}`);
}
const dir = path.dirname(outputPath);
try {
await fs2.mkdir(dir, { recursive: true });
} catch (error) {
return {
success: false,
error: `Failed to create directory: ${dir}`
};
}
const includeStats = options.includeStats ?? true;
switch (options.format) {
case "json":
return exportJSON(data, outputPath, includeStats);
case "csv":
return exportCSV(data, outputPath, includeStats);
case "markdown":
return exportMarkdown(data, outputPath, includeStats);
default:
return { success: false, error: `Unsupported export format: ${options.format}` };
}
}
async function exportToFile(data, format, outputPath) {
const result = await exportData(data, { format, outputPath });
if (!result.success) {
throw new Error(result.error || "Export failed");
}
return result.filePath || "Export completed";
}
// src/finder.ts
import { promises as fs3 } from "fs";
import path2 from "path";
import os from "os";
class SessionFinder {
projectsDir;
constructor(projectsDir) {
this.projectsDir = projectsDir || path2.join(os.homedir(), ".claude", "projects");
if (global.DEBUG_MODE) {
console.log(`\uD83D\uDD0D SessionFinder initialized with projects directory: ${this.projectsDir}`);
}
}
pathToHyphenated(projectPath) {
return projectPath.replace(/^\//, "").replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").toLowerCase();
}
isWithinDateRange(file, options) {
if (!options.from && !options.to)
return true;
const fileDate = file.lastModified;
if (options.from && fileDate < options.from)
return false;
if (options.to && fileDate > options.to)
return false;
return true;
}
parseSessionId(filename) {
const match = filename.match(/^session_([a-zA-Z0-9]+)\.jsonl$/);
return match ? match[1] : null;
}
async find(options = {}) {
const sessionFiles = [];
if (global.DEBUG_MODE) {
console.log(`\uD83D\uDCC2 Scanning for transcript files in: ${this.projectsDir}`);
}
try {
await fs3.access(this.projectsDir);
if (global.DEBUG_MODE) {
console.log("✅ Projects directory exists and is accessible");
}
} catch (error) {
if (global.DEBUG_MODE) {
console.log("❌ Projects directory does not exist or is not accessible");
console.log(` Error: ${error.message}`);
}
return sessionFiles;
}
const projectDirs = await fs3.readdir(this.projectsDir);
if (global.DEBUG_MODE) {
console.log(`\uD83D\uDCC1 Found ${projectDirs.length} items in projects directory: [${projectDirs.join(", ")}]`);
}
for (const projectDir of projectDirs) {
const projectDirPath = path2.join(this.projectsDir, projectDir);
const stat = await fs3.stat(projectDirPath);
if (!stat.isDirectory())
continue;
const projectPath = "/" + projectDir.replace(/-/g, "/");
if (options.projectPath && !projectPath.includes(options.projectPath)) {
continue;
}
const files = await fs3.readdir(projectDirPath);
const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
if (global.DEBUG_MODE) {
console.log(` \uD83D\uDCC1 Project: ${projectPath} (${projectDir})`);
console.log(` All files found: [${files.join(", ")}]`);
console.log(` Filtered to ${jsonlFiles.length} .jsonl files: [${jsonlFiles.join(", ")}]`);
}
for (const file of jsonlFiles) {
const filePath = path2.join(projectDirPath, file);
const fileStat = await fs3.stat(filePath);
if (global.DEBUG_MODE) {
console.log(` Processing file: ${file}`);
console.log(` Size: ${fileStat.size} bytes`);
console.log(` Modified: ${fileStat.mtime.toISOString()}`);
}
const sessionId = this.parseSessionId(file);
if (!sessionId) {
const fallbackId = file.replace(".jsonl", "");
if (global.DEBUG_MODE) {
console.log(` No session ID match, using fallback: ${fallbackId}`);
}
const sessionFile2 = {
sessionId: fallbackId,
projectPath,
filePath,
lastModified: fileStat.mtime,
size: fileStat.size
};
if (this.isWithinDateRange(sessionFile2, options)) {
sessionFiles.push(sessionFile2);
if (global.DEBUG_MODE) {
console.log(` ✅ Added to session files`);
}
} else {
if (global.DEBUG_MODE) {
console.log(` ❌ Filtered out by date range`);
}
}
continue;
}
if (global.DEBUG_MODE) {
console.log(` Parsed session ID: ${sessionId}`);
}
const sessionFile = {
sessionId,
projectPath,
filePath,
lastModified: fileStat.mtime,
size: fileStat.size
};
if (this.isWithinDateRange(sessionFile, options)) {
sessionFiles.push(sessionFile);
if (global.DEBUG_MODE) {
console.log(` ✅ Added to session files`);
}
} else {
if (global.DEBUG_MODE) {
console.log(` ❌ Filtered out by date range`);
}
}
}
}
if (global.DEBUG_MODE) {
console.log(`\uD83C\uDFAF Total found: ${sessionFiles.length} session files`);
if (sessionFiles.length > 0) {
console.log(` Most recent: ${sessionFiles[0]?.sessionId} (${sessionFiles[0]?.lastModified.toISOString()})`);
}
}
return sessionFiles.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
}
async findById(sessionId) {
const allSessions = await this.find();
return allSessions.find((s) => s.sessionId === sessionId) || null;
}
async getProjects() {
const sessions = await this.find();
const projects = new Set(sessions.map((s) => s.projectPath));
return Array.from(projects).sort();
}
}
var finder_default = new SessionFinder;
// src/aggregator.ts
function formatDateToDay(dateString) {
const date = new Date(dateString);
return date.toISOString().split("T")[0];
}
function calculatePercentile(sortedValues, percentile) {
if (sortedValues.length === 0)
return 0;
const index = Math.ceil(percentile / 100 * sortedValues.length) - 1;
return sortedValues[Math.max(0, index)];
}
function groupByDay(responseTimes) {
const groups = new Map;
for (const responseTime of responseTimes) {
const day = formatDateToDay(responseTime.userMessageTimestamp);
if (!groups.has(day)) {
groups.set(day, []);
}
groups.get(day).push(responseTime);
}
return groups;
}
function calculateDailyStats(date, responseTimes) {
const responseTimesMs = responseTimes.map((rt) => rt.responseTimeMs);
const sortedResponseTimes = [...responseTimesMs].sort((a, b) => a - b);
const totalResponseTimeMs = responseTimesMs.reduce((sum, time) => sum + time, 0);
const averageResponseTimeMs = totalResponseTimeMs / responseTimes.length;
const sessions = new Set(responseTimes.map((rt) => rt.sessionId));
return {
date,
totalResponseTimeMs,
averageResponseTimeMs,
responseCount: responseTimes.length,
sessions,
percentiles: {
p50: calculatePercentile(sortedResponseTimes, 50),
p90: calculatePercentile(sortedResponseTimes, 90),
p99: calculatePercentile(sortedResponseTimes, 99)
}
};
}
function createSessionMetrics(responseTimes) {
const sessionMap = new Map;
for (const responseTime of responseTimes) {
if (!sessionMap.has(responseTime.sessionId)) {
sessionMap.set(responseTime.sessionId, []);
}
sessionMap.get(responseTime.sessionId).push(responseTime);
}
const sessionMetrics = new Map;
for (const [sessionId, sessionResponses] of sessionMap) {
const sortedResponses = [...sessionResponses].sort((a, b) => new Date(a.userMessageTimestamp).getTime() - new Date(b.userMessageTimestamp).getTime());
const totalResponseTimeMs = sessionResponses.reduce((sum, rt) => sum + rt.responseTimeMs, 0);
sessionMetrics.set(sessionId, {
sessionId,
projectPath: sessionResponses[0].projectPath,
totalResponses: sessionResponses.length,
totalResponseTimeMs,
averageResponseTimeMs: totalResponseTimeMs / sessionResponses.length,
firstMessage: sortedResponses[0].userMessageTimestamp,
lastMessage: sortedResponses[sortedResponses.length - 1].assistantMessageTimestamp
});
}
return sessionMetrics;
}
function generateSummaryStatistics(responseTimes, sessionMetrics) {
if (responseTimes.length === 0) {
return {
totalResponseTimeMs: 0,
totalResponses: 0,
averageResponseTimeMs: 0,
uniqueSessions: 0,
dateRange: {
from: "",
to: ""
}
};
}
const totalResponseTimeMs = responseTimes.reduce((sum, rt) => sum + rt.responseTimeMs, 0);
const timestamps = responseTimes.map((rt) => new Date(rt.userMessageTimestamp).getTime());
const minTimestamp = Math.min(...timestamps);
const maxTimestamp = Math.max(...timestamps);
return {
totalResponseTimeMs,
totalResponses: responseTimes.length,
averageResponseTimeMs: totalResponseTimeMs / responseTimes.length,
uniqueSessions: sessionMetrics.size,
dateRange: {
from: new Date(minTimestamp).toISOString(),
to: new Date(maxTimestamp).toISOString()
}
};
}
function aggregateResponseTimes(responseTimes) {
const groupedByDay = groupByDay(responseTimes);
const dailyStats = new Map;
for (const [date, dayResponses] of groupedByDay) {
dailyStats.set(date, calculateDailyStats(date, dayResponses));
}
const sessionMetrics = createSessionMetrics(responseTimes);
const summary = generateSummaryStatistics(responseTimes, sessionMetrics);
return {
daily: dailyStats,
sessions: sessionMetrics,
summary
};
}
// src/parser/index.ts
import { readFile } from "fs/promises";
function parseTranscriptLine(line) {
if (!line.trim()) {
return null;
}
try {
const parsed = JSON.parse(line);
if (!parsed.type || !parsed.timestamp) {
return null;
}
const validTypes = ["user", "assistant", "system", "tool_result"];
if (!validTypes.includes(parsed.type)) {
return null;
}
const entry = {
type: parsed.type,
timestamp: parsed.timestamp
};
if (parsed.message !== undefined) {
entry.message = parsed.message;
}
if (parsed.usage !== undefined) {
entry.usage = {
input_tokens: parsed.usage.input_tokens,
output_tokens: parsed.usage.output_tokens,
cache_read_input_tokens: parsed.usage.cache_read_input_tokens,
cache_write_input_tokens: parsed.usage.cache_write_input_tokens
};
}
if (parsed.tool !== undefined) {
entry.tool = parsed.tool;
}
if (parsed.result !== undefined) {
entry.result = parsed.result;
}
return entry;
} catch (error) {
console.error(`Failed to parse line: ${error instanceof Error ? error.message : "Unknown error"}`);
return null;
}
}
async function parseTranscriptFile(filePath) {
try {
const fileContent = await readFile(filePath, "utf-8");
const lines = fileContent.split(`
`);
const entries = [];
for (const line of lines) {
const entry = parseTranscriptLine(line);
if (entry) {
entries.push(entry);
}
}
return entries;
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to read transcript file: ${error.message}`);
}
throw new Error("Failed to read transcript file: Unknown error");
}
}
// src/calculator.ts
function calculateResponseTimes(entries, sessionId, projectPath) {
const responseTimes = [];
let lastUserMessage = null;
for (const entry of entries) {
if (entry.type === "user" && entry.message) {
lastUserMessage = entry;
} else if (entry.type === "assistant" && entry.message && lastUserMessage) {
const userTime = new Date(lastUserMessage.timestamp).getTime();
const assistantTime = new Date(entry.timestamp).getTime();
const responseTimeMs = assistantTime - userTime;
if (responseTimeMs > 0) {
responseTimes.push({
userMessageTimestamp: lastUserMessage.timestamp,
assistantMessageTimestamp: entry.timestamp,
responseTimeMs,
sessionId,
projectPath
});
}
lastUserMessage = null;
} else if (entry.type === "user" && !entry.message) {
lastUserMessage = null;
}
}
return responseTimes;
}
function filterOutliers(responseTimes, maxResponseTimeMs = 5 * 60 * 1000) {
return responseTimes.filter((rt) => rt.responseTimeMs <= maxResponseTimeMs);
}
// src/parser/utils.ts
import { basename, dirname as dirname2 } from "path";
function extractSessionId(filePath) {
const dir = dirname2(filePath);
const sessionId = basename(dir);
return sessionId;
}
function extractProjectPath(filePath) {
return dirname2(dirname2(filePath));
}
async function processTranscript(filePath, options) {
const sessionId = options?.sessionId || extractSessionId(filePath);
const projectPath = options?.projectPath || extractProjectPath(filePath);
const entries = await parseTranscriptFile(filePath);
let responseTimes = calculateResponseTimes(entries, sessionId, projectPath);
if (options?.filterOutliers !== false) {
responseTimes = filterOutliers(responseTimes, options?.maxResponseTimeMs);
}
return {
entries,
responseTimes,
sessionId,
projectPath
};
}
// src/assistant-sequence-analyzer.ts
import { readFile as readFile2 } from "fs/promises";
function extractMessageText(message) {
if (typeof message === "string") {
return message;
}
if (message && typeof message === "object") {
if (message.content && typeof message.content === "string") {
return message.content;
}
if (message.content && Array.isArray(message.content)) {
const textContent = message.content.filter((c) => c.type === "text" && !c.tool_use_id).map((c) => c.text || "").join(" ").trim();
if (textContent) {
return textContent;
}
const toolResult = message.content.find((c) => c.type === "tool_result");
if (toolResult) {
return "[Tool result response]";
}
}
if (message.text) {
return message.text;
}
}
return "[No text content]";
}
async function analyzeAssistantSequences(filePath, sessionId, projectPath) {
const fileContent = await readFile2(filePath, "utf-8");
const lines = fileContent.split(`
`).filter((line) => line.trim());
const rawEntries = [];
for (const line of lines) {
try {
const rawEntry = JSON.parse(line);
rawEntries.push(rawEntry);
} catch (error) {
}
}
if (global.DEBUG_MODE) {
console.log(`
DEBUG: Analyzing ${filePath}`);
console.log(`DEBUG: Found ${rawEntries.length} total entries`);
console.log(`DEBUG: User entries: ${rawEntries.filter((e) => e.type === "user").length}`);
console.log(`DEBUG: Assistant entries: ${rawEntries.filter((e) => e.type === "assistant").length}`);
}
const sequences = [];
let i = 0;
while (i < rawEntries.length) {
const entry = rawEntries[i];
if (entry.type === "user" && entry.message) {
const userEntry = entry;
if (userEntry.isCompactSummary === true) {
i++;
continue;
}
const messageObj = userEntry.message;
if (typeof messageObj === "string") {
if (messageObj.includes("<command-") || messageObj.includes("<system-reminder>") || messageObj.includes("<user-prompt-submit-hook>")) {
i++;
continue;
}
}
if (messageObj && messageObj.role === "user" && messageObj.content) {
if (Array.isArray(messageObj.content)) {
const hasToolResult = messageObj.content.some((c) => c.tool_use_id || c.type === "tool_result");
if (hasToolResult) {
i++;
continue;
}
}
}
if (typeof messageObj === "string") {
} else if (!messageObj.role && !messageObj.content) {
i++;
continue;
}
const userMessage = extractMessageText(userEntry.message);
const userTimestamp = userEntry.timestamp;
if (global.DEBUG_MODE) {
console.log(`
DEBUG: Found potential user message at index ${i}:`);
console.log(` Message: "${userMessage}"`);
console.log(` Timestamp: ${userTimestamp}`);
}
if (!userMessage || userMessage === "[No text content]") {
if (global.DEBUG_MODE) {
console.log(` Skipping: Empty message`);
}
i++;
continue;
}
let firstAssistantIndex = -1;
let lastAssistantIndex = -1;
let messageCount = 0;
let toolUseCount = 0;
let previousAssistantTime = null;
const MAX_GAP_MS = 10 * 60 * 1000;
const MAX_SEQUENCE_DURATION_MS = 12 * 60 * 60 * 1000;
const sequenceStartTime = new Date(userTimestamp);
for (let j = i + 1;j < rawEntries.length; j++) {
const entry2 = rawEntries[j];
if (entry2.type === "user") {
if (entry2.isCompactSummary === true) {
if (global.DEBUG_MODE) {
console.log(` Compact summary found, ending sequence`);
}
break;
}
const msgObj = entry2.message;
if (msgObj && typeof msgObj === "string") {
if (msgObj.includes("<command-") || msgObj.includes("<system-reminder>") || msgObj.includes("<user-prompt-submit-hook>")) {
continue;
}
break;
}
if (msgObj && msgObj.role === "user" && msgObj.content) {
if (typeof msgObj.content === "string") {
break;
}
if (Array.isArray(msgObj.content)) {
const hasToolResult = msgObj.content.some((c) => c.tool_use_id || c.type === "tool_result");
if (!hasToolResult) {
break;
}
}
}
}
if (entry2.type === "assistant") {
const currentTime = new Date(entry2.timestamp);
const totalDuration = currentTime.getTime() - sequenceStartTime.getTime();
if (totalDuration > MAX_SEQUENCE_DURATION_MS) {
if (global.DEBUG_MODE) {
console.log(` Total duration of ${(totalDuration / 1000 / 60 / 60).toFixed(1)} hours exceeds 12 hours, ending sequence`);
}
break;
}
if (previousAssistantTime) {
const gapMs = currentTime.getTime() - previousAssistantTime.getTime();
if (gapMs > MAX_GAP_MS) {
if (global.DEBUG_MODE) {
console.log(` Gap of ${gapMs}ms (${(gapMs / 1000 / 60).toFixed(1)} minutes) exceeds 10 minutes, ending sequence`);
}
break;
}
}
if (firstAssistantIndex === -1) {
firstAssistantIndex = j;
}
lastAssistantIndex = j;
previousAssistantTime = currentTime;
if (entry2.message) {
messageCount++;
const msg = entry2.message;
if (msg && msg.content && Array.isArray(msg.content)) {
const toolUses = msg.content.filter((c) => c.type === "tool_use").length;
toolUseCount += toolUses;
}
}
}
if (entry2.type === "user" && entry2.message) {
const msgObj = entry2.message;
if (msgObj && msgObj.role === "user" && msgObj.content && Array.isArray(msgObj.content)) {
const hasToolResult = msgObj.content.some((c) => c.tool_use_id || c.type === "tool_result");
if (hasToolResult) {
previousAssistantTime = new Date(entry2.timestamp);
}
}
}
}
if (firstAssistantIndex !== -1 && lastAssistantIndex !== -1) {
const extendsToEnd = lastAssistantIndex === rawEntries.length - 1 || lastAssistantIndex === rawEntries.length - 2 && rawEntries[rawEntries.length - 1].type === "user" && rawEntries[rawEntries.length - 1].message?.content?.some?.((c) => c.tool_use_id || c.type === "tool_result");
const userTime = new Date(userTimestamp);
const firstTimestamp = new Date(rawEntries[firstAssistantIndex].timestamp);
const lastTimestamp = new Date(rawEntries[lastAssistantIndex].timestamp);
const responseTimeMs = firstTimestamp.getTime() - userTime.getTime();
const durationMs = lastTimestamp.getTime() - firstTimestamp.getTime();
if (extendsToEnd && durationMs > 10 * 60 * 1000) {
if (global.DEBUG_MODE) {
console.log(` Skipping sequence that extends to end of file with duration ${(durationMs / 1000 / 60).toFixed(1)} minutes`);
}
i = lastAssistantIndex + 1;
continue;
}
if (global.DEBUG_MODE) {
console.log(` Found sequence:`);
console.log(` First assistant at index: ${firstAssistantIndex}`);
console.log(` Last assistant at index: ${lastAssistantIndex}`);
console.log(` Duration: ${durationMs}ms`);
}
sequences.push({
userMessage: userMessage.substring(0, 100) + (userMessage.length > 100 ? "..." : ""),
userTimestamp,
firstAssistantTimestamp: rawEntries[firstAssistantIndex].timestamp,
lastAssistantTimestamp: rawEntries[lastAssistantIndex].timestamp,
responseTimeMs,
durationMs,
messageCount,
toolUseCount
});
} else {
if (global.DEBUG_MODE) {
console.log(` No assistant messages found after this user message`);
}
}
i = lastAssistantIndex !== -1 ? lastAssistantIndex + 1 : i + 1;
} else {
i++;
}
}
const longestSequence = sequences.length > 0 ? sequences.reduce((longest, current) => current.durationMs > longest.durationMs ? current : longest) : null;
const timeDistribution = {
"0-10s": 0,
"10-30s": 0,
"30-60s": 0,
"1-5m": 0,
"5m+": 0
};
sequences.forEach((seq) => {
const seconds = seq.durationMs / 1000;
if (seconds <= 10) {
timeDistribution["0-10s"]++;
} else if (seconds <= 30) {
timeDistribution["10-30s"]++;
} else if (seconds <= 60) {
timeDistribution["30-60s"]++;
} else if (seconds <= 300) {
timeDistribution["1-5m"]++;
} else {
timeDistribution["5m+"]++;
}
});
return {
sessionId,
projectPath,
sequences,
longestSequence,
timeDistribution
};
}
function formatDuration2(ms) {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours > 0) {
const remainingMinutes = minutes % 60;
return `${hours}h ${remainingMinutes}m`;
} else if (minutes > 0) {
const remainingSeconds = seconds % 60;
return `${minutes}m ${remainingSeconds}s`;
} else {
return `${seconds}s`;
}
}
// src/display-sequences.ts
import chalk from "chalk";
function displaySequenceAnalysis(analyses) {
console.clear();
console.log(chalk.cyan.bold(`
\uD83E\uDD16 Claude Assistant Response Sequence Analysis
`));
const sortedAnalyses = [...analyses].sort((a, b) => {
const aDuration = a.longestSequence?.durationMs || 0;
const bDuration = b.longestSequence?.durationMs || 0;
return bDuration - aDuration;
});
const allSequences = analyses.flatMap((a) => a.sequences);
const totalSequences = allSequences.length;
const longestOverall = allSequences.reduce((longest, current) => !longest || current.durationMs > longest.durationMs ? current : longest, null);
console.log(chalk.yellow.bold("Overall Statistics:"));
console.log(` Total Response Sequences: ${chalk.white.bold(totalSequences)}`);
console.log(` Sessions Analyzed: ${chalk.white.bold(analyses.length)}`);
if (longestOverall) {
console.log(` Longest Sequence Overall: ${chalk.white.bold(formatDuration2(longestOverall.durationMs))}`);
}
console.log("");
console.log(chalk.yellow.bold("Time Distribution (All Sessions):"));
const totalDistribution = {
"0-10s": 0,
"10-30s": 0,
"30-60s": 0,
"1-5m": 0,
"5m+": 0
};
analyses.forEach((analysis) => {
Object.entries(analysis.timeDistribution).forEach(([range, count]) => {
totalDistribution[range] += count;
});
});
console.log(chalk.gray("─".repeat(40)));
Object.entries(totalDistribution).forEach(([range, count]) => {
const percentage = totalSequences > 0 ? (count / totalSequences * 100).toFixed(1) : "0";
const bar = "█".repeat(Math.floor(Number(percentage) / 2));
console.log(` ${chalk.cyan(range.padEnd(8))} ${chalk.white(count.toString().padStart(4))} ${chalk.gray(`(${percentage}%)`)} ${chalk.blue(bar)}`);
});
console.log(chalk.gray("─".repeat(40)));
console.log(`
` + chalk.yellow.bold("Session Analysis (Sorted by Longest Sequence):"));
console.log(chalk.gray("═".repeat(80)));
sortedAnalyses.forEach((analysis, index) => {
const sessionName = analysis.sessionId.substring(0, 8) + "...";
const projectName = analysis.projectPath.split("/").pop() || "Unknown";
console.log(`
${chalk.cyan.bold(`${index + 1}. Session:`)} ${chalk.white(sessionName)} ${chalk.gray(`(${projectName})`)}`);
console.log(` ${chalk.dim("Total Sequences:")} ${analysis.sequences.length}`);
if (analysis.longestSequence) {
const seq = analysis.longestSequence;
console.log(`
${chalk.yellow("Longest Sequence:")}`);
console.log(` ${chalk.dim("Response Time:")} ${chalk.white.bold(formatDuration2(seq.responseTimeMs))} ${chalk.dim("(user → first assistant)")}`);
console.log(` ${chalk.dim("Processing Time:")} ${chalk.white.bold(formatDuration2(seq.durationMs))} ${chalk.dim("(first → last assistant)")}`);
const displayMessage = seq.userMessage || "[No message text]";
const truncatedMessage = displayMessage.length > 80 ? displayMessage.substring(0, 77) + "..." : displayMessage;
console.log(` ${chalk.dim("User Query:")} "${chalk.italic(truncatedMessage)}"`);
console.log(` ${chalk.dim("Messages:")} ${seq.messageCount} ${chalk.dim("| Tool Uses:")} ${seq.toolUseCount}`);
console.log(` ${chalk.dim("Started:")} ${new Date(seq.firstAssistantTimestamp).toLocaleTimeString()}`);
console.log(` ${chalk.dim("Ended:")} ${new Date(seq.lastAssistantTimestamp).toLocaleTimeString()}`);
}
console.log(`
${chalk.dim("Time Distribution:")}`);
Object.entries(analysis.timeDistribution).forEach(([range, count]) => {
if (count > 0) {
console.log(` ${range.padEnd(8)} ${chalk.white(count.toString().padStart(3))} sequences`);
}
});
if (analysis.sequences.length > 1) {
console.log(`
${chalk.dim("Top 3 Longest Sequences:")}`);
const topSequences = [...analysis.sequences].sort((a, b) => b.durationMs - a.durationMs).slice(0, 3);
topSequences.forEach((seq, i) => {
const msg = seq.userMessage || "[No message]";
const truncMsg = msg.length > 50 ? msg.substring(0, 47) + "..." : msg;
console.log(` ${i + 1}. ${chalk.white(formatDuration2(seq.durationMs))} - "${truncMsg}"`);
});
}
console.log(chalk.gray("─".repeat(80)));
});
console.log(`
` + chalk.dim(`Press Ctrl+C to exit
`));
}
// src/find-longest-sequence.ts
import chalk2 from "chalk";
import { promises as fs4 } from "fs";
import path3 from "path";
import figlet from "figlet";
// src/streak-calculator.ts
function calculateStreaks(sessions) {
if (sessions.length === 0) {
return {
currentStreak: 0,
longestStreak: 0,
totalDaysUsed: 0
};
}
const daysUsed = new Set;
const sessionDates = [];
for (const session of sessions) {
const dateKey = session.lastModified.toISOString().split("T")[0];
daysUsed.add(dateKey);
sessionDates.push(session.lastModified);
}
const sortedDays = Array.from(daysUsed).sort();
const sortedDates = sortedDays.map((d) => new Date(d));
let currentStreak = 0;
let longestStreak = 0;
let longestStreakStart;
let longestStreakEnd;
let tempStreakStart;
let tempStreak = 0;
const today = new Date;
today.setHours(0, 0, 0, 0);
const todayKey = today.toISOString().split("T")[0];
const hasToday = daysUsed.has(todayKey);
for (let i = 0;i < sortedDates.length; i++) {
const currentDate = sortedDates[i];
if (i === 0) {
tempStreak = 1;
tempStreakStart = currentDate;
} else {
const prevDate = sortedDates[i - 1];
const daysDiff = Math.floor((currentDate.getTime() - prevDate.getTime()) / (1000 * 60 * 60 * 24));
if (daysDiff === 1) {
tempStreak++;
} else {
if (tempStreak > longestStreak) {
longestStreak = tempStreak;
longestStreakStart = tempStreakStart;
longestStreakEnd = prevDate;
}
tempStreak = 1;
tempStreakStart = currentDate;
}
}
}
if (tempStreak > longestStreak) {
longestStreak = tempStreak;
longestStreakStart = tempStreakStart;
longestStreakEnd = sortedDates[sortedDates.length - 1];
}
if (hasToday || sortedDates.length > 0 && isYesterday(sortedDates[sortedDates.length - 1])) {
currentStreak = 1;
let checkDate = new Date(sortedDates[sortedDates.length - 1]);
for (let i = sortedDates.length - 2;i >= 0; i--) {
const daysDiff = Math.floor((checkDate.getTime() - sortedDates[i].getTime()) / (1000 * 60 * 60 * 24));
if (daysDiff === 1) {
currentStreak++;
checkDate = sortedDates[i];
} else {
break;
}
}
}
return {
currentStreak,
longestStreak,
longestStreakStart,
longestStreakEnd,
totalDaysUsed: daysUsed.size
};
}
function isYesterday(date) {
const yesterday = new Date;
yesterday.setDate(yesterday.getDate() - 1);
yesterday.setHours(0, 0, 0, 0);
const checkDate = new Date(date);
checkDate.setHours(0, 0, 0, 0);
return checkDate.getTime() === yesterday.getTime();
}
function formatStreakMessage(streakInfo) {
if (streakInfo.longestStreak === 0) {
return "No usage streak found";
}
const messages = [];
const longestStreakText = streakInfo.longestStreak === 1 ? "1 day" : `${streakInfo.longestStreak} days`;
if (streakInfo.longestStreakStart && streakInfo.longestStreakEnd) {
const start = streakInfo.longestStreakStart.toLocaleDateString("en-US", { month: "short", day: "numeric" });
const end = streakInfo.longestStreakEnd.toLocaleDateString("en-US", { month: "short", day: "numeric" });
if (start === end) {
messages.push(`\uD83D\uDD25 Longest streak: ${longestStreakText} (${start})`);
} else {
messages.push(`\uD83D\uDD25 Longest streak: ${longestStreakText} (${start} - ${end})`);
}
} else {
messages.push(`\uD83D\uDD25 Longest streak: ${longestStreakText}`);
}
if (streakInfo.currentStreak > 0 && streakInfo.currentStreak !== streakInfo.longestStreak) {
const currentStreakText = streakInfo.currentStreak === 1 ? "1 day" : `${streakInfo.currentStreak} days`;
messages.push(`\uD83D\uDCC8 Current streak: ${currentStreakText}`);
}
return messages.join(`
`);
}
// src/find-longest-sequence.ts
async function findLongestSequence(options = {}) {
process.stdout.write("Loading...");
let files = [];
let totalSequences = 0;
if (options.file) {
const stat = await fs4.stat(options.file);
if (!stat.isFile() || !options.file.endsWith(".jsonl")) {
process.stdout.write("\r\x1B[K");
console.error("Invalid file: must be a .jsonl file");
return;
}
const sessionId = path3.basename(options.file, ".jsonl");
files = [{
sessionId,
projectPath: path3.dirname(options.file),
filePath: options.file,
lastModified: stat.mtime,
size: stat.size
}];
} else if (options.dir) {
const dirStat = await fs4.stat(options.dir);
if (!dirStat.isDirectory()) {
process.stdout.write("\r\x1B[K");
console.error("Invalid directory");
return;
}
const dirFiles = await fs4.readdir(options.dir);
const jsonlFiles = dirFiles.filter((f) => f.endsWith(".jsonl"));
for (const file of jsonlFiles) {
const filePath = path3.join(options.dir, file);
const fileStat = await fs4.stat(filePath);
const sessionId = file.replace(".jsonl", "");
files.push({
sessionId,
projectPath: options.dir,
filePath,
lastModified: fileStat.mtime,
size: fileStat.size
});
}
if (files.length === 0) {
process.stdout.write("\r\x1B[K");
console.error("No .jsonl files found in directory");
return;
}
} else {
const finder = new SessionFinder;
files = await finder.find({});
if (files.length === 0) {
process.stdout.write("\r\x1B[K");
console.error("No transcript files found");
return;
}
}
let longestSequence = null;
let longestSessionId = "";
let longestProjectPath = "";
for (const file of files) {
try {
const analysis = await analyzeAssistantSequences(file.filePath, file.sessionId, file.projectPath);
totalSequences += analysis.sequences.length;
for (const sequence of analysis.sequences) {
if (!longestSequence || sequence.durationMs > longestSequence.durationMs) {
longestSequence = sequence;
longestSessionId = file.sessionId;
longestProjectPath = file.projectPath;
}
}
} catch (error) {
}
}
if (!longestSequence) {
process.stdout.write("\r\x1B[K");
console.log("No sequences found in any session");
return;
}
console.clear();
console.log(chalk2.cyan.bold(`\uD83C\uDFC6 Claude agentic highscore: ${chalk2.yellow.bold(formatDuration2(longestSequence.durationMs))} (best from ${totalSequences} total sessions)`));
const streakInfo = calculateStreaks(files);
console.log(chalk2.magenta.bold(formatStreakMessage(streakInfo)));
console.log(chalk2.gray(`
Try it yourself with: npx claude-highscore@latest`));
console.log(chalk2.gray(`
Brought to you by`));
const everyText = figlet.textSync("EVERY", {
font: "Big",
horizontalLayout: "fitted",
verticalLayout: "default"
});
console.log(`
` + everyText);
console.log(chalk2.gray("@every"));
}
// src/cli.tsx
import * as fs5 from "fs/promises";
import path4 from "path";
var program = new Command;
program.name("cctime").description("Claude Code Transcript Time Analyzer - Analyze response times from claude.ai transcripts").version(version).option("-f, --file <path>", "Path to claude transcript file").option("-d, --dir <path>", "Directory containing multiple transcript files").option("-o, --output <format>", "Output format (json, csv, table)", "table").option("-e, --export <path>", "Export results to file without interactive mode").option("--export-format <format>", "Export format for non-interactive mode (json, csv, markdown)", "json").option("--config <path>", "Path to config file", "~/.cctime/config.json").option("-w, --watch", "Watch for changes in session files").option("--analyze-sequences", "Analyze assistant response sequences").option("--longest", "Find the single longest assistant processing time").option("--debug", "Enable debug logging for troubleshooting").parse(process.argv);
var options = program.opts();
async function main() {
try {
global.DEBUG_MODE = options.debug || false;
if (options.debug) {
console.log("\uD83D\uDC1B Debug mode enabled");
console.log("\uD83C\uDFE0 Home directory:", __require("os").homedir());
console.log("\uD83D\uDCC1 Expected Claude projects directory:", __require("path").join(__require("os").homedir(), ".claude", "projects"));
}
const config = await loadConfig(options.config);
const configManager = new ConfigManager(config);
if (options.export) {
console.log("Loading transcript data...");
try {
let files = [];
if (options.file) {
const stat2 = await fs5.stat(options.file);
if (!stat2.isFile() || !options.file.endsWith(".jsonl")) {
console.error("Invalid file: must be a .jsonl file");
process.exit(1);
}
const sessionId = path4.basename(options.file, ".jsonl");
files = [{
sessionId,
projectPath: path4.dirname(options.file),
filePath: options.file,
lastModified: stat2.mtime,
size: stat2.size
}];
} else if (options.dir) {
const dirStat = await fs5.stat(options.dir);
if (!dirStat.isDirectory()) {
console.error("Invalid directory");
process.exit(1);
}
const dirFiles = await fs5.readdir(options.dir);
const jsonlFiles = dirFiles.filter((f) => f.endsWith(".jsonl"));
for (const file of jsonlFiles) {
const filePath = path4.join(options.dir, file);
const fileStat = await fs5.stat(filePath);
const sessionId = file.replace(".jsonl", "");
files.push({
sessionId,
projectPath: options.dir,
filePath,
lastModified: fileStat.mtime,
size: fileStat.size
});
}
} else {
const finder = new SessionFinder;
files = await finder.find({});
}
if (files.length === 0) {
console.error("No transcript files found");
process.exit(1);
}
console.log(`Found ${files.length} transcript files`);
const allResponseTimes = [];
for (const file of files) {
try {
const { responseTimes } = await processTranscript(file.filePath, {
sessionId: file.sessionId,
projectPath: file.projectPath
});
allResponseTimes.push(...responseTimes);
} catch (error) {
console.error(`Error processing ${file.filePath}:`, error.message);
}
}
if (allResponseTimes.length === 0) {
console.error("No response times found");
process.exit(1);
}
const processedData = aggregateResponseTimes(allResponseTimes);
const format = options.exportFormat;
const resultPath = await exportToFile(processedData, format, options.export);
console.log(`Successfully exported to: ${resultPath}`);
process.exit(0);
} catch (error) {
console.error("Export failed:", error.message);
process.exit(1);
}
} else if (options.longest) {
try {
await findLongestSequence({ file: options.file, dir: options.dir });
} catch (error) {
console.error("Error:", error.message);
process.exit(1);
}
} else if (