ccusage-tracker 0.1.1 → 0.1.3
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 +5 -4
- package/dist/index.js +67 -27
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
CLI for [ccusage-tracker](https://github.com/ericcai0814/ccusage-tracker) — a self-hosted Claude Code usage tracker for teams.
|
|
4
4
|
|
|
5
|
-
Install Claude Code SessionStart/SessionEnd hooks that report token usage to a self-hosted tracker server. Runs on macOS, Linux, and Windows.
|
|
5
|
+
Install Claude Code SessionStart/SessionEnd/Stop hooks that report token usage to a self-hosted tracker server. Runs on macOS, Linux, and Windows.
|
|
6
6
|
|
|
7
7
|
## Quick start
|
|
8
8
|
|
|
@@ -24,12 +24,13 @@ After installation, the binary is also available as `tracker` (if installed glob
|
|
|
24
24
|
|
|
25
25
|
## What it does
|
|
26
26
|
|
|
27
|
-
`setup` writes a config file to `~/.config/ccusage-tracker/config.json` and adds
|
|
27
|
+
`setup` writes a config file to `~/.config/ccusage-tracker/config.json` and adds three hooks to your Claude Code `~/.claude/settings.json`:
|
|
28
28
|
|
|
29
29
|
- **SessionStart** — records the model at session start
|
|
30
|
-
- **
|
|
30
|
+
- **Stop** — primary reporting path: POSTs token usage + session metrics after each assistant turn, throttled to once per 5 minutes
|
|
31
|
+
- **SessionEnd** — backup path: same payload at session exit, in case Stop missed the last window
|
|
31
32
|
|
|
32
|
-
Hook scripts are downloaded from your tracker server, so they always match the server version.
|
|
33
|
+
Hook scripts are downloaded from your tracker server, so they always match the server version. Re-running `setup` migrates old (no `--mode`) commands in-place — no manual cleanup needed.
|
|
33
34
|
|
|
34
35
|
## Requirements
|
|
35
36
|
|
package/dist/index.js
CHANGED
|
@@ -36,11 +36,18 @@ function writeConfig(config) {
|
|
|
36
36
|
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, copyFileSync, mkdirSync as mkdirSync2 } from "node:fs";
|
|
37
37
|
import { join as join2 } from "node:path";
|
|
38
38
|
import { homedir as homedir2 } from "node:os";
|
|
39
|
+
var HOOK_TIMEOUT_SEC = 25;
|
|
39
40
|
function getClaudeSettingsPath() {
|
|
40
41
|
return join2(homedir2(), ".claude", "settings.json");
|
|
41
42
|
}
|
|
43
|
+
function sessionEndScriptPath() {
|
|
44
|
+
return join2(homedir2(), ".config", "ccusage-tracker", "session-end.mjs");
|
|
45
|
+
}
|
|
42
46
|
function getHookCommand() {
|
|
43
|
-
return "node " +
|
|
47
|
+
return "node " + sessionEndScriptPath() + " --mode=session-end";
|
|
48
|
+
}
|
|
49
|
+
function getStopHookCommand() {
|
|
50
|
+
return "node " + sessionEndScriptPath() + " --mode=stop";
|
|
44
51
|
}
|
|
45
52
|
function getStartHookCommand() {
|
|
46
53
|
return "node " + join2(homedir2(), ".config", "ccusage-tracker", "session-start.mjs");
|
|
@@ -48,32 +55,62 @@ function getStartHookCommand() {
|
|
|
48
55
|
function isCcusageTrackerHook(command) {
|
|
49
56
|
return !!command && command.includes("ccusage-tracker");
|
|
50
57
|
}
|
|
51
|
-
function
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
function matcherEquivalent(a, b) {
|
|
59
|
+
const norm = (s) => s === "" ? "*" : s;
|
|
60
|
+
return norm(a) === norm(b);
|
|
61
|
+
}
|
|
62
|
+
function upsertHook(existing, command, opts = {}) {
|
|
63
|
+
const newEntry = { type: "command", command };
|
|
64
|
+
if (opts.timeout !== undefined)
|
|
65
|
+
newEntry.timeout = opts.timeout;
|
|
66
|
+
const newMatcher = { matcher: opts.matcher ?? "*", hooks: [newEntry] };
|
|
67
|
+
const filtered = existing.map((m) => ({
|
|
68
|
+
matcher: m.matcher,
|
|
69
|
+
hooks: (m.hooks ?? []).filter((h) => !isCcusageTrackerHook(h.command))
|
|
70
|
+
})).filter((m) => m.hooks.length > 0);
|
|
71
|
+
const next = [...filtered, newMatcher];
|
|
72
|
+
const changed = !sameMatchers(existing, next);
|
|
73
|
+
return { matchers: changed ? next : existing, changed };
|
|
74
|
+
}
|
|
75
|
+
function sameMatchers(a, b) {
|
|
76
|
+
if (a.length !== b.length)
|
|
77
|
+
return false;
|
|
78
|
+
for (let i = 0;i < a.length; i++) {
|
|
79
|
+
if (!matcherEquivalent(a[i].matcher, b[i].matcher))
|
|
80
|
+
return false;
|
|
81
|
+
const ha = a[i].hooks ?? [];
|
|
82
|
+
const hb = b[i].hooks ?? [];
|
|
83
|
+
if (ha.length !== hb.length)
|
|
84
|
+
return false;
|
|
85
|
+
for (let j = 0;j < ha.length; j++) {
|
|
86
|
+
if (ha[j].command !== hb[j].command)
|
|
87
|
+
return false;
|
|
88
|
+
if (ha[j].timeout !== hb[j].timeout)
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
55
91
|
}
|
|
56
|
-
|
|
57
|
-
matcher: "",
|
|
58
|
-
hooks: [{ type: "command", command }]
|
|
59
|
-
};
|
|
60
|
-
return { matchers: [...existing, newMatcher], installed: true };
|
|
92
|
+
return true;
|
|
61
93
|
}
|
|
62
94
|
function applyTrackerHooks(settings) {
|
|
63
|
-
const start =
|
|
64
|
-
const end =
|
|
65
|
-
const
|
|
95
|
+
const start = upsertHook(settings.hooks?.SessionStart ?? [], getStartHookCommand());
|
|
96
|
+
const end = upsertHook(settings.hooks?.SessionEnd ?? [], getHookCommand(), { timeout: HOOK_TIMEOUT_SEC });
|
|
97
|
+
const stop = upsertHook(settings.hooks?.Stop ?? [], getStopHookCommand(), { timeout: HOOK_TIMEOUT_SEC });
|
|
98
|
+
const anyChanged = start.changed || end.changed || stop.changed;
|
|
99
|
+
const updated = anyChanged ? {
|
|
66
100
|
...settings,
|
|
67
101
|
hooks: {
|
|
68
102
|
...settings.hooks,
|
|
69
103
|
SessionStart: start.matchers,
|
|
70
|
-
SessionEnd: end.matchers
|
|
104
|
+
SessionEnd: end.matchers,
|
|
105
|
+
Stop: stop.matchers
|
|
71
106
|
}
|
|
72
107
|
} : settings;
|
|
73
108
|
return {
|
|
74
109
|
updated,
|
|
75
|
-
|
|
76
|
-
|
|
110
|
+
sessionStartChanged: start.changed,
|
|
111
|
+
sessionEndChanged: end.changed,
|
|
112
|
+
stopChanged: stop.changed,
|
|
113
|
+
anyChanged
|
|
77
114
|
};
|
|
78
115
|
}
|
|
79
116
|
function installHook(scripts) {
|
|
@@ -83,20 +120,21 @@ function installHook(scripts) {
|
|
|
83
120
|
if (existsSync2(settingsPath)) {
|
|
84
121
|
const raw = readFileSync2(settingsPath, "utf-8");
|
|
85
122
|
settings = JSON.parse(raw);
|
|
86
|
-
const backupPath = settingsPath + ".backup";
|
|
87
|
-
copyFileSync(settingsPath, backupPath);
|
|
88
|
-
backedUp = true;
|
|
89
123
|
}
|
|
90
124
|
const destDir = join2(homedir2(), ".config", "ccusage-tracker");
|
|
91
125
|
mkdirSync2(destDir, { recursive: true });
|
|
92
126
|
writeFileSync2(join2(destDir, "session-end.mjs"), scripts.sessionEnd);
|
|
93
127
|
writeFileSync2(join2(destDir, "session-start.mjs"), scripts.sessionStart);
|
|
94
|
-
const { updated,
|
|
95
|
-
if (
|
|
128
|
+
const { updated, sessionStartChanged, sessionEndChanged, stopChanged, anyChanged } = applyTrackerHooks(settings);
|
|
129
|
+
if (anyChanged) {
|
|
130
|
+
if (existsSync2(settingsPath)) {
|
|
131
|
+
copyFileSync(settingsPath, settingsPath + ".backup");
|
|
132
|
+
backedUp = true;
|
|
133
|
+
}
|
|
96
134
|
writeFileSync2(settingsPath, JSON.stringify(updated, null, 2) + `
|
|
97
135
|
`);
|
|
98
136
|
}
|
|
99
|
-
return {
|
|
137
|
+
return { sessionEndChanged, sessionStartChanged, stopChanged, backedUp };
|
|
100
138
|
}
|
|
101
139
|
function isHookInstalled() {
|
|
102
140
|
const settingsPath = getClaudeSettingsPath();
|
|
@@ -104,7 +142,8 @@ function isHookInstalled() {
|
|
|
104
142
|
return false;
|
|
105
143
|
try {
|
|
106
144
|
const settings = JSON.parse(readFileSync2(settingsPath, "utf-8"));
|
|
107
|
-
|
|
145
|
+
const hasIn = (arr) => arr?.some((m) => m.hooks?.some((h) => isCcusageTrackerHook(h.command))) ?? false;
|
|
146
|
+
return hasIn(settings.hooks?.Stop) || hasIn(settings.hooks?.SessionEnd);
|
|
108
147
|
} catch {
|
|
109
148
|
return false;
|
|
110
149
|
}
|
|
@@ -210,14 +249,15 @@ Config saved.`);
|
|
|
210
249
|
]);
|
|
211
250
|
if (sessionEnd && sessionStart) {
|
|
212
251
|
try {
|
|
213
|
-
const {
|
|
252
|
+
const { sessionEndChanged, sessionStartChanged, stopChanged, backedUp } = deps.installHook({
|
|
214
253
|
sessionEnd,
|
|
215
254
|
sessionStart
|
|
216
255
|
});
|
|
217
|
-
|
|
218
|
-
|
|
256
|
+
const anyChanged = sessionEndChanged || sessionStartChanged || stopChanged;
|
|
257
|
+
if (anyChanged) {
|
|
258
|
+
deps.log("SessionStart + SessionEnd + Stop hooks installed/updated." + (backedUp ? " (settings.json backed up)" : ""));
|
|
219
259
|
} else {
|
|
220
|
-
deps.log("SessionStart + SessionEnd hooks already
|
|
260
|
+
deps.log("SessionStart + SessionEnd + Stop hooks already up to date.");
|
|
221
261
|
}
|
|
222
262
|
} catch (err) {
|
|
223
263
|
deps.warn("Warning: Could not install hooks automatically. " + err.message);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccusage-tracker",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "CLI for ccusage-tracker — install Claude Code SessionStart/SessionEnd hooks to report token usage to a self-hosted team tracker.",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "CLI for ccusage-tracker — install Claude Code SessionStart/SessionEnd/Stop hooks to report token usage to a self-hosted team tracker.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"ccusage-tracker": "dist/index.js",
|