claude-status-bar
Version:
Powerline-style status bar for Claude Code CLI with i18n support (English/Korean)
1,709 lines (1,683 loc) • 67.3 kB
JavaScript
#!/usr/bin/env node
import { program } from 'commander';
import { z } from 'zod';
import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'fs';
import { join, dirname } from 'path';
import { homedir } from 'os';
import { render, useApp, useInput, Box, Text } from 'ink';
import { jsxs, jsx } from 'react/jsx-runtime';
import { exec, execSync } from 'child_process';
import i18next from 'i18next';
import { initReactI18next, useTranslation } from 'react-i18next';
import { Chalk } from 'chalk';
import { promisify } from 'util';
import { createHash } from 'crypto';
import React2, { useState } from 'react';
var ClaudeInputSchema = z.object({
session_id: z.string().optional().default(""),
transcript_path: z.string().optional().default(""),
model: z.object({
id: z.string().optional().default("unknown"),
display_name: z.string().optional().default("Claude")
}).optional().default({ id: "unknown", display_name: "Claude" }),
workspace: z.object({
current_dir: z.string().optional().default(""),
project_dir: z.string().optional().default("")
}).optional().default({ current_dir: "", project_dir: "" }),
cost: z.object({
// Claude Code 실제 필드명
total_cost_usd: z.number().optional(),
total_duration_ms: z.number().optional(),
total_api_duration_ms: z.number().optional(),
// 실제 API 호출 시간
total_lines_added: z.number().optional(),
total_lines_removed: z.number().optional(),
// 이전 버전 호환성
api_cost: z.number().optional(),
duration_ms: z.number().optional(),
lines_added: z.number().optional(),
lines_removed: z.number().optional()
}).optional(),
version: z.string().optional(),
cwd: z.string().optional().default("")
// Claude Code가 전달할 수 있는 추가 필드들 허용
}).passthrough();
var MODEL_MAX_TOKENS = {
"claude-opus-4-5-20251101": 2e5,
"claude-sonnet-4-5-20250929": 2e5,
"claude-sonnet-4-20250514": 2e5,
"claude-3-5-sonnet-20241022": 2e5,
"claude-3-5-haiku-20241022": 2e5,
"claude-3-opus-20240229": 2e5,
"claude-3-sonnet-20240229": 2e5,
"claude-3-haiku-20240307": 2e5
};
function getModelMaxTokens(modelId) {
if (MODEL_MAX_TOKENS[modelId]) {
return MODEL_MAX_TOKENS[modelId];
}
for (const [key, value] of Object.entries(MODEL_MAX_TOKENS)) {
if (modelId.includes(key) || key.includes(modelId)) {
return value;
}
}
return 2e5;
}
async function readStdin() {
return new Promise((resolve, reject) => {
let data = "";
if (process.stdin.isTTY) {
resolve("");
return;
}
process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk) => {
data += chunk;
});
process.stdin.on("end", () => {
resolve(data.trim());
});
process.stdin.on("error", (error) => {
reject(error);
});
setTimeout(() => {
if (data.length === 0) {
resolve("");
}
}, 5e3);
});
}
async function parseClaudeInput() {
try {
const raw = await readStdin();
if (!raw) {
if (process.env.DEBUG_STATUSLINE) {
console.error("[statusline] No stdin data received");
}
return null;
}
if (process.env.DEBUG_STATUSLINE) {
console.error("[statusline] Raw input:", raw.substring(0, 500));
}
const parsed = JSON.parse(raw);
if (process.env.DEBUG_STATUSLINE) {
console.error("[statusline] Parsed keys:", Object.keys(parsed));
}
try {
const logPath = join(homedir(), ".claude", "statusline-debug.json");
writeFileSync(logPath, JSON.stringify(parsed, null, 2));
} catch {
}
const validated = ClaudeInputSchema.parse(parsed);
return validated;
} catch (error) {
if (process.env.DEBUG_STATUSLINE) {
console.error("[statusline] Parse error:", error);
}
return null;
}
}
function createMockClaudeInput() {
return {
session_id: "mock-session-123",
transcript_path: "",
model: {
id: "claude-sonnet-4-20250514",
display_name: "Claude Sonnet 4"
},
workspace: {
current_dir: process.cwd(),
project_dir: process.cwd()
},
cost: {
api_cost: 0.0523,
duration_ms: 125e3
},
version: "1.0.0",
cwd: process.cwd()
};
}
// src/themes/powerline-dark.ts
var powerlineDark = {
id: "powerline-dark",
name: "Powerline Dark",
symbols: {
separator: "\uE0B0",
//
separatorThin: "\uE0B1",
//
branch: "\uE0A0",
//
readonly: "\uE0A2",
//
modified: "\u25CF"
},
colors: {
segments: {
model: { bg: "#5c6bc0", fg: "#ffffff" },
git: { bg: "#ffffff", fg: "#37474f" },
tokens: { bg: "#42a5f5", fg: "#0d47a1" },
cost: { bg: "#ffa726", fg: "#e65100" },
session: { bg: "#78909c", fg: "#263238" },
cwd: { bg: "#5c6bc0", fg: "#ffffff" },
context: { bg: "#ab47bc", fg: "#ffffff" },
todo: { bg: "#26a69a", fg: "#004d40" },
memory: { bg: "#ec407a", fg: "#ffffff" },
files: { bg: "#66bb6a", fg: "#1b5e20" }
},
status: {
normal: "#66bb6a",
warning: "#ffa726",
error: "#ef5350",
success: "#66bb6a"
},
progress: {
filled: "#42a5f5",
empty: "#424242",
warning: "#ffa726",
critical: "#ef5350"
}
}
};
// src/themes/powerline-light.ts
var powerlineLight = {
id: "powerline-light",
name: "Powerline Light",
symbols: {
separator: "\uE0B0",
//
separatorThin: "\uE0B1",
//
branch: "\uE0A0",
//
readonly: "\uE0A2",
//
modified: "\u25CF"
},
colors: {
segments: {
model: { bg: "#7986cb", fg: "#1a237e" },
git: { bg: "#ffffff", fg: "#37474f" },
tokens: { bg: "#64b5f6", fg: "#0d47a1" },
cost: { bg: "#ffb74d", fg: "#e65100" },
session: { bg: "#90a4ae", fg: "#263238" },
cwd: { bg: "#7986cb", fg: "#1a237e" },
context: { bg: "#ba68c8", fg: "#4a148c" },
todo: { bg: "#4db6ac", fg: "#004d40" },
memory: { bg: "#f48fb1", fg: "#880e4f" },
files: { bg: "#81c784", fg: "#1b5e20" }
},
status: {
normal: "#4caf50",
warning: "#ff9800",
error: "#f44336",
success: "#4caf50"
},
progress: {
filled: "#2196f3",
empty: "#bdbdbd",
warning: "#ff9800",
critical: "#f44336"
}
}
};
// src/themes/minimal.ts
var minimal = {
id: "minimal",
name: "Minimal (ASCII)",
symbols: {
separator: ">",
separatorThin: "|",
branch: "",
readonly: "[RO]",
modified: "*"
},
colors: {
segments: {
model: { bg: "#6366f1", fg: "#ffffff" },
git: { bg: "#ffffff", fg: "#37474f" },
tokens: { bg: "#3b82f6", fg: "#ffffff" },
cost: { bg: "#f59e0b", fg: "#ffffff" },
session: { bg: "#6b7280", fg: "#ffffff" },
cwd: { bg: "#6366f1", fg: "#ffffff" },
context: { bg: "#a855f7", fg: "#ffffff" },
todo: { bg: "#14b8a6", fg: "#ffffff" },
memory: { bg: "#ec4899", fg: "#ffffff" },
files: { bg: "#22c55e", fg: "#ffffff" }
},
status: {
normal: "#22c55e",
warning: "#f59e0b",
error: "#ef4444",
success: "#22c55e"
},
progress: {
filled: "#3b82f6",
empty: "#374151",
warning: "#f59e0b",
critical: "#ef4444"
}
}
};
// src/themes/index.ts
var themes = {
"powerline-dark": powerlineDark,
"powerline-light": powerlineLight,
"minimal": minimal
};
function getTheme(themeId) {
return themes[themeId] ?? powerlineDark;
}
function getAvailableThemes() {
return Object.values(themes);
}
// src/widgets/registry.ts
var WidgetRegistry = class {
widgets = /* @__PURE__ */ new Map();
/**
* 위젯 등록
*/
register(widget) {
if (this.widgets.has(widget.id)) {
console.warn(`Widget "${widget.id}" is already registered. Overwriting.`);
}
this.widgets.set(widget.id, widget);
}
/**
* ID로 위젯 가져오기
*/
get(id) {
return this.widgets.get(id);
}
/**
* 모든 위젯 가져오기
*/
getAll() {
return Array.from(this.widgets.values());
}
/**
* 활성화된 위젯만 가져오기 (순서대로 정렬)
*/
getEnabled(widgetConfigs) {
return this.getAll().filter((widget) => {
const config2 = widgetConfigs[widget.id];
return config2 ? config2.enabled : widget.defaultEnabled;
}).sort((a, b) => {
const orderA = widgetConfigs[a.id]?.order ?? a.defaultOrder;
const orderB = widgetConfigs[b.id]?.order ?? b.defaultOrder;
return orderA - orderB;
});
}
/**
* 위젯 개수
*/
get size() {
return this.widgets.size;
}
/**
* 레지스트리 초기화
*/
clear() {
this.widgets.clear();
}
};
var widgetRegistry = new WidgetRegistry();
// src/utils/format.ts
function formatTokens(tokens) {
if (tokens >= 1e6) {
return `${(tokens / 1e6).toFixed(1)}M`;
}
if (tokens >= 1e3) {
return `${(tokens / 1e3).toFixed(1)}K`;
}
return tokens.toString();
}
function formatCost(cost) {
if (cost < 0.01) {
return `$${cost.toFixed(4)}`;
}
if (cost < 1) {
return `$${cost.toFixed(3)}`;
}
return `$${cost.toFixed(2)}`;
}
function formatDuration(ms) {
const seconds = Math.floor(ms / 1e3);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours > 0) {
const remainingMinutes = minutes % 60;
return `${hours}h${remainingMinutes > 0 ? ` ${remainingMinutes}m` : ""}`;
}
if (minutes > 0) {
const remainingSeconds = seconds % 60;
return `${minutes}m${remainingSeconds > 0 ? ` ${remainingSeconds}s` : ""}`;
}
return `${seconds}s`;
}
function shortenPath(path, maxLength = 30) {
const home = process.env.HOME || process.env.USERPROFILE || "";
let shortened = path;
if (home && path.startsWith(home)) {
shortened = "~" + path.slice(home.length);
}
shortened = shortened.replace(/\\/g, "/");
if (shortened.length > maxLength) {
const parts = shortened.split("/");
if (parts.length > 3) {
const first = parts[0];
const last = parts.slice(-2).join("/");
shortened = `${first}/.../${last}`;
}
}
return shortened;
}
function formatPercent(value) {
return `${Math.round(value)}%`;
}
function shortenModelName(modelId) {
const patterns = [
[/claude-opus-4-5/i, "opus-4.5"],
[/claude-sonnet-4-5/i, "sonnet-4.5"],
[/claude-sonnet-4/i, "sonnet-4"],
[/claude-3-5-sonnet/i, "sonnet-3.5"],
[/claude-3-5-haiku/i, "haiku-3.5"],
[/claude-3-opus/i, "opus-3"],
[/claude-3-sonnet/i, "sonnet-3"],
[/claude-3-haiku/i, "haiku-3"]
];
for (const [pattern, replacement] of patterns) {
if (pattern.test(modelId)) {
return replacement;
}
}
return modelId.length > 15 ? modelId.slice(0, 15) + "..." : modelId;
}
var ModelWidgetComponent = ({ data }) => {
const modelName = shortenModelName(
data.model?.display_name || data.model?.id || "unknown"
);
return /* @__PURE__ */ jsx(Text, { children: modelName });
};
var ModelWidget = {
id: "model",
name: "Model",
description: "Displays the current Claude model name",
defaultEnabled: true,
defaultOrder: 0,
colorKey: "model",
Component: ModelWidgetComponent
};
var gitCache = null;
var CACHE_TTL = 5e3;
function getGitInfo(cwd) {
const now = Date.now();
if (gitCache && gitCache.expires > now) {
return gitCache.value;
}
try {
const options = {
encoding: "utf8",
timeout: 500,
stdio: ["pipe", "pipe", "ignore"],
cwd: cwd || process.cwd()
};
const branch = execSync("git branch --show-current", options).trim();
let isDirty = false;
let linesAdded = 0;
let linesRemoved = 0;
let filesChanged = 0;
try {
const status = execSync("git status --porcelain", options).trim();
isDirty = status.length > 0;
if (isDirty) {
const diffUnstaged = execSync("git diff --numstat", options).trim();
const diffStaged = execSync("git diff --cached --numstat", options).trim();
const parseDiff = (diff) => {
if (!diff) return { added: 0, removed: 0, files: 0 };
let added = 0, removed = 0, files = 0;
for (const line of diff.split("\n")) {
const parts = line.split(" ");
if (parts.length >= 2) {
const a = parseInt(parts[0], 10);
const r = parseInt(parts[1], 10);
if (!isNaN(a)) added += a;
if (!isNaN(r)) removed += r;
files++;
}
}
return { added, removed, files };
};
const unstaged = parseDiff(diffUnstaged);
const staged = parseDiff(diffStaged);
linesAdded = unstaged.added + staged.added;
linesRemoved = unstaged.removed + staged.removed;
filesChanged = unstaged.files + staged.files;
}
} catch {
}
const result = { branch: branch || null, isDirty, linesAdded, linesRemoved, filesChanged };
gitCache = { value: result, expires: now + CACHE_TTL };
return result;
} catch {
const result = { branch: null, isDirty: false, linesAdded: 0, linesRemoved: 0, filesChanged: 0 };
gitCache = { value: result, expires: now + CACHE_TTL };
return result;
}
}
var GitBranchWidgetComponent = ({ data, theme }) => {
const gitInfo = getGitInfo(data.cwd || data.workspace?.current_dir);
if (!gitInfo.branch) {
return null;
}
const branchIcon = theme.symbols.branch;
const modifiedIcon = gitInfo.isDirty ? ` ${theme.symbols.modified}` : "";
return /* @__PURE__ */ jsxs(Text, { children: [
branchIcon ? `${branchIcon} ` : "",
gitInfo.branch,
modifiedIcon
] });
};
var GitBranchWidget = {
id: "git",
name: "Git Branch",
description: "Displays the current Git branch name",
defaultEnabled: true,
defaultOrder: 1,
colorKey: "git",
Component: GitBranchWidgetComponent
};
function parseTranscript(transcriptPath) {
if (!existsSync(transcriptPath)) {
return [];
}
try {
const content = readFileSync(transcriptPath, "utf-8");
const lines = content.split("\n").filter((line) => line.trim());
const messages = [];
for (const line of lines) {
try {
const parsed = JSON.parse(line);
if (parsed.type === "user" || parsed.type === "assistant") {
let textContent = "";
if (parsed.message?.content) {
if (typeof parsed.message.content === "string") {
textContent = parsed.message.content;
} else if (Array.isArray(parsed.message.content)) {
textContent = parsed.message.content.map((item) => item.text || item.content || "").join(" ");
}
}
messages.push({
type: parsed.type,
content: textContent
});
}
if (parsed.type === "tool_use" || parsed.tool_name) {
messages.push({
type: "tool_use",
tool_name: parsed.tool_name,
tool_input: parsed.tool_input
});
}
if (parsed.type === "tool_result" || parsed.tool_result !== void 0) {
messages.push({
type: "tool_result",
tool_result: parsed.tool_result
});
}
} catch {
}
}
return messages;
} catch {
return [];
}
}
function extractActualTokenUsage(transcriptPath) {
const result = {
inputTokens: 0,
outputTokens: 0,
cacheCreationTokens: 0,
cacheReadTokens: 0,
totalTokens: 0,
totalConsumed: 0,
contextTokens: 0
};
if (!existsSync(transcriptPath)) {
return result;
}
try {
const content = readFileSync(transcriptPath, "utf-8");
const lines = content.split("\n").filter((line) => line.trim());
let lastUsage = null;
for (const line of lines) {
try {
const parsed = JSON.parse(line);
if (parsed.type === "assistant" && parsed.message?.usage) {
const usage = parsed.message.usage;
lastUsage = usage;
result.totalConsumed += (usage.input_tokens || 0) + (usage.output_tokens || 0) + (usage.cache_creation_input_tokens || 0);
result.outputTokens += usage.output_tokens || 0;
}
} catch {
}
}
if (lastUsage) {
result.inputTokens = lastUsage.input_tokens || 0;
result.cacheCreationTokens = lastUsage.cache_creation_input_tokens || 0;
result.cacheReadTokens = lastUsage.cache_read_input_tokens || 0;
result.contextTokens = result.inputTokens + result.cacheCreationTokens + result.cacheReadTokens;
}
result.totalTokens = result.totalConsumed;
return result;
} catch {
return result;
}
}
function estimateTokens(text) {
if (!text) return 0;
const koreanChars = (text.match(/[\u3131-\uD79D]/g) || []).length;
const otherChars = text.length - koreanChars;
const koreanTokens = koreanChars / 1.5;
const otherTokens = otherChars / 4;
return Math.ceil(koreanTokens + otherTokens);
}
function calculateTotalTokens(messages, transcriptPath) {
return messages.reduce((sum, msg) => {
let tokens = 0;
if (typeof msg.content === "string") {
tokens += estimateTokens(msg.content);
}
if (msg.tool_input) {
tokens += estimateTokens(JSON.stringify(msg.tool_input));
}
if (msg.tool_result) {
tokens += estimateTokens(
typeof msg.tool_result === "string" ? msg.tool_result : JSON.stringify(msg.tool_result)
);
}
return sum + tokens;
}, 0);
}
function extractTodoProgress(messages) {
for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i];
if (msg.tool_name === "TodoWrite" && msg.tool_input) {
const input = msg.tool_input;
if (input.todos && Array.isArray(input.todos)) {
const todos = input.todos;
return {
completed: todos.filter((t2) => t2.status === "completed").length,
inProgress: todos.filter((t2) => t2.status === "in_progress").length,
pending: todos.filter((t2) => t2.status === "pending").length,
total: todos.length
};
}
}
}
return { completed: 0, inProgress: 0, pending: 0, total: 0 };
}
function calculateContextUsage(messages, maxTokens) {
const usedTokens = calculateTotalTokens(messages);
return Math.min(usedTokens / maxTokens * 100, 100);
}
var TokensWidgetComponent = ({ data }) => {
let tokens = 0;
if (data.transcript_path) {
try {
const messages = parseTranscript(data.transcript_path);
tokens = calculateTotalTokens(messages);
} catch {
}
}
const formattedTokens = formatTokens(tokens);
return /* @__PURE__ */ jsxs(Text, { children: [
formattedTokens,
" tokens"
] });
};
var TokensWidget = {
id: "tokens",
name: "Token Usage",
description: "Displays estimated token usage",
defaultEnabled: true,
defaultOrder: 2,
colorKey: "tokens",
Component: TokensWidgetComponent
};
var CostWidgetComponent = ({ data }) => {
const cost = data.cost?.total_cost_usd ?? data.cost?.api_cost ?? 0;
const formattedCost = formatCost(cost);
return /* @__PURE__ */ jsx(Text, { children: formattedCost });
};
var CostWidget = {
id: "cost",
name: "API Cost",
description: "Displays the API cost for this session",
defaultEnabled: true,
defaultOrder: 3,
colorKey: "cost",
Component: CostWidgetComponent
};
var SessionWidgetComponent = ({ data }) => {
const durationMs = data.cost?.total_duration_ms ?? data.cost?.duration_ms ?? 0;
const formattedDuration = formatDuration(durationMs);
return /* @__PURE__ */ jsx(Text, { children: formattedDuration });
};
var SessionWidget = {
id: "session",
name: "Session Time",
description: "Displays the session duration",
defaultEnabled: true,
defaultOrder: 4,
colorKey: "session",
Component: SessionWidgetComponent
};
var CwdWidgetComponent = ({ data, config: config2 }) => {
const cwd = data.cwd || data.workspace?.current_dir || process.cwd();
const maxLength = config2.options?.maxLength ?? 25;
const shortenedPath = shortenPath(cwd, maxLength);
return /* @__PURE__ */ jsx(Text, { children: shortenedPath });
};
var CwdWidget = {
id: "cwd",
name: "Working Directory",
description: "Displays the current working directory",
defaultEnabled: true,
defaultOrder: 5,
colorKey: "cwd",
Component: CwdWidgetComponent
};
function createProgressBar(percent, width = 10, filledChar = "\u2588", emptyChar = "\u2591") {
const filled = Math.round(percent / 100 * width);
const empty = width - filled;
return filledChar.repeat(filled) + emptyChar.repeat(empty);
}
var ContextWidgetComponent = ({ data, theme }) => {
let usagePercent = 0;
if (data.transcript_path) {
try {
const messages = parseTranscript(data.transcript_path);
const maxTokens = getModelMaxTokens(data.model?.id || "");
usagePercent = calculateContextUsage(messages, maxTokens);
} catch {
}
}
let statusColor = theme.colors.progress.filled;
if (usagePercent >= 90) {
statusColor = theme.colors.progress.critical;
} else if (usagePercent >= 70) {
statusColor = theme.colors.progress.warning;
}
const progressBar = createProgressBar(usagePercent);
const percentText = formatPercent(usagePercent);
return /* @__PURE__ */ jsxs(Text, { children: [
"CTX ",
/* @__PURE__ */ jsx(Text, { color: statusColor, children: progressBar }),
" ",
percentText
] });
};
var ContextWidget = {
id: "context",
name: "Context Window",
description: "Displays context window usage percentage",
defaultEnabled: true,
defaultOrder: 6,
colorKey: "context",
Component: ContextWidgetComponent
};
var TodoWidgetComponent = ({ data, theme }) => {
let todoProgress = { completed: 0, inProgress: 0, pending: 0, total: 0 };
if (data.transcript_path) {
try {
const messages = parseTranscript(data.transcript_path);
todoProgress = extractTodoProgress(messages);
} catch {
}
}
if (todoProgress.total === 0) {
return null;
}
const percent = Math.round(
todoProgress.completed / todoProgress.total * 100
);
let statusColor = theme.colors.status.normal;
if (todoProgress.inProgress > 0) {
statusColor = theme.colors.status.warning;
}
if (percent === 100) {
statusColor = theme.colors.status.success;
}
return /* @__PURE__ */ jsxs(Text, { children: [
"TODO",
" ",
/* @__PURE__ */ jsxs(Text, { color: statusColor, children: [
todoProgress.completed,
"/",
todoProgress.total
] }),
" ",
"[",
percent,
"%]"
] });
};
var TodoWidget = {
id: "todo",
name: "Todo Progress",
description: "Displays todo list progress",
defaultEnabled: false,
// 기본 비활성화
defaultOrder: 7,
colorKey: "todo",
Component: TodoWidgetComponent
};
function formatBytes(bytes) {
if (bytes >= 1024 * 1024 * 1024) {
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)}GB`;
}
if (bytes >= 1024 * 1024) {
return `${(bytes / 1024 / 1024).toFixed(0)}MB`;
}
if (bytes >= 1024) {
return `${(bytes / 1024).toFixed(0)}KB`;
}
return `${bytes}B`;
}
var MemoryWidgetComponent = () => {
const memory = process.memoryUsage();
const usedMemory = formatBytes(memory.heapUsed);
return /* @__PURE__ */ jsx(Text, { children: usedMemory });
};
var MemoryWidget = {
id: "memory",
name: "Memory Usage",
description: "Displays process memory usage",
defaultEnabled: false,
defaultOrder: 8,
colorKey: "memory",
Component: MemoryWidgetComponent
};
var FilesWidgetComponent = ({ data }) => {
const gitInfo = getGitInfo(data.cwd || data.workspace?.current_dir);
const linesAdded = gitInfo.linesAdded;
const linesRemoved = gitInfo.linesRemoved;
if (linesAdded === 0 && linesRemoved === 0) {
return /* @__PURE__ */ jsx(Text, { children: "no changes" });
}
return /* @__PURE__ */ jsxs(Text, { children: [
"+",
linesAdded,
"/-",
linesRemoved
] });
};
var FilesWidget = {
id: "files",
name: "File Changes",
description: "Displays lines added/removed in working directory",
defaultEnabled: false,
defaultOrder: 9,
colorKey: "files",
Component: FilesWidgetComponent
};
// src/i18n/types.ts
var supportedLanguages = ["en", "ko"];
// src/i18n/detection.ts
function detectLocale() {
const envLocale = process.env.LANG || process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANGUAGE;
if (envLocale) {
const lang = envLocale.split(/[._-]/)[0].toLowerCase();
if (supportedLanguages.includes(lang)) {
return lang;
}
}
if (process.platform === "win32") {
try {
const output = execSync(
'powershell -Command "[System.Globalization.CultureInfo]::CurrentUICulture.Name"',
{ encoding: "utf-8", timeout: 3e3 }
).trim();
const lang = output.split("-")[0].toLowerCase();
if (supportedLanguages.includes(lang)) {
return lang;
}
} catch {
}
}
return "en";
}
// src/i18n/locales/en/cli.json
var cli_default = {
description: "Powerline-style status bar for Claude Code CLI",
commands: {
render: {
description: "Render the status bar (reads JSON from stdin)",
demo: "Use demo data instead of stdin"
},
config: {
description: "Manage configuration (opens interactive TUI by default)",
show: "Show current configuration as JSON",
reset: "Reset to default configuration",
theme: "Set theme"
},
themes: {
description: "List available themes"
},
widgets: {
description: "List available widgets"
}
},
messages: {
configReset: "Configuration reset to defaults.",
themeSet: "Theme set to: {{theme}}",
invalidTheme: "Invalid theme: {{theme}}. Available: {{available}}",
availableThemes: "Available themes:",
availableWidgets: "Available widgets:",
current: "(current)",
error: "Error:"
}
};
// src/i18n/locales/en/tui.json
var tui_default = {
header: {
title: "Claude Status Bar Configuration"
},
tabs: {
widgets: "Widgets",
themes: "Themes"
},
footer: {
instructions: "\u2191\u2193: Navigate | Space: Toggle | Tab: Switch | S: Save | Q: Quit",
saved: "Saved!"
},
labels: {
current: "(current)"
}
};
// src/i18n/locales/en/widgets.json
var widgets_default = {
model: {
name: "Model",
description: "Displays the current Claude model name"
},
git: {
name: "Git Branch",
description: "Displays the current Git branch name"
},
tokens: {
name: "Token Usage",
description: "Displays estimated token usage"
},
cost: {
name: "API Cost",
description: "Displays the API cost for this session"
},
session: {
name: "Session Time",
description: "Displays the session duration"
},
cwd: {
name: "Working Directory",
description: "Displays the current working directory"
},
context: {
name: "Context Window",
description: "Displays context window usage percentage"
},
todo: {
name: "Todo Progress",
description: "Displays todo list progress"
},
memory: {
name: "Memory Usage",
description: "Displays process memory usage"
},
files: {
name: "File Changes",
description: "Displays lines added/removed"
}
};
// src/i18n/locales/en/renderer.json
var renderer_default = {
noWidgets: "No widgets to display",
truncated: "...",
labels: {
tok: "tok",
ctx: "CTX",
todo: "TODO",
mem: "MEM",
noChanges: "no changes",
unknown: "unknown",
files: "files"
}
};
// src/i18n/locales/ko/cli.json
var cli_default2 = {
description: "Claude Code CLI\uC6A9 Powerline \uC2A4\uD0C0\uC77C \uC0C1\uD0DC\uBC14",
commands: {
render: {
description: "\uC0C1\uD0DC\uBC14 \uB80C\uB354\uB9C1 (stdin\uC5D0\uC11C JSON \uC77D\uAE30)",
demo: "stdin \uB300\uC2E0 \uB370\uBAA8 \uB370\uC774\uD130 \uC0AC\uC6A9"
},
config: {
description: "\uC124\uC815 \uAD00\uB9AC (\uAE30\uBCF8: \uB300\uD654\uD615 TUI \uC2E4\uD589)",
show: "\uD604\uC7AC \uC124\uC815\uC744 JSON\uC73C\uB85C \uD45C\uC2DC",
reset: "\uAE30\uBCF8 \uC124\uC815\uC73C\uB85C \uCD08\uAE30\uD654",
theme: "\uD14C\uB9C8 \uC124\uC815"
},
themes: {
description: "\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uD14C\uB9C8 \uBAA9\uB85D"
},
widgets: {
description: "\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uC704\uC82F \uBAA9\uB85D"
}
},
messages: {
configReset: "\uC124\uC815\uC774 \uAE30\uBCF8\uAC12\uC73C\uB85C \uCD08\uAE30\uD654\uB418\uC5C8\uC2B5\uB2C8\uB2E4.",
themeSet: "\uD14C\uB9C8 \uC124\uC815\uB428: {{theme}}",
invalidTheme: "\uC798\uBABB\uB41C \uD14C\uB9C8: {{theme}}. \uC0AC\uC6A9 \uAC00\uB2A5: {{available}}",
availableThemes: "\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uD14C\uB9C8:",
availableWidgets: "\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uC704\uC82F:",
current: "(\uD604\uC7AC)",
error: "\uC624\uB958:"
}
};
// src/i18n/locales/ko/tui.json
var tui_default2 = {
header: {
title: "Claude \uC0C1\uD0DC\uBC14 \uC124\uC815"
},
tabs: {
widgets: "\uC704\uC82F",
themes: "\uD14C\uB9C8"
},
footer: {
instructions: "\u2191\u2193: \uC774\uB3D9 | Space: \uC120\uD0DD | Tab: \uD0ED \uC804\uD658 | S: \uC800\uC7A5 | Q: \uC885\uB8CC",
saved: "\uC800\uC7A5\uB428!"
},
labels: {
current: "(\uD604\uC7AC)"
}
};
// src/i18n/locales/ko/widgets.json
var widgets_default2 = {
model: {
name: "\uBAA8\uB378",
description: "\uD604\uC7AC Claude \uBAA8\uB378\uBA85 \uD45C\uC2DC"
},
git: {
name: "Git \uBE0C\uB79C\uCE58",
description: "\uD604\uC7AC Git \uBE0C\uB79C\uCE58\uBA85 \uD45C\uC2DC"
},
tokens: {
name: "\uD1A0\uD070 \uC0AC\uC6A9\uB7C9",
description: "\uC608\uC0C1 \uD1A0\uD070 \uC0AC\uC6A9\uB7C9 \uD45C\uC2DC"
},
cost: {
name: "API \uBE44\uC6A9",
description: "\uC774 \uC138\uC158\uC758 API \uBE44\uC6A9 \uD45C\uC2DC"
},
session: {
name: "\uC138\uC158 \uC2DC\uAC04",
description: "\uC138\uC158 \uACBD\uACFC \uC2DC\uAC04 \uD45C\uC2DC"
},
cwd: {
name: "\uC791\uC5C5 \uB514\uB809\uD1A0\uB9AC",
description: "\uD604\uC7AC \uC791\uC5C5 \uB514\uB809\uD1A0\uB9AC \uD45C\uC2DC"
},
context: {
name: "\uCEE8\uD14D\uC2A4\uD2B8 \uC708\uB3C4\uC6B0",
description: "\uCEE8\uD14D\uC2A4\uD2B8 \uC708\uB3C4\uC6B0 \uC0AC\uC6A9\uB960 \uD45C\uC2DC"
},
todo: {
name: "\uD560\uC77C \uC9C4\uD589\uB960",
description: "\uD560\uC77C \uBAA9\uB85D \uC9C4\uD589 \uC0C1\uD669 \uD45C\uC2DC"
},
memory: {
name: "\uBA54\uBAA8\uB9AC \uC0AC\uC6A9\uB7C9",
description: "\uD504\uB85C\uC138\uC2A4 \uBA54\uBAA8\uB9AC \uC0AC\uC6A9\uB7C9 \uD45C\uC2DC"
},
files: {
name: "\uD30C\uC77C \uBCC0\uACBD",
description: "\uCD94\uAC00/\uC0AD\uC81C\uB41C \uB77C\uC778 \uC218 \uD45C\uC2DC"
}
};
// src/i18n/locales/ko/renderer.json
var renderer_default2 = {
noWidgets: "\uD45C\uC2DC\uD560 \uC704\uC82F\uC774 \uC5C6\uC2B5\uB2C8\uB2E4",
truncated: "...",
labels: {
tok: "\uD1A0\uD070",
ctx: "\uCEE8\uD14D\uC2A4\uD2B8",
todo: "\uD560\uC77C",
mem: "\uBA54\uBAA8\uB9AC",
noChanges: "\uBCC0\uACBD \uC5C6\uC74C",
unknown: "\uC54C \uC218 \uC5C6\uC74C",
files: "\uD30C\uC77C"
}
};
// src/i18n/index.ts
var resources = {
en: {
cli: cli_default,
tui: tui_default,
widgets: widgets_default,
renderer: renderer_default
},
ko: {
cli: cli_default2,
tui: tui_default2,
widgets: widgets_default2,
renderer: renderer_default2
}
};
var initialized = false;
var currentLocale = "en";
function initI18n(locale = "auto") {
if (initialized) return i18next;
if (locale === "auto") {
currentLocale = detectLocale();
} else if (locale === "en" || locale === "ko") {
currentLocale = locale;
} else {
currentLocale = "en";
}
i18next.use(initReactI18next).init({
lng: currentLocale,
fallbackLng: "en",
defaultNS: "cli",
ns: ["cli", "tui", "widgets", "renderer"],
resources,
interpolation: {
escapeValue: false
},
returnNull: false
});
initialized = true;
return i18next;
}
var t = i18next.t.bind(i18next);
i18next.changeLanguage.bind(i18next);
// src/widgets/types.ts
function getWidgetName(widget) {
const translated = t(`widgets:${widget.id}.name`);
if (translated === `${widget.id}.name`) {
return widget.name;
}
return translated;
}
function getWidgetDescription(widget) {
const translated = t(`widgets:${widget.id}.description`);
if (translated === `${widget.id}.description`) {
return widget.description;
}
return translated;
}
// src/widgets/index.ts
function registerBuiltinWidgets() {
widgetRegistry.register(ModelWidget);
widgetRegistry.register(GitBranchWidget);
widgetRegistry.register(TokensWidget);
widgetRegistry.register(CostWidget);
widgetRegistry.register(SessionWidget);
widgetRegistry.register(CwdWidget);
widgetRegistry.register(ContextWidget);
widgetRegistry.register(TodoWidget);
widgetRegistry.register(MemoryWidget);
widgetRegistry.register(FilesWidget);
}
var WidgetConfigSchema = z.object({
enabled: z.boolean().default(true),
order: z.number().int().min(0).default(0),
options: z.record(z.unknown()).optional()
});
var BehaviorConfigSchema = z.object({
contextWarningThreshold: z.number().min(0).max(100).default(70),
contextDangerThreshold: z.number().min(0).max(100).default(90)
});
var AppConfigSchema = z.object({
version: z.literal(1).default(1),
theme: z.string().default("powerline-dark"),
locale: z.enum(["auto", "en", "ko"]).default("auto"),
widgets: z.record(WidgetConfigSchema).default({}),
behavior: BehaviorConfigSchema.default({})
});
// src/config/defaults.ts
var defaultConfig = {
version: 1,
theme: "powerline-dark",
widgets: {
model: { enabled: true, order: 0 },
git: { enabled: true, order: 1 },
tokens: { enabled: true, order: 2 },
cost: { enabled: true, order: 3 },
session: { enabled: true, order: 4 },
cwd: { enabled: true, order: 5 },
context: { enabled: true, order: 6 },
todo: { enabled: false, order: 7 }
},
behavior: {
contextWarningThreshold: 70,
contextDangerThreshold: 90
}
};
// src/config/loader.ts
function getConfigPaths() {
const home = homedir();
return [
join(process.cwd(), ".claude-status-bar.json"),
join(home, ".claude-status-bar", "config.json"),
join(home, ".config", "claude-status-bar", "config.json")
];
}
function getDefaultConfigPath() {
const home = homedir();
return join(home, ".claude-status-bar", "config.json");
}
function loadConfig() {
for (const configPath of getConfigPaths()) {
if (existsSync(configPath)) {
try {
const raw = readFileSync(configPath, "utf-8");
const parsed = JSON.parse(raw);
const validated = AppConfigSchema.parse(parsed);
return validated;
} catch (error) {
console.error(`Error loading config from ${configPath}:`, error);
}
}
}
return defaultConfig;
}
function saveConfig(config2, path) {
const configPath = getDefaultConfigPath();
const dir = dirname(configPath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
writeFileSync(configPath, JSON.stringify(config2, null, 2), "utf-8");
}
// src/utils/terminal.ts
function getTerminalWidth() {
return process.stdout.columns || 80;
}
function stripAnsi(text) {
return text.replace(/\x1b\[[0-9;]*m/g, "");
}
function getDisplayWidth(text) {
const stripped = stripAnsi(text);
let width = 0;
for (const char of stripped) {
const code = char.charCodeAt(0);
if (code >= 4352 && code <= 4607 || // 한글 자모
code >= 12288 && code <= 40959 || // CJK
code >= 44032 && code <= 55203 || // 한글 음절
code >= 63744 && code <= 64255 || // CJK 호환
code >= 65280 && code <= 65519) {
width += 2;
} else {
width += 1;
}
}
return width;
}
// src/core/renderer.ts
var chalk = new Chalk({ level: 3 });
function createProgressBar2(percent, width = 10, filledChar = "\u2588", emptyChar = "\u2591") {
const filled = Math.round(percent / 100 * width);
const empty = width - filled;
return filledChar.repeat(filled) + emptyChar.repeat(empty);
}
function getWidgetContent(widgetId, data, theme) {
try {
switch (widgetId) {
case "model":
return shortenModelName(data.model?.display_name || data.model?.id || t("renderer:labels.unknown"));
case "git": {
const gitInfo = getGitInfo(data.cwd || data.workspace?.current_dir);
if (!gitInfo.branch) return null;
const linesAdded = gitInfo.linesAdded;
const linesRemoved = gitInfo.linesRemoved;
const branch = chalk.hex("#37474f")(gitInfo.branch);
const added = chalk.hex("#2e7d32").bold(`+${linesAdded}`);
const removed = chalk.hex("#c62828").bold(`-${linesRemoved}`);
return chalk.bgHex("#ffffff")(`${branch} ${added} ${removed}`);
}
case "tokens": {
let tokens = 0;
if (data.transcript_path) {
const usage = extractActualTokenUsage(data.transcript_path);
tokens = usage.totalTokens;
}
return `${formatTokens(tokens)} ${t("renderer:labels.tok")}`;
}
case "cost":
return formatCost(data.cost?.total_cost_usd ?? data.cost?.api_cost ?? 0);
case "session":
return formatDuration(data.cost?.total_duration_ms ?? data.cost?.duration_ms ?? 0);
case "cwd":
return shortenPath(data.cwd || data.workspace?.current_dir || process.cwd(), 20);
case "context": {
let usagePercent = 0;
if (data.transcript_path) {
const usage = extractActualTokenUsage(data.transcript_path);
const maxTokens = getModelMaxTokens(data.model?.id || "");
usagePercent = Math.min(usage.contextTokens / maxTokens * 100, 100);
}
const bar = createProgressBar2(usagePercent, 8);
return `${t("renderer:labels.ctx")} ${bar} ${formatPercent(usagePercent)}`;
}
case "todo": {
let todoProgress = { completed: 0, inProgress: 0, pending: 0, total: 0 };
if (data.transcript_path) {
const messages = parseTranscript(data.transcript_path);
todoProgress = extractTodoProgress(messages);
}
if (todoProgress.total === 0) return null;
const percent = Math.round(todoProgress.completed / todoProgress.total * 100);
return `${t("renderer:labels.todo")} ${todoProgress.completed}/${todoProgress.total} [${percent}%]`;
}
case "memory": {
const memory = process.memoryUsage();
const usedMB = Math.round(memory.heapUsed / 1024 / 1024);
return `${t("renderer:labels.mem")} ${usedMB}MB`;
}
case "files": {
const gitInfo = getGitInfo(data.cwd || data.workspace?.current_dir);
const filesChanged = gitInfo.filesChanged || 0;
if (filesChanged === 0) return null;
return `${filesChanged} ${t("renderer:labels.files")}`;
}
default:
return null;
}
} catch (error) {
return null;
}
}
function renderSegment(content, bgColor, fgColor, nextBgColor, separator, isLastInLine = false) {
const hasAnsi = content.includes("\x1B[");
let segment;
if (hasAnsi) {
const prefix = chalk.bgHex(bgColor)(" ");
const suffix = chalk.bgHex(bgColor)(" ");
segment = prefix + content + suffix;
} else {
segment = chalk.bgHex(bgColor).hex(fgColor)(` ${content} `);
}
let sep = "";
if (isLastInLine) {
sep = chalk.hex(bgColor)(separator) + "\x1B[0m";
} else if (nextBgColor) {
sep = chalk.bgHex(nextBgColor).hex(bgColor)(separator);
} else {
sep = chalk.hex(bgColor)(separator) + "\x1B[0m";
}
return segment + sep;
}
function getSegmentDisplayWidth(content, separator) {
return getDisplayWidth(content) + 2 + getDisplayWidth(separator);
}
function fitSegmentsToWidth(segments, _separator, _maxWidth) {
return segments;
}
function renderStatusBar(data, theme, widgets, widgetConfigs) {
const activeWidgets = widgets.filter((widget) => {
const config2 = widgetConfigs[widget.id];
return config2 ? config2.enabled : widget.defaultEnabled;
}).sort((a, b) => {
const orderA = widgetConfigs[a.id]?.order ?? a.defaultOrder;
const orderB = widgetConfigs[b.id]?.order ?? b.defaultOrder;
return orderA - orderB;
});
let segments = [];
for (const widget of activeWidgets) {
const content = getWidgetContent(widget.id, data);
if (content !== null) {
segments.push({ widget, content });
}
}
if (segments.length === 0) {
return chalk.gray(t("renderer:noWidgets"));
}
const terminalWidth = getTerminalWidth();
const separator = theme.symbols.separator;
segments = fitSegmentsToWidth(segments);
if (segments.length === 0) {
return chalk.gray(t("renderer:truncated"));
}
let output = "";
let currentLineWidth = 0;
let lineSegments = [];
for (let i = 0; i < segments.length; i++) {
const { widget, content } = segments[i];
const colors = theme.colors.segments[widget.colorKey];
const segmentWidth = getSegmentDisplayWidth(content, separator);
if (currentLineWidth > 0 && currentLineWidth + segmentWidth > terminalWidth) {
for (let j = 0; j < lineSegments.length; j++) {
const seg = lineSegments[j];
const isLast = j === lineSegments.length - 1;
const nextSeg = lineSegments[j + 1];
const nextBgColor = isLast ? null : nextSeg?.colors.bg;
output += renderSegment(seg.content, seg.colors.bg, seg.colors.fg, nextBgColor, separator, isLast);
}
output += "\n";
lineSegments = [];
currentLineWidth = 0;
}
lineSegments.push({ widget, content, colors });
currentLineWidth += segmentWidth;
}
for (let j = 0; j < lineSegments.length; j++) {
const seg = lineSegments[j];
const isLast = j === lineSegments.length - 1;
const nextSeg = lineSegments[j + 1];
const nextBgColor = isLast ? null : nextSeg?.colors.bg;
output += renderSegment(seg.content, seg.colors.bg, seg.colors.fg, nextBgColor, separator, isLast);
}
return output;
}
// src/utils/cache.ts
var Cache = class {
cache = /* @__PURE__ */ new Map();
ttl;
maxSize;
constructor(options) {
this.ttl = options.ttl;
this.maxSize = options.maxSize ?? 100;
}
/**
* 캐시에서 값 조회
* TTL 만료 시 undefined 반환
*/
get(key) {
const entry = this.cache.get(key);
if (!entry) return void 0;
if (Date.now() - entry.timestamp > this.ttl) {
this.cache.delete(key);
return void 0;
}
return entry.value;
}
/**
* 캐시에 값 저장
* @param key 캐시 키
* @param value 저장할 값
* @param mtime 파일 수정 시간 (선택적, 파일 기반 캐시 무효화용)
*/
set(key, value, mtime) {
if (this.cache.size >= this.maxSize) {
const oldestKey = this.cache.keys().next().value;
if (oldestKey) {
this.cache.delete(oldestKey);
}
}
this.cache.set(key, {
value,
timestamp: Date.now(),
mtime
});
}
/**
* 캐시에 키가 존재하고 유효한지 확인
*/
has(key) {
return this.get(key) !== void 0;
}
/**
* 특정 키 또는 전체 캐시 무효화
*/
invalidate(key) {
if (key) {
this.cache.delete(key);
} else {
this.cache.clear();
}
}
/**
* 파일 수정 시간 기반 캐시 유효성 검사
* 현재 mtime이 캐시된 mtime과 다르면 무효
*/
isValid(key, currentMtime) {
const entry = this.cache.get(key);
if (!entry) return false;
if (Date.now() - entry.timestamp > this.ttl) {
this.cache.delete(key);
return false;
}
if (currentMtime !== void 0 && entry.mtime !== void 0) {
if (currentMtime > entry.mtime) {
this.cache.delete(key);
return false;
}
}
return true;
}
/**
* 캐시된 항목의 mtime 조회
*/
getMtime(key) {
return this.cache.get(key)?.mtime;
}
/**
* 캐시 크기 반환
*/
get size() {
return this.cache.size;
}
/**
* 캐시 초기화
*/
clear() {
this.cache.clear();
}
};
function createCache(options) {
return new Cache(options);
}
var execPromise = promisify(exec);
async function executeAsync(command, options) {
const { cwd, timeout = 500, encoding = "utf8" } = options ?? {};
try {
const result = await execPromise(command, {
cwd: cwd || process.cwd(),
timeout,
encoding,
windowsHide: true
});
return result.stdout.trim();
} catch (error) {
return "";
}
}
async function batchExecute(commands, options) {
const results = await Promise.allSettled(
commands.map((cmd) => executeAsync(cmd, options))
);
return results.map(
(result) => result.status === "fulfilled" ? result.value : ""
);
}
var DedupedExecutor = class {
pending = /* @__PURE__ */ new Map();
async execute(key, fn) {
const existing = this.pending.get(key);
if (existing) {
return existing;
}
const promise = fn().finally(() => {
this.pending.delete(key);
});
this.pending.set(key, promise);
return promise;
}
/**
* 진행 중인 요청 수 반환
*/
get pendingCount() {
return this.pending.size;
}
};
// src/utils/git-async.ts
var gitCache2 = createCache({ ttl: 5e3, maxSize: 5 });
var dedupedExecutor = new DedupedExecutor();
function parseDiffStats(diff) {
if (!diff) return { added: 0, removed: 0, files: 0 };
let added = 0;
let removed = 0;
let files = 0;
for (const line of diff.split("\n")) {
const parts = line.split(" ");
if (parts.length >= 3) {
const a = parseInt(parts[0], 10);
const r = parseInt(parts[1], 10);
if (!isNaN(a)) added += a;
if (!isNaN(r)) removed += r;
files++;
}
}
return { added, removed, files };
}
async function fetchGitInfoAsync(cwd) {
const defaultResult = {
branch: null,
isDirty: false,
linesAdded: 0,
linesRemoved: 0,
filesChanged: 0
};
const workDir = cwd || process.cwd();
const options = { cwd: workDir, timeout: 500 };
const commands = [
"git branch --show-current",
"git status --porcelain",
"git diff --numstat",
"git diff --cached --numstat"
];
const [branch, status, diffUnstaged, diffStaged] = await batchExecute(commands, options);
if (!branch) {
return defaultResult;
}
const isDirty = status.length > 0;
const unstaged = parseDiffStats(diffUnstaged);
const staged = parseDiffStats(diffStaged);
return {
branch: branch || null,
isDirty,
linesAdded: unstaged.added + staged.added,
linesRemoved: unstaged.removed + staged.removed,
filesChanged: unstaged.files + staged.files
};
}
async function getGitInfoAsync(cwd) {
const workDir = cwd || process.cwd();
const cacheKey = workDir;
const cached = gitCache2.get(cacheKey);
if (cached) {
return cached;
}
const result = await dedupedExecutor.execute(cacheKey, () => fetchGitInfoAsync(cwd));
gitCache2.set(cacheKey, result);
return result;
}
var transcriptCache = createCache({ ttl: 2e3, maxSize: 10 });
function getDefaultData() {
return {
messages: [],
tokenUsage: {
inputTokens: 0,
outputTokens: 0,
cacheCreationTokens: 0,
cacheReadTokens: 0,
totalTokens: 0,
totalConsumed: 0,
contextTokens: 0
},
todoProgress: {
completed: 0,
inProgress: 0,
pending: 0,
total: 0
}
};
}
function getTranscriptData(transcriptPath) {
if (!transcriptPath || !existsSync(transcriptPath)) {
return getDefaultData();
}
try {
const stats = statSync(transcriptPath);
const currentMtime = stats.mtimeMs;
if (transcriptCache.isValid(transcriptPath, currentMtime)) {
const cached = transcriptCache.get(transcriptPath);
if (cached) {
return cached;
}
}
const content = readFileSync(transcriptPath, "utf-8");
const lines = content.split("\n").filter((line) => line.trim());
const messages = [];
let lastUsage = null;
let totalConsumed = 0;
let outputTokens = 0;
let latestTodos = [];
for (const line of lines) {
try {
const parsed = JSON.parse(line);
if (parsed.type === "user" || parsed.type === "assistant") {
let textContent = "";
if (parsed.message?.content) {
if (typeof parsed.message.content === "string") {
textContent = parsed.message.content;
} else if (Array.isArray(parsed.message.content)) {
textContent = parsed.message.content.map((item) => item.text || item.content || "").join(" ");
}
}
messages.push({
type: parsed.type,
content: textContent
});
if (parsed.type === "assistant" && parsed.message?.usage) {
const usage = parsed.message.usage;
lastUsage = usage;
totalConsumed += (usage.input_tokens || 0) + (usage.output_tokens || 0) + (usage.cache_creation_input_tokens || 0);
outputTokens += usage.output_tokens || 0;
}
}
if (parsed.type === "tool_use" || parsed.tool_name) {
messages.push({
type: "tool_use",
tool_name: parsed.tool_name,
tool_input: parsed.tool_input
});
if (parsed.tool_name === "TodoWrite" && parsed.tool_input) {
const input = parsed.tool_input;
if (input.todos && Array.isArray(input.todos)) {
latestTodos = input.todos;
}
}
}
if (parsed.type === "tool_result" || parsed.tool_result !== void 0) {
messages.push({
type: "tool_result",
tool_result: parsed.tool_result
});
}
} catch {
}
}
const result = {
messages,
tokenUsage: {
inputTokens: lastUsage?.input_tokens || 0,
outputTokens,
cacheCreationTokens: lastUsage?.cache_creation_input_tokens || 0,
cacheReadTokens: lastUsage?.cache_read_input_tokens || 0,
totalTokens: totalConsumed,
totalConsumed,
contextTokens: (lastUsage?.input_tokens || 0) + (lastUsage?.cache_creation_input_tokens || 0) + (lastUsage?.cache_read_input_tokens || 0)
},
todoProgress: {
completed: latestTodos.filter((t2) => t2.st