cc-costline 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ declare const CACHE_DIR: string;
2
+ export interface CacheData {
3
+ cost7d: number;
4
+ cost30d: number;
5
+ updatedAt: string;
6
+ }
7
+ export interface ConfigData {
8
+ period: "7d" | "30d" | "both";
9
+ }
10
+ export declare function readCache(): CacheData | null;
11
+ export declare function writeCache(data: CacheData): void;
12
+ export declare function readConfig(): ConfigData;
13
+ export declare function writeConfig(data: ConfigData): void;
14
+ export { CACHE_DIR };
package/dist/cache.js ADDED
@@ -0,0 +1,33 @@
1
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ const CACHE_DIR = join(homedir(), ".cc-costline");
5
+ const CACHE_FILE = join(CACHE_DIR, "cache.json");
6
+ const CONFIG_FILE = join(CACHE_DIR, "config.json");
7
+ export function readCache() {
8
+ try {
9
+ const raw = readFileSync(CACHE_FILE, "utf-8");
10
+ return JSON.parse(raw);
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ export function writeCache(data) {
17
+ mkdirSync(CACHE_DIR, { recursive: true });
18
+ writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2) + "\n");
19
+ }
20
+ export function readConfig() {
21
+ try {
22
+ const raw = readFileSync(CONFIG_FILE, "utf-8");
23
+ return JSON.parse(raw);
24
+ }
25
+ catch {
26
+ return { period: "7d" };
27
+ }
28
+ }
29
+ export function writeConfig(data) {
30
+ mkdirSync(CACHE_DIR, { recursive: true });
31
+ writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2) + "\n");
32
+ }
33
+ export { CACHE_DIR };
@@ -0,0 +1,9 @@
1
+ interface ModelPricing {
2
+ input: number;
3
+ output: number;
4
+ cacheCreation: number;
5
+ cacheRead: number;
6
+ }
7
+ export declare function getPricing(model: string): ModelPricing;
8
+ export declare function calculateCost(model: string, inputTokens: number, outputTokens: number, cacheCreationTokens: number, cacheReadTokens: number): number;
9
+ export {};
@@ -0,0 +1,39 @@
1
+ // Per-million-token pricing in USD (source: Anthropic pricing page)
2
+ const MODEL_PRICING = {
3
+ // Opus family
4
+ "claude-opus-4-6": { input: 5, output: 25, cacheCreation: 6.25, cacheRead: 0.5 },
5
+ "claude-opus-4-5-20251101": { input: 5, output: 25, cacheCreation: 6.25, cacheRead: 0.5 },
6
+ "claude-opus-4-1-20250805": { input: 15, output: 75, cacheCreation: 18.75, cacheRead: 1.5 },
7
+ // Sonnet family
8
+ "claude-sonnet-4-5-20250929": { input: 3, output: 15, cacheCreation: 3.75, cacheRead: 0.3 },
9
+ "claude-sonnet-4-20250514": { input: 3, output: 15, cacheCreation: 3.75, cacheRead: 0.3 },
10
+ "claude-3-5-sonnet-20241022": { input: 3, output: 15, cacheCreation: 3.75, cacheRead: 0.3 },
11
+ // Haiku family
12
+ "claude-haiku-4-5-20251001": { input: 1, output: 5, cacheCreation: 1.25, cacheRead: 0.1 },
13
+ "claude-3-5-haiku-20241022": { input: 0.8, output: 4, cacheCreation: 1, cacheRead: 0.08 },
14
+ };
15
+ // Family fallbacks for unknown model IDs
16
+ const FAMILY_FALLBACK = {
17
+ opus: MODEL_PRICING["claude-opus-4-6"],
18
+ sonnet: MODEL_PRICING["claude-sonnet-4-5-20250929"],
19
+ haiku: MODEL_PRICING["claude-haiku-4-5-20251001"],
20
+ };
21
+ export function getPricing(model) {
22
+ if (MODEL_PRICING[model])
23
+ return MODEL_PRICING[model];
24
+ // Try family fallback
25
+ const lower = model.toLowerCase();
26
+ for (const [family, pricing] of Object.entries(FAMILY_FALLBACK)) {
27
+ if (lower.includes(family))
28
+ return pricing;
29
+ }
30
+ // Default to sonnet pricing
31
+ return FAMILY_FALLBACK.sonnet;
32
+ }
33
+ export function calculateCost(model, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens) {
34
+ const pricing = getPricing(model);
35
+ return (inputTokens * pricing.input +
36
+ outputTokens * pricing.output +
37
+ cacheCreationTokens * pricing.cacheCreation +
38
+ cacheReadTokens * pricing.cacheRead) / 1e6;
39
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { collectCosts } from "./collector.js";
6
+ import { writeCache, writeConfig, readConfig, CACHE_DIR } from "./cache.js";
7
+ import { render } from "./statusline.js";
8
+ const SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
9
+ const RENDER_COMMAND = "cc-costline render";
10
+ const REFRESH_COMMAND = "cc-costline refresh";
11
+ // ─── Helpers ──────────────────────────────────────────────
12
+ function readSettings() {
13
+ try {
14
+ return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
15
+ }
16
+ catch {
17
+ return {};
18
+ }
19
+ }
20
+ function saveSettings(settings) {
21
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
22
+ }
23
+ function readStdin() {
24
+ try {
25
+ return readFileSync("/dev/stdin", "utf-8");
26
+ }
27
+ catch {
28
+ return "";
29
+ }
30
+ }
31
+ // ─── Commands ─────────────────────────────────────────────
32
+ function cmdInstall() {
33
+ const settings = readSettings();
34
+ // 1. Set statusLine command
35
+ settings.statusLine = {
36
+ type: "command",
37
+ command: RENDER_COMMAND,
38
+ };
39
+ // 2. Add SessionEnd hook for refresh
40
+ if (!settings.hooks)
41
+ settings.hooks = {};
42
+ for (const event of ["SessionEnd", "Stop"]) {
43
+ if (!settings.hooks[event])
44
+ settings.hooks[event] = [];
45
+ // Remove any old cc-costline / cc-statusline hooks first
46
+ settings.hooks[event] = settings.hooks[event].filter((h) => !h.hooks?.some((hh) => hh.command?.includes("cc-costline") || hh.command?.includes("cc-statusline")));
47
+ // Add fresh hook
48
+ settings.hooks[event].push({
49
+ matcher: "",
50
+ hooks: [
51
+ {
52
+ type: "command",
53
+ command: REFRESH_COMMAND,
54
+ timeout: 60,
55
+ async: true,
56
+ },
57
+ ],
58
+ });
59
+ }
60
+ saveSettings(settings);
61
+ // 3. Create config dir + default config
62
+ mkdirSync(CACHE_DIR, { recursive: true });
63
+ if (!existsSync(join(CACHE_DIR, "config.json"))) {
64
+ writeConfig({ period: "7d" });
65
+ }
66
+ // 4. Initial refresh
67
+ console.log("✓ settings.json updated (statusLine + hooks)");
68
+ console.log("✓ Config directory created: " + CACHE_DIR);
69
+ console.log(" Running initial cost calculation...");
70
+ cmdRefresh();
71
+ console.log("✓ Installation complete!");
72
+ }
73
+ function cmdUninstall() {
74
+ const settings = readSettings();
75
+ // Remove statusLine if it's ours
76
+ if (settings.statusLine?.command?.includes("cc-costline") ||
77
+ settings.statusLine?.command?.includes("cc-statusline")) {
78
+ delete settings.statusLine;
79
+ }
80
+ // Remove our hooks from SessionEnd and Stop
81
+ for (const event of ["SessionEnd", "Stop"]) {
82
+ if (!settings.hooks?.[event])
83
+ continue;
84
+ settings.hooks[event] = settings.hooks[event].filter((h) => !h.hooks?.some((hh) => hh.command?.includes("cc-costline") || hh.command?.includes("cc-statusline")));
85
+ if (settings.hooks[event].length === 0) {
86
+ delete settings.hooks[event];
87
+ }
88
+ }
89
+ saveSettings(settings);
90
+ console.log("✓ Removed cc-costline from settings.json");
91
+ console.log(" Cache directory preserved at: " + CACHE_DIR);
92
+ }
93
+ function cmdConfig(args) {
94
+ const periodIdx = args.indexOf("--period");
95
+ if (periodIdx === -1 || !args[periodIdx + 1]) {
96
+ const config = readConfig();
97
+ console.log("Current config:", JSON.stringify(config, null, 2));
98
+ console.log("\nUsage: cc-costline config --period <7d|30d|both>");
99
+ return;
100
+ }
101
+ const period = args[periodIdx + 1];
102
+ if (!["7d", "30d", "both"].includes(period)) {
103
+ console.error("Invalid period. Use: 7d, 30d, or both");
104
+ process.exit(1);
105
+ }
106
+ writeConfig({ period: period });
107
+ console.log(`✓ Period set to: ${period}`);
108
+ }
109
+ function cmdRefresh() {
110
+ const result = collectCosts();
111
+ writeCache({
112
+ cost7d: result.cost7d,
113
+ cost30d: result.cost30d,
114
+ updatedAt: new Date().toISOString(),
115
+ });
116
+ console.log(`✓ Cache updated — 7d: $${result.cost7d.toFixed(2)} | 30d: $${result.cost30d.toFixed(2)}`);
117
+ }
118
+ function cmdRender() {
119
+ const input = readStdin();
120
+ if (!input.trim())
121
+ return;
122
+ const output = render(input);
123
+ if (output)
124
+ process.stdout.write(output);
125
+ }
126
+ // ─── Main ─────────────────────────────────────────────────
127
+ const args = process.argv.slice(2);
128
+ const command = args[0];
129
+ switch (command) {
130
+ case "install":
131
+ cmdInstall();
132
+ break;
133
+ case "uninstall":
134
+ cmdUninstall();
135
+ break;
136
+ case "config":
137
+ cmdConfig(args.slice(1));
138
+ break;
139
+ case "refresh":
140
+ cmdRefresh();
141
+ break;
142
+ case "render":
143
+ cmdRender();
144
+ break;
145
+ default:
146
+ console.log(`cc-costline v0.1.0 — Enhanced statusline for Claude Code
147
+
148
+ Commands:
149
+ install Configure Claude Code to use cc-costline
150
+ uninstall Remove cc-costline from Claude Code settings
151
+ config View/update display settings
152
+ refresh Manually recalculate cost cache
153
+ render Output statusline (reads stdin from Claude Code)
154
+
155
+ Examples:
156
+ npx @ventuss/cc-costline install
157
+ npx @ventuss/cc-costline config --period 7d
158
+ npx @ventuss/cc-costline config --period 30d
159
+ npx @ventuss/cc-costline config --period both
160
+ npx @ventuss/cc-costline refresh`);
161
+ break;
162
+ }
@@ -0,0 +1,6 @@
1
+ interface CollectResult {
2
+ cost7d: number;
3
+ cost30d: number;
4
+ }
5
+ export declare function collectCosts(): CollectResult;
6
+ export {};
@@ -0,0 +1,93 @@
1
+ import { readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { calculateCost } from "./calculator.js";
5
+ const CLAUDE_PROJECTS_DIR = ".claude/projects";
6
+ /** Recursively find all .jsonl files under a directory */
7
+ function findJsonlFiles(dir) {
8
+ const results = [];
9
+ let entries;
10
+ try {
11
+ entries = readdirSync(dir);
12
+ }
13
+ catch {
14
+ return results;
15
+ }
16
+ for (const entry of entries) {
17
+ const full = join(dir, entry);
18
+ let stat;
19
+ try {
20
+ stat = statSync(full);
21
+ }
22
+ catch {
23
+ continue;
24
+ }
25
+ if (stat.isDirectory()) {
26
+ results.push(...findJsonlFiles(full));
27
+ }
28
+ else if (entry.endsWith(".jsonl")) {
29
+ results.push(full);
30
+ }
31
+ }
32
+ return results;
33
+ }
34
+ export function collectCosts() {
35
+ const projectsDir = join(homedir(), CLAUDE_PROJECTS_DIR);
36
+ const files = findJsonlFiles(projectsDir);
37
+ if (files.length === 0) {
38
+ return { cost7d: 0, cost30d: 0 };
39
+ }
40
+ const now = Date.now();
41
+ const cutoff7d = now - 7 * 24 * 60 * 60 * 1000;
42
+ const cutoff30d = now - 30 * 24 * 60 * 60 * 1000;
43
+ let cost7d = 0;
44
+ let cost30d = 0;
45
+ // Deduplication set (same as ccclub)
46
+ const seen = new Set();
47
+ for (const file of files) {
48
+ let content;
49
+ try {
50
+ content = readFileSync(file, "utf-8");
51
+ }
52
+ catch {
53
+ continue;
54
+ }
55
+ const lines = content.split("\n");
56
+ for (const line of lines) {
57
+ if (!line.trim())
58
+ continue;
59
+ let parsed;
60
+ try {
61
+ parsed = JSON.parse(line);
62
+ }
63
+ catch {
64
+ continue;
65
+ }
66
+ if (parsed.type !== "assistant" || !parsed.message?.usage)
67
+ continue;
68
+ const ts = new Date(parsed.timestamp).getTime();
69
+ if (isNaN(ts) || ts < cutoff30d)
70
+ continue;
71
+ const usage = parsed.message.usage;
72
+ const requestId = parsed.requestId || "";
73
+ const sessionId = parsed.sessionId || "";
74
+ const dedupeKey = requestId
75
+ ? `${sessionId}:${requestId}`
76
+ : `${sessionId}:${parsed.timestamp}:${usage.input_tokens}:${usage.output_tokens}`;
77
+ if (seen.has(dedupeKey))
78
+ continue;
79
+ seen.add(dedupeKey);
80
+ const model = parsed.message.model || "unknown";
81
+ const inputTokens = usage.input_tokens || 0;
82
+ const outputTokens = usage.output_tokens || 0;
83
+ const cacheCreationTokens = usage.cache_creation_input_tokens || 0;
84
+ const cacheReadTokens = usage.cache_read_input_tokens || 0;
85
+ const cost = calculateCost(model, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens);
86
+ cost30d += cost;
87
+ if (ts >= cutoff7d) {
88
+ cost7d += cost;
89
+ }
90
+ }
91
+ }
92
+ return { cost7d, cost30d };
93
+ }
@@ -0,0 +1 @@
1
+ export declare function render(input: string): string;
@@ -0,0 +1,102 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { readCache, readConfig } from "./cache.js";
3
+ // ANSI colors (matching original statusline.sh)
4
+ const FG_GRAY = "\x1b[38;5;245m";
5
+ const FG_GRAY_DIM = "\x1b[38;5;102m";
6
+ const FG_YELLOW = "\x1b[38;2;229;192;123m";
7
+ const FG_GREEN = "\x1b[38;5;29m";
8
+ const FG_ORANGE = "\x1b[38;5;208m";
9
+ const FG_RED = "\x1b[38;5;167m";
10
+ const FG_MODEL = "\x1b[38;2;202;124;94m";
11
+ const FG_CYAN = "\x1b[38;5;109m";
12
+ const RESET = "\x1b[0m";
13
+ function formatTokens(t) {
14
+ if (t >= 1_000_000)
15
+ return (t / 1_000_000).toFixed(1) + "M";
16
+ if (t >= 1_000)
17
+ return (t / 1_000).toFixed(1) + "k";
18
+ return String(t);
19
+ }
20
+ function formatCost(n) {
21
+ if (n >= 1000)
22
+ return "$" + Math.round(n).toLocaleString("en-US");
23
+ if (n >= 100)
24
+ return "$" + n.toFixed(0);
25
+ if (n >= 10)
26
+ return "$" + n.toFixed(1);
27
+ return "$" + n.toFixed(2);
28
+ }
29
+ function ctxColor(pct) {
30
+ if (pct >= 80)
31
+ return FG_RED;
32
+ if (pct >= 60)
33
+ return FG_ORANGE;
34
+ return FG_GREEN;
35
+ }
36
+ export function render(input) {
37
+ let data;
38
+ try {
39
+ data = JSON.parse(input);
40
+ }
41
+ catch {
42
+ return "";
43
+ }
44
+ // Session data from Claude Code stdin
45
+ const cost = data.cost?.total_cost_usd ?? 0;
46
+ const linesAdd = data.cost?.total_lines_added ?? 0;
47
+ const linesDel = data.cost?.total_lines_removed ?? 0;
48
+ const model = data.model?.display_name ?? "—";
49
+ const contextPct = Math.floor(data.context_window?.used_percentage ?? 0);
50
+ // Token stats from transcript
51
+ let inTokens = 0;
52
+ let outTokens = 0;
53
+ const transcriptPath = data.transcript_path ?? "";
54
+ if (transcriptPath) {
55
+ try {
56
+ const content = readFileSync(transcriptPath, "utf-8");
57
+ const lines = content.split("\n");
58
+ for (const line of lines) {
59
+ if (!line.trim())
60
+ continue;
61
+ try {
62
+ const entry = JSON.parse(line);
63
+ if (entry.type === "assistant" && entry.message?.usage) {
64
+ inTokens += entry.message.usage.input_tokens || 0;
65
+ outTokens += entry.message.usage.output_tokens || 0;
66
+ }
67
+ }
68
+ catch {
69
+ // skip malformed lines
70
+ }
71
+ }
72
+ }
73
+ catch {
74
+ // transcript not readable
75
+ }
76
+ }
77
+ const inFmt = formatTokens(inTokens);
78
+ const outFmt = formatTokens(outTokens);
79
+ // Cached cost data
80
+ const cache = readCache();
81
+ const config = readConfig();
82
+ let costSuffix = "";
83
+ if (cache) {
84
+ const { period } = config;
85
+ if (period === "7d") {
86
+ costSuffix = ` ${FG_CYAN}(7d:${formatCost(cache.cost7d)})${RESET}`;
87
+ }
88
+ else if (period === "30d") {
89
+ costSuffix = ` ${FG_CYAN}(30d:${formatCost(cache.cost30d)})${RESET}`;
90
+ }
91
+ else {
92
+ costSuffix = ` ${FG_CYAN}(7d:${formatCost(cache.cost7d)} 30d:${formatCost(cache.cost30d)})${RESET}`;
93
+ }
94
+ }
95
+ const parts = [
96
+ `${FG_GRAY_DIM}Token: ↑${inFmt} ↓${outFmt}${RESET}`,
97
+ `${FG_YELLOW}${formatCost(cost)}${costSuffix}${RESET}`,
98
+ `${FG_GRAY_DIM}Code: ${FG_GREEN}+${linesAdd}${RESET} ${FG_GRAY_DIM}-${linesDel}${RESET}`,
99
+ `${ctxColor(contextPct)}${contextPct}%${RESET} ${FG_GRAY_DIM}by${RESET} ${FG_MODEL}${model}${RESET}`,
100
+ ];
101
+ return "\n " + parts.join(` ${FG_GRAY}|${RESET} `) + "\n";
102
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "cc-costline",
3
+ "version": "0.1.0",
4
+ "description": "Enhanced statusline for Claude Code with 7d/30d cost tracking",
5
+ "type": "module",
6
+ "bin": {
7
+ "cc-costline": "dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsc --watch"
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "engines": {
17
+ "node": ">=22"
18
+ },
19
+ "keywords": [
20
+ "claude-code",
21
+ "statusline",
22
+ "cost-tracking"
23
+ ],
24
+ "author": "ventuss",
25
+ "license": "MIT",
26
+ "devDependencies": {
27
+ "typescript": "^5.7.0"
28
+ }
29
+ }