claude-stats 0.1.0 → 0.2.1
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/index.js +267 -21
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -43,11 +43,31 @@ var require_pricing = __commonJS({
|
|
|
43
43
|
exports2.calculateCost = calculateCost2;
|
|
44
44
|
exports2.formatModelName = formatModelName;
|
|
45
45
|
exports2.MODEL_PRICING = {
|
|
46
|
+
// Haiku 4.5
|
|
47
|
+
"claude-haiku-4-5-20251001": { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 },
|
|
48
|
+
"claude-haiku-4-5": { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 },
|
|
49
|
+
// Opus 4.5
|
|
46
50
|
"claude-opus-4-5-20251101": { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 },
|
|
51
|
+
"claude-opus-4-5": { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 },
|
|
47
52
|
"claude-opus-4-5-thinking": { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 },
|
|
53
|
+
// Sonnet 4.5 (standard tier - below 200K tokens)
|
|
48
54
|
"claude-sonnet-4-5-20250929": { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 },
|
|
55
|
+
"claude-sonnet-4-5": { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 },
|
|
49
56
|
"claude-sonnet-4-5-thinking": { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 },
|
|
50
|
-
|
|
57
|
+
// Opus 4 (original)
|
|
58
|
+
"claude-opus-4-20250514": { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 },
|
|
59
|
+
"claude-opus-4-1": { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 },
|
|
60
|
+
"claude-opus-4": { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 },
|
|
61
|
+
// Sonnet 4 (original)
|
|
62
|
+
"claude-sonnet-4-20250514": { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 },
|
|
63
|
+
"claude-sonnet-4": { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 },
|
|
64
|
+
// Legacy models
|
|
65
|
+
"claude-3-5-sonnet-20241022": { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 },
|
|
66
|
+
"claude-3-5-sonnet": { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 },
|
|
67
|
+
"claude-3-opus-20240229": { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 },
|
|
68
|
+
"claude-3-opus": { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 },
|
|
69
|
+
"claude-3-haiku-20240307": { input: 0.25, output: 1.25, cache_read: 0.03, cache_write: 0.3 },
|
|
70
|
+
"claude-3-haiku": { input: 0.25, output: 1.25, cache_read: 0.03, cache_write: 0.3 }
|
|
51
71
|
};
|
|
52
72
|
exports2.DEFAULT_PRICING = {
|
|
53
73
|
input: 3,
|
|
@@ -65,7 +85,7 @@ var require_pricing = __commonJS({
|
|
|
65
85
|
return Math.round(cost * 1e4) / 1e4;
|
|
66
86
|
}
|
|
67
87
|
function formatModelName(model) {
|
|
68
|
-
return model.replace("claude-", "").replace("-20251101", "").replace("-20250929", "").replace("-thinking", " (think)").replace("gemini-", "").replace("-high", "");
|
|
88
|
+
return model.replace("claude-", "").replace("-20251101", "").replace("-20251001", "").replace("-20250929", "").replace("-20250514", "").replace("-20241022", "").replace("-20240229", "").replace("-20240307", "").replace("-thinking", " (think)").replace("gemini-", "").replace("-high", "");
|
|
69
89
|
}
|
|
70
90
|
}
|
|
71
91
|
});
|
|
@@ -763,9 +783,16 @@ function getOpenFiles() {
|
|
|
763
783
|
}
|
|
764
784
|
return openFiles;
|
|
765
785
|
}
|
|
786
|
+
function getLocalDateString(date) {
|
|
787
|
+
const year = date.getFullYear();
|
|
788
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
789
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
790
|
+
return `${year}-${month}-${day}`;
|
|
791
|
+
}
|
|
766
792
|
function getTodayUsageFromSessions() {
|
|
767
|
-
const
|
|
768
|
-
const
|
|
793
|
+
const now = /* @__PURE__ */ new Date();
|
|
794
|
+
const today = getLocalDateString(now);
|
|
795
|
+
const todayStart = new Date(now);
|
|
769
796
|
todayStart.setHours(0, 0, 0, 0);
|
|
770
797
|
const todayStartTs = todayStart.getTime();
|
|
771
798
|
const modelTokens = {};
|
|
@@ -798,7 +825,10 @@ function getTodayUsageFromSessions() {
|
|
|
798
825
|
try {
|
|
799
826
|
const msg = JSON.parse(line);
|
|
800
827
|
const ts = msg.timestamp || "";
|
|
801
|
-
if (!ts
|
|
828
|
+
if (!ts) continue;
|
|
829
|
+
const msgDate = new Date(ts);
|
|
830
|
+
const msgLocalDate = getLocalDateString(msgDate);
|
|
831
|
+
if (msgLocalDate !== today) continue;
|
|
802
832
|
if (msg.type === "assistant") {
|
|
803
833
|
messages++;
|
|
804
834
|
const usage = msg.message?.usage || {};
|
|
@@ -1042,10 +1072,157 @@ function collectData() {
|
|
|
1042
1072
|
};
|
|
1043
1073
|
}
|
|
1044
1074
|
|
|
1075
|
+
// src/daemon.ts
|
|
1076
|
+
var fs3 = __toESM(require("fs"));
|
|
1077
|
+
var path3 = __toESM(require("path"));
|
|
1078
|
+
var os4 = __toESM(require("os"));
|
|
1079
|
+
var import_child_process3 = require("child_process");
|
|
1080
|
+
var PID_DIR = path3.join(os4.homedir(), ".claude-stats");
|
|
1081
|
+
var PID_FILE = path3.join(PID_DIR, "daemon.pid");
|
|
1082
|
+
var LOG_FILE = path3.join(PID_DIR, "daemon.log");
|
|
1083
|
+
function ensurePidDir() {
|
|
1084
|
+
if (!fs3.existsSync(PID_DIR)) {
|
|
1085
|
+
fs3.mkdirSync(PID_DIR, { recursive: true });
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
function writePid(pid) {
|
|
1089
|
+
ensurePidDir();
|
|
1090
|
+
fs3.writeFileSync(PID_FILE, pid.toString());
|
|
1091
|
+
}
|
|
1092
|
+
function readPid() {
|
|
1093
|
+
try {
|
|
1094
|
+
if (!fs3.existsSync(PID_FILE)) {
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
const pid = parseInt(fs3.readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
1098
|
+
return isNaN(pid) ? null : pid;
|
|
1099
|
+
} catch {
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
function removePid() {
|
|
1104
|
+
try {
|
|
1105
|
+
if (fs3.existsSync(PID_FILE)) {
|
|
1106
|
+
fs3.unlinkSync(PID_FILE);
|
|
1107
|
+
}
|
|
1108
|
+
} catch {
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
function isProcessRunning(pid) {
|
|
1112
|
+
try {
|
|
1113
|
+
process.kill(pid, 0);
|
|
1114
|
+
return true;
|
|
1115
|
+
} catch {
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
function getDaemonStatus() {
|
|
1120
|
+
const pid = readPid();
|
|
1121
|
+
if (pid === null) {
|
|
1122
|
+
return { running: false, pid: null };
|
|
1123
|
+
}
|
|
1124
|
+
const running = isProcessRunning(pid);
|
|
1125
|
+
if (!running) {
|
|
1126
|
+
removePid();
|
|
1127
|
+
return { running: false, pid: null };
|
|
1128
|
+
}
|
|
1129
|
+
return { running: true, pid };
|
|
1130
|
+
}
|
|
1131
|
+
function startDaemon(options) {
|
|
1132
|
+
const status = getDaemonStatus();
|
|
1133
|
+
if (status.running) {
|
|
1134
|
+
return { success: false, error: `Daemon is already running (PID: ${status.pid})` };
|
|
1135
|
+
}
|
|
1136
|
+
ensurePidDir();
|
|
1137
|
+
const args = [
|
|
1138
|
+
"start",
|
|
1139
|
+
"--foreground",
|
|
1140
|
+
"--server",
|
|
1141
|
+
options.server,
|
|
1142
|
+
"--token",
|
|
1143
|
+
options.token,
|
|
1144
|
+
"--interval",
|
|
1145
|
+
String(options.interval || 1e4)
|
|
1146
|
+
];
|
|
1147
|
+
if (options.name) {
|
|
1148
|
+
args.push("--name", options.name);
|
|
1149
|
+
}
|
|
1150
|
+
let scriptPath = process.argv[1];
|
|
1151
|
+
const logFd = fs3.openSync(LOG_FILE, "a");
|
|
1152
|
+
const child = (0, import_child_process3.spawn)(process.execPath, [scriptPath, ...args], {
|
|
1153
|
+
detached: true,
|
|
1154
|
+
stdio: ["ignore", logFd, logFd],
|
|
1155
|
+
env: {
|
|
1156
|
+
...process.env,
|
|
1157
|
+
CLAUDE_STATS_DAEMON: "1"
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
child.unref();
|
|
1161
|
+
if (child.pid) {
|
|
1162
|
+
writePid(child.pid);
|
|
1163
|
+
fs3.closeSync(logFd);
|
|
1164
|
+
return { success: true, pid: child.pid };
|
|
1165
|
+
}
|
|
1166
|
+
fs3.closeSync(logFd);
|
|
1167
|
+
return { success: false, error: "Failed to start daemon process" };
|
|
1168
|
+
}
|
|
1169
|
+
function stopDaemon() {
|
|
1170
|
+
const status = getDaemonStatus();
|
|
1171
|
+
if (!status.running || status.pid === null) {
|
|
1172
|
+
return { success: false, error: "Daemon is not running" };
|
|
1173
|
+
}
|
|
1174
|
+
try {
|
|
1175
|
+
process.kill(status.pid, "SIGTERM");
|
|
1176
|
+
let attempts = 0;
|
|
1177
|
+
while (attempts < 10 && isProcessRunning(status.pid)) {
|
|
1178
|
+
(0, import_child_process3.execSync)("sleep 0.1");
|
|
1179
|
+
attempts++;
|
|
1180
|
+
}
|
|
1181
|
+
if (isProcessRunning(status.pid)) {
|
|
1182
|
+
process.kill(status.pid, "SIGKILL");
|
|
1183
|
+
}
|
|
1184
|
+
removePid();
|
|
1185
|
+
return { success: true };
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
removePid();
|
|
1188
|
+
return { success: false, error: `Failed to stop daemon: ${error instanceof Error ? error.message : error}` };
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
function getLogFile() {
|
|
1192
|
+
return LOG_FILE;
|
|
1193
|
+
}
|
|
1194
|
+
function getRecentLogs(lines = 20) {
|
|
1195
|
+
try {
|
|
1196
|
+
if (!fs3.existsSync(LOG_FILE)) {
|
|
1197
|
+
return [];
|
|
1198
|
+
}
|
|
1199
|
+
const content = fs3.readFileSync(LOG_FILE, "utf-8");
|
|
1200
|
+
const allLines = content.split("\n").filter((l) => l.trim());
|
|
1201
|
+
return allLines.slice(-lines);
|
|
1202
|
+
} catch {
|
|
1203
|
+
return [];
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
function isDaemonProcess() {
|
|
1207
|
+
return process.env.CLAUDE_STATS_DAEMON === "1";
|
|
1208
|
+
}
|
|
1209
|
+
function writeDaemonPid() {
|
|
1210
|
+
writePid(process.pid);
|
|
1211
|
+
}
|
|
1212
|
+
function setupDaemonCleanup() {
|
|
1213
|
+
const cleanup = () => {
|
|
1214
|
+
removePid();
|
|
1215
|
+
process.exit(0);
|
|
1216
|
+
};
|
|
1217
|
+
process.on("SIGINT", cleanup);
|
|
1218
|
+
process.on("SIGTERM", cleanup);
|
|
1219
|
+
process.on("exit", () => removePid());
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1045
1222
|
// src/index.ts
|
|
1046
1223
|
var DEFAULT_SERVER = "https://cm.cban.top";
|
|
1047
1224
|
var program = new import_commander.Command();
|
|
1048
|
-
program.name("claude-stats").description("Monitor and stream Claude Code usage statistics").version("0.
|
|
1225
|
+
program.name("claude-stats").description("Monitor and stream Claude Code usage statistics").version("0.2.0");
|
|
1049
1226
|
function promptForInput(question, hidden = false) {
|
|
1050
1227
|
return new Promise((resolve) => {
|
|
1051
1228
|
const rl = readline.createInterface({
|
|
@@ -1083,12 +1260,28 @@ function promptForInput(question, hidden = false) {
|
|
|
1083
1260
|
}
|
|
1084
1261
|
});
|
|
1085
1262
|
}
|
|
1086
|
-
program.command("start", { isDefault: true }).description("Start streaming usage data to the monitor server").option("-s, --server <url>", "Server URL", process.env.MONITOR_SERVER || DEFAULT_SERVER).option("-t, --token <token>", "Authentication token", process.env.MONITOR_TOKEN).option("-n, --name <name>", "Machine name (defaults to hostname)", process.env.MONITOR_NAME).option("-i, --interval <ms>", "Polling interval in milliseconds", "10000").action(async (options) => {
|
|
1263
|
+
program.command("start", { isDefault: true }).description("Start streaming usage data to the monitor server").option("-s, --server <url>", "Server URL", process.env.MONITOR_SERVER || DEFAULT_SERVER).option("-t, --token <token>", "Authentication token", process.env.MONITOR_TOKEN).option("-n, --name <name>", "Machine name (defaults to hostname)", process.env.MONITOR_NAME).option("-i, --interval <ms>", "Polling interval in milliseconds", "10000").option("-f, --foreground", "Run in foreground instead of background").action(async (options) => {
|
|
1087
1264
|
let server = options.server;
|
|
1088
1265
|
let token = options.token;
|
|
1089
1266
|
if (server && !server.startsWith("http://") && !server.startsWith("https://")) {
|
|
1090
1267
|
server = `https://${server}`;
|
|
1091
1268
|
}
|
|
1269
|
+
if (isDaemonProcess()) {
|
|
1270
|
+
setupDaemonCleanup();
|
|
1271
|
+
writeDaemonPid();
|
|
1272
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Daemon started`);
|
|
1273
|
+
console.log(`Server: ${server}`);
|
|
1274
|
+
console.log(`Machine name: ${options.name || "default"}`);
|
|
1275
|
+
const streamer = new Streamer({
|
|
1276
|
+
server,
|
|
1277
|
+
token,
|
|
1278
|
+
name: options.name,
|
|
1279
|
+
interval: parseInt(options.interval, 10),
|
|
1280
|
+
getAccountId
|
|
1281
|
+
});
|
|
1282
|
+
streamer.start(collectData);
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1092
1285
|
if (!token) {
|
|
1093
1286
|
console.log("Claude Stats Monitor");
|
|
1094
1287
|
console.log("====================");
|
|
@@ -1100,21 +1293,74 @@ program.command("start", { isDefault: true }).description("Start streaming usage
|
|
|
1100
1293
|
process.exit(1);
|
|
1101
1294
|
}
|
|
1102
1295
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1296
|
+
const status = getDaemonStatus();
|
|
1297
|
+
if (status.running) {
|
|
1298
|
+
console.log(`Daemon is already running (PID: ${status.pid})`);
|
|
1299
|
+
console.log('Use "claude-stats stop" to stop it first.');
|
|
1300
|
+
process.exit(1);
|
|
1301
|
+
}
|
|
1302
|
+
if (options.foreground) {
|
|
1303
|
+
console.log("Claude Stats Monitor");
|
|
1304
|
+
console.log("====================");
|
|
1305
|
+
fetchUsageLimits().then((limits) => {
|
|
1306
|
+
if (limits) {
|
|
1307
|
+
console.log(`Usage limits loaded (Session: ${limits.session?.percent_used ?? "?"}% used)`);
|
|
1308
|
+
}
|
|
1309
|
+
});
|
|
1310
|
+
const streamer = new Streamer({
|
|
1311
|
+
server,
|
|
1312
|
+
token,
|
|
1313
|
+
name: options.name,
|
|
1314
|
+
interval: parseInt(options.interval, 10),
|
|
1315
|
+
getAccountId
|
|
1316
|
+
});
|
|
1317
|
+
streamer.start(collectData);
|
|
1318
|
+
} else {
|
|
1319
|
+
console.log("Starting Claude Stats Monitor in background...");
|
|
1320
|
+
const result = startDaemon({
|
|
1321
|
+
server,
|
|
1322
|
+
token,
|
|
1323
|
+
name: options.name,
|
|
1324
|
+
interval: parseInt(options.interval, 10)
|
|
1325
|
+
});
|
|
1326
|
+
if (result.success) {
|
|
1327
|
+
console.log(`Daemon started successfully (PID: ${result.pid})`);
|
|
1328
|
+
console.log(`Logs: ${getLogFile()}`);
|
|
1329
|
+
console.log("");
|
|
1330
|
+
console.log('Use "claude-stats status" to check status');
|
|
1331
|
+
console.log('Use "claude-stats stop" to stop the daemon');
|
|
1332
|
+
console.log('Use "claude-stats logs" to view recent logs');
|
|
1333
|
+
} else {
|
|
1334
|
+
console.error(`Failed to start daemon: ${result.error}`);
|
|
1335
|
+
process.exit(1);
|
|
1108
1336
|
}
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1339
|
+
program.command("stop").description("Stop the background daemon").action(() => {
|
|
1340
|
+
const result = stopDaemon();
|
|
1341
|
+
if (result.success) {
|
|
1342
|
+
console.log("Daemon stopped successfully");
|
|
1343
|
+
} else {
|
|
1344
|
+
console.error(result.error);
|
|
1345
|
+
process.exit(1);
|
|
1346
|
+
}
|
|
1347
|
+
});
|
|
1348
|
+
program.command("status").description("Check if the daemon is running").action(() => {
|
|
1349
|
+
const status = getDaemonStatus();
|
|
1350
|
+
if (status.running) {
|
|
1351
|
+
console.log(`Daemon is running (PID: ${status.pid})`);
|
|
1352
|
+
console.log(`Logs: ${getLogFile()}`);
|
|
1353
|
+
} else {
|
|
1354
|
+
console.log("Daemon is not running");
|
|
1355
|
+
}
|
|
1356
|
+
});
|
|
1357
|
+
program.command("logs").description("View recent daemon logs").option("-n, --lines <number>", "Number of lines to show", "20").action((options) => {
|
|
1358
|
+
const lines = getRecentLogs(parseInt(options.lines, 10));
|
|
1359
|
+
if (lines.length === 0) {
|
|
1360
|
+
console.log("No logs found");
|
|
1361
|
+
} else {
|
|
1362
|
+
console.log(lines.join("\n"));
|
|
1363
|
+
}
|
|
1118
1364
|
});
|
|
1119
1365
|
program.command("test").description("Collect and display current data without streaming").action(() => {
|
|
1120
1366
|
console.log("Collecting data...\n");
|