claude-stats 0.1.0 → 0.2.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.
Files changed (2) hide show
  1. package/dist/index.js +232 -16
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1042,10 +1042,157 @@ function collectData() {
1042
1042
  };
1043
1043
  }
1044
1044
 
1045
+ // src/daemon.ts
1046
+ var fs3 = __toESM(require("fs"));
1047
+ var path3 = __toESM(require("path"));
1048
+ var os4 = __toESM(require("os"));
1049
+ var import_child_process3 = require("child_process");
1050
+ var PID_DIR = path3.join(os4.homedir(), ".claude-stats");
1051
+ var PID_FILE = path3.join(PID_DIR, "daemon.pid");
1052
+ var LOG_FILE = path3.join(PID_DIR, "daemon.log");
1053
+ function ensurePidDir() {
1054
+ if (!fs3.existsSync(PID_DIR)) {
1055
+ fs3.mkdirSync(PID_DIR, { recursive: true });
1056
+ }
1057
+ }
1058
+ function writePid(pid) {
1059
+ ensurePidDir();
1060
+ fs3.writeFileSync(PID_FILE, pid.toString());
1061
+ }
1062
+ function readPid() {
1063
+ try {
1064
+ if (!fs3.existsSync(PID_FILE)) {
1065
+ return null;
1066
+ }
1067
+ const pid = parseInt(fs3.readFileSync(PID_FILE, "utf-8").trim(), 10);
1068
+ return isNaN(pid) ? null : pid;
1069
+ } catch {
1070
+ return null;
1071
+ }
1072
+ }
1073
+ function removePid() {
1074
+ try {
1075
+ if (fs3.existsSync(PID_FILE)) {
1076
+ fs3.unlinkSync(PID_FILE);
1077
+ }
1078
+ } catch {
1079
+ }
1080
+ }
1081
+ function isProcessRunning(pid) {
1082
+ try {
1083
+ process.kill(pid, 0);
1084
+ return true;
1085
+ } catch {
1086
+ return false;
1087
+ }
1088
+ }
1089
+ function getDaemonStatus() {
1090
+ const pid = readPid();
1091
+ if (pid === null) {
1092
+ return { running: false, pid: null };
1093
+ }
1094
+ const running = isProcessRunning(pid);
1095
+ if (!running) {
1096
+ removePid();
1097
+ return { running: false, pid: null };
1098
+ }
1099
+ return { running: true, pid };
1100
+ }
1101
+ function startDaemon(options) {
1102
+ const status = getDaemonStatus();
1103
+ if (status.running) {
1104
+ return { success: false, error: `Daemon is already running (PID: ${status.pid})` };
1105
+ }
1106
+ ensurePidDir();
1107
+ const args = [
1108
+ "start",
1109
+ "--foreground",
1110
+ "--server",
1111
+ options.server,
1112
+ "--token",
1113
+ options.token,
1114
+ "--interval",
1115
+ String(options.interval || 1e4)
1116
+ ];
1117
+ if (options.name) {
1118
+ args.push("--name", options.name);
1119
+ }
1120
+ let scriptPath = process.argv[1];
1121
+ const logFd = fs3.openSync(LOG_FILE, "a");
1122
+ const child = (0, import_child_process3.spawn)(process.execPath, [scriptPath, ...args], {
1123
+ detached: true,
1124
+ stdio: ["ignore", logFd, logFd],
1125
+ env: {
1126
+ ...process.env,
1127
+ CLAUDE_STATS_DAEMON: "1"
1128
+ }
1129
+ });
1130
+ child.unref();
1131
+ if (child.pid) {
1132
+ writePid(child.pid);
1133
+ fs3.closeSync(logFd);
1134
+ return { success: true, pid: child.pid };
1135
+ }
1136
+ fs3.closeSync(logFd);
1137
+ return { success: false, error: "Failed to start daemon process" };
1138
+ }
1139
+ function stopDaemon() {
1140
+ const status = getDaemonStatus();
1141
+ if (!status.running || status.pid === null) {
1142
+ return { success: false, error: "Daemon is not running" };
1143
+ }
1144
+ try {
1145
+ process.kill(status.pid, "SIGTERM");
1146
+ let attempts = 0;
1147
+ while (attempts < 10 && isProcessRunning(status.pid)) {
1148
+ (0, import_child_process3.execSync)("sleep 0.1");
1149
+ attempts++;
1150
+ }
1151
+ if (isProcessRunning(status.pid)) {
1152
+ process.kill(status.pid, "SIGKILL");
1153
+ }
1154
+ removePid();
1155
+ return { success: true };
1156
+ } catch (error) {
1157
+ removePid();
1158
+ return { success: false, error: `Failed to stop daemon: ${error instanceof Error ? error.message : error}` };
1159
+ }
1160
+ }
1161
+ function getLogFile() {
1162
+ return LOG_FILE;
1163
+ }
1164
+ function getRecentLogs(lines = 20) {
1165
+ try {
1166
+ if (!fs3.existsSync(LOG_FILE)) {
1167
+ return [];
1168
+ }
1169
+ const content = fs3.readFileSync(LOG_FILE, "utf-8");
1170
+ const allLines = content.split("\n").filter((l) => l.trim());
1171
+ return allLines.slice(-lines);
1172
+ } catch {
1173
+ return [];
1174
+ }
1175
+ }
1176
+ function isDaemonProcess() {
1177
+ return process.env.CLAUDE_STATS_DAEMON === "1";
1178
+ }
1179
+ function writeDaemonPid() {
1180
+ writePid(process.pid);
1181
+ }
1182
+ function setupDaemonCleanup() {
1183
+ const cleanup = () => {
1184
+ removePid();
1185
+ process.exit(0);
1186
+ };
1187
+ process.on("SIGINT", cleanup);
1188
+ process.on("SIGTERM", cleanup);
1189
+ process.on("exit", () => removePid());
1190
+ }
1191
+
1045
1192
  // src/index.ts
