@delt/claude-jev-advisor 0.1.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/dist/cli.js ADDED
@@ -0,0 +1,642 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import os from "os";
5
+ import path8 from "path";
6
+ import { fileURLToPath } from "url";
7
+
8
+ // src/cli/run.ts
9
+ import path7 from "path";
10
+
11
+ // src/features.ts
12
+ var FEATURES = ["rm", "context"];
13
+ var HOOK_SPECS = {
14
+ rm: [{ event: "PreToolUse", matcher: "Bash", script: "rm-hook.js", args: [] }],
15
+ context: [
16
+ { event: "Stop", script: "context-hook.js", args: ["stop"] },
17
+ { event: "SessionEnd", script: "context-hook.js", args: ["session-end"] }
18
+ ]
19
+ };
20
+ var SUPPORTED_PLATFORMS = {
21
+ rm: ["win32"],
22
+ context: null
23
+ };
24
+ var HOOK_TIMEOUT_SECONDS = 15;
25
+ var LEGACY_RM_GUARD = /\/\.claude\/hooks\/rm-guard\/rm-guard\.mjs/;
26
+ var STATUSLINE_SCRIPT = "statusline.js";
27
+ var MOD_PLUGIN_NAME = "jev-advisor";
28
+ var MOD_DIR_PATTERN = /claude-jev-advisor\/mod\/?$/;
29
+ function ownScriptPattern(script) {
30
+ return new RegExp(`claude-jev-advisor/dist/${script.replace(/\./g, "\\.")}`);
31
+ }
32
+ function isFeature(value) {
33
+ return FEATURES.includes(value);
34
+ }
35
+
36
+ // src/install.ts
37
+ import path5 from "path";
38
+
39
+ // src/config.ts
40
+ import fs from "fs";
41
+ import path2 from "path";
42
+
43
+ // src/config-shape.ts
44
+ var DEFAULT_CONFIG = {
45
+ lang: "ko",
46
+ keyFile: null,
47
+ display: "mod",
48
+ context: { enabled: true, minTokens: 1e5, compactMinTokens: 2e5, redRemainingPct: 20, unitDoneYes: 0.7, goalDoneYes: 0.8 },
49
+ rm: { enabled: true, jev: true, throwawayYes: 0.8, maxDirFiles: 50 }
50
+ };
51
+ var LANGS = ["ko", "en"];
52
+ var DISPLAYS = ["mod", "statusline", "message"];
53
+ function isRecord(value) {
54
+ return typeof value === "object" && value !== null && !Array.isArray(value);
55
+ }
56
+ function mergeSection(defaults, raw) {
57
+ const out = { ...defaults };
58
+ if (!isRecord(raw)) return out;
59
+ for (const [key, value] of Object.entries(defaults)) {
60
+ if (typeof raw[key] === typeof value) out[key] = raw[key];
61
+ }
62
+ return out;
63
+ }
64
+ function normalizeConfig(raw) {
65
+ const r = isRecord(raw) ? raw : {};
66
+ return {
67
+ lang: typeof r.lang === "string" && LANGS.includes(r.lang) ? r.lang : DEFAULT_CONFIG.lang,
68
+ keyFile: typeof r.keyFile === "string" && r.keyFile !== "" ? r.keyFile : null,
69
+ display: typeof r.display === "string" && DISPLAYS.includes(r.display) ? r.display : DEFAULT_CONFIG.display,
70
+ context: mergeSection(DEFAULT_CONFIG.context, r.context),
71
+ rm: mergeSection(DEFAULT_CONFIG.rm, r.rm)
72
+ };
73
+ }
74
+
75
+ // src/paths.ts
76
+ import path from "path";
77
+ var claudeDir = (home) => path.join(home, ".claude");
78
+ var dataDir = (home) => path.join(claudeDir(home), "claude-jev-advisor");
79
+ var configPath = (home) => path.join(dataDir(home), "config.json");
80
+ var settingsPath = (home) => path.join(claudeDir(home), "settings.json");
81
+ var backupsDir = (home) => path.join(claudeDir(home), "backups");
82
+ var stateDir = (home) => path.join(dataDir(home), "state");
83
+ var statusLineBeforePath = (home) => path.join(dataDir(home), "statusline-before.json");
84
+
85
+ // src/config.ts
86
+ function readConfig(home) {
87
+ try {
88
+ return normalizeConfig(JSON.parse(fs.readFileSync(configPath(home), "utf8").replace(/^\uFEFF/, "")));
89
+ } catch {
90
+ return normalizeConfig({});
91
+ }
92
+ }
93
+ function writeConfig(home, config) {
94
+ const file = configPath(home);
95
+ fs.mkdirSync(path2.dirname(file), { recursive: true });
96
+ fs.writeFileSync(file, `${JSON.stringify(config, null, 2)}
97
+ `);
98
+ }
99
+ function updateConfig(home, change) {
100
+ const next = change(readConfig(home));
101
+ writeConfig(home, next);
102
+ return next;
103
+ }
104
+
105
+ // src/display/settings.ts
106
+ import fs2 from "fs";
107
+ import path3 from "path";
108
+
109
+ // src/settings.ts
110
+ var forwardSlashes = (text) => text.replace(/\\/g, "/");
111
+ function hookCommand(scriptPath, ...args) {
112
+ return [`node "${forwardSlashes(scriptPath)}"`, ...args].join(" ");
113
+ }
114
+ function commandMatches(command, pattern) {
115
+ return pattern.test(forwardSlashes(command));
116
+ }
117
+ function withHook(settings, entry) {
118
+ const group = {
119
+ ...entry.matcher === void 0 ? {} : { matcher: entry.matcher },
120
+ hooks: [{ type: "command", command: entry.command, timeout: entry.timeout }]
121
+ };
122
+ const hooks = { ...settings.hooks ?? {} };
123
+ hooks[entry.event] = [...hooks[entry.event] ?? [], group];
124
+ return { ...settings, hooks };
125
+ }
126
+ function withoutCommands(settings, pattern) {
127
+ if (!settings.hooks) return settings;
128
+ const isOurs = (h) => typeof h.command === "string" && commandMatches(h.command, pattern);
129
+ const hooks = {};
130
+ let removedAny = false;
131
+ for (const [event, groups] of Object.entries(settings.hooks)) {
132
+ const kept = [];
133
+ let emptiedByUs = false;
134
+ for (const group of groups) {
135
+ if (!Array.isArray(group.hooks) || !group.hooks.some(isOurs)) {
136
+ kept.push(group);
137
+ continue;
138
+ }
139
+ removedAny = true;
140
+ const remaining = group.hooks.filter((h) => !isOurs(h));
141
+ if (remaining.length) kept.push({ ...group, hooks: remaining });
142
+ else emptiedByUs = true;
143
+ }
144
+ if (kept.length || !emptiedByUs) hooks[event] = kept;
145
+ }
146
+ if (!removedAny) return settings;
147
+ if (!Object.keys(hooks).length) {
148
+ const { hooks: _removed, ...rest } = settings;
149
+ return rest;
150
+ }
151
+ return { ...settings, hooks };
152
+ }
153
+ function findCommands(settings, pattern) {
154
+ const found = [];
155
+ for (const [event, groups] of Object.entries(settings.hooks ?? {})) {
156
+ for (const group of groups) {
157
+ if (!Array.isArray(group.hooks)) continue;
158
+ for (const hook of group.hooks) {
159
+ if (typeof hook.command === "string" && commandMatches(hook.command, pattern)) found.push({ event, matcher: group.matcher, command: hook.command });
160
+ }
161
+ }
162
+ }
163
+ return found;
164
+ }
165
+
166
+ // src/display/settings.ts
167
+ var PLUGIN_DIRS_ENV = "CLAUDE_CODE_PLUGIN_DIRS";
168
+ var STATUSLINE_REFRESH_SECONDS = 3;
169
+ function isRecord2(value) {
170
+ return typeof value === "object" && value !== null && !Array.isArray(value);
171
+ }
172
+ var forwardSlashes2 = (text) => text.replace(/\\/g, "/");
173
+ function withoutKey(settings, key) {
174
+ const { [key]: _removed, ...rest } = settings;
175
+ return rest;
176
+ }
177
+ function withoutModDisplay(settings, delimiter) {
178
+ let next = settings;
179
+ const env = isRecord2(settings.env) ? settings.env : null;
180
+ const dirs = env?.[PLUGIN_DIRS_ENV];
181
+ if (env && typeof dirs === "string") {
182
+ const all = dirs.split(delimiter);
183
+ const kept = all.filter((dir) => !MOD_DIR_PATTERN.test(forwardSlashes2(dir)));
184
+ if (kept.length !== all.length) {
185
+ const { [PLUGIN_DIRS_ENV]: _ours, ...otherEnv } = env;
186
+ const nextEnv = kept.length ? { ...env, [PLUGIN_DIRS_ENV]: kept.join(delimiter) } : otherEnv;
187
+ next = Object.keys(nextEnv).length ? { ...next, env: nextEnv } : withoutKey(next, "env");
188
+ }
189
+ }
190
+ const configs = isRecord2(next.pluginConfigs) ? next.pluginConfigs : null;
191
+ if (configs && MOD_PLUGIN_NAME in configs) {
192
+ const { [MOD_PLUGIN_NAME]: _ours, ...others } = configs;
193
+ next = Object.keys(others).length ? { ...next, pluginConfigs: others } : withoutKey(next, "pluginConfigs");
194
+ }
195
+ return next;
196
+ }
197
+ function withModDisplay(settings, modDir, dataDirPath, delimiter) {
198
+ const base = withoutModDisplay(settings, delimiter);
199
+ const env = isRecord2(base.env) ? base.env : {};
200
+ const dirs = typeof env[PLUGIN_DIRS_ENV] === "string" && env[PLUGIN_DIRS_ENV] !== "" ? `${env[PLUGIN_DIRS_ENV]}${delimiter}` : "";
201
+ const configs = isRecord2(base.pluginConfigs) ? base.pluginConfigs : {};
202
+ return {
203
+ ...base,
204
+ env: { ...env, [PLUGIN_DIRS_ENV]: `${dirs}${forwardSlashes2(modDir)}` },
205
+ pluginConfigs: { ...configs, [MOD_PLUGIN_NAME]: { options: { dataDir: forwardSlashes2(dataDirPath) } } }
206
+ };
207
+ }
208
+ function isOurStatusLine(value) {
209
+ return isRecord2(value) && typeof value.command === "string" && commandMatches(value.command, ownScriptPattern(STATUSLINE_SCRIPT));
210
+ }
211
+ function readStatusLineBefore(home) {
212
+ try {
213
+ const value = JSON.parse(fs2.readFileSync(statusLineBeforePath(home), "utf8"));
214
+ return isRecord2(value) ? value : null;
215
+ } catch {
216
+ return null;
217
+ }
218
+ }
219
+ function writeStatusLineBefore(home, value) {
220
+ const file = statusLineBeforePath(home);
221
+ if (!value) {
222
+ fs2.rmSync(file, { force: true });
223
+ return;
224
+ }
225
+ fs2.mkdirSync(path3.dirname(file), { recursive: true });
226
+ fs2.writeFileSync(file, `${JSON.stringify(value, null, 2)}
227
+ `);
228
+ }
229
+ function applyStatusLineDisplay(settings, home, command) {
230
+ const current = settings.statusLine;
231
+ const ours = isOurStatusLine(current);
232
+ const userLine = ours ? readStatusLineBefore(home) : isRecord2(current) ? current : null;
233
+ if (command !== null) {
234
+ writeStatusLineBefore(home, userLine);
235
+ return { ...settings, statusLine: { type: "command", command, refreshInterval: STATUSLINE_REFRESH_SECONDS } };
236
+ }
237
+ if (!ours) return settings;
238
+ return userLine ? { ...settings, statusLine: userLine } : withoutKey(settings, "statusLine");
239
+ }
240
+ function forgetStatusLineBefore(home, settings) {
241
+ if (!isOurStatusLine(settings.statusLine)) writeStatusLineBefore(home, null);
242
+ }
243
+
244
+ // src/settings-file.ts
245
+ import fs3 from "fs";
246
+ import path4 from "path";
247
+ function readSettingsFile(file) {
248
+ let text;
249
+ try {
250
+ text = fs3.readFileSync(file, "utf8");
251
+ } catch (err) {
252
+ if (err.code === "ENOENT") return {};
253
+ throw err;
254
+ }
255
+ const parsed = JSON.parse(text.replace(/^\uFEFF/, ""));
256
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`${file} does not hold a JSON object`);
257
+ return parsed;
258
+ }
259
+ function backupName(now) {
260
+ const two = (n) => String(n).padStart(2, "0");
261
+ const stamp = `${now.getFullYear()}-${two(now.getMonth() + 1)}-${two(now.getDate())}-${two(now.getHours())}${two(now.getMinutes())}${two(now.getSeconds())}`;
262
+ return `settings.json.${stamp}-before-claude-jev-advisor`;
263
+ }
264
+ function backupSettingsFile(file, dir, now) {
265
+ if (!fs3.existsSync(file)) return null;
266
+ fs3.mkdirSync(dir, { recursive: true });
267
+ const baseName = backupName(now);
268
+ let target = path4.join(dir, baseName);
269
+ let suffix = 2;
270
+ while (true) {
271
+ try {
272
+ fs3.copyFileSync(file, target, fs3.constants.COPYFILE_EXCL);
273
+ return target;
274
+ } catch (err) {
275
+ if (err.code === "EEXIST") {
276
+ target = path4.join(dir, `${baseName}-${suffix}`);
277
+ suffix++;
278
+ } else {
279
+ throw err;
280
+ }
281
+ }
282
+ }
283
+ }
284
+ function writeSettingsFile(file, settings) {
285
+ fs3.mkdirSync(path4.dirname(file), { recursive: true });
286
+ const temp = `${file}.claude-jev-advisor.tmp`;
287
+ fs3.writeFileSync(temp, `${JSON.stringify(settings, null, 2)}
288
+ `);
289
+ try {
290
+ fs3.renameSync(temp, file);
291
+ } catch (err) {
292
+ try {
293
+ fs3.unlinkSync(temp);
294
+ } catch {
295
+ }
296
+ throw err;
297
+ }
298
+ }
299
+
300
+ // src/install.ts
301
+ function withoutFeature(settings, feature) {
302
+ return HOOK_SPECS[feature].reduce((s, spec) => withoutCommands(s, ownScriptPattern(spec.script)), settings);
303
+ }
304
+ function withDisplay(settings, opts) {
305
+ let next = withoutModDisplay(settings, opts.delimiter);
306
+ const statusLine = opts.display === "statusline" ? hookCommand(path5.join(opts.distDir, STATUSLINE_SCRIPT)) : null;
307
+ next = applyStatusLineDisplay(next, opts.home, statusLine);
308
+ if (opts.display === "mod") next = withModDisplay(next, path5.join(opts.distDir, "..", "mod"), dataDir(opts.home), opts.delimiter);
309
+ return next;
310
+ }
311
+ function save(home, settings, now) {
312
+ const file = settingsPath(home);
313
+ const backup = backupSettingsFile(file, backupsDir(home), now);
314
+ writeSettingsFile(file, settings);
315
+ return backup;
316
+ }
317
+ var same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
318
+ function install(opts) {
319
+ const delimiter = opts.delimiter ?? path5.delimiter;
320
+ const file = settingsPath(opts.home);
321
+ const before = readSettingsFile(file);
322
+ let next = before;
323
+ const installed = [];
324
+ const skipped = [];
325
+ let replacedLegacyRmGuard = false;
326
+ for (const feature of opts.features) {
327
+ const platforms = SUPPORTED_PLATFORMS[feature];
328
+ if (platforms && !platforms.includes(opts.platform)) {
329
+ skipped.push({ feature, reason: `${feature} works on ${platforms.join(", ")} only` });
330
+ continue;
331
+ }
332
+ for (const spec of HOOK_SPECS[feature]) {
333
+ if (!commandMatches(hookCommand(path5.join(opts.distDir, spec.script)), ownScriptPattern(spec.script))) {
334
+ throw new Error(`cannot install from ${opts.distDir}: the hook path must contain claude-jev-advisor/dist/`);
335
+ }
336
+ }
337
+ next = withoutFeature(next, feature);
338
+ if (feature === "rm" && findCommands(next, LEGACY_RM_GUARD).length) {
339
+ next = withoutCommands(next, LEGACY_RM_GUARD);
340
+ replacedLegacyRmGuard = true;
341
+ }
342
+ for (const spec of HOOK_SPECS[feature]) {
343
+ const command = hookCommand(path5.join(opts.distDir, spec.script), ...spec.args);
344
+ next = withHook(next, { event: spec.event, matcher: spec.matcher, command, timeout: HOOK_TIMEOUT_SECONDS });
345
+ }
346
+ installed.push(feature);
347
+ }
348
+ const display = installed.includes("context") ? opts.display ?? readConfig(opts.home).display : null;
349
+ if (display !== null) next = withDisplay(next, { home: opts.home, distDir: opts.distDir, display, delimiter });
350
+ const backup = same(before, next) ? null : save(opts.home, next, opts.now);
351
+ forgetStatusLineBefore(opts.home, next);
352
+ updateConfig(opts.home, (c) => ({
353
+ ...c,
354
+ lang: opts.lang ?? c.lang,
355
+ keyFile: opts.keyFile ?? c.keyFile,
356
+ display: opts.display ?? c.display,
357
+ rm: installed.includes("rm") ? { ...c.rm, enabled: true } : c.rm,
358
+ context: installed.includes("context") ? { ...c.context, enabled: true } : c.context
359
+ }));
360
+ return { settingsFile: file, backup, installed, skipped, replacedLegacyRmGuard, display };
361
+ }
362
+ function uninstall(opts) {
363
+ const delimiter = opts.delimiter ?? path5.delimiter;
364
+ const file = settingsPath(opts.home);
365
+ const before = readSettingsFile(file);
366
+ let next = before;
367
+ const removed = [];
368
+ for (const feature of opts.features) {
369
+ let after = withoutFeature(next, feature);
370
+ if (feature === "context") after = withDisplay(after, { home: opts.home, distDir: "", display: null, delimiter });
371
+ if (!same(after, next)) removed.push(feature);
372
+ next = after;
373
+ }
374
+ const backup = removed.length ? save(opts.home, next, opts.now) : null;
375
+ forgetStatusLineBefore(opts.home, next);
376
+ return { settingsFile: file, backup, removed };
377
+ }
378
+ function setEnabled(home, features, enabled) {
379
+ updateConfig(home, (c) => ({
380
+ ...c,
381
+ rm: features.includes("rm") ? { ...c.rm, enabled } : c.rm,
382
+ context: features.includes("context") ? { ...c.context, enabled } : c.context
383
+ }));
384
+ }
385
+
386
+ // src/status.ts
387
+ import fs5 from "fs";
388
+ import path6 from "path";
389
+
390
+ // src/display/line.ts
391
+ var PHRASES = {
392
+ ko: {
393
+ clear: "\uC0C8\uB86D\uAC8C \uC2DC\uC791\uD558\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694? /clear",
394
+ compact: "\uC9C0\uAE08\uAE4C\uC9C0 \uC815\uB9AC\uD558\uACE0 \uC774\uC5B4\uAC00\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694? /compact",
395
+ compactLater: "\uC791\uC5C5\uC774 \uB05D\uB098\uBA74 \uC815\uB9AC\uD558\uACE0 \uC774\uC5B4\uAC00\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694? /compact"
396
+ },
397
+ en: {
398
+ clear: "Start fresh? /clear",
399
+ compact: "Wrap up what you have and continue? /compact",
400
+ compactLater: "When this work is done, wrap up and continue? /compact"
401
+ }
402
+ };
403
+ function isRecord3(value) {
404
+ return typeof value === "object" && value !== null && !Array.isArray(value);
405
+ }
406
+ function formatSize(tokens) {
407
+ return `${Math.round(tokens / 1e3)}k`;
408
+ }
409
+ function remainingPct(size, threshold) {
410
+ return Math.max(0, Math.round((threshold - size) / threshold * 100));
411
+ }
412
+ function adviceLine({ size, threshold, judgment, config }) {
413
+ if (size === null) return "";
414
+ const k = formatSize(size);
415
+ const phrases = PHRASES[config.lang];
416
+ const counted = size >= config.context.minTokens ? judgment : null;
417
+ if (threshold !== null && threshold > 0 && size >= threshold * (1 - config.context.redRemainingPct / 100)) {
418
+ const head = `\u{1F534} ${k} ${remainingPct(size, threshold)}%`;
419
+ if (!counted) return head;
420
+ if (counted.phase === "working") return `${head} \xB7 ${phrases.compactLater}`;
421
+ return `${head} \xB7 ${counted.clear ? phrases.clear : phrases.compact}`;
422
+ }
423
+ if (!counted) return k;
424
+ if (counted.phase === "working") return `\u{1F7E2} ${k}`;
425
+ if (counted.clear) return `\u{1F7E1} ${k} ${phrases.clear}`;
426
+ if (size >= config.context.compactMinTokens) return `\u{1F7E1} ${k} ${phrases.compact}`;
427
+ return `\u{1F7E2} ${k}`;
428
+ }
429
+ function parseState(raw) {
430
+ if (!isRecord3(raw) || typeof raw.sessionId !== "string" || typeof raw.at !== "number") return null;
431
+ const j = raw.judgment;
432
+ const judgment = isRecord3(j) && (j.phase === "working" || j.phase === "unit_done") && typeof j.clear === "boolean" ? { phase: j.phase, clear: j.clear } : null;
433
+ return { sessionId: raw.sessionId, at: raw.at, size: typeof raw.size === "number" ? raw.size : null, judgment };
434
+ }
435
+
436
+ // src/jev.ts
437
+ import fs4 from "fs";
438
+ var KEY_PREFIX = "TYPESAFE_API_KEY=";
439
+ function readJevKey(env, keyFile) {
440
+ const fromEnv = env.TYPESAFE_API_KEY?.trim();
441
+ if (fromEnv) return fromEnv;
442
+ if (!keyFile) return null;
443
+ let text;
444
+ try {
445
+ text = fs4.readFileSync(keyFile, "utf8");
446
+ } catch {
447
+ return null;
448
+ }
449
+ const line = text.replace(/^\uFEFF/, "").split(/\r?\n/).find((l) => l.startsWith(KEY_PREFIX));
450
+ const key = line?.slice(KEY_PREFIX.length).trim().replace(/^["']|["']$/g, "");
451
+ return key || null;
452
+ }
453
+
454
+ // src/status.ts
455
+ var SCRIPT_IN_COMMAND = /^node "([^"]+)"/;
456
+ function isRecord4(value) {
457
+ return typeof value === "object" && value !== null && !Array.isArray(value);
458
+ }
459
+ function displaySetUp(settings, config) {
460
+ if (config.display === "message") return true;
461
+ if (config.display === "statusline") return isOurStatusLine(settings.statusLine);
462
+ const env = isRecord4(settings.env) ? settings.env : {};
463
+ const dirs = typeof env[PLUGIN_DIRS_ENV] === "string" ? env[PLUGIN_DIRS_ENV] : "";
464
+ const hasDir = dirs.split(path6.delimiter).some((dir) => MOD_DIR_PATTERN.test(dir.replace(/\\/g, "/")));
465
+ return hasDir && isRecord4(settings.pluginConfigs) && MOD_PLUGIN_NAME in settings.pluginConfigs;
466
+ }
467
+ function newestState(home) {
468
+ let newest = null;
469
+ try {
470
+ for (const name of fs5.readdirSync(stateDir(home))) {
471
+ if (!name.endsWith(".json")) continue;
472
+ const file = path6.join(stateDir(home), name);
473
+ const mtime = fs5.statSync(file).mtimeMs;
474
+ if (!newest || mtime > newest.mtime) newest = { file, mtime };
475
+ }
476
+ return newest ? parseState(JSON.parse(fs5.readFileSync(newest.file, "utf8"))) : null;
477
+ } catch {
478
+ return null;
479
+ }
480
+ }
481
+ function contextLines(home, settings, config, env) {
482
+ const c = config.context;
483
+ const lines = [
484
+ ` display: ${config.display}${displaySetUp(settings, config) ? "" : ` (not set up in settings.json - run "claude-jev-advisor install context --display ${config.display}")`}`,
485
+ ` key: ${readJevKey(env, config.keyFile) ? "set" : "missing"}`,
486
+ ` thresholds: judge from ${formatSize(c.minTokens)}, /compact from ${formatSize(c.compactMinTokens)}, red at ${c.redRemainingPct}% left`
487
+ ];
488
+ const last = newestState(home);
489
+ if (last) {
490
+ const line = adviceLine({ size: last.size, threshold: null, judgment: last.judgment, config }) || "(no size)";
491
+ lines.push(` last judgment: ${new Date(last.at).toISOString()} ${line}`);
492
+ }
493
+ return lines;
494
+ }
495
+ function statusLines(home, env = process.env) {
496
+ const lines = [`settings: ${settingsPath(home)}`, `config: ${configPath(home)}`];
497
+ let settings;
498
+ try {
499
+ settings = readSettingsFile(settingsPath(home));
500
+ } catch (err) {
501
+ return [...lines, `settings.json could not be read: ${err.message}`];
502
+ }
503
+ const config = readConfig(home);
504
+ for (const feature of FEATURES) {
505
+ const state = config[feature].enabled ? "on" : "off";
506
+ const commands = HOOK_SPECS[feature].flatMap((spec) => findCommands(settings, ownScriptPattern(spec.script)));
507
+ if (!commands.length) {
508
+ lines.push(`${feature}: not installed (${state})`);
509
+ continue;
510
+ }
511
+ lines.push(`${feature}: installed, ${state}`);
512
+ for (const c of commands) {
513
+ const script = SCRIPT_IN_COMMAND.exec(c.command)?.[1];
514
+ if (script && !fs5.existsSync(script)) {
515
+ lines.push(` broken: ${c.event} hook points to a missing file (${script}) - run "claude-jev-advisor install ${feature}" again, or "claude-jev-advisor uninstall ${feature}"`);
516
+ }
517
+ }
518
+ if (feature === "context") lines.push(...contextLines(home, settings, config, env));
519
+ }
520
+ if (findCommands(settings, LEGACY_RM_GUARD).length) {
521
+ lines.push('legacy rm-guard hook (~/.claude/hooks/rm-guard) is still registered - "claude-jev-advisor install rm" replaces it');
522
+ }
523
+ return lines;
524
+ }
525
+
526
+ // src/cli/run.ts
527
+ var USAGE = [
528
+ "Usage: claude-jev-advisor <command> [helpers] [options]",
529
+ "",
530
+ "Commands:",
531
+ " install [rm] [context] register hooks in ~/.claude/settings.json (backs it up first)",
532
+ " uninstall [rm] [context] remove this package's hooks and display from ~/.claude/settings.json",
533
+ " on [rm] [context] switch helpers on (open sessions too)",
534
+ " off [rm] [context] switch helpers off (open sessions too)",
535
+ " status show what is registered and switched on",
536
+ "",
537
+ "Options for install: --lang ko|en --key-file <path> --display mod|statusline|message",
538
+ 'The display is set up when context is installed: "install context --display <mode>" switches it.',
539
+ "Leaving out the helpers means all of them.",
540
+ 'Run "claude-jev-advisor uninstall" before "npm rm -g": npm does not run uninstall scripts.'
541
+ ].join("\n");
542
+ var INSTALL_OPTIONS = ["lang", "key-file", "display"];
543
+ var LANGS2 = ["ko", "en"];
544
+ var DISPLAYS2 = ["mod", "statusline", "message"];
545
+ function parse(argv) {
546
+ const [command = "help", ...rest] = argv;
547
+ const features = [];
548
+ const options = {};
549
+ for (let i = 0; i < rest.length; i++) {
550
+ const arg = rest[i];
551
+ if (arg.startsWith("--")) {
552
+ const value = rest[i + 1];
553
+ if (value === void 0 || value.startsWith("--")) return `${arg} needs a value`;
554
+ options[arg.slice(2)] = value;
555
+ i++;
556
+ } else if (isFeature(arg)) {
557
+ if (!features.includes(arg)) features.push(arg);
558
+ } else {
559
+ return `unknown helper "${arg}" (known: ${FEATURES.join(", ")})`;
560
+ }
561
+ }
562
+ return { command, features: features.length ? features : [...FEATURES], options };
563
+ }
564
+ function checkOptions(command, options) {
565
+ const names = Object.keys(options);
566
+ if (command !== "install") return names.length ? `options are only for install (got --${names[0]})` : null;
567
+ const unknown = names.find((n) => !INSTALL_OPTIONS.includes(n));
568
+ if (unknown) return `unknown option --${unknown}`;
569
+ if (options.lang !== void 0 && !LANGS2.includes(options.lang)) return "--lang takes ko or en";
570
+ if (options.display !== void 0 && !DISPLAYS2.includes(options.display)) return "--display takes mod, statusline or message";
571
+ return null;
572
+ }
573
+ function runCli(argv, io) {
574
+ const parsed = parse(argv);
575
+ const problem = typeof parsed === "string" ? parsed : checkOptions(parsed.command, parsed.options);
576
+ if (typeof parsed === "string" || problem) {
577
+ io.err(problem ?? String(parsed));
578
+ io.err(USAGE);
579
+ return 2;
580
+ }
581
+ const { command, features, options } = parsed;
582
+ try {
583
+ switch (command) {
584
+ case "install": {
585
+ const r = install({
586
+ home: io.home,
587
+ features,
588
+ distDir: io.distDir,
589
+ platform: io.platform,
590
+ now: io.now(),
591
+ lang: options.lang,
592
+ keyFile: options["key-file"] === void 0 || path7.isAbsolute(options["key-file"]) ? options["key-file"] : path7.resolve(options["key-file"]),
593
+ display: options.display
594
+ });
595
+ for (const f of r.installed) io.out(`installed ${f}`);
596
+ for (const s of r.skipped) io.out(`skipped ${s.feature}: ${s.reason}`);
597
+ if (r.replacedLegacyRmGuard) io.out("replaced the legacy rm-guard hook (its files under ~/.claude/hooks/rm-guard were left in place)");
598
+ if (r.display) io.out(`display: ${r.display}`);
599
+ if (r.backup) io.out(`backup: ${r.backup}`);
600
+ if (r.installed.length) io.out("Hooks apply to Claude Code sessions started from now on.");
601
+ return 0;
602
+ }
603
+ case "uninstall": {
604
+ const r = uninstall({ home: io.home, features, now: io.now() });
605
+ if (!r.removed.length) io.out("nothing to remove");
606
+ for (const f of r.removed) io.out(`removed ${f}`);
607
+ if (r.backup) io.out(`backup: ${r.backup}`);
608
+ return 0;
609
+ }
610
+ case "on":
611
+ case "off":
612
+ setEnabled(io.home, features, command === "on");
613
+ io.out(`${features.join(", ")}: ${command}`);
614
+ return 0;
615
+ case "status":
616
+ for (const line of statusLines(io.home)) io.out(line);
617
+ return 0;
618
+ case "help":
619
+ case "--help":
620
+ case "-h":
621
+ io.out(USAGE);
622
+ return 0;
623
+ default:
624
+ io.err(`unknown command "${command}"`);
625
+ io.err(USAGE);
626
+ return 2;
627
+ }
628
+ } catch (err) {
629
+ io.err(`claude-jev-advisor: ${err.message}`);
630
+ return 1;
631
+ }
632
+ }
633
+
634
+ // src/cli.ts
635
+ process.exitCode = runCli(process.argv.slice(2), {
636
+ home: os.homedir(),
637
+ distDir: path8.dirname(fileURLToPath(import.meta.url)),
638
+ platform: process.platform,
639
+ now: () => /* @__PURE__ */ new Date(),
640
+ out: (line) => console.log(line),
641
+ err: (line) => console.error(line)
642
+ });