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.
- package/dist/index.js +232 -16
- 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.
|
|
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
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
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
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
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");
|