1046
1193
  var DEFAULT_SERVER = "https://cm.cban.top";
1047
1194
  var program = new import_commander.Command();
1048
- program.name("claude-stats").description("Monitor and stream Claude Code usage statistics").version("0.1.0");
1195
+ program.name("claude-stats").description("Monitor and stream Claude Code usage statistics").version("0.2.0");
1049
1196
  function promptForInput(question, hidden = false) {
1050
1197
  return new Promise((resolve) => {
1051
1198
  const rl = readline.createInterface({
@@ -1083,12 +1230,28 @@ function promptForInput(question, hidden = false) {
1083
1230
  }
1084
1231
  });
1085
1232
  }
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) => {
1233
+ 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
1234
  let server = options.server;
1088
1235
  let token = options.token;
1089
1236
  if (server && !server.startsWith("http://") && !server.startsWith("https://")) {
1090
1237
  server = `https://${server}`;
1091
1238
  }
1239
+ if (isDaemonProcess()) {
1240
+ setupDaemonCleanup();
1241
+ writeDaemonPid();
1242
+ console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Daemon started`);
1243
+ console.log(`Server: ${server}`);
1244
+ console.log(`Machine name: ${options.name || "default"}`);
1245
+ const streamer = new Streamer({
1246
+ server,
1247
+ token,
1248
+ name: options.name,
1249
+ interval: parseInt(options.interval, 10),
1250
+ getAccountId
1251
+ });
1252
+ streamer.start(collectData);
1253
+ return;
1254
+ }
1092
1255
  if (!token) {
1093
1256
  console.log("Claude Stats Monitor");
1094
1257
  console.log("====================");
@@ -1100,21 +1263,74 @@ program.command("start", { isDefault: true }).description("Start streaming usage
1100
1263
  process.exit(1);
1101
1264
  }
1102
1265
  }
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)`);
1266
+ const status = getDaemonStatus();
1267
+ if (status.running) {
1268
+ console.log(`Daemon is already running (PID: ${status.pid})`);
1269
+ console.log('Use "claude-stats stop" to stop it first.');
1270
+ process.exit(1);
1271
+ }
1272
+ if (options.foreground) {
1273
+ console.log("Claude Stats Monitor");
1274
+ console.log("====================");
1275
+ fetchUsageLimits().then((limits) => {
1276
+ if (limits) {
1277
+ console.log(`Usage limits loaded (Session: ${limits.session?.percent_used ?? "?"}% used)`);
1278
+ }
1279
+ });
1280
+ const streamer = new Streamer({
1281
+ server,
1282
+ token,
1283
+ name: options.name,
1284
+ interval: parseInt(options.interval, 10),
1285
+ getAccountId
1286
+ });
1287
+ streamer.start(collectData);
1288
+ } else {
1289
+ console.log("Starting Claude Stats Monitor in background...");
1290
+ const result = startDaemon({
1291
+ server,
1292
+ token,
1293
+ name: options.name,
1294
+ interval: parseInt(options.interval, 10)
1295
+ });
1296
+ if (result.success) {
1297
+ console.log(`Daemon started successfully (PID: ${result.pid})`);
1298
+ console.log(`Logs: ${getLogFile()}`);
1299
+ console.log("");
1300
+ console.log('Use "claude-stats status" to check status');
1301
+ console.log('Use "claude-stats stop" to stop the daemon');
1302
+ console.log('Use "claude-stats logs" to view recent logs');
1303
+ } else {
1304
+ console.error(`Failed to start daemon: ${result.error}`);
1305
+ process.exit(1);
1108
1306
  }
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);
1307
+ }
1308
+ });
1309
+ program.command("stop").description("Stop the background daemon").action(() => {
1310
+ const result = stopDaemon();
1311
+ if (result.success) {
1312
+ console.log("Daemon stopped successfully");
1313
+ } else {
1314
+ console.error(result.error);
1315
+ process.exit(1);
1316
+ }
1317
+ });
1318
+ program.command("status").description("Check if the daemon is running").action(() => {
1319
+ const status = getDaemonStatus();
1320
+ if (status.running) {
1321
+ console.log(`Daemon is running (PID: ${status.pid})`);
1322
+ console.log(`Logs: ${getLogFile()}`);
1323
+ } else {
1324
+ console.log("Daemon is not running");
1325
+ }
1326
+ });
1327
+ program.command("logs").description("View recent daemon logs").option("-n, --lines <number>", "Number of lines to show", "20").action((options) => {
1328
+ const lines = getRecentLogs(parseInt(options.lines, 10));
1329
+ if (lines.length === 0) {
1330
+ console.log("No logs found");
1331
+ } else {
1332
+ console.log(lines.join("\n"));
1333
+ }
1118
1334
  });
1119
1335
  program.command("test").description("Collect and display current data without streaming").action(() => {
1120
1336
  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.0",
4
4
  "description": "Monitor and stream Claude Code usage statistics to a remote dashboard",
5
5
  "keywords": [
6
6
  "claude",