claude-statusline-amdo
Version:
Custom statusline for Claude Code. Forked from Owloops/claude-powerline
1,538 lines (1,519 loc) • 99.5 kB
JavaScript
#!/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