Skip to content

Commit 0d4b2f5

Browse files
committed
feat: TypeScript CLI with auto pricing and npm release
1 parent dc2d74e commit 0d4b2f5

12 files changed

Lines changed: 1139 additions & 0 deletions

File tree

.github/workflows/npm.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: NPM
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
build:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v4
12+
- uses: actions/setup-node@v4
13+
with:
14+
node-version: '20'
15+
- name: Install deps
16+
run: npm ci || npm i
17+
- name: Build
18+
run: npm run build
19+
- name: Smoke
20+
run: node dist/cli.js --help

.github/workflows/release-npm.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: Release (npm)
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*.*.*'
7+
8+
jobs:
9+
publish:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: actions/setup-node@v4
14+
with:
15+
node-version: '20'
16+
registry-url: 'https://registry.npmjs.org'
17+
- name: Install
18+
run: npm ci || npm i
19+
- name: Build
20+
run: npm run build
21+
- name: Publish
22+
run: npm publish --access public
23+
env:
24+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
25+

dist/cli.js

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
#!/usr/bin/env node
2+
"use strict";
3+
/* eslint-disable no-console */
4+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5+
if (k2 === undefined) k2 = k;
6+
var desc = Object.getOwnPropertyDescriptor(m, k);
7+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
8+
desc = { enumerable: true, get: function() { return m[k]; } };
9+
}
10+
Object.defineProperty(o, k2, desc);
11+
}) : (function(o, m, k, k2) {
12+
if (k2 === undefined) k2 = k;
13+
o[k2] = m[k];
14+
}));
15+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
16+
Object.defineProperty(o, "default", { enumerable: true, value: v });
17+
}) : function(o, v) {
18+
o["default"] = v;
19+
});
20+
var __importStar = (this && this.__importStar) || (function () {
21+
var ownKeys = function(o) {
22+
ownKeys = Object.getOwnPropertyNames || function (o) {
23+
var ar = [];
24+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
25+
return ar;
26+
};
27+
return ownKeys(o);
28+
};
29+
return function (mod) {
30+
if (mod && mod.__esModule) return mod;
31+
var result = {};
32+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
33+
__setModuleDefault(result, mod);
34+
return result;
35+
};
36+
})();
37+
Object.defineProperty(exports, "__esModule", { value: true });
38+
const os = __importStar(require("os"));
39+
const path = __importStar(require("path"));
40+
const daily_1 = require("./daily");
41+
const VERSION = '0.1.0';
42+
function printHelp() {
43+
const defRoot = path.join(os.homedir(), '.codex', 'sessions');
44+
console.log(`usage: cxusage [daily] [--root ROOT] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--tz TZ] [--by day|model] [--md|--json] [--empty]\n`);
45+
console.log('Analyze Claude Code usage from Codex session logs.');
46+
console.log('\noptions:');
47+
console.log(' --root ROOT Sessions root (default: ' + defRoot + ')');
48+
console.log(' --from DATE Start date YYYY-MM-DD (inclusive)');
49+
console.log(' --to DATE End date YYYY-MM-DD (inclusive)');
50+
console.log(' --tz TZ IANA timezone, e.g. Asia/Shanghai; default local');
51+
console.log(' --by {day,model} Group granularity: day (default) or model-within-day');
52+
console.log(' --md Output as Markdown table');
53+
console.log(' --json Output as JSON lines');
54+
console.log(' --empty Show empty days (zero rows for missing days)');
55+
console.log('\ncommands:');
56+
console.log(' daily Aggregate per day (optionally by model)');
57+
}
58+
function parseArgs(argv) {
59+
const out = {};
60+
let i = 0;
61+
let cmd = null;
62+
let help = false;
63+
let version = false;
64+
if (argv[i] && !argv[i].startsWith('-')) {
65+
cmd = argv[i++];
66+
}
67+
for (; i < argv.length; i++) {
68+
const a = argv[i];
69+
if (a === '--help' || a === '-h') {
70+
help = true;
71+
continue;
72+
}
73+
if (a === '--version') {
74+
version = true;
75+
continue;
76+
}
77+
if (a === '--md') {
78+
out.md = true;
79+
continue;
80+
}
81+
if (a === '--json') {
82+
out.json = true;
83+
continue;
84+
}
85+
if (a === '--empty') {
86+
out.empty = true;
87+
continue;
88+
}
89+
if (a === '--by') {
90+
out.by = argv[++i];
91+
continue;
92+
}
93+
if (a === '--root') {
94+
out.root = argv[++i];
95+
continue;
96+
}
97+
if (a === '--from') {
98+
out.from = argv[++i];
99+
continue;
100+
}
101+
if (a === '--to') {
102+
out.to = argv[++i];
103+
continue;
104+
}
105+
if (a === '--tz') {
106+
out.tz = argv[++i];
107+
continue;
108+
}
109+
}
110+
return { cmd, args: out, help, version };
111+
}
112+
async function main() {
113+
const { cmd, args, help, version } = parseArgs(process.argv.slice(2));
114+
if (version) {
115+
console.log('cxusage ' + VERSION);
116+
process.exit(0);
117+
}
118+
if (help) {
119+
printHelp();
120+
process.exit(0);
121+
}
122+
const sub = cmd || 'daily';
123+
switch (sub) {
124+
case 'daily':
125+
process.exit(await (0, daily_1.executeDaily)(args));
126+
default:
127+
console.error('Unknown command: ' + sub);
128+
printHelp();
129+
process.exit(2);
130+
}
131+
}
132+
main();

