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.
- package/dist/cache.d.ts +14 -0
- package/dist/cache.js +33 -0
- package/dist/calculator.d.ts +9 -0
- package/dist/calculator.js +39 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +162 -0
- package/dist/collector.d.ts +6 -0
- package/dist/collector.js +93 -0
- package/dist/statusline.d.ts +1 -0
- package/dist/statusline.js +102 -0
- package/package.json +29 -0
package/dist/cache.d.ts
ADDED
|
@@ -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
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,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
|
+
}
|