UNPKG

claude-statusline-amdo

Version:

Custom statusline for Claude Code. Forked from Owloops/claude-powerline

1,538 lines (1,519 loc) 99.5 kB
#!/usr/bin/env node var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) { if (typeof require !== "undefined") return require.apply(this, arguments); throw Error('Dynamic require of "' + x + '" is not supported'); }); // src/index.ts import process2 from "process"; import path4 from "path"; import fs3 from "fs"; import { execSync as execSync4 } from "child_process"; import os2 from "os"; import { json } from "stream/consumers"; // src/utils/colors.ts function hexToAnsi(hex, isBackground) { if (isBackground && (hex.toLowerCase() === "transparent" || hex.toLowerCase() === "none")) { return "\x1B[49m"; } const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); return `\x1B[${isBackground ? "48" : "38"};2;${r};${g};${b}m`; } function extractBgToFg(ansiCode) { if (!ansiCode || typeof ansiCode !== "string") { return "\x1B[0m"; } const match = ansiCode.match(/48;2;(\d+);(\d+);(\d+)/); if (match) { return `\x1B[38;2;${match[1]};${match[2]};${match[3]}m`; } return ansiCode.replace("48", "38"); } // src/themes/dark.ts var darkTheme = { directory: { bg: "#8b4513", fg: "#ffffff" }, git: { bg: "#404040", fg: "#ffffff" }, model: { bg: "#2d2d2d", fg: "#ffffff" }, session: { bg: "#202020", fg: "#00ffff" }, block: { bg: "#2a2a2a", fg: "#87ceeb" }, today: { bg: "#1a1a1a", fg: "#98fb98" }, tmux: { bg: "#2f4f2f", fg: "#90ee90" }, context: { bg: "#4a5568", fg: "#cbd5e0" }, contextprogressbar: { bg: "transparent", fg: "#cbd5e0" }, metrics: { bg: "#374151", fg: "#d1d5db" }, version: { bg: "#3a3a4a", fg: "#b8b8d0" } }; // src/themes/light.ts var lightTheme = { directory: { bg: "#ff6b47", fg: "#ffffff" }, git: { bg: "#4fb3d9", fg: "#ffffff" }, model: { bg: "#87ceeb", fg: "#000000" }, session: { bg: "#da70d6", fg: "#ffffff" }, block: { bg: "#6366f1", fg: "#ffffff" }, today: { bg: "#10b981", fg: "#ffffff" }, tmux: { bg: "#32cd32", fg: "#ffffff" }, context: { bg: "#718096", fg: "#ffffff" }, contextprogressbar: { bg: "transparent", fg: "#ffffff" }, metrics: { bg: "#6b7280", fg: "#ffffff" }, version: { bg: "#8b7dd8", fg: "#ffffff" } }; // src/themes/nord.ts var nordTheme = { directory: { bg: "#434c5e", fg: "#d8dee9" }, git: { bg: "#3b4252", fg: "#a3be8c" }, model: { bg: "#4c566a", fg: "#81a1c1" }, session: { bg: "#2e3440", fg: "#88c0d0" }, block: { bg: "#3b4252", fg: "#81a1c1" }, today: { bg: "#2e3440", fg: "#8fbcbb" }, tmux: { bg: "#2e3440", fg: "#8fbcbb" }, context: { bg: "#5e81ac", fg: "#eceff4" }, contextprogressbar: { bg: "transparent", fg: "#eceff4" }, metrics: { bg: "#b48ead", fg: "#2e3440" }, version: { bg: "#434c5e", fg: "#88c0d0" } }; // src/themes/tokyo-night.ts var tokyoNightTheme = { directory: { bg: "#2f334d", fg: "#82aaff" }, git: { bg: "#1e2030", fg: "#c3e88d" }, model: { bg: "#191b29", fg: "#fca7ea" }, session: { bg: "#222436", fg: "#86e1fc" }, block: { bg: "#2d3748", fg: "#7aa2f7" }, today: { bg: "#1a202c", fg: "#4fd6be" }, tmux: { bg: "#191b29", fg: "#4fd6be" }, context: { bg: "#414868", fg: "#c0caf5" }, contextprogressbar: { bg: "transparent", fg: "#c0caf5" }, metrics: { bg: "#3d59a1", fg: "#c0caf5" }, version: { bg: "#292e42", fg: "#bb9af7" } }; // src/themes/rose-pine.ts var rosePineTheme = { directory: { bg: "#26233a", fg: "#c4a7e7" }, git: { bg: "#1f1d2e", fg: "#9ccfd8" }, model: { bg: "#191724", fg: "#ebbcba" }, session: { bg: "#26233a", fg: "#f6c177" }, block: { bg: "#2a273f", fg: "#eb6f92" }, today: { bg: "#232136", fg: "#9ccfd8" }, tmux: { bg: "#26233a", fg: "#908caa" }, context: { bg: "#393552", fg: "#e0def4" }, contextprogressbar: { bg: "transparent", fg: "#e0def4" }, metrics: { bg: "#524f67", fg: "#e0def4" }, version: { bg: "#2a273f", fg: "#c4a7e7" } }; // src/themes/index.ts var BUILT_IN_THEMES = { dark: darkTheme, light: lightTheme, nord: nordTheme, "tokyo-night": tokyoNightTheme, "rose-pine": rosePineTheme }; function getTheme(themeName) { return BUILT_IN_THEMES[themeName] || null; } // src/segments/git.ts import { execSync } from "child_process"; import fs from "fs"; import path from "path"; // src/utils/logger.ts var debug = (message, ...args) => { if (process.env.CLAUDE_POWERLINE_DEBUG) { console.error(`[DEBUG] ${message}`, ...args); } }; // src/segments/git.ts var GitService = class { cache = /* @__PURE__ */ new Map(); CACHE_TTL = 1e3; isGitRepo(workingDir) { try { return fs.existsSync(path.join(workingDir, ".git")); } catch { return false; } } getGitInfo(workingDir, options = {}, projectDir) { const gitDir = projectDir && this.isGitRepo(projectDir) ? projectDir : workingDir; const optionsKey = JSON.stringify(options); const cacheKey = `${gitDir}:${optionsKey}`; const cached = this.cache.get(cacheKey); const now = Date.now(); if (cached && now - cached.timestamp < this.CACHE_TTL) { return cached.data; } if (!this.isGitRepo(gitDir)) { this.cache.set(cacheKey, { data: null, timestamp: now }); return null; } try { const branch = this.getBranch(gitDir); const status = this.getStatus(gitDir); const { ahead, behind } = this.getAheadBehind(gitDir); const result = { branch: branch || "detached", status, ahead, behind }; if (options.showSha) { result.sha = this.getSha(gitDir) || void 0; } if (options.showWorkingTree) { const counts = this.getWorkingTreeCounts(gitDir); result.staged = counts.staged; result.unstaged = counts.unstaged; result.untracked = counts.untracked; result.conflicts = counts.conflicts; } if (options.showOperation) { result.operation = this.getOngoingOperation(gitDir) || void 0; } if (options.showTag) { result.tag = this.getNearestTag(gitDir) || void 0; } if (options.showTimeSinceCommit) { result.timeSinceCommit = this.getTimeSinceLastCommit(gitDir) || void 0; } if (options.showStashCount) { result.stashCount = this.getStashCount(gitDir); } if (options.showUpstream) { result.upstream = this.getUpstream(gitDir) || void 0; } if (options.showRepoName) { result.repoName = this.getRepoName(gitDir) || void 0; result.isWorktree = this.isWorktree(gitDir); } this.cache.set(cacheKey, { data: result, timestamp: now }); return result; } catch { this.cache.set(cacheKey, { data: null, timestamp: now }); return null; } } getBranch(workingDir) { try { return execSync("git branch --show-current", { cwd: workingDir, encoding: "utf8", timeout: 5e3 }).trim() || null; } catch (error) { debug(`Git branch command failed in ${workingDir}:`, error); return null; } } getStatus(workingDir) { try { const gitStatus = execSync("git status --porcelain", { cwd: workingDir, encoding: "utf8", timeout: 5e3 }).trim(); if (!gitStatus) return "clean"; if (gitStatus.includes("UU") || gitStatus.includes("AA") || gitStatus.includes("DD")) { return "conflicts"; } return "dirty"; } catch (error) { debug(`Git status command failed in ${workingDir}:`, error); return "clean"; } } getWorkingTreeCounts(workingDir) { try { const gitStatus = execSync("git status --porcelain=v1", { cwd: workingDir, encoding: "utf8", timeout: 5e3 }); let staged = 0; let unstaged = 0; let untracked = 0; let conflicts = 0; if (!gitStatus.trim()) { return { staged, unstaged, untracked, conflicts }; } const lines = gitStatus.split("\n"); for (const line of lines) { if (!line || line.length < 2) continue; const indexStatus = line.charAt(0); const worktreeStatus = line.charAt(1); if (indexStatus === "?" && worktreeStatus === "?") { untracked++; continue; } const statusPair = indexStatus + worktreeStatus; if (["DD", "AU", "UD", "UA", "DU", "AA", "UU"].includes(statusPair)) { conflicts++; continue; } if (indexStatus !== " " && indexStatus !== "?") { staged++; } if (worktreeStatus !== " " && worktreeStatus !== "?") { unstaged++; } } return { staged, unstaged, untracked, conflicts }; } catch (error) { debug(`Git working tree counts failed in ${workingDir}:`, error); return { staged: 0, unstaged: 0, untracked: 0, conflicts: 0 }; } } getAheadBehind(workingDir) { try { const aheadResult = execSync("git rev-list --count @{u}..HEAD", { cwd: workingDir, encoding: "utf8", timeout: 5e3 }).trim(); const behindResult = execSync("git rev-list --count HEAD..@{u}", { cwd: workingDir, encoding: "utf8", timeout: 5e3 }).trim(); return { ahead: parseInt(aheadResult) || 0, behind: parseInt(behindResult) || 0 }; } catch (error) { debug(`Git ahead/behind command failed in ${workingDir}:`, error); return { ahead: 0, behind: 0 }; } } getSha(workingDir) { try { const sha = execSync("git rev-parse --short=7 HEAD", { cwd: workingDir, encoding: "utf8", timeout: 5e3 }).trim(); return sha || null; } catch { return null; } } getOngoingOperation(workingDir) { try { const gitDir = path.join(workingDir, ".git"); if (fs.existsSync(path.join(gitDir, "MERGE_HEAD"))) return "MERGE"; if (fs.existsSync(path.join(gitDir, "CHERRY_PICK_HEAD"))) return "CHERRY-PICK"; if (fs.existsSync(path.join(gitDir, "REVERT_HEAD"))) return "REVERT"; if (fs.existsSync(path.join(gitDir, "BISECT_LOG"))) return "BISECT"; if (fs.existsSync(path.join(gitDir, "rebase-merge")) || fs.existsSync(path.join(gitDir, "rebase-apply"))) return "REBASE"; return null; } catch { return null; } } getNearestTag(workingDir) { try { const tag = execSync("git describe --tags --abbrev=0", { cwd: workingDir, encoding: "utf8", timeout: 5e3 }).trim(); return tag || null; } catch { return null; } } getTimeSinceLastCommit(workingDir) { try { const timestamp = execSync("git log -1 --format=%ct", { cwd: workingDir, encoding: "utf8", timeout: 5e3 }).trim(); if (!timestamp) return null; const commitTime = parseInt(timestamp) * 1e3; const now = Date.now(); return Math.floor((now - commitTime) / 1e3); } catch { return null; } } getStashCount(workingDir) { try { const stashList = execSync("git stash list", { cwd: workingDir, encoding: "utf8", timeout: 5e3 }).trim(); if (!stashList) return 0; return stashList.split("\n").length; } catch { return 0; } } getUpstream(workingDir) { try { const upstream = execSync("git rev-parse --abbrev-ref @{u}", { cwd: workingDir, encoding: "utf8", timeout: 5e3 }).trim(); return upstream || null; } catch { return null; } } getRepoName(workingDir) { try { const remoteUrl = execSync("git config --get remote.origin.url", { cwd: workingDir, encoding: "utf8", timeout: 5e3 }).trim(); if (!remoteUrl) return path.basename(workingDir); if (!remoteUrl || remoteUrl.trim() === "") { return path.basename(workingDir); } const match = remoteUrl.match(/\/([^/]+?)(\.git)?$/); return match?.[1] || path.basename(workingDir); } catch { return path.basename(workingDir); } } isWorktree(workingDir) { try { const gitDir = path.join(workingDir, ".git"); if (fs.existsSync(gitDir) && fs.statSync(gitDir).isFile()) { return true; } return false; } catch { return false; } } }; // src/segments/tmux.ts import { execSync as execSync2 } from "child_process"; var TmuxService = class { getSessionId() { try { if (!process.env.TMUX_PANE) { debug(`TMUX_PANE not set, not in tmux session`); return null; } debug(`Getting tmux session ID, TMUX_PANE: ${process.env.TMUX_PANE}`); const sessionId = execSync2("tmux display-message -p '#S'", { encoding: "utf8", timeout: 1e3 }).trim(); debug(`Tmux session ID: ${sessionId || "empty"}`); return sessionId || null; } catch (error) { debug(`Error getting tmux session ID:`, error); return null; } } isInTmux() { return !!process.env.TMUX_PANE; } }; // src/segments/pricing.ts var OFFLINE_PRICING_DATA = { "claude-3-haiku-20240307": { name: "Claude 3 Haiku", input: 0.25, output: 1.25, cache_write_5m: 0.3, cache_write_1h: 0.5, cache_read: 0.03 }, "claude-3-5-haiku-20241022": { name: "Claude 3.5 Haiku", input: 0.8, output: 4, cache_write_5m: 1, cache_write_1h: 1.6, cache_read: 0.08 }, "claude-3-5-haiku-latest": { name: "Claude 3.5 Haiku Latest", input: 1, output: 5, cache_write_5m: 1.25, cache_write_1h: 2, cache_read: 0.1 }, "claude-3-opus-latest": { name: "Claude 3 Opus Latest", input: 15, output: 75, cache_write_5m: 18.75, cache_write_1h: 30, cache_read: 1.5 }, "claude-3-opus-20240229": { name: "Claude 3 Opus", input: 15, output: 75, cache_write_5m: 18.75, cache_write_1h: 30, cache_read: 1.5 }, "claude-3-5-sonnet-latest": { name: "Claude 3.5 Sonnet Latest", input: 3, output: 15, cache_write_5m: 3.75, cache_write_1h: 6, cache_read: 0.3 }, "claude-3-5-sonnet-20240620": { name: "Claude 3.5 Sonnet", input: 3, output: 15, cache_write_5m: 3.75, cache_write_1h: 6, cache_read: 0.3 }, "claude-3-5-sonnet-20241022": { name: "Claude 3.5 Sonnet", input: 3, output: 15, cache_write_5m: 3.75, cache_write_1h: 6, cache_read: 0.3 }, "claude-opus-4-20250514": { name: "Claude Opus 4", input: 15, output: 75, cache_write_5m: 18.75, cache_write_1h: 30, cache_read: 1.5 }, "claude-opus-4-1": { name: "Claude Opus 4.1", input: 15, output: 75, cache_write_5m: 18.75, cache_write_1h: 30, cache_read: 1.5 }, "claude-opus-4-1-20250805": { name: "Claude Opus 4.1", input: 15, output: 75, cache_write_5m: 18.75, cache_write_1h: 30, cache_read: 1.5 }, "claude-sonnet-4-20250514": { name: "Claude Sonnet 4", input: 3, output: 15, cache_write_5m: 3.75, cache_write_1h: 6, cache_read: 0.3 }, "claude-4-opus-20250514": { name: "Claude 4 Opus", input: 15, output: 75, cache_write_5m: 18.75, cache_write_1h: 30, cache_read: 1.5 }, "claude-4-sonnet-20250514": { name: "Claude 4 Sonnet", input: 3, output: 15, cache_write_5m: 3.75, cache_write_1h: 6, cache_read: 0.3 }, "claude-3-7-sonnet-latest": { name: "Claude 3.7 Sonnet Latest", input: 3, output: 15, cache_write_5m: 3.75, cache_write_1h: 6, cache_read: 0.3 }, "claude-3-7-sonnet-20250219": { name: "Claude 3.7 Sonnet", input: 3, output: 15, cache_write_5m: 3.75, cache_write_1h: 6, cache_read: 0.3 } }; var PricingService = class { static memoryCache = /* @__PURE__ */ new Map(); static CACHE_TTL = 24 * 60 * 60 * 1e3; static GITHUB_PRICING_URL = "https://raw.githubusercontent.com/Owloops/claude-powerline/main/pricing.json"; static getCacheFilePath() { const { homedir: homedir2 } = __require("os"); const { join: join2 } = __require("path"); const { mkdirSync } = __require("fs"); const cacheDir = join2(homedir2(), ".claude", "cache"); try { mkdirSync(cacheDir, { recursive: true }); } catch { } return join2(cacheDir, "pricing.json"); } static loadDiskCache() { try { const { readFileSync: readFileSync3 } = __require("fs"); const cacheFile = this.getCacheFilePath(); const content = readFileSync3(cacheFile, "utf-8"); const cached = JSON.parse(content); if (cached && cached.data && cached.timestamp) { return cached; } } catch { } return null; } static saveDiskCache(data) { try { const { writeFileSync } = __require("fs"); const cacheFile = this.getCacheFilePath(); const cacheData = { data, timestamp: Date.now() }; writeFileSync(cacheFile, JSON.stringify(cacheData)); } catch (error) { debug("Failed to save pricing cache to disk:", error); } } static async getCurrentPricing() { const now = Date.now(); const memCached = this.memoryCache.get("pricing"); if (memCached && now - memCached.timestamp < this.CACHE_TTL) { debug( `Using memory cached pricing data for ${Object.keys(memCached.data).length} models` ); return memCached.data; } const diskCached = this.loadDiskCache(); if (diskCached && now - diskCached.timestamp < this.CACHE_TTL) { this.memoryCache.clear(); this.memoryCache.set("pricing", diskCached); debug( `Using disk cached pricing data for ${Object.keys(diskCached.data).length} models` ); return diskCached.data; } try { const response = await globalThis.fetch(this.GITHUB_PRICING_URL, { headers: { "User-Agent": "claude-powerline", "Cache-Control": "no-cache" } }); if (response.ok) { const data = await response.json(); const dataObj = data; const meta = dataObj._meta; const pricingData = {}; for (const [key, value] of Object.entries(dataObj)) { if (key !== "_meta") { pricingData[key] = value; } } if (this.validatePricingData(pricingData)) { this.memoryCache.clear(); this.memoryCache.set("pricing", { data: pricingData, timestamp: now }); this.saveDiskCache(pricingData); debug( `Fetched fresh pricing from GitHub for ${Object.keys(pricingData).length} models` ); debug(`Pricing last updated: ${meta?.updated || "unknown"}`); return pricingData; } } } catch (error) { debug("Failed to fetch pricing from GitHub, using fallback:", error); } if (diskCached) { this.memoryCache.set("pricing", diskCached); debug( `Using stale cached pricing data for ${Object.keys(diskCached.data).length} models` ); return diskCached.data; } debug( `Using offline pricing data for ${Object.keys(OFFLINE_PRICING_DATA).length} models` ); return OFFLINE_PRICING_DATA; } static validatePricingData(data) { if (!data || typeof data !== "object") return false; for (const [, value] of Object.entries(data)) { if (!value || typeof value !== "object") return false; const pricing = value; if (typeof pricing.input !== "number" || typeof pricing.output !== "number" || typeof pricing.cache_read !== "number") { return false; } } return true; } static async getModelPricing(modelId) { const allPricing = await this.getCurrentPricing(); if (allPricing[modelId]) { return allPricing[modelId]; } return this.fuzzyMatchModel(modelId, allPricing); } static fuzzyMatchModel(modelId, allPricing) { const lowerModelId = modelId.toLowerCase(); for (const [key, pricing] of Object.entries(allPricing)) { if (key.toLowerCase() === lowerModelId) { return pricing; } } const patterns = [ { pattern: ["opus-4-1", "claude-opus-4-1"], fallback: "claude-opus-4-1-20250805" }, { pattern: ["opus-4", "claude-opus-4"], fallback: "claude-opus-4-20250514" }, { pattern: ["sonnet-4", "claude-sonnet-4"], fallback: "claude-sonnet-4-20250514" }, { pattern: ["sonnet-3.7", "3-7-sonnet"], fallback: "claude-3-7-sonnet-20250219" }, { pattern: ["3-5-sonnet", "sonnet-3.5"], fallback: "claude-3-5-sonnet-20241022" }, { pattern: ["3-5-haiku", "haiku-3.5"], fallback: "claude-3-5-haiku-20241022" }, { pattern: ["haiku", "3-haiku"], fallback: "claude-3-haiku-20240307" }, { pattern: ["opus"], fallback: "claude-opus-4-20250514" }, { pattern: ["sonnet"], fallback: "claude-3-5-sonnet-20241022" } ]; for (const { pattern, fallback } of patterns) { if (pattern.some((p) => lowerModelId.includes(p))) { if (allPricing[fallback]) { return allPricing[fallback]; } } } return allPricing["claude-3-5-sonnet-20241022"] || { name: `${modelId} (Unknown Model)`, input: 3, cache_write_5m: 3.75, cache_write_1h: 6, cache_read: 0.3, output: 15 }; } static async calculateCostForEntry(entry) { const message = entry.message; const usage = message?.usage; if (!usage) return 0; const modelId = this.extractModelId(entry); const pricing = await this.getModelPricing(modelId); const inputTokens = usage.input_tokens || 0; const outputTokens = usage.output_tokens || 0; const cacheCreationTokens = usage.cache_creation_input_tokens || 0; const cacheReadTokens = usage.cache_read_input_tokens || 0; const inputCost = inputTokens / 1e6 * pricing.input; const outputCost = outputTokens / 1e6 * pricing.output; const cacheReadCost = cacheReadTokens / 1e6 * pricing.cache_read; const cacheCreationCost = cacheCreationTokens / 1e6 * pricing.cache_write_5m; return inputCost + outputCost + cacheCreationCost + cacheReadCost; } static extractModelId(entry) { if (entry.model && typeof entry.model === "string") { return entry.model; } const message = entry.message; if (message?.model) { const model = message.model; if (typeof model === "string") { return model; } return model?.id || "claude-3-5-sonnet-20241022"; } if (entry.model_id && typeof entry.model_id === "string") { return entry.model_id; } return "claude-3-5-sonnet-20241022"; } }; // src/utils/claude.ts import { readdir, readFile, stat } from "fs/promises"; import { existsSync } from "fs"; import { join, posix } from "path"; import { homedir } from "os"; function getClaudePaths() { const paths = []; const envPath = process.env.CLAUDE_CONFIG_DIR; if (envPath) { envPath.split(",").forEach((path5) => { const trimmedPath = path5.trim(); if (existsSync(trimmedPath)) { paths.push(trimmedPath); } }); } if (paths.length === 0) { const homeDir = homedir(); const configPath = join(homeDir, ".config", "claude"); const claudePath = join(homeDir, ".claude"); if (existsSync(configPath)) { paths.push(configPath); } else if (existsSync(claudePath)) { paths.push(claudePath); } } return paths; } async function findProjectPaths(claudePaths) { const projectPaths = []; for (const claudePath of claudePaths) { const projectsDir = join(claudePath, "projects"); if (existsSync(projectsDir)) { try { const entries = await readdir(projectsDir, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { const projectPath = posix.join(projectsDir, entry.name); projectPaths.push(projectPath); } } } catch (error) { debug(`Failed to read projects directory ${projectsDir}:`, error); } } } return projectPaths; } async function findTranscriptFile(sessionId) { const claudePaths = getClaudePaths(); const projectPaths = await findProjectPaths(claudePaths); for (const projectPath of projectPaths) { const transcriptPath = posix.join(projectPath, `${sessionId}.jsonl`); if (existsSync(transcriptPath)) { return transcriptPath; } } return null; } async function getEarliestTimestamp(filePath) { try { const content = await readFile(filePath, "utf-8"); const lines = content.trim().split("\n"); let earliestDate = null; for (const line of lines) { if (!line.trim()) continue; try { const json2 = JSON.parse(line); if (json2.timestamp && typeof json2.timestamp === "string") { const date = new Date(json2.timestamp); if (!isNaN(date.getTime())) { if (earliestDate === null || date < earliestDate) { earliestDate = date; } } } } catch { continue; } } return earliestDate; } catch (error) { debug(`Failed to get earliest timestamp for ${filePath}:`, error); return null; } } async function sortFilesByTimestamp(files, oldestFirst = true) { const filesWithTimestamps = await Promise.all( files.map(async (file) => ({ file, timestamp: await getEarliestTimestamp(file) })) ); return filesWithTimestamps.sort((a, b) => { if (a.timestamp === null && b.timestamp === null) return 0; if (a.timestamp === null) return 1; if (b.timestamp === null) return -1; const sortOrder = oldestFirst ? 1 : -1; return sortOrder * (a.timestamp.getTime() - b.timestamp.getTime()); }).map((item) => item.file); } async function getFileModificationDate(filePath) { try { const stats = await stat(filePath); return stats.mtime; } catch { return null; } } function createUniqueHash(entry) { const messageId = entry.message?.id || (typeof entry.raw.message === "object" && entry.raw.message !== null && "id" in entry.raw.message ? entry.raw.message.id : void 0); const requestId = "requestId" in entry.raw ? entry.raw.requestId : void 0; if (!messageId || !requestId) { return null; } return `${messageId}:${requestId}`; } async function parseJsonlFile(filePath) { try { const content = await readFile(filePath, "utf-8"); const lines = content.trim().split("\n").filter((line) => line.trim()); const entries = []; for (const line of lines) { try { const raw = JSON.parse(line); if (!raw.timestamp) continue; const entry = { timestamp: new Date(raw.timestamp), message: raw.message, costUSD: typeof raw.costUSD === "number" ? raw.costUSD : void 0, isSidechain: raw.isSidechain === true, raw }; entries.push(entry); } catch (parseError) { debug(`Failed to parse JSONL line: ${parseError}`); continue; } } return entries; } catch (error) { debug(`Failed to read file ${filePath}:`, error); return []; } } async function loadEntriesFromProjects(timeFilter, fileFilter, sortFiles = false) { const entries = []; const claudePaths = getClaudePaths(); const projectPaths = await findProjectPaths(claudePaths); const processedHashes = /* @__PURE__ */ new Set(); const allFiles = []; for (const projectPath of projectPaths) { try { const files = await readdir(projectPath); const jsonlFiles = files.filter((file) => file.endsWith(".jsonl")); const fileStatsPromises = jsonlFiles.map(async (file) => { const filePath = posix.join(projectPath, file); if (existsSync(filePath)) { const mtime = await getFileModificationDate(filePath); return { filePath, mtime }; } return null; }); const fileStats = await Promise.all(fileStatsPromises); for (const stat2 of fileStats) { if (stat2?.mtime && (!fileFilter || fileFilter(stat2.filePath, stat2.mtime))) { allFiles.push(stat2.filePath); } } } catch (dirError) { debug(`Failed to read project directory ${projectPath}:`, dirError); continue; } } if (sortFiles) { const sortedFiles = await sortFilesByTimestamp(allFiles, false); allFiles.length = 0; allFiles.push(...sortedFiles); } for (const filePath of allFiles) { const fileEntries = await parseJsonlFile(filePath); for (const entry of fileEntries) { const uniqueHash = createUniqueHash(entry); if (uniqueHash && processedHashes.has(uniqueHash)) { debug(`Skipping duplicate entry: ${uniqueHash}`); continue; } if (uniqueHash) { processedHashes.add(uniqueHash); } if (!timeFilter || timeFilter(entry)) { entries.push(entry); } } } return entries; } // src/segments/session.ts function convertToSessionEntry(entry) { return { timestamp: entry.timestamp.toISOString(), message: { usage: { input_tokens: entry.message?.usage?.input_tokens || 0, output_tokens: entry.message?.usage?.output_tokens || 0, cache_creation_input_tokens: entry.message?.usage?.cache_creation_input_tokens, cache_read_input_tokens: entry.message?.usage?.cache_read_input_tokens } }, costUSD: entry.costUSD }; } var SessionProvider = class { async getSessionUsage(sessionId) { try { const transcriptPath = await findTranscriptFile(sessionId); if (!transcriptPath) { debug(`No transcript found for session: ${sessionId}`); return null; } debug(`Found transcript at: ${transcriptPath}`); const parsedEntries = await parseJsonlFile(transcriptPath); if (parsedEntries.length === 0) { return { totalCost: 0, entries: [] }; } const entries = []; let totalCost = 0; for (const entry of parsedEntries) { if (entry.message?.usage) { const sessionEntry = convertToSessionEntry(entry); if (sessionEntry.costUSD !== void 0) { totalCost += sessionEntry.costUSD; } else { const cost = await PricingService.calculateCostForEntry(entry.raw); sessionEntry.costUSD = cost; totalCost += cost; } entries.push(sessionEntry); } } debug( `Parsed ${entries.length} usage entries, total cost: $${totalCost.toFixed(4)}` ); return { totalCost, entries }; } catch (error) { debug(`Error reading session usage for ${sessionId}:`, error); return null; } } calculateTokenBreakdown(entries) { return entries.reduce( (breakdown, entry) => ({ input: breakdown.input + (entry.message.usage.input_tokens || 0), output: breakdown.output + (entry.message.usage.output_tokens || 0), cacheCreation: breakdown.cacheCreation + (entry.message.usage.cache_creation_input_tokens || 0), cacheRead: breakdown.cacheRead + (entry.message.usage.cache_read_input_tokens || 0) }), { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 } ); } async getSessionInfo(sessionId) { const sessionUsage = await this.getSessionUsage(sessionId); if (!sessionUsage || sessionUsage.entries.length === 0) { return { cost: null, tokens: null, tokenBreakdown: null }; } const tokenBreakdown = this.calculateTokenBreakdown(sessionUsage.entries); const totalTokens = tokenBreakdown.input + tokenBreakdown.output + tokenBreakdown.cacheCreation + tokenBreakdown.cacheRead; return { cost: sessionUsage.totalCost, tokens: totalTokens, tokenBreakdown }; } }; var UsageProvider = class { sessionProvider = new SessionProvider(); async getUsageInfo(sessionId) { try { debug(`Starting usage info retrieval for session: ${sessionId}`); const sessionInfo = await this.sessionProvider.getSessionInfo(sessionId); return { session: sessionInfo }; } catch (error) { debug(`Error getting usage info for session ${sessionId}:`, error); return { session: { cost: null, tokens: null, tokenBreakdown: null } }; } } }; // src/segments/context.ts import { readFileSync } from "fs"; var ContextProvider = class { thresholds = { LOW: 50, MEDIUM: 80 }; getContextUsageThresholds() { return this.thresholds; } getContextLimit(_modelId) { return 2e5; } async calculateContextTokens(transcriptPath, modelId) { try { debug(`Calculating context tokens from transcript: ${transcriptPath}`); try { const content = readFileSync(transcriptPath, "utf-8"); if (!content) { debug("Transcript file is empty"); return null; } } catch { debug("Could not read transcript file"); return null; } const parsedEntries = await parseJsonlFile(transcriptPath); if (parsedEntries.length === 0) { debug("No entries in transcript"); return null; } let mostRecentEntry = null; for (let i = parsedEntries.length - 1; i >= 0; i--) { const entry = parsedEntries[i]; if (!entry) continue; if (!entry.message?.usage?.input_tokens) continue; if (entry.isSidechain === true) continue; mostRecentEntry = entry; debug( `Context segment: Found most recent entry at ${entry.timestamp.toISOString()}, stopping search` ); break; } if (mostRecentEntry?.message?.usage) { const usage = mostRecentEntry.message.usage; const contextLength = (usage.input_tokens || 0) + (usage.cache_read_input_tokens || 0) + (usage.cache_creation_input_tokens || 0); const contextLimit = modelId ? this.getContextLimit(modelId) : 2e5; debug( `Most recent main chain context: ${contextLength} tokens (limit: ${contextLimit})` ); const percentage = Math.min( 100, Math.max(0, Math.round(contextLength / contextLimit * 100)) ); const usableLimit = Math.round(contextLimit * 0.75); const usablePercentage = Math.min( 100, Math.max(0, Math.round(contextLength / usableLimit * 100)) ); const contextLeftPercentage = Math.max(0, 100 - usablePercentage); return { inputTokens: contextLength, percentage, usablePercentage, contextLeftPercentage, maxTokens: contextLimit, usableTokens: usableLimit }; } debug("No main chain entries with usage data found"); return null; } catch (error) { debug( `Error reading transcript: ${error instanceof Error ? error.message : String(error)}` ); return null; } } }; // src/segments/context-progressbar.ts import { readFileSync as readFileSync2 } from "fs"; var ContextProgressBarProvider = class { thresholds = { GREEN_MAX: 40, YELLOW_MAX: 60 }; getContextProgressBarThresholds() { return this.thresholds; } getContextLimit(_modelId) { return 2e5; } getColorForPercentage(percentage) { if (percentage < this.thresholds.GREEN_MAX) { return "green"; } else if (percentage < this.thresholds.YELLOW_MAX) { return "yellow"; } else { return "red"; } } async calculateContextProgressBar(transcriptPath, modelId) { try { debug(`Calculating context progress bar from transcript: ${transcriptPath}`); try { const content = readFileSync2(transcriptPath, "utf-8"); if (!content) { debug("Transcript file is empty"); return null; } } catch { debug("Could not read transcript file"); return null; } const parsedEntries = await parseJsonlFile(transcriptPath); if (parsedEntries.length === 0) { debug("No entries in transcript"); return null; } let mostRecentEntry = null; for (let i = parsedEntries.length - 1; i >= 0; i--) { const entry = parsedEntries[i]; if (!entry) continue; if (!entry.message?.usage?.input_tokens) continue; if (entry.isSidechain === true) continue; mostRecentEntry = entry; debug( `Context Progress Bar segment: Found most recent entry at ${entry.timestamp.toISOString()}, stopping search` ); break; } if (mostRecentEntry?.message?.usage) { const usage = mostRecentEntry.message.usage; const contextLength = (usage.input_tokens || 0) + (usage.cache_read_input_tokens || 0) + (usage.cache_creation_input_tokens || 0); const contextLimit = modelId ? this.getContextLimit(modelId) : 2e5; debug( `Most recent main chain context: ${contextLength} tokens (limit: ${contextLimit})` ); const percentage = Math.min( 100, Math.max(0, Math.round(contextLength / contextLimit * 100)) ); const usableLimit = Math.round(contextLimit * 0.75); const usablePercentage = Math.min( 100, Math.max(0, Math.round(contextLength / usableLimit * 100)) ); const contextLeftPercentage = Math.max(0, 100 - usablePercentage); const color = this.getColorForPercentage(percentage); return { inputTokens: contextLength, percentage, usablePercentage, contextLeftPercentage, maxTokens: contextLimit, usableTokens: usableLimit, color }; } debug("No main chain entries with usage data found"); return null; } catch (error) { debug( `Error reading transcript: ${error instanceof Error ? error.message : String(error)}` ); return null; } } }; // src/segments/metrics.ts import { readFile as readFile2 } from "fs/promises"; var MetricsProvider = class { async loadTranscriptEntries(sessionId) { try { const transcriptPath = await findTranscriptFile(sessionId); if (!transcriptPath) { debug(`No transcript found for session: ${sessionId}`); return []; } debug(`Loading transcript from: ${transcriptPath}`); const content = await readFile2(transcriptPath, "utf-8"); const lines = content.trim().split("\n").filter((line) => line.trim()); const entries = []; for (const line of lines) { try { const entry = JSON.parse(line); if (entry.isSidechain === true) { continue; } entries.push(entry); } catch (parseError) { debug(`Failed to parse JSONL line: ${parseError}`); continue; } } debug(`Loaded ${entries.length} transcript entries`); return entries; } catch (error) { debug(`Error loading transcript for ${sessionId}:`, error); return []; } } calculateResponseTimes(entries) { const userMessages = []; const assistantMessages = []; let lastUserMessageIndex = -1; let lastUserMessageTime = null; let lastResponseEndTime = null; let lastResponseEndIndex = -1; for (let i = 0; i < entries.length; i++) { const entry = entries[i]; if (!entry || !entry.timestamp) continue; try { const timestamp = new Date(entry.timestamp); const messageType = entry.type || entry.message?.role || entry.message?.type; const isToolResult = entry.type === "user" && entry.message?.content?.[0]?.type === "tool_result"; const isRealUserMessage = messageType === "user" && !isToolResult; if (isRealUserMessage) { userMessages.push(timestamp); lastUserMessageTime = timestamp; lastUserMessageIndex = i; lastResponseEndTime = null; lastResponseEndIndex = -1; debug( `Found user message at index ${i}, timestamp ${timestamp.toISOString()}` ); } else if (lastUserMessageIndex >= 0) { const isPartOfResponse = messageType === "assistant" || isToolResult || messageType === "system" || entry.message?.usage; if (isPartOfResponse) { lastResponseEndTime = timestamp; lastResponseEndIndex = i; if (messageType === "assistant" || entry.message?.usage) { assistantMessages.push(timestamp); debug( `Found assistant message at index ${i}, timestamp ${timestamp.toISOString()}` ); } else if (isToolResult) { debug( `Found tool result at index ${i}, timestamp ${timestamp.toISOString()}` ); } else { debug( `Found ${messageType} message at index ${i}, timestamp ${timestamp.toISOString()}` ); } } } } catch { continue; } } if (userMessages.length === 0 || assistantMessages.length === 0) { return { average: null, last: null }; } const responseTimes = []; for (const assistantTime of assistantMessages) { const priorUsers = userMessages.filter( (userTime) => userTime < assistantTime ); if (priorUsers.length > 0) { const userTime = new Date( Math.max(...priorUsers.map((d) => d.getTime())) ); const responseTime = (assistantTime.getTime() - userTime.getTime()) / 1e3; if (responseTime > 0.1 && responseTime < 300) { responseTimes.push(responseTime); debug(`Valid response time: ${responseTime.toFixed(1)}s`); } else { debug( `Rejected response time: ${responseTime.toFixed(1)}s (outside 0.1s-5m range)` ); } } } let lastResponseTime = null; if (lastUserMessageTime && lastResponseEndTime && lastResponseEndIndex > lastUserMessageIndex) { const timeDiff = lastResponseEndTime.getTime() - lastUserMessageTime.getTime(); const positionDiff = lastResponseEndIndex - lastUserMessageIndex; if (timeDiff === 0 && positionDiff > 0) { lastResponseTime = positionDiff * 0.1; debug( `Estimated last response time from position difference: ${lastResponseTime.toFixed(2)}s (${positionDiff} messages)` ); } else if (timeDiff > 0) { lastResponseTime = timeDiff / 1e3; debug( `Last response time from timestamps: ${lastResponseTime.toFixed(2)}s` ); } debug( `Last user message at index ${lastUserMessageIndex}, timestamp ${lastUserMessageTime.toISOString()}` ); debug( `Last response end at index ${lastResponseEndIndex}, timestamp ${lastResponseEndTime.toISOString()}` ); } if (responseTimes.length === 0 && lastResponseTime === null) { return { average: null, last: null }; } const avgResponseTime = responseTimes.length > 0 ? responseTimes.reduce((sum, time) => sum + time, 0) / responseTimes.length : null; debug( `Calculated average response time: ${avgResponseTime?.toFixed(2) || "null"}s from ${responseTimes.length} measurements` ); debug(`Last response time: ${lastResponseTime?.toFixed(2) || "null"}s`); return { average: avgResponseTime, last: lastResponseTime }; } calculateSessionDuration(entries) { const timestamps = []; for (const entry of entries) { if (!entry.timestamp) continue; try { timestamps.push(new Date(entry.timestamp)); } catch { continue; } } if (timestamps.length < 2) { return null; } timestamps.sort((a, b) => a.getTime() - b.getTime()); const lastTimestamp = timestamps[timestamps.length - 1]; const firstTimestamp = timestamps[0]; if (!lastTimestamp || !firstTimestamp) { return null; } const duration = (lastTimestamp.getTime() - firstTimestamp.getTime()) / 1e3; return duration > 0 ? duration : null; } calculateBurnRateDuration(entries) { if (entries.length === 0) return null; const now = /* @__PURE__ */ new Date(); const timestamps = entries.map((entry) => entry.timestamp).filter(Boolean).map((ts) => new Date(ts)).filter((ts) => now.getTime() - ts.getTime() < 2 * 60 * 60 * 1e3).sort((a, b) => a.getTime() - b.getTime()); if (timestamps.length === 0) return null; const sessionStart = timestamps[0]; if (!sessionStart) return null; const durationFromStart = Math.max( (now.getTime() - sessionStart.getTime()) / 1e3, 30 * 60 ); return durationFromStart; } calculateMessageCount(entries) { return entries.filter((entry) => { const messageType = entry.type || entry.message?.role || entry.message?.type; const isToolResult = entry.type === "user" && entry.message?.content?.[0]?.type === "tool_result"; return messageType === "user" && !isToolResult; }).length; } async calculateTotalCost(entries) { let total = 0; const processedEntries = /* @__PURE__ */ new Set(); for (const entry of entries) { const entryKey = `${entry.timestamp}-${JSON.stringify(entry.message?.usage || {})}`; if (processedEntries.has(entryKey)) { debug(`Skipping duplicate entry at ${entry.timestamp}`); continue; } processedEntries.add(entryKey); if (typeof entry.costUSD === "number") { total += entry.costUSD; } else if (entry.message?.usage) { const cost = await PricingService.calculateCostForEntry(entry); total += cost; } } return Math.round(total * 1e4) / 1e4; } calculateTotalTokens(entries) { const processedEntries = /* @__PURE__ */ new Set(); return entries.reduce((total, entry) => { const usage = entry.message?.usage; if (!usage) return total; const entryKey = `${entry.timestamp}-${JSON.stringify(usage)}`; if (processedEntries.has(entryKey)) { debug(`Skipping duplicate token entry at ${entry.timestamp}`); return total; } processedEntries.add(entryKey); return total + (usage.input_tokens || 0) + (usage.output_tokens || 0) + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0); }, 0); } async getMetricsInfo(sessionId) { try { debug(`Starting metrics calculation for session: ${sessionId}`); const entries = await this.loadTranscriptEntries(sessionId); if (entries.length === 0) { return { responseTime: null, lastResponseTime: null, sessionDuration: null, messageCount: null, costBurnRate: null, tokenBurnRate: null }; } const responseTimes = this.calculateResponseTimes(entries); const sessionDuration = this.calculateSessionDuration(entries); const messageCount = this.calculateMessageCount(entries); let costBurnRate = null; let tokenBurnRate = null; const burnRateDuration = this.calculateBurnRateDuration(entries); if (burnRateDuration && burnRateDuration > 60) { const hoursElapsed = burnRateDuration / 3600; if (hoursElapsed <= 0) { debug(`Invalid hours elapsed: ${hoursElapsed}`); } else { const totalCost = await this.calculateTotalCost(entries); const totalTokens = this.calculateTotalTokens(entries); if (totalCost > 0) { costBurnRate = Math.round(totalCost / hoursElapsed * 100) / 100; debug( `Cost burn rate: $${costBurnRate}/h (total: $${totalCost}, duration: ${hoursElapsed}h)` ); } if (totalTokens > 0) { tokenBurnRate = Math.round(totalTokens / hoursElapsed); debug( `Token burn rate: ${tokenBurnRate}/h (total: ${totalTokens}, duration: ${hoursElapsed}h)` ); } } } debug( `Metrics calculated: avgResponseTime=${responseTimes.average?.toFixed(2) || "null"}s, lastResponseTime=${responseTimes.last?.toFixed(2) || "null"}s, sessionDuration=${sessionDuration?.toFixed(0) || "null"}s, messageCount=${messageCount}` ); return { responseTime: responseTimes.average, lastResponseTime: responseTimes.last, sessionDuration, messageCount, costBurnRate, tokenBurnRate }; } catch (error) { debug(`Error calculating metrics for session ${sessionId}:`, error); return { responseTime: null, lastResponseTime: null, sessionDuration: null, messageCount: null, costBurnRate: null, tokenBurnRate: null }; } } }; // src/segments/version.ts import { execSync as execSync3 } from "child_process"; var VersionProvider = class { cachedVersion = null; cacheTimestamp = 0; CACHE_TTL = 3e4; getClaudeVersion() { const now = Date.now(); if (this.cachedVersion !== null && now - this.cacheTimestamp < this.CACHE_TTL) { return this.cachedVersion; } try { const output = execSync3("claude --version", { encoding: "utf8", timeout: 1e3 }).trim(); const match = output.match(/^([\d.]+)/); if (match) { this.cachedVersion = `v${match[1]}`; this.cacheTimestamp = now; debug(`Claude Code version: ${this.cachedVersion}`); return this.cachedVersion; } debug(`Could not parse version from: ${output}`); return null; } catc