dist/daily.js

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
"use strict";
2+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3+
if (k2 === undefined) k2 = k;
4+
var desc = Object.getOwnPropertyDescriptor(m, k);
5+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6+
desc = { enumerable: true, get: function() { return m[k]; } };
7+
}
8+
Object.defineProperty(o, k2, desc);
9+
}) : (function(o, m, k, k2) {
10+
if (k2 === undefined) k2 = k;
11+
o[k2] = m[k];
12+
}));
13+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14+
Object.defineProperty(o, "default", { enumerable: true, value: v });
15+
}) : function(o, v) {
16+
o["default"] = v;
17+
});
18+
var __importStar = (this && this.__importStar) || (function () {
19+
var ownKeys = function(o) {
20+
ownKeys = Object.getOwnPropertyNames || function (o) {
21+
var ar = [];
22+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23+
return ar;
24+
};
25+
return ownKeys(o);
26+
};
27+
return function (mod) {
28+
if (mod && mod.__esModule) return mod;
29+
var result = {};
30+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31+
__setModuleDefault(result, mod);
32+
return result;
33+
};
34+
})();
35+
Object.defineProperty(exports, "__esModule", { value: true });
36+
exports.executeDaily = executeDaily;
37+
const fs_1 = require("fs");
38+
const path = __importStar(require("path"));
39+
const utils_1 = require("./utils");
40+
const pricing_1 = require("./pricing");
41+
const DEFAULT_ROOT = path.join(process.env.HOME || process.env.USERPROFILE || '', '.codex', 'sessions');
42+
const TOKEN_KEYS_IN = ["input_tokens", "prompt_tokens", "request_tokens"];
43+
const TOKEN_KEYS_OUT = ["output_tokens", "completion_tokens", "response_tokens"];
44+
const MODEL_KEYS = ["model", "model_name"];
45+
const TIME_KEYS = ["created_at", "timestamp", "time", "ts", "created", "start_time", "end_time"];
46+
function getEnvPrice(name, def = '0') {
47+
const v = process.env[name] ?? process.env[name.replace('CXUSAGE_', 'CODUSAGE_')] ?? def;
48+
const n = Number(v);
49+
return isFinite(n) ? n : 0;
50+
}
51+
async function executeDaily(args) {
52+
const root = args.root || DEFAULT_ROOT;
53+
const tz = args.tz;
54+
const by = args.by || 'day';
55+
const files = await (0, utils_1.listJsonlFiles)(root);
56+
const agg = new Map();
57+
const aggModel = new Map();
58+
let minDate = null;
59+
let maxDate = null;
60+
for (const file of files) {
61+
let mtime = new Date();
62+
try {
63+
const st = await fs_1.promises.stat(file);
64+
mtime = st.mtime;
65+
}
66+
catch { }
67+
const content = await fs_1.promises.readFile(file, 'utf8');
68+
const lines = content.split(/\r?\n/);
69+
for (const line of lines) {
70+
const s = line.trim();
71+
if (!s)
72+
continue;
73+
let obj;
74+
try {
75+
obj = JSON.parse(s);
76+
}
77+
catch {
78+
continue;
79+
}
80+
const inTok = (0, utils_1.sumKeys)(obj, TOKEN_KEYS_IN);
81+
const outTok = (0, utils_1.sumKeys)(obj, TOKEN_KEYS_OUT);
82+
const whenRaw = (0, utils_1.findFirst)(obj, TIME_KEYS);
83+
const when = (0, utils_1.parseWhen)(whenRaw, mtime);
84+
const day = (0, utils_1.formatDateInTZ)(when, tz);
85+
if (!minDate || day < minDate)
86+
minDate = day;
87+
if (!maxDate || day > maxDate)
88+
maxDate = day;
89+
const model = (() => {
90+
const m = (0, utils_1.findFirst)(obj, MODEL_KEYS);
91+
return typeof m === 'string' ? m : 'unknown';
92+
})();
93+
const cur = agg.get(day) || { in: 0, out: 0, n: 0 };
94+
cur.in += inTok;
95+
cur.out += outTok;
96+
cur.n += 1;
97+
agg.set(day, cur);
98+
let mm = aggModel.get(day);
99+
if (!mm) {
100+
mm = new Map();
101+
aggModel.set(day, mm);
102+
}
103+
const curm = mm.get(model) || { in: 0, out: 0, n: 0 };
104+
curm.in += inTok;
105+
curm.out += outTok;
106+
curm.n += 1;
107+
mm.set(model, curm);
108+
}
109+
}
110+
const df = args.from || minDate || (0, utils_1.formatDateInTZ)(new Date(), tz);
111+
const dt = args.to || maxDate || (0, utils_1.formatDateInTZ)(new Date(), tz);
112+
// Load pricing from public API (OpenRouter models). If unavailable, costs are 0.
113+
const prices = await (0, pricing_1.loadPricing)();
114+
const rows = [];
115+
for (let cur = new Date(df + 'T00:00:00Z');;) {
116+
const curStr = (0, utils_1.formatDateInTZ)(cur, tz);
117+
if (curStr > dt)
118+
break;
119+
if (by === 'day') {
120+
const info = agg.get(curStr) || { in: 0, out: 0, n: 0 };
121+
if (!args.empty && info.n === 0) {
122+
// skip empty
123+
}
124+
else {
125+
const tot = info.in + info.out;
126+
// Sum costs across models for this day using per-model pricing
127+
let cost = 0;
128+
const mm = aggModel.get(curStr);
129+
if (mm) {
130+
for (const [m, inf] of mm.entries()) {
131+
cost += (0, pricing_1.estimateCostFor)(m, inf.in, inf.out, prices);
132+
}
133+
}
134+
rows.push({
135+
date: curStr,
136+
events: info.n | 0,
137+
input_tokens: Math.trunc(info.in),
138+
output_tokens: Math.trunc(info.out),
139+
total_tokens: Math.trunc(tot),
140+
est_cost: cost,
141+
});
142+
}
143+
}
144+
else {
145+
const mm = aggModel.get(curStr);
146+
if (!args.empty && !mm) {
147+
// skip
148+
}
149+
else if (!mm || mm.size === 0) {
150+
rows.push({ date: curStr, model: '-', events: 0, input_tokens: 0, output_tokens: 0, total_tokens: 0, est_cost: 0 });
151+
}
152+
else {
153+
const arr = Array.from(mm.entries());
154+
arr.sort((a, b) => {
155+
const ta = a[1].in + a[1].out;
156+
const tb = b[1].in + b[1].out;
157+
if (ta !== tb)
158+
return tb - ta;
159+
return a[0].localeCompare(b[0]);
160+
});
161+
for (const [m, info] of arr) {
162+
const tot = info.in + info.out;
163+
const cost = (0, pricing_1.estimateCostFor)(m, info.in, info.out, prices);
164+
rows.push({
165+
date: curStr,
166+
model: m,
167+
events: info.n | 0,
168+
input_tokens: Math.trunc(info.in),
169+
output_tokens: Math.trunc(info.out),
170+
total_tokens: Math.trunc(tot),
171+
est_cost: cost,
172+
});
173+
}
174+
}
175+
}
176+
cur = new Date(cur.getTime() + 24 * 3600 * 1000);
177+
}
178+
if (args.json) {
179+
for (const r of rows)
180+
console.log(JSON.stringify(r));
181+
return 0;
182+
}
183+
if (args.md) {
184+
if (by === 'day') {
185+
console.log('| date | events | input_tokens | output_tokens | total_tokens | est_cost |');
186+
console.log('|---|---:|---:|---:|---:|---:|');
187+
for (const r of rows) {
188+
console.log(`| ${r.date} | ${r.events} | ${r.input_tokens.toLocaleString()} | ${r.output_tokens.toLocaleString()} | ${r.total_tokens.toLocaleString()} | $${r.est_cost.toFixed(2)} |`);
189+
}
190+
}
191+
else {
192+
console.log('| date | model | events | input_tokens | output_tokens | total_tokens | est_cost |');
193+
console.log('|---|---|---:|---:|---:|---:|---:|');
194+
for (const r of rows) {
195+
console.log(`| ${r.date} | ${r.model} | ${r.events} | ${r.input_tokens.toLocaleString()} | ${r.output_tokens.toLocaleString()} | ${r.total_tokens.toLocaleString()} | $${r.est_cost.toFixed(2)} |`);
196+
}
197+
}
198+
return 0;
199+
}
200+
const head = by === 'day'
201+
? ["date", "events", "input_tokens", "output_tokens", "total_tokens", "est_cost"]
202+
: ["date", "model", "events", "input_tokens", "output_tokens", "total_tokens", "est_cost"];
203+
const data = [head, ...rows.map(r => head.map(k => k === 'est_cost' ? `$${r.est_cost.toFixed(2)}` : String(r[k] ?? '')))];
204+
const widths = head.map((_, i) => Math.max(...data.map(row => row[i].length)));
205+
const fmt = (row) => row.map((v, i) => i === 0 ? v.padEnd(widths[i]) : v.padStart(widths[i])).join(' ');
206+
console.log(fmt(head));
207+
console.log(widths.map(w => '-'.repeat(w)).join(' '));
208+
for (const r of rows) {
209+
const row = [r.date];
210+
if (by === 'model')
211+
row.push(r.model || '');
212+
row.push(String(r.events), r.input_tokens.toLocaleString(), r.output_tokens.toLocaleString(), r.total_tokens.toLocaleString(), `$${r.est_cost.toFixed(2)}`);
213+
console.log(fmt(row));
214+
}
215+
return 0;
216+
}

0 commit comments

Comments
 (0)