ccusage-tracker 0.1.0 → 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 +91 -35
- 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,21 +142,31 @@ 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
|
}
|
|
111
150
|
}
|
|
112
151
|
|
|
113
152
|
// src/commands/setup.ts
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
153
|
+
var sharedRl = null;
|
|
154
|
+
var lineIterator = null;
|
|
155
|
+
async function defaultPrompt(question) {
|
|
156
|
+
if (!sharedRl || !lineIterator) {
|
|
157
|
+
sharedRl = createInterface({ input: process.stdin, output: process.stdout });
|
|
158
|
+
lineIterator = sharedRl[Symbol.asyncIterator]();
|
|
159
|
+
}
|
|
160
|
+
process.stdout.write(question);
|
|
161
|
+
const { value, done } = await lineIterator.next();
|
|
162
|
+
return done ? "" : value.trim();
|
|
163
|
+
}
|
|
164
|
+
function closeSharedPrompt() {
|
|
165
|
+
if (sharedRl) {
|
|
166
|
+
sharedRl.close();
|
|
167
|
+
sharedRl = null;
|
|
168
|
+
lineIterator = null;
|
|
169
|
+
}
|
|
122
170
|
}
|
|
123
171
|
async function defaultCheckServer(serverUrl) {
|
|
124
172
|
try {
|
|
@@ -160,6 +208,13 @@ var defaultDeps = {
|
|
|
160
208
|
};
|
|
161
209
|
async function setupCommand(overrides) {
|
|
162
210
|
const deps = { ...defaultDeps, ...overrides };
|
|
211
|
+
try {
|
|
212
|
+
await runSetup(deps);
|
|
213
|
+
} finally {
|
|
214
|
+
closeSharedPrompt();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async function runSetup(deps) {
|
|
163
218
|
deps.log(`ccusage-tracker setup
|
|
164
219
|
`);
|
|
165
220
|
const name = await deps.prompt("Your name: ");
|
|
@@ -194,14 +249,15 @@ Config saved.`);
|
|
|
194
249
|
]);
|
|
195
250
|
if (sessionEnd && sessionStart) {
|
|
196
251
|
try {
|
|
197
|
-
const {
|
|
252
|
+
const { sessionEndChanged, sessionStartChanged, stopChanged, backedUp } = deps.installHook({
|
|
198
253
|
sessionEnd,
|
|
199
254
|
sessionStart
|
|
200
255
|
});
|
|
201
|
-
|
|
202
|
-
|
|
256
|
+
const anyChanged = sessionEndChanged || sessionStartChanged || stopChanged;
|
|
257
|
+
if (anyChanged) {
|
|
258
|
+
deps.log("SessionStart + SessionEnd + Stop hooks installed/updated." + (backedUp ? " (settings.json backed up)" : ""));
|
|
203
259
|
} else {
|
|
204
|
-
deps.log("SessionStart + SessionEnd hooks already
|
|
260
|
+
deps.log("SessionStart + SessionEnd + Stop hooks already up to date.");
|
|
205
261
|
}
|
|
206
262
|
} catch (err) {
|
|
207
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",
|