@jv-k/claude-gauge 0.0.1 → 1.0.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 +622 -3
- package/dist/cli.js +395 -0
- package/dist/launcher.js +78 -0
- package/dist/settings-file.js +137 -0
- package/dist/settings.js +257 -0
- package/dist/statusline.js +2304 -0
- package/dist/tokenline.js +293 -0
- package/dist/wizard.js +264 -0
- package/package.json +26 -4
package/dist/settings.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The settings writer's pure half: plan() takes the settings Claude Code
|
|
3
|
+
// reads and the user's choices, and returns the next settings, the status
|
|
4
|
+
// line to save for uninstall, and what it found. It reads no file and writes
|
|
5
|
+
// none; settings-file.ts and the CLI do that.
|
|
6
|
+
//
|
|
7
|
+
// claude-gauge owns at most three things in settings.json: `statusLine` when
|
|
8
|
+
// it runs claude-gauge's status line, its token line entry in `hooks.Stop`,
|
|
9
|
+
// and the `--instruct` entries in `hooks.SessionStart`. plan() changes only
|
|
10
|
+
// those, and keeps every other key, including the status line's
|
|
11
|
+
// `refreshInterval`.
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.isForeign = exports.scriptOf = void 0;
|
|
14
|
+
exports.plan = plan;
|
|
15
|
+
exports.commandFor = commandFor;
|
|
16
|
+
exports.switchesOf = switchesOf;
|
|
17
|
+
exports.installed = installed;
|
|
18
|
+
exports.installedSwitches = installedSwitches;
|
|
19
|
+
exports.ownerOf = ownerOf;
|
|
20
|
+
// A command that runs one of claude-gauge's scripts, wherever the copy lives:
|
|
21
|
+
// ~/.claude/claude-gauge/dist/, its runtime/ copy, the scripts from before
|
|
22
|
+
// they moved into dist/, or a Windows path to any of them. The folder name
|
|
23
|
+
// must be a whole path component and the script name must end the word, so
|
|
24
|
+
// not-claude-gauge/ or statusline.js.old is someone else's.
|
|
25
|
+
const SCRIPT = /(?<![^\s'"\\/])claude-gauge[\\/](?:[^\s'"]*[\\/])?(statusline|tokenline)\.js(?![^\s'"])/;
|
|
26
|
+
const scriptOf = (command) => (typeof command === 'string' ? SCRIPT.exec(command)?.[1] : undefined);
|
|
27
|
+
exports.scriptOf = scriptOf;
|
|
28
|
+
const isInstruct = (command) => typeof command === 'string' && /(?:^|\s)--instruct\b/.test(command);
|
|
29
|
+
// The token line's Stop hook, as opposed to its --instruct SessionStart one.
|
|
30
|
+
const isTokenLine = (h) => scriptOf(h.command) === 'tokenline' && !isInstruct(h.command);
|
|
31
|
+
const isOurs = (h) => scriptOf(h.command) !== undefined;
|
|
32
|
+
// Whether writing claude-gauge's status line over this owner's replaces
|
|
33
|
+
// someone else's, which needs the user's consent.
|
|
34
|
+
const isForeign = (owner) => owner === 'other' || owner === 'claude-hud';
|
|
35
|
+
exports.isForeign = isForeign;
|
|
36
|
+
function ownerOf(statusLine) {
|
|
37
|
+
if (statusLine === undefined || statusLine === null)
|
|
38
|
+
return 'none';
|
|
39
|
+
const command = statusLine.command;
|
|
40
|
+
if (scriptOf(command) === 'statusline')
|
|
41
|
+
return 'claude-gauge';
|
|
42
|
+
if (typeof command === 'string' && /claude-hud/i.test(command))
|
|
43
|
+
return 'claude-hud';
|
|
44
|
+
return 'other';
|
|
45
|
+
}
|
|
46
|
+
const isObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
47
|
+
// The settings' hooks, checked as far as plan() reads them. A shape it does
|
|
48
|
+
// not know stops the plan, because writing over it would lose the user's
|
|
49
|
+
// hooks.
|
|
50
|
+
function readHooks(settings) {
|
|
51
|
+
const hooks = settings.hooks;
|
|
52
|
+
if (hooks === undefined)
|
|
53
|
+
return undefined;
|
|
54
|
+
if (!isObject(hooks))
|
|
55
|
+
throw new Error('settings.json: hooks is not an object, so claude-gauge leaves it alone. Fix it by hand.');
|
|
56
|
+
for (const [event, entries] of Object.entries(hooks)) {
|
|
57
|
+
const readable = (e) => isObject(e) && (e.hooks === undefined || (Array.isArray(e.hooks) && e.hooks.every(isObject)));
|
|
58
|
+
if (!Array.isArray(entries) || !entries.every(readable)) {
|
|
59
|
+
throw new Error(`settings.json: hooks.${event} is not a list of hook entries, so claude-gauge leaves it alone. Fix it by hand.`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return hooks;
|
|
63
|
+
}
|
|
64
|
+
// Takes the hook commands that `drop` matches out of `event`, then any entry
|
|
65
|
+
// and event that this left empty. Lists that were empty before stay.
|
|
66
|
+
function removeHooks(hooks, event, drop) {
|
|
67
|
+
const entries = hooks[event];
|
|
68
|
+
if (!entries)
|
|
69
|
+
return;
|
|
70
|
+
let removed = false;
|
|
71
|
+
const kept = [];
|
|
72
|
+
for (const e of entries) {
|
|
73
|
+
const commands = e.hooks ?? [];
|
|
74
|
+
const left = commands.filter((h) => !drop(h));
|
|
75
|
+
if (left.length === commands.length) {
|
|
76
|
+
kept.push(e);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
removed = true;
|
|
80
|
+
if (left.length)
|
|
81
|
+
kept.push({ ...e, hooks: left });
|
|
82
|
+
}
|
|
83
|
+
if (!removed)
|
|
84
|
+
return;
|
|
85
|
+
if (kept.length)
|
|
86
|
+
hooks[event] = kept;
|
|
87
|
+
else
|
|
88
|
+
delete hooks[event];
|
|
89
|
+
}
|
|
90
|
+
// Points the token line's Stop hook at `command`: the first existing one is
|
|
91
|
+
// updated in place, any others are removed, and with none an entry is added
|
|
92
|
+
// at the end.
|
|
93
|
+
function setTokenLine(hooks, command) {
|
|
94
|
+
const stop = hooks.Stop ?? [];
|
|
95
|
+
let done = false;
|
|
96
|
+
const next = [];
|
|
97
|
+
for (const e of stop) {
|
|
98
|
+
const commands = e.hooks ?? [];
|
|
99
|
+
if (!commands.some(isTokenLine)) {
|
|
100
|
+
next.push(e);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const left = [];
|
|
104
|
+
for (const h of commands) {
|
|
105
|
+
if (!isTokenLine(h))
|
|
106
|
+
left.push(h);
|
|
107
|
+
else if (!done) {
|
|
108
|
+
left.push({ ...h, type: 'command', command });
|
|
109
|
+
done = true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (left.length)
|
|
113
|
+
next.push({ ...e, hooks: left });
|
|
114
|
+
}
|
|
115
|
+
if (!done)
|
|
116
|
+
next.push({ hooks: [{ type: 'command', command }] });
|
|
117
|
+
hooks.Stop = next;
|
|
118
|
+
}
|
|
119
|
+
function plan(current, choices) {
|
|
120
|
+
const settings = JSON.parse(JSON.stringify(current ?? {}));
|
|
121
|
+
const found = ownerOf(settings.statusLine);
|
|
122
|
+
if (settings.statusLine === null)
|
|
123
|
+
delete settings.statusLine;
|
|
124
|
+
if (settings.statusLine !== undefined && !isObject(settings.statusLine)) {
|
|
125
|
+
throw new Error('settings.json: statusLine is not an object, so claude-gauge leaves it alone. Fix it by hand.');
|
|
126
|
+
}
|
|
127
|
+
const hooks = readHooks(settings);
|
|
128
|
+
const statusChoice = choices.uninstall ? null : choices.statusLine;
|
|
129
|
+
const tokenChoice = choices.uninstall ? null : choices.tokenLine;
|
|
130
|
+
const takeOver = typeof statusChoice === 'string' && isForeign(found);
|
|
131
|
+
if (takeOver && !choices.replace) {
|
|
132
|
+
return { settings, dropBackup: false, found, blocked: true, changed: false };
|
|
133
|
+
}
|
|
134
|
+
let backup;
|
|
135
|
+
if (typeof statusChoice === 'string') {
|
|
136
|
+
if (found !== 'claude-gauge') {
|
|
137
|
+
backup = { statusLine: settings.statusLine ?? null };
|
|
138
|
+
}
|
|
139
|
+
settings.statusLine = { ...settings.statusLine, type: 'command', command: statusChoice };
|
|
140
|
+
}
|
|
141
|
+
else if (statusChoice === null && found === 'claude-gauge') {
|
|
142
|
+
if (choices.previous)
|
|
143
|
+
settings.statusLine = choices.previous;
|
|
144
|
+
else
|
|
145
|
+
delete settings.statusLine;
|
|
146
|
+
}
|
|
147
|
+
if (tokenChoice !== undefined || choices.uninstall) {
|
|
148
|
+
const next = hooks ?? {};
|
|
149
|
+
if (typeof tokenChoice === 'string')
|
|
150
|
+
setTokenLine(next, tokenChoice);
|
|
151
|
+
else if (choices.uninstall)
|
|
152
|
+
for (const event of Object.keys(next))
|
|
153
|
+
removeHooks(next, event, isOurs);
|
|
154
|
+
else
|
|
155
|
+
removeHooks(next, 'Stop', isTokenLine);
|
|
156
|
+
// A hooks object that held only claude-gauge's entries goes with them.
|
|
157
|
+
// One that was absent, or empty already, stays as it was.
|
|
158
|
+
const hadHooks = isObject(current?.hooks) && Object.keys(current.hooks).length > 0;
|
|
159
|
+
if (Object.keys(next).length)
|
|
160
|
+
settings.hooks = next;
|
|
161
|
+
else if (hadHooks)
|
|
162
|
+
delete settings.hooks;
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
settings,
|
|
166
|
+
...(backup ? { backup } : {}),
|
|
167
|
+
dropBackup: statusChoice === null,
|
|
168
|
+
found,
|
|
169
|
+
blocked: false,
|
|
170
|
+
changed: JSON.stringify(settings) !== JSON.stringify(current ?? {}),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
// claude-gauge's commands in the settings: the status line, the token
|
|
174
|
+
// line's Stop hook, and whether any hook in any event runs one of its
|
|
175
|
+
// scripts. Hooks it cannot read stop it, as they stop plan().
|
|
176
|
+
function installed(settings) {
|
|
177
|
+
const statusLine = ownerOf(settings.statusLine) === 'claude-gauge' ? settings.statusLine.command : undefined;
|
|
178
|
+
const hooks = readHooks(settings) ?? {};
|
|
179
|
+
const commandsIn = (entries) => entries.flatMap((e) => e.hooks ?? []);
|
|
180
|
+
const tokenLine = commandsIn(hooks.Stop ?? []).find(isTokenLine)?.command;
|
|
181
|
+
const anyHook = Object.values(hooks).some((entries) => commandsIn(entries).some(isOurs));
|
|
182
|
+
return { statusLine, tokenLine, any: statusLine !== undefined || anyHook };
|
|
183
|
+
}
|
|
184
|
+
function installedSwitches(settings) {
|
|
185
|
+
const { statusLine, tokenLine } = installed(settings);
|
|
186
|
+
return {
|
|
187
|
+
...(statusLine === undefined ? {} : { statusLine: switchesOf(statusLine) }),
|
|
188
|
+
...(tokenLine === undefined ? {} : { tokenLine: switchesOf(tokenLine) }),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
// A shell word for a settings command: as is when plain, in double quotes
|
|
192
|
+
// when it holds only spaces or other characters both bash and cmd.exe read
|
|
193
|
+
// literally there, else in single quotes. Claude Code runs the command
|
|
194
|
+
// through a shell on every OS.
|
|
195
|
+
const shellWord = (s) => /^[\w@%+=:,./~-]+$/.test(s) ? s : /^[^"$`\\!']*$/.test(s) ? `"${s}"` : `'${s.replace(/'/g, `'\\''`)}'`;
|
|
196
|
+
// The command that runs `script` with the switches the user chose: one
|
|
197
|
+
// string split at spaces, `--show ctx,5h,7d --segments 10`, or the words
|
|
198
|
+
// themselves, which keeps a value that holds a space. Each word is quoted on
|
|
199
|
+
// its own, so nothing in it can run as a second command.
|
|
200
|
+
function commandFor(script, switches = '') {
|
|
201
|
+
const words = typeof switches === 'string' ? switches.split(/\s+/).filter(Boolean) : switches;
|
|
202
|
+
return ['node', script.replace(/\\/g, '/'), ...words].map(shellWord).join(' ');
|
|
203
|
+
}
|
|
204
|
+
// The words of a command as the shell splits them, which undoes shellWord:
|
|
205
|
+
// single quotes keep everything, double quotes keep all but an escaped " \ $
|
|
206
|
+
// or `, and a backslash outside quotes keeps the next character. Each word
|
|
207
|
+
// keeps its text as written too, `raw`, where a Windows path still has its
|
|
208
|
+
// backslashes.
|
|
209
|
+
function shellWords(command) {
|
|
210
|
+
const words = [];
|
|
211
|
+
let value = '';
|
|
212
|
+
let start = -1;
|
|
213
|
+
let quote = null;
|
|
214
|
+
for (let i = 0; i < command.length; i++) {
|
|
215
|
+
const c = command[i];
|
|
216
|
+
if (quote === "'") {
|
|
217
|
+
if (c === "'")
|
|
218
|
+
quote = null;
|
|
219
|
+
else
|
|
220
|
+
value += c;
|
|
221
|
+
}
|
|
222
|
+
else if (quote === '"') {
|
|
223
|
+
if (c === '"')
|
|
224
|
+
quote = null;
|
|
225
|
+
else if (c === '\\' && i + 1 < command.length && '"\\$`'.includes(command[i + 1]))
|
|
226
|
+
value += command[++i];
|
|
227
|
+
else
|
|
228
|
+
value += c;
|
|
229
|
+
}
|
|
230
|
+
else if (/\s/.test(c)) {
|
|
231
|
+
if (start >= 0)
|
|
232
|
+
words.push({ value, raw: command.slice(start, i) });
|
|
233
|
+
value = '';
|
|
234
|
+
start = -1;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
if (start < 0)
|
|
238
|
+
start = i;
|
|
239
|
+
if (c === "'" || c === '"')
|
|
240
|
+
quote = c;
|
|
241
|
+
else if (c === '\\' && i + 1 < command.length)
|
|
242
|
+
value += command[++i];
|
|
243
|
+
else
|
|
244
|
+
value += c;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (start >= 0)
|
|
248
|
+
words.push({ value, raw: command.slice(start) });
|
|
249
|
+
return words;
|
|
250
|
+
}
|
|
251
|
+
// The switches a claude-gauge command runs its script with, as words: the
|
|
252
|
+
// inverse of commandFor. None when the command runs no claude-gauge script.
|
|
253
|
+
function switchesOf(command) {
|
|
254
|
+
const words = shellWords(command);
|
|
255
|
+
const at = words.findIndex((w) => scriptOf(w.raw) !== undefined);
|
|
256
|
+
return at < 0 ? [] : words.slice(at + 1).map((w) => w.value);
|
|
257
|
+
}
|