@statforge/claudestat 1.7.0 → 1.8.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/README.md +33 -2
- package/dist/config.d.ts +7 -0
- package/dist/config.js +36 -0
- package/dist/daemon.js +56 -8
- package/dist/db.d.ts +11 -0
- package/dist/db.js +30 -0
- package/dist/doctor.js +20 -2
- package/dist/export.d.ts +2 -1
- package/dist/export.js +41 -6
- package/dist/index.js +405 -30
- package/dist/insights.d.ts +1 -0
- package/dist/insights.js +26 -0
- package/dist/install.js +28 -1
- package/dist/intelligence.d.ts +11 -4
- package/dist/intelligence.js +43 -17
- package/dist/logger.d.ts +6 -0
- package/dist/logger.js +49 -0
- package/dist/notifier.d.ts +15 -0
- package/dist/notifier.js +26 -0
- package/dist/paths.d.ts +18 -0
- package/dist/paths.js +34 -0
- package/dist/routes/helpers.d.ts +5 -0
- package/dist/routes/helpers.js +21 -1
- package/dist/routes/misc.js +19 -1
- package/dist/routes/projects.js +10 -1
- package/dist/service.js +11 -0
- package/hooks/event.js +44 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -282,7 +282,7 @@ Claude Code event
|
|
|
282
282
|
## Troubleshooting
|
|
283
283
|
|
|
284
284
|
**`claudestat start` hangs for ~5 seconds**
|
|
285
|
-
Normal — `require('express')` takes a few seconds on first load.
|
|
285
|
+
Normal — `require('express')` takes a few seconds on first load. The daemon is running; wait for the "Daemon started" confirmation.
|
|
286
286
|
|
|
287
287
|
**Hooks are not firing / dashboard shows no events**
|
|
288
288
|
Run `claudestat doctor` — it checks every component and prints the exact fix command.
|
|
@@ -302,7 +302,38 @@ The daemon polls quota every 60s and logs warnings at 70%, 85%, and 95%. Check a
|
|
|
302
302
|
**Working with multiple projects**
|
|
303
303
|
claudestat tracks every project automatically. The Projects tab groups sessions by working directory.
|
|
304
304
|
|
|
305
|
-
|
|
305
|
+
**Dashboard shows 0 cost / $0.00 for all sessions**
|
|
306
|
+
Token data comes from Claude Code's JSONL files, not from hook events. Make sure Claude Code is writing JSONL logs — check `~/.claude/projects/` for `.jsonl` files. If the directory is empty, Claude Code may not have logging enabled.
|
|
307
|
+
|
|
308
|
+
**Daemon stops after terminal closes**
|
|
309
|
+
The daemon must be started with `nohup` to persist beyond the shell session:
|
|
310
|
+
```bash
|
|
311
|
+
nohup claudestat start &
|
|
312
|
+
```
|
|
313
|
+
Or use `claudestat setup` which installs a system service (launchd on macOS, systemd on Linux).
|
|
314
|
+
|
|
315
|
+
**`claudestat export` produces empty output**
|
|
316
|
+
If no sessions appear, the daemon may not have been running during your Claude Code sessions. Check `claudestat status` and restart with `claudestat start`. For historical data only (without a running daemon), export still reads from the local SQLite database — so past sessions captured while the daemon was running are always available.
|
|
317
|
+
|
|
318
|
+
**Loop detector fires too often / not enough**
|
|
319
|
+
Adjust the threshold and window:
|
|
320
|
+
```bash
|
|
321
|
+
claudestat config --loop-threshold 5 # default: 8 calls
|
|
322
|
+
claudestat config --loop-window 90 # default: 120 seconds
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**MCP server not responding**
|
|
326
|
+
Restart the daemon (`claudestat restart`) and verify it's registered:
|
|
327
|
+
```bash
|
|
328
|
+
claude mcp list
|
|
329
|
+
```
|
|
330
|
+
If not listed, re-run: `claude mcp add claudestat -s user -- claudestat-mcp`
|
|
331
|
+
|
|
332
|
+
**OpenCode sessions not appearing**
|
|
333
|
+
claudestat reads OpenCode data from `~/.local/share/opencode/opencode.db`. If the file does not exist, OpenCode has not run yet or uses a different data path on your system. Run `opencode` at least once to initialize it.
|
|
334
|
+
|
|
335
|
+
**Node.js experimental SQLite warning on startup**
|
|
336
|
+
Expected — `node:sqlite` is experimental in Node 22. The warning is suppressed automatically. If you see it repeatedly, ensure you are running Node.js 22 or later (`node --version`).
|
|
306
337
|
|
|
307
338
|
## FAQ
|
|
308
339
|
|
package/dist/config.d.ts
CHANGED
|
@@ -32,6 +32,13 @@ export interface ClaudestatConfig {
|
|
|
32
32
|
reportDay: number;
|
|
33
33
|
reportTime: string;
|
|
34
34
|
alertsEnabled: boolean;
|
|
35
|
+
killSwitchForce: boolean;
|
|
36
|
+
logLevel: 'debug' | 'info' | 'warn' | 'error';
|
|
37
|
+
port: number;
|
|
38
|
+
loopThreshold: number;
|
|
39
|
+
loopWindowSecs: number;
|
|
40
|
+
projectAliases: Record<string, string>;
|
|
41
|
+
webhookUrl: string | null;
|
|
35
42
|
}
|
|
36
43
|
/** Lee la config del disco. Valores ausentes se rellenan con defaults. */
|
|
37
44
|
export declare function readConfig(): ClaudestatConfig;
|
package/dist/config.js
CHANGED
|
@@ -43,6 +43,13 @@ const DEFAULTS = {
|
|
|
43
43
|
reportDay: 1,
|
|
44
44
|
reportTime: '09:00',
|
|
45
45
|
alertsEnabled: true,
|
|
46
|
+
killSwitchForce: false,
|
|
47
|
+
logLevel: 'info',
|
|
48
|
+
port: 7337,
|
|
49
|
+
loopThreshold: 8,
|
|
50
|
+
loopWindowSecs: 120,
|
|
51
|
+
projectAliases: {},
|
|
52
|
+
webhookUrl: null,
|
|
46
53
|
};
|
|
47
54
|
/** Lee la config del disco. Valores ausentes se rellenan con defaults. */
|
|
48
55
|
function readConfig() {
|
|
@@ -100,6 +107,30 @@ function validateConfig(raw) {
|
|
|
100
107
|
}
|
|
101
108
|
if ('alertsEnabled' in cfg && typeof cfg.alertsEnabled !== 'boolean')
|
|
102
109
|
return 'alertsEnabled debe ser boolean';
|
|
110
|
+
if ('killSwitchForce' in cfg && typeof cfg.killSwitchForce !== 'boolean')
|
|
111
|
+
return 'killSwitchForce must be boolean';
|
|
112
|
+
if ('logLevel' in cfg && !['debug', 'info', 'warn', 'error'].includes(cfg.logLevel))
|
|
113
|
+
return 'logLevel must be: debug, info, warn, error';
|
|
114
|
+
if ('loopThreshold' in cfg) {
|
|
115
|
+
const v = cfg.loopThreshold;
|
|
116
|
+
if (typeof v !== 'number' || !Number.isInteger(v) || v < 2 || v > 50)
|
|
117
|
+
return 'loopThreshold must be an integer between 2 and 50';
|
|
118
|
+
}
|
|
119
|
+
if ('loopWindowSecs' in cfg) {
|
|
120
|
+
const v = cfg.loopWindowSecs;
|
|
121
|
+
if (typeof v !== 'number' || !Number.isInteger(v) || v < 10 || v > 600)
|
|
122
|
+
return 'loopWindowSecs must be an integer between 10 and 600';
|
|
123
|
+
}
|
|
124
|
+
if ('projectAliases' in cfg) {
|
|
125
|
+
const v = cfg.projectAliases;
|
|
126
|
+
if (typeof v !== 'object' || v === null || Array.isArray(v))
|
|
127
|
+
return 'projectAliases must be an object { "/path": "Alias" }';
|
|
128
|
+
}
|
|
129
|
+
if ('webhookUrl' in cfg) {
|
|
130
|
+
const v = cfg.webhookUrl;
|
|
131
|
+
if (v !== null && (typeof v !== 'string' || (!v.startsWith('http://') && !v.startsWith('https://'))))
|
|
132
|
+
return 'webhookUrl must be null or a valid http/https URL';
|
|
133
|
+
}
|
|
103
134
|
if ('reportsEnabled' in cfg && typeof cfg.reportsEnabled !== 'boolean')
|
|
104
135
|
return 'reportsEnabled debe ser boolean';
|
|
105
136
|
if ('reportFrequency' in cfg && !['weekly', 'biweekly', 'monthly'].includes(cfg.reportFrequency))
|
|
@@ -113,6 +144,11 @@ function validateConfig(raw) {
|
|
|
113
144
|
if (typeof cfg.reportTime !== 'string' || !/^\d{2}:\d{2}$/.test(cfg.reportTime))
|
|
114
145
|
return 'reportTime debe tener formato HH:MM';
|
|
115
146
|
}
|
|
147
|
+
if ('port' in cfg) {
|
|
148
|
+
const v = cfg.port;
|
|
149
|
+
if (typeof v !== 'number' || !Number.isInteger(v) || v < 1024 || v > 65535)
|
|
150
|
+
return 'port must be an integer between 1024 and 65535';
|
|
151
|
+
}
|
|
116
152
|
return null;
|
|
117
153
|
}
|
|
118
154
|
/** Devuelve el nivel de warning para un % dado, o null si no alcanza ningún threshold. */
|
package/dist/daemon.js
CHANGED
|
@@ -75,7 +75,8 @@ const projects_cache_1 = require("./cache/projects-cache");
|
|
|
75
75
|
const rate_limiter_1 = require("./middleware/rate-limiter");
|
|
76
76
|
const summarizer_1 = require("./summarizer");
|
|
77
77
|
const paths_1 = require("./paths");
|
|
78
|
-
const
|
|
78
|
+
const logger_1 = require("./logger");
|
|
79
|
+
const PORT = (0, config_1.readConfig)().port;
|
|
79
80
|
const app = (0, express_1.default)();
|
|
80
81
|
app.use(express_1.default.json());
|
|
81
82
|
// ─── Shutdown graceful (cross-platform, no depende de SIGTERM) ────────────────
|
|
@@ -131,7 +132,7 @@ function migrateSessionProjects() {
|
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
134
|
if (tagged > 0)
|
|
134
|
-
|
|
135
|
+
logger_1.logger.info(`${tagged} sessions tagged with project`);
|
|
135
136
|
}
|
|
136
137
|
/**
|
|
137
138
|
* Genera summaries IA para las últimas N sesiones que no tienen uno.
|
|
@@ -148,11 +149,11 @@ async function migrateSessionSummaries(limit = 5) {
|
|
|
148
149
|
const summary = await (0, summarizer_1.summarizeSession)(events, s.total_cost_usd ?? 0, projectName);
|
|
149
150
|
if (summary) {
|
|
150
151
|
db_1.dbOps.updateSessionSummary(s.id, summary);
|
|
151
|
-
|
|
152
|
+
logger_1.logger.info(`Summary generated for session ${s.id.slice(0, 8)}: "${summary}"`);
|
|
152
153
|
}
|
|
153
154
|
}
|
|
154
155
|
catch (err) {
|
|
155
|
-
|
|
156
|
+
logger_1.logger.error(`Error generating summary: ${err}`);
|
|
156
157
|
}
|
|
157
158
|
}
|
|
158
159
|
}
|
|
@@ -206,6 +207,7 @@ function checkAlertLevel(level, lastLevel, logMsg, notifTitle, notifBody) {
|
|
|
206
207
|
const prevRank = lastLevel ? LEVEL_RANK[lastLevel] ?? 0 : 0;
|
|
207
208
|
const currRank = LEVEL_RANK[level];
|
|
208
209
|
if (currRank > prevRank) {
|
|
210
|
+
logger_1.logger.warn(logMsg);
|
|
209
211
|
process.stderr.write(`${LEVEL_COLOR[level]}${logMsg}\x1b[0m\n`);
|
|
210
212
|
(0, notifier_1.sendDesktopNotification)(notifTitle, notifBody);
|
|
211
213
|
}
|
|
@@ -220,9 +222,37 @@ function startAlertPolling() {
|
|
|
220
222
|
const data = (0, quota_tracker_1.computeQuota)(cfg.plan ?? undefined);
|
|
221
223
|
const resetMins = Math.ceil(data.cycleResetMs / 60000);
|
|
222
224
|
// ── Cycle 5h alerts ──────────────────────────────────────────────────────
|
|
223
|
-
|
|
225
|
+
const cycleLevel = (0, config_1.getWarnLevel)(data.cyclePct, cfg.warnThresholds);
|
|
226
|
+
if (cycleLevel && cycleLevel !== _lastCycleAlertLevel) {
|
|
227
|
+
const webhookUrl = (0, config_1.readConfig)().webhookUrl;
|
|
228
|
+
if (webhookUrl) {
|
|
229
|
+
(0, notifier_1.sendWebhookAlert)(webhookUrl, {
|
|
230
|
+
title: 'claudestat — 5h cycle alert',
|
|
231
|
+
body: `${data.cyclePct}% of cycle used · resets in ${resetMins}m`,
|
|
232
|
+
level: cycleLevel,
|
|
233
|
+
cyclePct: data.cyclePct,
|
|
234
|
+
resetInMins: resetMins,
|
|
235
|
+
burnRate: data.burnRateTokensPerMin,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
_lastCycleAlertLevel = checkAlertLevel(cycleLevel, _lastCycleAlertLevel, `[claudestat] ⚠️ 5h cycle at ${data.cyclePct}% (${data.cyclePrompts}/${data.cycleLimit} prompts)`, 'claudestat — 5h cycle alert', `${data.cyclePct}% of cycle used · resets in ${resetMins}m`);
|
|
224
240
|
// ── Weekly alerts ────────────────────────────────────────────────────────
|
|
225
|
-
|
|
241
|
+
const weeklyLevel = (0, config_1.getWarnLevel)(data.weeklyPctAll, cfg.weeklyWarnThresholds);
|
|
242
|
+
if (weeklyLevel && weeklyLevel !== _lastWeeklyAlertLevel) {
|
|
243
|
+
const webhookUrl = (0, config_1.readConfig)().webhookUrl;
|
|
244
|
+
if (webhookUrl) {
|
|
245
|
+
(0, notifier_1.sendWebhookAlert)(webhookUrl, {
|
|
246
|
+
title: 'claudestat — Weekly usage alert',
|
|
247
|
+
body: `${data.weeklyPctAll}% of weekly quota used`,
|
|
248
|
+
level: weeklyLevel,
|
|
249
|
+
cyclePct: data.cyclePct,
|
|
250
|
+
weeklyPct: data.weeklyPctAll,
|
|
251
|
+
resetInMins: resetMins,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
_lastWeeklyAlertLevel = checkAlertLevel(weeklyLevel, _lastWeeklyAlertLevel, `[claudestat] ⚠️ Weekly usage at ${data.weeklyPctAll}%`, 'claudestat — Weekly usage alert', `${data.weeklyPctAll}% of weekly quota used`);
|
|
226
256
|
// ── Reset reminder ───────────────────────────────────────────────────────
|
|
227
257
|
const reminderMs = (cfg.resetReminderMins ?? 10) * 60000;
|
|
228
258
|
if (reminderMs > 0) {
|
|
@@ -231,11 +261,28 @@ function startAlertPolling() {
|
|
|
231
261
|
}
|
|
232
262
|
else if (data.cycleResetMs <= reminderMs && data.cycleResetMs > 0 && !_resetReminderFired) {
|
|
233
263
|
const mins = Math.ceil(data.cycleResetMs / 60000);
|
|
264
|
+
logger_1.logger.info(`Quota resets in ${mins}m — good time to wrap up`);
|
|
234
265
|
process.stderr.write(`\x1b[36m[claudestat] ⏰ Quota resets in ${mins}m — good time to wrap up\x1b[0m\n`);
|
|
235
266
|
(0, notifier_1.sendDesktopNotification)('claudestat — Quota reset soon', `Your 5h cycle resets in ${mins} min — good time to start a new task`);
|
|
236
267
|
_resetReminderFired = true;
|
|
237
268
|
}
|
|
238
269
|
}
|
|
270
|
+
// Write or remove pause signal file based on kill switch state
|
|
271
|
+
const signalFile = (0, paths_1.getPauseSignalFile)();
|
|
272
|
+
if (cfg.killSwitchEnabled && data.cyclePct >= cfg.killSwitchThreshold) {
|
|
273
|
+
const msg = `Quota at ${data.cyclePct}% — threshold is ${cfg.killSwitchThreshold}%. Resets in ${Math.ceil(data.cycleResetMs / 60000)}m.`;
|
|
274
|
+
try {
|
|
275
|
+
fs_1.default.writeFileSync(signalFile, msg);
|
|
276
|
+
}
|
|
277
|
+
catch { }
|
|
278
|
+
(0, stream_1.broadcast)({ type: 'kill_switch', payload: { blocked: true, reason: msg } });
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
try {
|
|
282
|
+
fs_1.default.unlinkSync(signalFile);
|
|
283
|
+
}
|
|
284
|
+
catch { }
|
|
285
|
+
}
|
|
239
286
|
}
|
|
240
287
|
catch {
|
|
241
288
|
// quota read failed — ignore
|
|
@@ -262,6 +309,7 @@ function cleanPid() {
|
|
|
262
309
|
function startDaemon() {
|
|
263
310
|
_server = app.listen(PORT, '127.0.0.1', () => {
|
|
264
311
|
writePid();
|
|
312
|
+
(0, paths_1.writePortFile)(PORT);
|
|
265
313
|
process.on('exit', cleanPid);
|
|
266
314
|
process.on('SIGTERM', () => { if (_server)
|
|
267
315
|
shutdown(_server); process.exit(0); });
|
|
@@ -289,7 +337,7 @@ function startDaemon() {
|
|
|
289
337
|
// Se ejecuta en background para no retrasar el inicio del servidor
|
|
290
338
|
setImmediate(() => {
|
|
291
339
|
const projects = (0, projects_cache_1.getProjectsCached)();
|
|
292
|
-
|
|
340
|
+
logger_1.logger.info(`${projects?.length ?? 0} projects scanned`);
|
|
293
341
|
});
|
|
294
342
|
// Refresh automático del cache de proyectos cada 2 minutos
|
|
295
343
|
// Recoge cambios en HANDOFF.md aunque el daemon lleve horas corriendo
|
|
@@ -311,7 +359,7 @@ function startDaemon() {
|
|
|
311
359
|
return; // ya existe
|
|
312
360
|
const markdown = (0, reports_1.generateReport)(dateLabel, cfg);
|
|
313
361
|
db_1.dbOps.insertWeeklyReport(dateLabel, markdown);
|
|
314
|
-
|
|
362
|
+
logger_1.logger.info(`Report auto-generated: ${dateLabel}`);
|
|
315
363
|
}, 60000);
|
|
316
364
|
// Al arrancar: liberar intents activos de sesiones anteriores (ya no hay nadie que los use)
|
|
317
365
|
db_1.dbOps.releaseOrphanedIntents();
|
package/dist/db.d.ts
CHANGED
|
@@ -176,6 +176,17 @@ export declare const dbOps: {
|
|
|
176
176
|
week_start: number;
|
|
177
177
|
week_end: number;
|
|
178
178
|
};
|
|
179
|
+
getPrevWeekInsight(): {
|
|
180
|
+
total_sessions: any;
|
|
181
|
+
total_cost: any;
|
|
182
|
+
input_tokens: any;
|
|
183
|
+
output_tokens: any;
|
|
184
|
+
cache_read: any;
|
|
185
|
+
total_loops: any;
|
|
186
|
+
avg_efficiency: any;
|
|
187
|
+
week_start: any;
|
|
188
|
+
week_end: any;
|
|
189
|
+
};
|
|
179
190
|
setMeta(key: string, value: string): void;
|
|
180
191
|
getMeta(key: string): string | undefined;
|
|
181
192
|
getCostProjection(days?: number): {
|
package/dist/db.js
CHANGED
|
@@ -719,6 +719,36 @@ exports.dbOps = {
|
|
|
719
719
|
const since = Date.now() - days * 86400000;
|
|
720
720
|
return stmts.getWeeklyInsight.get(since);
|
|
721
721
|
},
|
|
722
|
+
getPrevWeekInsight() {
|
|
723
|
+
const now = Date.now();
|
|
724
|
+
const weekEnd = now - 7 * 86400000;
|
|
725
|
+
const weekStart = now - 14 * 86400000;
|
|
726
|
+
const row = db.prepare(`
|
|
727
|
+
SELECT
|
|
728
|
+
COUNT(*) AS total_sessions,
|
|
729
|
+
COALESCE(SUM(total_cost_usd), 0) AS total_cost,
|
|
730
|
+
COALESCE(SUM(total_input_tokens), 0) AS input_tokens,
|
|
731
|
+
COALESCE(SUM(total_output_tokens), 0) AS output_tokens,
|
|
732
|
+
COALESCE(SUM(total_cache_read), 0) AS cache_read,
|
|
733
|
+
COALESCE(SUM(loops_detected), 0) AS total_loops,
|
|
734
|
+
COALESCE(AVG(efficiency_score), 100) AS avg_efficiency,
|
|
735
|
+
MIN(started_at) AS week_start,
|
|
736
|
+
MAX(started_at) AS week_end
|
|
737
|
+
FROM sessions
|
|
738
|
+
WHERE started_at >= ? AND started_at < ?
|
|
739
|
+
`).get(weekStart, weekEnd);
|
|
740
|
+
return {
|
|
741
|
+
total_sessions: row.total_sessions ?? 0,
|
|
742
|
+
total_cost: row.total_cost ?? 0,
|
|
743
|
+
input_tokens: row.input_tokens ?? 0,
|
|
744
|
+
output_tokens: row.output_tokens ?? 0,
|
|
745
|
+
cache_read: row.cache_read ?? 0,
|
|
746
|
+
total_loops: row.total_loops ?? 0,
|
|
747
|
+
avg_efficiency: row.avg_efficiency ?? 100,
|
|
748
|
+
week_start: row.week_start ?? weekStart,
|
|
749
|
+
week_end: row.week_end ?? null,
|
|
750
|
+
};
|
|
751
|
+
},
|
|
722
752
|
setMeta(key, value) {
|
|
723
753
|
stmts.upsertMeta.run(key, value);
|
|
724
754
|
},
|
package/dist/doctor.js
CHANGED
|
@@ -6,8 +6,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.runDoctor = runDoctor;
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
9
10
|
const child_process_1 = require("child_process");
|
|
10
11
|
const paths_1 = require("./paths");
|
|
12
|
+
const config_1 = require("./config");
|
|
11
13
|
async function runDoctor() {
|
|
12
14
|
const checks = [];
|
|
13
15
|
const G = '\x1b[32m✓\x1b[0m';
|
|
@@ -66,15 +68,16 @@ async function runDoctor() {
|
|
|
66
68
|
fix: hookOk ? undefined : 'claudestat install',
|
|
67
69
|
});
|
|
68
70
|
// 6. Daemon reachable
|
|
71
|
+
const cfgPort = (0, config_1.readConfig)().port;
|
|
69
72
|
const daemonOk = await (async () => { try {
|
|
70
|
-
const res = await fetch(
|
|
73
|
+
const res = await fetch(`http://localhost:${cfgPort}/health`);
|
|
71
74
|
return res.ok;
|
|
72
75
|
}
|
|
73
76
|
catch {
|
|
74
77
|
return false;
|
|
75
78
|
} })();
|
|
76
79
|
checks.push({
|
|
77
|
-
label: 'Daemon running (localhost:
|
|
80
|
+
label: 'Daemon running (localhost:' + cfgPort + ')',
|
|
78
81
|
ok: daemonOk,
|
|
79
82
|
fix: daemonOk ? undefined : 'claudestat start',
|
|
80
83
|
});
|
|
@@ -166,6 +169,21 @@ async function runDoctor() {
|
|
|
166
169
|
`nvm use default && npm install -g @statforge/claudestat\n Then restart terminal`,
|
|
167
170
|
});
|
|
168
171
|
}
|
|
172
|
+
// 12. Daemon service node matches current node (only if service file exists)
|
|
173
|
+
if (process.platform === 'darwin') {
|
|
174
|
+
const plistPath = path_1.default.join(process.env.HOME ?? os_1.default.homedir(), 'Library', 'LaunchAgents', 'com.statforge.claudestat.plist');
|
|
175
|
+
if (fs_1.default.existsSync(plistPath)) {
|
|
176
|
+
const plistContent = fs_1.default.readFileSync(plistPath, 'utf8');
|
|
177
|
+
const currentNode = process.execPath;
|
|
178
|
+
const nodeOk = plistContent.includes(currentNode);
|
|
179
|
+
checks.push({
|
|
180
|
+
label: 'Daemon service uses current Node binary',
|
|
181
|
+
ok: nodeOk,
|
|
182
|
+
note: nodeOk ? undefined : `Service file uses a different node than ${currentNode}`,
|
|
183
|
+
fix: nodeOk ? undefined : 'claudestat setup --uninstall && claudestat setup',
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
169
187
|
// 11. MCP server registered in Claude Code
|
|
170
188
|
let mcpOk = false;
|
|
171
189
|
let mcpNote;
|
package/dist/export.d.ts
CHANGED
package/dist/export.js
CHANGED
|
@@ -5,9 +5,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.runExport = runExport;
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
|
-
const os_1 = __importDefault(require("os"));
|
|
9
8
|
const path_1 = __importDefault(require("path"));
|
|
10
9
|
const db_1 = require("./db");
|
|
10
|
+
const MD_HEADERS = ['Date', 'Project', 'Cost (USD)', 'Input tokens', 'Output tokens', 'Efficiency', 'Loops'];
|
|
11
11
|
function parseDate(str, endOfDay = false) {
|
|
12
12
|
const ms = Date.parse(str);
|
|
13
13
|
if (isNaN(ms))
|
|
@@ -23,6 +23,23 @@ const CSV_HEADERS = [
|
|
|
23
23
|
'total_cost_usd', 'total_input_tokens', 'total_output_tokens',
|
|
24
24
|
'efficiency_score', 'loops_detected',
|
|
25
25
|
];
|
|
26
|
+
function parseSince(since) {
|
|
27
|
+
const match = since.match(/^(\d+)d$/);
|
|
28
|
+
if (!match)
|
|
29
|
+
throw new Error(`Invalid --since format: "${since}" — use e.g. "7d" or "30d"`);
|
|
30
|
+
return Date.now() - parseInt(match[1], 10) * 86400000;
|
|
31
|
+
}
|
|
32
|
+
function toMarkdownRow(r) {
|
|
33
|
+
return [
|
|
34
|
+
String(r.started_at).slice(0, 10),
|
|
35
|
+
r.project_path ? String(r.project_path).split('/').pop() ?? '—' : '—',
|
|
36
|
+
`$${Number(r.total_cost_usd).toFixed(4)}`,
|
|
37
|
+
String(r.total_input_tokens),
|
|
38
|
+
String(r.total_output_tokens),
|
|
39
|
+
`${r.efficiency_score}/100`,
|
|
40
|
+
String(r.loops_detected),
|
|
41
|
+
].map(v => v.replace(/\|/g, '\\|')).join(' | ');
|
|
42
|
+
}
|
|
26
43
|
function toRow(s) {
|
|
27
44
|
return {
|
|
28
45
|
id: s.id,
|
|
@@ -40,6 +57,10 @@ function runExport(opts) {
|
|
|
40
57
|
let fromMs;
|
|
41
58
|
let toMs;
|
|
42
59
|
try {
|
|
60
|
+
if (opts.since && !opts.from) {
|
|
61
|
+
const sinceMs = parseSince(opts.since);
|
|
62
|
+
opts.from = new Date(sinceMs).toISOString().slice(0, 10);
|
|
63
|
+
}
|
|
43
64
|
if (opts.from)
|
|
44
65
|
fromMs = parseDate(opts.from);
|
|
45
66
|
if (opts.to)
|
|
@@ -71,12 +92,26 @@ function runExport(opts) {
|
|
|
71
92
|
];
|
|
72
93
|
output = lines.join('\n') + '\n';
|
|
73
94
|
}
|
|
95
|
+
else if (opts.format === 'markdown') {
|
|
96
|
+
const separator = MD_HEADERS.map(() => '---').join(' | ');
|
|
97
|
+
const lines = [
|
|
98
|
+
MD_HEADERS.join(' | '),
|
|
99
|
+
separator,
|
|
100
|
+
...rows.map(toMarkdownRow),
|
|
101
|
+
];
|
|
102
|
+
output = lines.join('\n') + '\n';
|
|
103
|
+
}
|
|
74
104
|
else {
|
|
75
105
|
output = JSON.stringify(rows, null, 2) + '\n';
|
|
76
106
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
107
|
+
if (!opts.output) {
|
|
108
|
+
process.stdout.write(output);
|
|
109
|
+
console.error(`✓ Exported ${rows.length} session(s)`); // stderr para no contaminar stdout
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
const dest = path_1.default.resolve(opts.output);
|
|
113
|
+
fs_1.default.mkdirSync(path_1.default.dirname(dest), { recursive: true });
|
|
114
|
+
fs_1.default.writeFileSync(dest, output);
|
|
115
|
+
console.log(`✓ Exported ${rows.length} session(s) → ${dest}`);
|
|
116
|
+
}
|
|
82
117
|
}
|