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.
Files changed (2) hide show
  1. package/dist/index.js +91 -35
  2. 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 " + join2(homedir2(), ".config", "ccusage-tracker", "session-end.mjs");
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 appendHookIfMissing(existing, command) {
52
- const present = existing.some((m) => m.hooks?.some((h) => isCcusageTrackerHook(h.command)));
53
- if (present) {
54
- return { matchers: existing, installed: false };
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
- const newMatcher = {
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 = appendHookIfMissing(settings.hooks?.SessionStart ?? [], getStartHookCommand());
64
- const end = appendHookIfMissing(settings.hooks?.SessionEnd ?? [], getHookCommand());
65
- const updated = start.installed || end.installed ? {
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
- sessionStartInstalled: start.installed,
76
- sessionEndInstalled: end.installed
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, sessionStartInstalled, sessionEndInstalled } = applyTrackerHooks(settings);
95
- if (sessionStartInstalled || sessionEndInstalled) {
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 { sessionEndInstalled, sessionStartInstalled, backedUp };
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
- return settings.hooks?.SessionEnd?.some((m) => m.hooks?.some((h) => isCcusageTrackerHook(h.command))) ?? false;
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
- function defaultPrompt(question) {
115
- const rl = createInterface({ input: process.stdin, output: process.stdout });
116
- return new Promise((resolve) => {
117
- rl.question(question, (answer) => {
118
- rl.close();
119
- resolve(answer.trim());
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 { sessionEndInstalled, sessionStartInstalled, backedUp } = deps.installHook({
252
+ const { sessionEndChanged, sessionStartChanged, stopChanged, backedUp } = deps.installHook({
198
253
  sessionEnd,
199
254
  sessionStart
200
255
  });
201
- if (sessionEndInstalled || sessionStartInstalled) {
202
- deps.log("SessionStart + SessionEnd hooks installed." + (backedUp ? " (settings.json backed up)" : ""));
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 installed.");
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.0",
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",