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.
Files changed (2) hide show
  1. package/dist/index.js +267 -21
  2. 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
- "claude-haiku-4-5": { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }
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 today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
768
- const todayStart = /* @__PURE__ */ new Date();
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.startsWith(today)) continue;
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.1.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
- console.log("Claude Stats Monitor");
1104
- console.log("====================");
1105
- fetchUsageLimits().then((limits) => {
1106
- if (limits) {
1107
- console.log(`Usage limits loaded (Session: ${limits.session?.percent_used ?? "?"}% used)`);
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
- const streamer = new Streamer({
1111
- server,
1112
- token,
1113
- name: options.name,
1114
- interval: parseInt(options.interval, 10),
1115
- getAccountId
1116
- });
1117
- streamer.start(collectData);
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-stats",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Monitor and stream Claude Code usage statistics to a remote dashboard",
5
5
  "keywords": [
6
6
  "claude",