ccusage-tracker 0.1.1 → 0.1.2
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 +67 -27
- package/package.json +2 -2
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.2",
|
|
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",
|